/**
 * @license
 * Copyright (C) 2015 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 '../../../styles/shared-styles';
import '../../shared/gr-dropdown-list/gr-dropdown-list';
import '../../shared/gr-select/gr-select';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
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-patch-range-select_html';
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter';
import {appContext} from '../../../services/app-context';
import {
  computeLatestPatchNum,
  findSortedIndex,
  getParentIndex,
  getRevisionByPatchNum,
  isMergeParent,
  patchNumEquals,
  sortRevisions,
  PatchSet,
} from '../../../utils/patch-set-util';
import {customElement, property, observe} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {hasOwnProperty} from '../../../utils/common-util';
import {
  ParentPatchSetNum,
  PatchSetNum,
  RevisionInfo,
  Timestamp,
} from '../../../types/common';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {
  DropdownItem,
  DropDownValueChangeEvent,
  GrDropdownList,
} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {GeneratedWebLink} from '../../core/gr-navigation/gr-navigation';

// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;

export interface PatchRangeChangeDetail {
  patchNum?: PatchSetNum;
  basePatchNum?: PatchSetNum;
}

export type PatchRangeChangeEvent = CustomEvent<PatchRangeChangeDetail>;

export interface FilesWebLinks {
  meta_a: GeneratedWebLink[];
  meta_b: GeneratedWebLink[];
}

export interface GrPatchRangeSelect {
  $: {
    patchNumDropdown: GrDropdownList;
  };
}

/**
 * Fired when the patch range changes
 *
 * @event patch-range-change
 *
 * @property {string} patchNum
 * @property {string} basePatchNum
 * @extends PolymerElement
 */
@customElement('gr-patch-range-select')
export class GrPatchRangeSelect extends GestureEventListeners(
  LegacyElementMixin(PolymerElement)
) {
  static get template() {
    return htmlTemplate;
  }

  @property({type: Array})
  availablePatches?: PatchSet[];

  @property({
    type: Object,
    computed:
      '_computeBaseDropdownContent(availablePatches, patchNum,' +
      '_sortedRevisions, changeComments, revisionInfo)',
  })
  _baseDropdownContent?: DropdownItem[];

  @property({
    type: Object,
    computed:
      '_computePatchDropdownContent(availablePatches,' +
      'basePatchNum, _sortedRevisions, changeComments)',
  })
  _patchDropdownContent?: DropdownItem[];

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

  @property({type: Object})
  changeComments?: ChangeComments;

  @property({type: Object})
  filesWeblinks?: FilesWebLinks;

  @property({type: String})
  patchNum?: PatchSetNum;

  @property({type: String})
  basePatchNum?: PatchSetNum;

  @property({type: Object})
  revisions?: RevisionInfo[];

  @property({type: Object})
  revisionInfo?: RevisionInfoClass;

  @property({type: Array})
  _sortedRevisions?: RevisionInfo[];

  private readonly reporting: ReportingService = appContext.reportingService;

  constructor() {
    super();
    this.reporting = appContext.reportingService;
  }

  _getShaForPatch(patch: PatchSet) {
    return patch.sha.substring(0, 10);
  }

  _computeBaseDropdownContent(
    availablePatches?: PatchSet[],
    patchNum?: PatchSetNum,
    _sortedRevisions?: RevisionInfo[],
    changeComments?: ChangeComments,
    revisionInfo?: RevisionInfoClass
  ): DropdownItem[] | undefined {
    // Polymer 2: check for undefined
    if (
      availablePatches === undefined ||
      patchNum === undefined ||
      _sortedRevisions === undefined ||
      changeComments === undefined ||
      revisionInfo === undefined
    ) {
      return undefined;
    }

    const parentCounts = revisionInfo.getParentCountMap();
    const currentParentCount = hasOwnProperty(parentCounts, patchNum)
      ? parentCounts[patchNum as number]
      : 1;
    const maxParents = revisionInfo.getMaxParents();
    const isMerge = currentParentCount > 1;

    const dropdownContent: DropdownItem[] = [];
    for (const basePatch of availablePatches) {
      const basePatchNum = basePatch.num;
      const entry: DropdownItem = this._createDropdownEntry(
        basePatchNum,
        'Patchset ',
        _sortedRevisions,
        changeComments,
        this._getShaForPatch(basePatch)
      );
      dropdownContent.push({
        ...entry,
        disabled: this._computeLeftDisabled(
          basePatch.num,
          patchNum,
          _sortedRevisions
        ),
      });
    }

    dropdownContent.push({
      text: isMerge ? 'Auto Merge' : 'Base',
      value: 'PARENT',
    });

    for (let idx = 0; isMerge && idx < maxParents; idx++) {
      dropdownContent.push({
        disabled: idx >= currentParentCount,
        triggerText: `Parent ${idx + 1}`,
        text: `Parent ${idx + 1}`,
        mobileText: `Parent ${idx + 1}`,
        value: -(idx + 1),
      });
    }

    return dropdownContent;
  }

  _computeMobileText(
    patchNum: PatchSetNum,
    changeComments: ChangeComments,
    revisions: RevisionInfo[]
  ) {
    return (
      `${patchNum}` +
      `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
      `${this._computePatchSetDescription(revisions, patchNum, true)}`
    );
  }

  _computePatchDropdownContent(
    availablePatches?: PatchSet[],
    basePatchNum?: PatchSetNum,
    _sortedRevisions?: RevisionInfo[],
    changeComments?: ChangeComments
  ): DropdownItem[] | undefined {
    // Polymer 2: check for undefined
    if (
      availablePatches === undefined ||
      basePatchNum === undefined ||
      _sortedRevisions === undefined ||
      changeComments === undefined
    ) {
      return undefined;
    }

    const dropdownContent: DropdownItem[] = [];
    for (const patch of availablePatches) {
      const patchNum = patch.num;
      const entry = this._createDropdownEntry(
        patchNum,
        patchNum === 'edit' ? '' : 'Patchset ',
        _sortedRevisions,
        changeComments,
        this._getShaForPatch(patch)
      );
      dropdownContent.push({
        ...entry,
        disabled: this._computeRightDisabled(
          basePatchNum,
          patchNum,
          _sortedRevisions
        ),
      });
    }
    return dropdownContent;
  }

  _computeText(
    patchNum: PatchSetNum,
    prefix: string,
    changeComments: ChangeComments,
    sha: string
  ) {
    return (
      `${prefix}${patchNum}` +
      `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
      ` | ${sha}`
    );
  }

  _createDropdownEntry(
    patchNum: PatchSetNum,
    prefix: string,
    sortedRevisions: RevisionInfo[],
    changeComments: ChangeComments,
    sha: string
  ) {
    const entry: DropdownItem = {
      triggerText: `${prefix}${patchNum}`,
      text: this._computeText(patchNum, prefix, changeComments, sha),
      mobileText: this._computeMobileText(
        patchNum,
        changeComments,
        sortedRevisions
      ),
      bottomText: `${this._computePatchSetDescription(
        sortedRevisions,
        patchNum
      )}`,
      value: patchNum,
    };
    const date = this._computePatchSetDate(sortedRevisions, patchNum);
    if (date) {
      entry.date = date;
    }
    return entry;
  }

  @observe('revisions.*')
  _updateSortedRevisions(
    revisionsRecord: PolymerDeepPropertyChange<RevisionInfo[], RevisionInfo[]>
  ) {
    const revisions = revisionsRecord.base;
    if (!revisions) return;
    this._sortedRevisions = sortRevisions(Object.values(revisions));
  }

  /**
   * The basePatchNum should always be <= patchNum -- because sortedRevisions
   * is sorted in reverse order (higher patchset nums first), invalid base
   * patch nums have an index greater than the index of patchNum.
   *
   * @param basePatchNum The possible base patch num.
   * @param patchNum The current selected patch num.
   */
  _computeLeftDisabled(
    basePatchNum: PatchSetNum,
    patchNum: PatchSetNum,
    sortedRevisions: RevisionInfo[]
  ): boolean {
    return (
      findSortedIndex(basePatchNum, sortedRevisions) <=
      findSortedIndex(patchNum, sortedRevisions)
    );
  }

  /**
   * The basePatchNum should always be <= patchNum -- because sortedRevisions
   * is sorted in reverse order (higher patchset nums first), invalid patch
   * nums have an index greater than the index of basePatchNum.
   *
   * In addition, if the current basePatchNum is 'PARENT', all patchNums are
   * valid.
   *
   * If the current basePatchNum is a parent index, then only patches that have
   * at least that many parents are valid.
   *
   * @param basePatchNum The current selected base patch num.
   * @param patchNum The possible patch num.
   */
  _computeRightDisabled(
    basePatchNum: PatchSetNum,
    patchNum: PatchSetNum,
    sortedRevisions: RevisionInfo[]
  ): boolean {
    if (patchNumEquals(basePatchNum, ParentPatchSetNum)) {
      return false;
    }

    if (isMergeParent(basePatchNum)) {
      if (!this.revisionInfo) {
        return true;
      }
      // Note: parent indices use 1-offset.
      return (
        this.revisionInfo.getParentCount(patchNum) <
        getParentIndex(basePatchNum)
      );
    }

    return (
      findSortedIndex(basePatchNum, sortedRevisions) <=
      findSortedIndex(patchNum, sortedRevisions)
    );
  }

  _computePatchSetCommentsString(
    changeComments: ChangeComments,
    patchNum: PatchSetNum
  ) {
    if (!changeComments) {
      return;
    }

    const commentThreadCount = changeComments.computeCommentThreadCount({
      patchNum,
    });
    const commentThreadString = GrCountStringFormatter.computePluralString(
      commentThreadCount,
      'comment'
    );

    const unresolvedCount = changeComments.computeUnresolvedNum({patchNum});
    const unresolvedString = GrCountStringFormatter.computeString(
      unresolvedCount,
      'unresolved'
    );

    if (!commentThreadString.length && !unresolvedString.length) {
      return '';
    }

    return (
      ` (${commentThreadString}` +
      // Add a comma + space if both comment threads and unresolved
      (commentThreadString && unresolvedString ? ', ' : '') +
      `${unresolvedString})`
    );
  }

  _computePatchSetDescription(
    revisions: RevisionInfo[],
    patchNum: PatchSetNum,
    addFrontSpace?: boolean
  ) {
    const rev = getRevisionByPatchNum(revisions, patchNum);
    return rev?.description
      ? (addFrontSpace ? ' ' : '') +
          rev.description.substring(0, PATCH_DESC_MAX_LENGTH)
      : '';
  }

  _computePatchSetDate(
    revisions: RevisionInfo[],
    patchNum: PatchSetNum
  ): Timestamp | undefined {
    const rev = getRevisionByPatchNum(revisions, patchNum);
    return rev ? rev.created : undefined;
  }

  /**
   * Catches value-change events from the patchset dropdowns and determines
   * whether or not a patch change event should be fired.
   */
  _handlePatchChange(e: DropDownValueChangeEvent) {
    const detail: PatchRangeChangeDetail = {
      patchNum: this.patchNum,
      basePatchNum: this.basePatchNum,
    };
    const target = (dom(e) as EventApi).localTarget;
    const patchSetValue = e.detail.value as PatchSetNum;
    const latestPatchNum = computeLatestPatchNum(this.availablePatches);
    if (target === this.$.patchNumDropdown) {
      if (detail.patchNum === e.detail.value) return;
      this.reporting.reportInteraction('right-patchset-changed', {
        previous: detail.patchNum,
        current: e.detail.value,
        latest: latestPatchNum,
        commentCount: this.changeComments?.computeCommentThreadCount({
          patchNum: e.detail.value as PatchSetNum,
        }),
      });
      detail.patchNum = patchSetValue;
    } else {
      if (patchNumEquals(detail.basePatchNum, patchSetValue)) return;
      this.reporting.reportInteraction('left-patchset-changed', {
        previous: detail.basePatchNum,
        current: e.detail.value,
        commentCount: this.changeComments?.computeCommentThreadCount({
          patchNum: patchSetValue,
        }),
      });
      detail.basePatchNum = patchSetValue;
    }

    this.dispatchEvent(
      new CustomEvent('patch-range-change', {detail, bubbles: false})
    );
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'gr-patch-range-select': GrPatchRangeSelect;
  }
}
