// Copyright 2014 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 i18n from '../i18n/i18n.js';
import * as Platform from '../platform/platform.js';
import * as UI from '../ui/ui.js';

export const UIStrings = {
  /**
  *@description Tooltip text that appears when hovering over largeicon pan button in Transform Controller of the Layers panel
  */
  panModeX: 'Pan mode (X)',
  /**
  *@description Tooltip text that appears when hovering over largeicon rotate button in Transform Controller of the Layers panel
  */
  rotateModeV: 'Rotate mode (V)',
  /**
  *@description Tooltip text that appears when hovering over the largeicon center button in the Transform Controller of the Layers panel
  */
  resetTransform: 'Reset transform (0)',
};
const str_ = i18n.i18n.registerUIStrings('layer_viewer/TransformController.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class TransformController extends Common.ObjectWrapper.ObjectWrapper {
  _mode!: Modes;
  _scale: number;
  _offsetX: number;
  _offsetY: number;
  _rotateX: number;
  _rotateY: number;
  _oldRotateX: number;
  _oldRotateY: number;
  _originX: number;
  _originY: number;
  element: HTMLElement;
  _minScale: number;
  _maxScale: number;
  _controlPanelToolbar: UI.Toolbar.Toolbar;
  _modeButtons: {[x: string]: UI.Toolbar.ToolbarToggle};
  constructor(element: HTMLElement, disableRotate?: boolean) {
    super();
    this._scale = 1;
    this._offsetX = 0;
    this._offsetY = 0;
    this._rotateX = 0;
    this._rotateY = 0;
    this._oldRotateX = 0;
    this._oldRotateY = 0;
    this._originX = 0;
    this._originY = 0;
    this.element = element;
    this._registerShortcuts();
    UI.UIUtils.installDragHandle(
        element, this._onDragStart.bind(this), this._onDrag.bind(this), this._onDragEnd.bind(this), 'move', null);
    element.addEventListener('wheel', this._onMouseWheel.bind(this), false);
    this._minScale = 0;
    this._maxScale = Infinity;

    this._controlPanelToolbar = new UI.Toolbar.Toolbar('transform-control-panel');

    this._modeButtons = {};
    if (!disableRotate) {
      const panModeButton = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.panModeX), 'largeicon-pan');
      panModeButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._setMode.bind(this, Modes.Pan));
      this._modeButtons[Modes.Pan] = panModeButton;
      this._controlPanelToolbar.appendToolbarItem(panModeButton);
      const rotateModeButton = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.rotateModeV), 'largeicon-rotate');
      rotateModeButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._setMode.bind(this, Modes.Rotate));
      this._modeButtons[Modes.Rotate] = rotateModeButton;
      this._controlPanelToolbar.appendToolbarItem(rotateModeButton);
    }
    this._setMode(Modes.Pan);

    const resetButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.resetTransform), 'largeicon-center');
    resetButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.resetAndNotify.bind(this, undefined));
    this._controlPanelToolbar.appendToolbarItem(resetButton);

    this._reset();
  }

  toolbar(): UI.Toolbar.Toolbar {
    return this._controlPanelToolbar;
  }

  _registerShortcuts(): void {
    const zoomFactor = 1.1;
    UI.ShortcutRegistry.ShortcutRegistry.instance().addShortcutListener(this.element, {
      // TODO: Remove next line once crbug.com/1177242 is solved.
      // eslint-disable-next-line @typescript-eslint/space-before-function-paren
      'layers.reset-view': async(): Promise<true> => {
        this.resetAndNotify();
        return true;
      },
      // TODO: Remove next line once crbug.com/1177242 is solved.
      // eslint-disable-next-line @typescript-eslint/space-before-function-paren
      'layers.pan-mode': async(): Promise<true> => {
        this._setMode(Modes.Pan);
        return true;
      },
      // TODO: Remove next line once crbug.com/1177242 is solved.
      // eslint-disable-next-line @typescript-eslint/space-before-function-paren
      'layers.rotate-mode': async(): Promise<true> => {
        this._setMode(Modes.Rotate);
        return true;
      },
      'layers.zoom-in': this._onKeyboardZoom.bind(this, zoomFactor),
      'layers.zoom-out': this._onKeyboardZoom.bind(this, 1 / zoomFactor),
      'layers.up': this._onKeyboardPanOrRotate.bind(this, 0, -1),
      'layers.down': this._onKeyboardPanOrRotate.bind(this, 0, 1),
      'layers.left': this._onKeyboardPanOrRotate.bind(this, -1, 0),
      'layers.right': this._onKeyboardPanOrRotate.bind(this, 1, 0),
    });
  }

  _postChangeEvent(): void {
    this.dispatchEventToListeners(Events.TransformChanged);
  }

  _reset(): void {
    this._scale = 1;
    this._offsetX = 0;
    this._offsetY = 0;
    this._rotateX = 0;
    this._rotateY = 0;
  }

  _setMode(mode: Modes): void {
    if (this._mode === mode) {
      return;
    }
    this._mode = mode;
    this._updateModeButtons();
  }

  _updateModeButtons(): void {
    for (const mode in this._modeButtons) {
      this._modeButtons[mode].setToggled(mode === this._mode);
    }
  }

  resetAndNotify(event?: Event): void {
    this._reset();
    this._postChangeEvent();
    if (event) {
      event.preventDefault();
    }
    this.element.focus();
  }

  setScaleConstraints(minScale: number, maxScale: number): void {
    this._minScale = minScale;
    this._maxScale = maxScale;
    this._scale = Platform.NumberUtilities.clamp(this._scale, minScale, maxScale);
  }

  clampOffsets(minX: number, maxX: number, minY: number, maxY: number): void {
    this._offsetX = Platform.NumberUtilities.clamp(this._offsetX, minX, maxX);
    this._offsetY = Platform.NumberUtilities.clamp(this._offsetY, minY, maxY);
  }

  scale(): number {
    return this._scale;
  }

  offsetX(): number {
    return this._offsetX;
  }

  offsetY(): number {
    return this._offsetY;
  }

  rotateX(): number {
    return this._rotateX;
  }

  rotateY(): number {
    return this._rotateY;
  }

  _onScale(scaleFactor: number, x: number, y: number): void {
    scaleFactor =
        Platform.NumberUtilities.clamp(this._scale * scaleFactor, this._minScale, this._maxScale) / this._scale;
    this._scale *= scaleFactor;
    this._offsetX -= (x - this._offsetX) * (scaleFactor - 1);
    this._offsetY -= (y - this._offsetY) * (scaleFactor - 1);
    this._postChangeEvent();
  }

  _onPan(offsetX: number, offsetY: number): void {
    this._offsetX += offsetX;
    this._offsetY += offsetY;
    this._postChangeEvent();
  }

  _onRotate(rotateX: number, rotateY: number): void {
    this._rotateX = rotateX;
    this._rotateY = rotateY;
    this._postChangeEvent();
  }

  async _onKeyboardZoom(zoomFactor: number): Promise<boolean> {
    this._onScale(zoomFactor, this.element.clientWidth / 2, this.element.clientHeight / 2);
    return true;
  }

  async _onKeyboardPanOrRotate(xMultiplier: number, yMultiplier: number): Promise<boolean> {
    const panStepInPixels = 6;
    const rotateStepInDegrees = 5;

    if (this._mode === Modes.Rotate) {
      // Sic! _onRotate treats X and Y as "rotate around X" and "rotate around Y", so swap X/Y multiplers.
      this._onRotate(
          this._rotateX + yMultiplier * rotateStepInDegrees, this._rotateY + xMultiplier * rotateStepInDegrees);
    } else {
      this._onPan(xMultiplier * panStepInPixels, yMultiplier * panStepInPixels);
    }
    return true;
  }

  _onMouseWheel(event: Event): void {
    /** @const */
    const zoomFactor = 1.1;
    /** @const */
    const wheelZoomSpeed = 1 / 53;
    const mouseEvent = event as WheelEvent;
    const scaleFactor = Math.pow(zoomFactor, -mouseEvent.deltaY * wheelZoomSpeed);
    this._onScale(
        scaleFactor, mouseEvent.clientX - this.element.totalOffsetLeft(),
        mouseEvent.clientY - this.element.totalOffsetTop());
  }

  _onDrag(event: Event): void {
    const {clientX, clientY} = event as MouseEvent;
    if (this._mode === Modes.Rotate) {
      this._onRotate(
          this._oldRotateX + (this._originY - clientY) / this.element.clientHeight * 180,
          this._oldRotateY - (this._originX - clientX) / this.element.clientWidth * 180);
    } else {
      this._onPan(clientX - this._originX, clientY - this._originY);
      this._originX = clientX;
      this._originY = clientY;
    }
  }

  _onDragStart(event: MouseEvent): boolean {
    this.element.focus();
    this._originX = event.clientX;
    this._originY = event.clientY;
    this._oldRotateX = this._rotateX;
    this._oldRotateY = this._rotateY;
    return true;
  }

  _onDragEnd(): void {
    this._originX = 0;
    this._originY = 0;
    this._oldRotateX = 0;
    this._oldRotateY = 0;
  }
}

// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
  TransformChanged = 'TransformChanged',
}

export const enum Modes {
  Pan = 'Pan',
  Rotate = 'Rotate',
}
