// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../common/common.js';
import * as Diff from '../diff/diff.js';
import * as i18n from '../i18n/i18n.js';
import * as UI from '../ui/ui.js';
import * as Workspace from '../workspace/workspace.js';  // eslint-disable-line no-unused-vars
import * as WorkspaceDiff from '../workspace_diff/workspace_diff.js';

import {ChangesSidebar, Events} from './ChangesSidebar.js';
import {ChangesTextEditor} from './ChangesTextEditor.js';

export const UIStrings = {
  /**
  *@description Screen-reader accessible name for the code editor in the Changes tool showing the user's changes.
  */
  changesDiffViewer: 'Changes diff viewer',
  /**
  *@description Screen reader/tooltip label for a button in the Changes tool that reverts all changes to the currently open file.
  */
  revertAllChangesToCurrentFile: 'Revert all changes to current file',
  /**
  *@description Text in Changes View of the Changes tab
  */
  noChanges: 'No changes',
  /**
  *@description Text in Changes View of the Changes tab
  */
  binaryData: 'Binary data',
  /**
  *@description Text in Changes View of the Changes tab when one code insertion has occurred.
  */
  sInsertion: '1 insertion `(+)`,',
  /**
  * @description Text in Changes View of the Changes tab when multiple code insertions have
  * occurred.
  * @example {2} PH1
  */
  sInsertions: '{PH1} insertions `(+)`,',
  /**
  *@description Text in Changes View of the Changes tab when one code deletion has occurred.
  */
  sDeletion: '1 deletion `(-)`',
  /**
  * @description Text in Changes View of the Changes tab when multiple code deletions have occurred.
  * @example {2} PH1
  */
  sDeletions: '{PH1} deletions `(-)`',
  /**
  *@description Text in Changes View of the Changes tab
  *@example {2} PH1
  */
  SkippingDMatchingLines: '( … Skipping {PH1} matching lines … )',
};
const str_ = i18n.i18n.registerUIStrings('changes/ChangesView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

let changesViewInstance: ChangesView;

export class ChangesView extends UI.Widget.VBox {
  _emptyWidget: UI.EmptyWidget.EmptyWidget;
  _workspaceDiff: WorkspaceDiff.WorkspaceDiff.WorkspaceDiffImpl;
  _changesSidebar: ChangesSidebar;
  _selectedUISourceCode: Workspace.UISourceCode.UISourceCode|null;
  _diffRows: Row[];
  _maxLineDigits: number;
  _editor: ChangesTextEditor;
  _toolbar: UI.Toolbar.Toolbar;
  _diffStats: UI.Toolbar.ToolbarText;

  private constructor() {
    super(true);
    this.registerRequiredCSS('changes/changesView.css', {enableLegacyPatching: true});
    const splitWidget = new UI.SplitWidget.SplitWidget(true /* vertical */, false /* sidebar on left */);
    const mainWidget = new UI.Widget.Widget();
    splitWidget.setMainWidget(mainWidget);
    splitWidget.show(this.contentElement);

    this._emptyWidget = new UI.EmptyWidget.EmptyWidget('');
    this._emptyWidget.show(mainWidget.element);

    this._workspaceDiff = WorkspaceDiff.WorkspaceDiff.workspaceDiff();
    this._changesSidebar = new ChangesSidebar(this._workspaceDiff);
    this._changesSidebar.addEventListener(Events.SelectedUISourceCodeChanged, this._selectedUISourceCodeChanged, this);
    splitWidget.setSidebarWidget(this._changesSidebar);

    this._selectedUISourceCode = null;

    this._diffRows = [];

    this._maxLineDigits = 1;

    this._editor = new ChangesTextEditor({
      bracketMatchingSetting: undefined,
      devtoolsAccessibleName: i18nString(UIStrings.changesDiffViewer),
      lineNumbers: true,
      lineWrapping: false,
      mimeType: undefined,
      autoHeight: undefined,
      padBottom: undefined,
      maxHighlightLength: Infinity,  // Avoid CodeMirror bailing out of highlighting big diffs.
      placeholder: undefined,
      lineWiseCopyCut: undefined,
      inputStyle: undefined,
    });
    this._editor.setReadOnly(true);
    const editorContainer = mainWidget.element.createChild('div', 'editor-container');
    UI.ARIAUtils.markAsTabpanel(editorContainer);
    this._editor.show(editorContainer);
    this._editor.hideWidget();

    self.onInvokeElement(this._editor.element, this._click.bind(this));

    this._toolbar = new UI.Toolbar.Toolbar('changes-toolbar', mainWidget.element);
    const revertButton =
        new UI.Toolbar.ToolbarButton(i18nString(UIStrings.revertAllChangesToCurrentFile), 'largeicon-undo');
    revertButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._revert.bind(this));
    this._toolbar.appendToolbarItem(revertButton);
    this._diffStats = new UI.Toolbar.ToolbarText('');
    this._toolbar.appendToolbarItem(this._diffStats);
    this._toolbar.setEnabled(false);

    this._hideDiff(i18nString(UIStrings.noChanges));
    this._selectedUISourceCodeChanged();
  }

  static instance(opts: {forceNew: boolean|null} = {forceNew: null}): ChangesView {
    const {forceNew} = opts;
    if (!changesViewInstance || forceNew) {
      changesViewInstance = new ChangesView();
    }

    return changesViewInstance;
  }

  _selectedUISourceCodeChanged(): void {
    this._revealUISourceCode(this._changesSidebar.selectedUISourceCode());
  }

  _revert(): void {
    const uiSourceCode = this._selectedUISourceCode;
    if (!uiSourceCode) {
      return;
    }
    this._workspaceDiff.revertToOriginal(uiSourceCode);
  }

  _click(event: Event): void {
    const selection = this._editor.selection();
    if (!selection.isEmpty() || !this._selectedUISourceCode) {
      return;
    }
    const row = this._diffRows[selection.startLine];
    Common.Revealer.reveal(
        this._selectedUISourceCode.uiLocation(row.currentLineNumber - 1, selection.startColumn), false);
    event.consume(true);
  }

  _revealUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode|null): void {
    if (this._selectedUISourceCode === uiSourceCode) {
      return;
    }

    if (this._selectedUISourceCode) {
      this._workspaceDiff.unsubscribeFromDiffChange(this._selectedUISourceCode, this._refreshDiff, this);
    }
    if (uiSourceCode && this.isShowing()) {
      this._workspaceDiff.subscribeToDiffChange(uiSourceCode, this._refreshDiff, this);
    }

    this._selectedUISourceCode = uiSourceCode;
    this._refreshDiff();
  }

  wasShown(): void {
    this._refreshDiff();
  }

  _refreshDiff(): void {
    if (!this.isShowing()) {
      return;
    }

    if (!this._selectedUISourceCode) {
      this._renderDiffRows(null);
      return;
    }
    const uiSourceCode = this._selectedUISourceCode;
    if (!uiSourceCode.contentType().isTextType()) {
      this._hideDiff(i18nString(UIStrings.binaryData));
      return;
    }
    this._workspaceDiff.requestDiff(uiSourceCode).then((diff: Diff.Diff.DiffArray|null): void => {
      if (this._selectedUISourceCode !== uiSourceCode) {
        return;
      }
      this._renderDiffRows(diff);
    });
  }

  _hideDiff(message: string): void {
    this._diffStats.setText('');
    this._toolbar.setEnabled(false);
    this._editor.hideWidget();
    this._emptyWidget.text = message;
    this._emptyWidget.showWidget();
  }

  _renderDiffRows(diff: Diff.Diff.DiffArray|null): void {
    this._diffRows = [];

    if (!diff || (diff.length === 1 && diff[0][0] === Diff.Diff.Operation.Equal)) {
      this._hideDiff(i18nString(UIStrings.noChanges));
      return;
    }

    let insertions = 0;
    let deletions = 0;
    let currentLineNumber = 0;
    let baselineLineNumber = 0;
    const paddingLines = 3;

    const originalLines: string[] = [];
    const currentLines: string[] = [];

    for (let i = 0; i < diff.length; ++i) {
      const token = diff[i];
      switch (token[0]) {
        case Diff.Diff.Operation.Equal:
          this._diffRows.push(...createEqualRows(token[1], i === 0, i === diff.length - 1));
          originalLines.push(...token[1]);
          currentLines.push(...token[1]);
          break;
        case Diff.Diff.Operation.Insert:
          for (const line of token[1]) {
            this._diffRows.push(createRow(line, RowType.Addition));
          }
          insertions += token[1].length;
          currentLines.push(...token[1]);
          break;
        case Diff.Diff.Operation.Delete:
          deletions += token[1].length;
          originalLines.push(...token[1]);
          if (diff[i + 1] && diff[i + 1][0] === Diff.Diff.Operation.Insert) {
            i++;
            this._diffRows.push(...createModifyRows(token[1].join('\n'), diff[i][1].join('\n')));
            insertions += diff[i][1].length;
            currentLines.push(...diff[i][1]);
          } else {
            for (const line of token[1]) {
              this._diffRows.push(createRow(line, RowType.Deletion));
            }
          }
          break;
      }
    }

    this._maxLineDigits = Math.ceil(Math.log10(Math.max(currentLineNumber, baselineLineNumber)));

    let insertionText: Common.UIString.LocalizedString|'' = '';
    if (insertions === 1) {
      insertionText = i18nString(UIStrings.sInsertion);
    } else {
      insertionText = i18nString(UIStrings.sInsertions, {PH1: insertions});
    }

    let deletionText: Common.UIString.LocalizedString|'' = '';
    if (deletions === 1) {
      deletionText = i18nString(UIStrings.sDeletion);
    } else {
      deletionText = i18nString(UIStrings.sDeletions, {PH1: deletions});
    }

    this._diffStats.setText(`${insertionText} ${deletionText}`);
    this._toolbar.setEnabled(true);
    this._emptyWidget.hideWidget();

    this._editor.operation((): void => {
      this._editor.showWidget();
      this._editor.setHighlightMode({
        name: 'devtools-diff',
        diffRows: this._diffRows,
        mimeType: /** @type {!Workspace.UISourceCode.UISourceCode} */ (
                      this._selectedUISourceCode as Workspace.UISourceCode.UISourceCode)
                      .mimeType(),
        baselineLines: originalLines,
        currentLines: currentLines,
      });
      this._editor.setText(
          this._diffRows
              .map(
                  (row: Row): string =>
                      row.tokens.map((t: {text: string, className: string}): string => t.text).join(''))
              .join('\n'));
      this._editor.setLineNumberFormatter(this._lineFormatter.bind(this));
      this._editor.updateDiffGutter(this._diffRows);
    });

    function createEqualRows(lines: string[], atStart: boolean, atEnd: boolean): Row[] {
      const equalRows = [];
      if (!atStart) {
        for (let i = 0; i < paddingLines && i < lines.length; i++) {
          equalRows.push(createRow(lines[i], RowType.Equal));
        }
        if (lines.length > paddingLines * 2 + 1 && !atEnd) {
          equalRows.push(createRow(
              i18nString(UIStrings.SkippingDMatchingLines, {PH1: (lines.length - paddingLines * 2)}), RowType.Spacer));
        }
      }
      if (!atEnd) {
        const start = Math.max(lines.length - paddingLines - 1, atStart ? 0 : paddingLines);
        let skip = lines.length - paddingLines - 1;
        if (!atStart) {
          skip -= paddingLines;
        }
        if (skip > 0) {
          baselineLineNumber += skip;
          currentLineNumber += skip;
        }

        for (let i = start; i < lines.length; i++) {
          equalRows.push(createRow(lines[i], RowType.Equal));
        }
      }
      return equalRows;
    }

    function createModifyRows(before: string, after: string): Row[] {
      const internalDiff = Diff.Diff.DiffWrapper.charDiff(before, after, true /* cleanup diff */);
      const deletionRows = [createRow('', RowType.Deletion)];
      const insertionRows = [createRow('', RowType.Addition)];

      for (const token of internalDiff) {
        const text = token[1];
        const type = token[0];
        const className = type === Diff.Diff.Operation.Equal ? '' : 'inner-diff';
        const lines = text.split('\n');
        for (let i = 0; i < lines.length; i++) {
          if (i > 0 && type !== Diff.Diff.Operation.Insert) {
            deletionRows.push(createRow('', RowType.Deletion));
          }
          if (i > 0 && type !== Diff.Diff.Operation.Delete) {
            insertionRows.push(createRow('', RowType.Addition));
          }
          if (!lines[i]) {
            continue;
          }
          if (type !== Diff.Diff.Operation.Insert) {
            deletionRows[deletionRows.length - 1].tokens.push({text: lines[i], className});
          }
          if (type !== Diff.Diff.Operation.Delete) {
            insertionRows[insertionRows.length - 1].tokens.push({text: lines[i], className});
          }
        }
      }
      return deletionRows.concat(insertionRows);
    }

    function createRow(text: string, type: RowType): Row {
      if (type === RowType.Addition) {
        currentLineNumber++;
      }
      if (type === RowType.Deletion) {
        baselineLineNumber++;
      }
      if (type === RowType.Equal) {
        baselineLineNumber++;
        currentLineNumber++;
      }

      return {baselineLineNumber, currentLineNumber, tokens: text ? [{text, className: 'inner-diff'}] : [], type};
    }
  }

  _lineFormatter(lineNumber: number): string {
    const row = this._diffRows[lineNumber - 1];
    let showBaseNumber = row.type === RowType.Deletion;
    let showCurrentNumber = row.type === RowType.Addition;
    if (row.type === RowType.Equal) {
      showBaseNumber = true;
      showCurrentNumber = true;
    }
    const baseText = showBaseNumber ? String(row.baselineLineNumber) : '';
    const base = baseText.padStart(this._maxLineDigits, '\xA0');
    const currentText = showCurrentNumber ? String(row.currentLineNumber) : '';
    const current = currentText.padStart(this._maxLineDigits, '\xA0');
    return base + '\xA0' + current;
  }
}

export const enum RowType {
  Deletion = 'deletion',
  Addition = 'addition',
  Equal = 'equal',
  Spacer = 'spacer',
}

let diffUILocationRevealerInstance: DiffUILocationRevealer;
export class DiffUILocationRevealer implements Common.Revealer.Revealer {
  static instance(opts: {forceNew: boolean} = {forceNew: false}): DiffUILocationRevealer {
    const {forceNew} = opts;
    if (!diffUILocationRevealerInstance || forceNew) {
      diffUILocationRevealerInstance = new DiffUILocationRevealer();
    }

    return diffUILocationRevealerInstance;
  }

  async reveal(diffUILocation: Object, omitFocus?: boolean|undefined): Promise<void> {
    if (!(diffUILocation instanceof WorkspaceDiff.WorkspaceDiff.DiffUILocation)) {
      throw new Error('Internal error: not a diff ui location');
    }
    await UI.ViewManager.ViewManager.instance().showView('changes.changes');
    ChangesView.instance()._changesSidebar.selectUISourceCode(diffUILocation.uiSourceCode, omitFocus);
  }
}

export interface Token {
  text: string;
  className: string;
}
export interface Row {
  baselineLineNumber: number;
  currentLineNumber: number;
  tokens: Token[];
  type: RowType;
}
