/**
 * Copyright (c) 2017 The xterm.js authors. All rights reserved.
 * @license MIT
 */

import { ITerminal } from '../Types';
import { IMouseZoneManager, IMouseZone } from './Types';
import { Disposable } from '../common/Lifecycle';
import { addDisposableDomListener } from './Lifecycle';

const HOVER_DURATION = 500;

/**
 * The MouseZoneManager allows components to register zones within the terminal
 * that trigger hover and click callbacks.
 *
 * This class was intentionally made not so robust initially as the only case it
 * needed to support was single-line links which never overlap. Improvements can
 * be made in the future.
 */
export class MouseZoneManager extends Disposable implements IMouseZoneManager {
  private _zones: IMouseZone[] = [];

  private _areZonesActive: boolean = false;
  private _mouseMoveListener: (e: MouseEvent) => any;
  private _clickListener: (e: MouseEvent) => any;

  private _tooltipTimeout: number = null;
  private _currentZone: IMouseZone = null;
  private _lastHoverCoords: [number, number] = [null, null];

  constructor(
    private _terminal: ITerminal
  ) {
    super();

    this.register(addDisposableDomListener(this._terminal.element, 'mousedown', e => this._onMouseDown(e)));

    // These events are expensive, only listen to it when mouse zones are active
    this._mouseMoveListener = e => this._onMouseMove(e);
    this._clickListener = e => this._onClick(e);
  }

  public dispose(): void {
    super.dispose();
    this._deactivate();
  }

  public add(zone: IMouseZone): void {
    this._zones.push(zone);
    if (this._zones.length === 1) {
      this._activate();
    }
  }

  public clearAll(start?: number, end?: number): void {
    // Exit if there's nothing to clear
    if (this._zones.length === 0) {
      return;
    }

    // Clear all if start/end weren't set
    if (!end) {
      start = 0;
      end = this._terminal.rows - 1;
    }

    // Iterate through zones and clear them out if they're within the range
    for (let i = 0; i < this._zones.length; i++) {
      const zone = this._zones[i];
      if ((zone.y1 > start && zone.y1 <= end + 1) ||
          (zone.y2 > start && zone.y2 <= end + 1) ||
          (zone.y1 < start && zone.y2 > end + 1)) {
        if (this._currentZone && this._currentZone === zone) {
          this._currentZone.leaveCallback();
          this._currentZone = null;
        }
        this._zones.splice(i--, 1);
      }
    }

    // Deactivate the mouse zone manager if all the zones have been removed
    if (this._zones.length === 0) {
      this._deactivate();
    }
  }

  private _activate(): void {
    if (!this._areZonesActive) {
      this._areZonesActive = true;
      this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
      this._terminal.element.addEventListener('click', this._clickListener);
    }
  }

  private _deactivate(): void {
    if (this._areZonesActive) {
      this._areZonesActive = false;
      this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
      this._terminal.element.removeEventListener('click', this._clickListener);
    }
  }

  private _onMouseMove(e: MouseEvent): void {
    // TODO: Ideally this would only clear the hover state when the mouse moves
    // outside of the mouse zone
    if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) {
      this._onHover(e);
      // Record the current coordinates
      this._lastHoverCoords = [e.pageX, e.pageY];
    }
  }

  private _onHover(e: MouseEvent): void {
    const zone = this._findZoneEventAt(e);

    // Do nothing if the zone is the same
    if (zone === this._currentZone) {
      return;
    }

    // Fire the hover end callback and cancel any existing timer if a new zone
    // is being hovered
    if (this._currentZone) {
      this._currentZone.leaveCallback();
      this._currentZone = null;
      if (this._tooltipTimeout) {
        clearTimeout(this._tooltipTimeout);
      }
    }

    // Exit if there is not zone
    if (!zone) {
      return;
    }
    this._currentZone = zone;

    // Trigger the hover callback
    if (zone.hoverCallback) {
      zone.hoverCallback(e);
    }

    // Restart the tooltip timeout
    this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
  }

  private _onTooltip(e: MouseEvent): void {
    this._tooltipTimeout = null;
    const zone = this._findZoneEventAt(e);
    if (zone && zone.tooltipCallback) {
      zone.tooltipCallback(e);
    }
  }

  private _onMouseDown(e: MouseEvent): void {
    // Ignore the event if there are no zones active
    if (!this._areZonesActive) {
      return;
    }

    // Find the active zone, prevent event propagation if found to prevent other
    // components from handling the mouse event.
    const zone = this._findZoneEventAt(e);
    if (zone) {
      if (zone.willLinkActivate(e)) {
        e.preventDefault();
        e.stopImmediatePropagation();
      }
    }
  }

  private _onClick(e: MouseEvent): void {
    // Find the active zone and click it if found
    const zone = this._findZoneEventAt(e);
    if (zone) {
      zone.clickCallback(e);
      e.preventDefault();
      e.stopImmediatePropagation();
    }
  }

  private _findZoneEventAt(e: MouseEvent): IMouseZone {
    const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
    if (!coords) {
      return null;
    }
    const x = coords[0];
    const y = coords[1];
    for (let i = 0; i < this._zones.length; i++) {
      const zone = this._zones[i];
      if (zone.y1 === zone.y2) {
        // Single line link
        if (y === zone.y1 && x >= zone.x1 && x < zone.x2) {
          return zone;
        }
      } else {
        // Multi-line link
        if ((y === zone.y1 && x >= zone.x1) ||
            (y === zone.y2 && x < zone.x2) ||
            (y > zone.y1 && y < zone.y2)) {
          return zone;
        }
      }
    }
    return null;
  }
}

export class MouseZone implements IMouseZone {
  constructor(
    public x1: number,
    public y1: number,
    public x2: number,
    public y2: number,
    public clickCallback: (e: MouseEvent) => any,
    public hoverCallback: (e: MouseEvent) => any,
    public tooltipCallback: (e: MouseEvent) => any,
    public leaveCallback: () => void,
    public willLinkActivate: (e: MouseEvent) => boolean
  ) {
  }
}
