/**
 * @license
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '@polymer/iron-icon/iron-icon';
import '../../../styles/shared-styles';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
import '../gr-diff/gr-diff';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-apply-fix-dialog_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {customElement, property} from '@polymer/decorators';
import {ParsedChangeInfo} from '../../shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {
  NumericChangeId,
  DiffInfo,
  DiffPreferencesInfo,
  EditPatchSetNum,
  FixId,
  FixSuggestionInfo,
  PatchSetNum,
  RobotId,
} from '../../../types/common';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
import {isRobot} from '../../../utils/comment-util';
import {OpenFixPreviewEvent} from '../../../types/events';

export interface GrApplyFixDialog {
  $: {
    restAPI: RestApiService & Element;
    applyFixOverlay: GrOverlay;
  };
}

interface FilePreview {
  filepath: string;
  preview: DiffInfo;
}

@customElement('gr-apply-fix-dialog')
export class GrApplyFixDialog extends GestureEventListeners(
  LegacyElementMixin(PolymerElement)
) {
  static get template() {
    return htmlTemplate;
  }

  @property({type: Object})
  prefs?: DiffPreferencesInfo;

  @property({type: Object})
  change?: ParsedChangeInfo;

  @property({type: String})
  changeNum?: NumericChangeId;

  @property({type: Number})
  _patchNum?: PatchSetNum;

  @property({type: String})
  _robotId?: RobotId;

  @property({type: Object})
  _currentFix?: FixSuggestionInfo;

  @property({type: Array})
  _currentPreviews: FilePreview[] = [];

  @property({type: Array})
  _fixSuggestions?: FixSuggestionInfo[];

  @property({type: Boolean})
  _isApplyFixLoading = false;

  @property({type: Number})
  _selectedFixIdx = 0;

  @property({
    type: Boolean,
    computed:
      '_computeDisableApplyFixButton(_isApplyFixLoading, change, ' +
      '_patchNum)',
  })
  _disableApplyFixButton?: boolean;

  private refitOverlay?: () => void;

  /**
   * Given robot comment CustomEvent object, fetch diffs associated
   * with first robot comment suggested fix and open dialog.
   *
   * @param e to be passed from gr-comment with robot comment detail.
   * @return Promise that resolves either when all
   * preview diffs are fetched or no fix suggestions in custom event detail.
   */
  open(e: OpenFixPreviewEvent) {
    const detail = e.detail;
    const comment = detail.comment;
    if (!detail.patchNum || !comment || !isRobot(comment)) {
      return Promise.resolve();
    }
    this._patchNum = detail.patchNum;
    this._fixSuggestions = comment.fix_suggestions;
    this._robotId = comment.robot_id;
    if (!this._fixSuggestions || !this._fixSuggestions.length) {
      return Promise.resolve();
    }
    this._selectedFixIdx = 0;
    const promises = [];
    promises.push(
      this._showSelectedFixSuggestion(this._fixSuggestions[0]),
      this.$.applyFixOverlay.open()
    );
    return Promise.all(promises).then(() => {
      // ensures gr-overlay repositions overlay in center
      this.$.applyFixOverlay.dispatchEvent(
        new CustomEvent('iron-resize', {
          composed: true,
          bubbles: true,
        })
      );
    });
  }

  attached() {
    super.attached();
    this.refitOverlay = () => {
      // re-center the dialog as content changed
      this.$.applyFixOverlay.dispatchEvent(
        new CustomEvent('iron-resize', {
          composed: true,
          bubbles: true,
        })
      );
    };
    this.addEventListener('diff-context-expanded', this.refitOverlay);
  }

  detached() {
    super.detached();
    if (this.refitOverlay) {
      this.removeEventListener('diff-context-expanded', this.refitOverlay);
    }
  }

  _showSelectedFixSuggestion(fixSuggestion: FixSuggestionInfo) {
    this._currentFix = fixSuggestion;
    return this._fetchFixPreview(fixSuggestion.fix_id);
  }

  _fetchFixPreview(fixId: FixId) {
    if (!this.changeNum || !this._patchNum) {
      return Promise.reject(
        new Error('Both _patchNum and changeNum must be set')
      );
    }
    return this.$.restAPI
      .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
      .then(res => {
        if (res) {
          this._currentPreviews = Object.keys(res).map(key => {
            return {filepath: key, preview: res[key]};
          });
        }
      })
      .catch(err => {
        this._close();
        throw err;
      });
  }

  hasSingleFix(_fixSuggestions?: FixSuggestionInfo[]) {
    return (_fixSuggestions || []).length === 1;
  }

  overridePartialPrefs(prefs: DiffPreferencesInfo): DiffPreferencesInfo {
    // generate a smaller gr-diff than fullscreen for dialog
    return {...prefs, line_length: 50};
  }

  onCancel(e: CustomEvent) {
    if (e) {
      e.stopPropagation();
    }
    this._close();
  }

  addOneTo(_selectedFixIdx: number) {
    return _selectedFixIdx + 1;
  }

  _onPrevFixClick(e: CustomEvent) {
    if (e) e.stopPropagation();
    if (this._selectedFixIdx >= 1 && this._fixSuggestions) {
      this._selectedFixIdx -= 1;
      this._showSelectedFixSuggestion(
        this._fixSuggestions[this._selectedFixIdx]
      );
    }
  }

  _onNextFixClick(e: CustomEvent) {
    if (e) e.stopPropagation();
    if (
      this._fixSuggestions &&
      this._selectedFixIdx < this._fixSuggestions.length
    ) {
      this._selectedFixIdx += 1;
      this._showSelectedFixSuggestion(
        this._fixSuggestions[this._selectedFixIdx]
      );
    }
  }

  _noPrevFix(_selectedFixIdx: number) {
    return _selectedFixIdx === 0;
  }

  _noNextFix(_selectedFixIdx: number, fixSuggestions?: FixSuggestionInfo[]) {
    if (!fixSuggestions) return true;
    return _selectedFixIdx === fixSuggestions.length - 1;
  }

  _close() {
    this._currentFix = undefined;
    this._currentPreviews = [];
    this._isApplyFixLoading = false;

    this.dispatchEvent(
      new CustomEvent('close-fix-preview', {
        bubbles: true,
        composed: true,
      })
    );
    this.$.applyFixOverlay.close();
  }

  _getApplyFixButtonLabel(isLoading: boolean) {
    return isLoading ? 'Saving...' : 'Apply Fix';
  }

  _computeTooltip(change?: ParsedChangeInfo, patchNum?: PatchSetNum) {
    if (!change || !patchNum) return '';
    const latestPatchNum = change.revisions[change.current_revision]._number;
    return latestPatchNum !== patchNum
      ? 'Fix can only be applied to the latest patchset'
      : '';
  }

  _computeDisableApplyFixButton(
    isApplyFixLoading?: boolean,
    change?: ParsedChangeInfo,
    patchNum?: PatchSetNum
  ) {
    if (!change || isApplyFixLoading === undefined || patchNum === undefined) {
      return true;
    }
    const currentPatchNum = change.revisions[change.current_revision]._number;
    if (patchNum !== currentPatchNum) {
      return true;
    }
    return isApplyFixLoading;
  }

  _handleApplyFix(e: CustomEvent) {
    if (e) {
      e.stopPropagation();
    }

    const changeNum = this.changeNum;
    const patchNum = this._patchNum;
    const change = this.change;
    if (!changeNum || !patchNum || !change || !this._currentFix) {
      return Promise.reject(new Error('Not all required properties are set.'));
    }
    this._isApplyFixLoading = true;
    return this.$.restAPI
      .applyFixSuggestion(changeNum, patchNum, this._currentFix.fix_id)
      .then(res => {
        if (res && res.ok) {
          GerritNav.navigateToChange(change, EditPatchSetNum, patchNum);
          this._close();
        }
        this._isApplyFixLoading = false;
      });
  }

  getFixDescription(currentFix?: FixSuggestionInfo) {
    return currentFix && currentFix.description ? currentFix.description : '';
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'gr-apply-fix-dialog': GrApplyFixDialog;
  }
}
