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

export const enum ResizerType {
  WIDTH = 'width',
  HEIGHT = 'height',
  BIDIRECTION = 'bidirection',
}

export interface Draggable {
  type: ResizerType;
  initialWidth?: number;
  initialHeight?: number;
  update({width, height}: {width?: number, height?: number}): void;
}

export interface Delegate {
  getDraggable(x: number, y: number): Draggable|undefined;
}

const cursorByResizerType = new Map([
  [ResizerType.WIDTH, 'ew-resize'],
  [ResizerType.HEIGHT, 'ns-resize'],
  [ResizerType.BIDIRECTION, 'nwse-resize'],
]);

interface OriginInfo {
  coord: number;
  value: number;
}

export class DragResizeHandler {
  private document: Document;
  private delegate: Delegate;
  private originX?: OriginInfo;
  private originY?: OriginInfo;
  private boundMousemove: (event: MouseEvent) => void;
  private boundMousedown: (event: MouseEvent) => void;

  constructor(document: Document, delegate: Delegate) {
    this.document = document;
    this.delegate = delegate;
    this.boundMousemove = this.onMousemove.bind(this);
    this.boundMousedown = this.onMousedown.bind(this);
  }

  install() {
    this.document.body.addEventListener('mousemove', this.boundMousemove);
    this.document.body.addEventListener('mousedown', this.boundMousedown);
  }

  uninstall() {
    this.document.body.removeEventListener('mousemove', this.boundMousemove);
    this.document.body.removeEventListener('mousedown', this.boundMousedown);
  }

  /**
   * Updates the cursor style of the mouse is hovered over a resizeable area.
   */
  private onMousemove(event: MouseEvent) {
    const match = this.delegate.getDraggable(event.clientX, event.clientY);
    if (!match) {
      this.document.body.style.cursor = 'default';
      return;
    }
    this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default';
  }

  /**
   * Starts dragging
   */
  private onMousedown(event: MouseEvent) {
    const match = this.delegate.getDraggable(event.clientX, event.clientY);
    if (!match) {
      return;
    }

    const boundOnDrag = this.onDrag.bind(this, match);

    event.stopPropagation();
    event.preventDefault();

    if (match.initialWidth !== undefined &&
        (match.type === ResizerType.WIDTH || match.type === ResizerType.BIDIRECTION)) {
      this.originX = {
        coord: Math.round(event.clientX),
        value: match.initialWidth,
      };
    }

    if (match.initialHeight !== undefined &&
        (match.type === ResizerType.HEIGHT || match.type === ResizerType.BIDIRECTION)) {
      this.originY = {
        coord: Math.round(event.clientY),
        value: match.initialHeight,
      };
    }

    this.document.body.removeEventListener('mousemove', this.boundMousemove);
    this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default';

    const endDrag = (event: Event) => {
      event.stopPropagation();
      event.preventDefault();
      this.originX = undefined;
      this.originY = undefined;

      this.document.body.style.cursor = 'default';
      this.document.body.removeEventListener('mousemove', boundOnDrag);
      this.document.body.addEventListener('mousemove', this.boundMousemove);
    };

    this.document.body.addEventListener('mouseup', endDrag, {once: true});
    window.addEventListener('mouseout', endDrag, {once: true});

    this.document.body.addEventListener('mousemove', boundOnDrag);
  }

  /**
   * Computes the new value while the cursor is being dragged and calls InspectorOverlayHost with the new value.
   */
  private onDrag(match: Draggable, e: MouseEvent) {
    if (!this.originX && !this.originY) {
      return;
    }

    let width: number|undefined;
    let height: number|undefined;
    if (this.originX) {
      const delta = this.originX.coord - e.clientX;
      width = Math.round(this.originX.value - delta);
    }

    if (this.originY) {
      const delta = this.originY.coord - e.clientY;
      height = Math.round(this.originY.value - delta);
    }

    match.update({width, height});
  }
}
