/**
 * @author   Ikaros Kappler
 * @date     2018-03-19
 * @modified 2018-04-28 Added the param 'wasDragged'.
 * @modified 2018-08-16 Added the param 'dragAmount'.
 * @modified 2018-08-27 Added the param 'element'.
 * @modified 2018-11-11 Changed the scope from a simple global var to a member of window/_context.
 * @modified 2018-11-19 Renamed the 'mousedown' function to 'down' and the 'mouseup' function to 'up'.
 * @modified 2018-11-28 Added the 'wheel' listener.
 * @modified 2018-12-09 Cleaned up some code.
 * @modified 2019-02-10 Cleaned up some more code.
 * @modified 2020-03-25 Ported this class from vanilla-JS to Typescript.
 * @modified 2020-04-08 Fixed the click event (internally fired a 'mouseup' event) (1.0.10)
 * @modified 2020-04-08 Added the optional 'name' property. (1.0.11)
 * @modified 2020-04-08 The new version always installs internal listenrs to track drag events even
 *                      if there is no external drag listener installed (1.1.0).
 * @modified 2020-10-04 Added extended JSDoc comments.
 * @modified 2020-11-25 Added the `isTouchEvent` param.
 * @modified 2021-01-10 The mouse handler is now also working with SVGElements.
 * @modified 2022-08-16 Fixed a bug in the mouse button detection.
 * @version  1.2.1
 *
 * @file MouseHandler
 * @public
 **/

import { XYCoords } from "./interfaces";

export interface XMouseParams {
  button: number;
  element: HTMLElement | SVGElement;
  isTouchEvent: boolean;
  name: string;
  pos: { x: number; y: number };
  leftButton: boolean;
  middleButton: boolean;
  rightButton: boolean;
  mouseDownPos: { x: number; y: number };
  draggedFrom: { x: number; y: number };
  wasDragged: boolean;
  dragAmount: { x: number; y: number };
}

export class XMouseEvent extends MouseEvent {
  params: XMouseParams;
}
export class XWheelEvent extends WheelEvent {
  params: XMouseParams;
}

export type XMouseCallback = (e: XMouseEvent) => void;

export type XWheelCallback = (e: XWheelEvent) => void;

/**
 * @classdesc A simple mouse handler for demos.
 * Use to avoid load massive libraries like jQuery.
 *
 * @requires XYCoords
 */
export class MouseHandler {
  private name: string | undefined;
  private element: HTMLElement | SVGElement;
  private mouseDownPos: { x: number; y: number } | undefined = undefined;
  private mouseDragPos: { x: number; y: number } | undefined = undefined;
  // TODO: cc
  // private mousePos       : { x:number, y:number }|undefined = undefined;
  private mouseButton: number = -1;
  private listeners: Record<string, XMouseCallback> = {};
  private installed: Record<string, boolean> = {};
  private handlers: Record<string, XMouseCallback> = {};

  /**
   * The constructor.
   *
   * Pass the DOM element you want to receive mouse events from.
   *
   * Usage
   * =====
   * @example
   *   // Javascript
   *   new MouseHandler( document.getElementById('mycanvas') )
   *	    .drag( function(e) {
   *		console.log( 'Mouse dragged: ' + JSON.stringify(e) );
   *		if( e.params.leftMouse ) ;
   *		else if( e.params.rightMouse ) ;
   *	    } )
   *	    .move( function(e) {
   *		console.log( 'Mouse moved: ' + JSON.stringify(e.params) );
   *	    } )
   *          .up( function(e) {
   *              console.log( 'Mouse up. Was dragged?', e.params.wasDragged );
   *          } )
   *          .down( function(e) {
   *              console.log( 'Mouse down.' );
   *          } )
   *          .click( function(e) {
   *              console.log( 'Click.' );
   *          } )
   *          .wheel( function(e) {
   *              console.log( 'Wheel. delta='+e.deltaY );
   *          } )
   *
   * @example
   *   // Typescript
   *   new MouseHandler( document.getElementById('mycanvas') )
   *	    .drag( (e:XMouseEvent) => {
   *		console.log( 'Mouse dragged: ' + JSON.stringify(e) );
   *		if( e.params.leftMouse ) ;
   *		else if( e.params.rightMouse ) ;
   *	    } )
   *	    .move( (e:XMouseEvent) => {
   *		console.log( 'Mouse moved: ' + JSON.stringify(e.params) );
   *	    } )
   *          .up( (e:XMouseEvent) => {
   *              console.log( 'Mouse up. Was dragged?', e.params.wasDragged );
   *          } )
   *          .down( (e:XMouseEvent) => {
   *              console.log( 'Mouse down.' );
   *          } )
   *          .click( (e:XMouseEvent) => {
   *              console.log( 'Click.' );
   *          } )
   *          .wheel( (e:XWheelEvent) => {
   *              console.log( 'Wheel. delta='+e.deltaY );
   *          } )
   *
   * @constructor
   * @instance
   * @memberof MouseHandler
   * @param {HTMLElement} element
   **/
  constructor(element: HTMLElement | SVGElement, name?: string) {
    // +----------------------------------------------------------------------
    // | Some private vars to store the current mouse/position/button state.
    // +-------------------------------------------------
    this.name = name;
    this.element = element;
    this.mouseDownPos = undefined;
    this.mouseDragPos = undefined;
    // this.mousePos     = null;
    this.mouseButton = -1;
    this.listeners = {};
    this.installed = {};
    this.handlers = {};

    // +----------------------------------------------------------------------
    // | Define the internal event handlers.
    // |
    // | They will dispatch the modified event (relative mouse position,
    // | drag offset, ...) to the callbacks.
    // +-------------------------------------------------
    const _self: MouseHandler = this;
    this.handlers["mousemove"] = (e: MouseEvent) => {
      if (_self.listeners.mousemove) _self.listeners.mousemove(_self.mkParams(e, "mousemove"));
      if (_self.mouseDragPos && _self.listeners.drag) _self.listeners.drag(_self.mkParams(e, "drag"));
      if (_self.mouseDownPos) _self.mouseDragPos = _self.relPos(e);
    };
    this.handlers["mouseup"] = (e: MouseEvent) => {
      if (_self.listeners.mouseup) _self.listeners.mouseup(_self.mkParams(e, "mouseup"));
      _self.mouseDragPos = undefined;
      _self.mouseDownPos = undefined;
      _self.mouseButton = -1;
    };
    this.handlers["mousedown"] = (e: MouseEvent) => {
      _self.mouseDragPos = _self.relPos(e);
      _self.mouseDownPos = _self.relPos(e);
      _self.mouseButton = e.button;
      if (_self.listeners.mousedown) _self.listeners.mousedown(_self.mkParams(e, "mousedown"));
    };
    this.handlers["click"] = (e: MouseEvent) => {
      if (_self.listeners.click) _self.listeners.click(_self.mkParams(e, "click"));
    };
    this.handlers["wheel"] = (e: MouseEvent) => {
      if (_self.listeners.wheel) _self.listeners.wheel(_self.mkParams(e, "wheel"));
    };

    this.element.addEventListener("mousemove", this.handlers["mousemove"] as EventListener);
    this.element.addEventListener("mouseup", this.handlers["mouseup"] as EventListener);
    this.element.addEventListener("mousedown", this.handlers["mousedown"] as EventListener);
    this.element.addEventListener("click", this.handlers["click"] as EventListener);
    this.element.addEventListener("wheel", this.handlers["wheel"] as EventListener);
  }

  /**
   * Get relative position from the given MouseEvent.
   *
   * @name relPos
   * @memberof MouseHandler
   * @instance
   * @private
   * @param {MouseEvent} e - The mouse event to get the relative position for.
   * @return {XYCoords} The relative mouse coordinates.
   */
  private relPos(e: MouseEvent): XYCoords {
    return { x: e.offsetX, y: e.offsetY };
  }

  /**
   * Build the extended event params.
   *
   * @name mkParams
   * @memberof MouseHandler
   * @instance
   * @private
   * @param {MouseEvent} event - The mouse event to get the relative position for.
   * @param {string} eventName - The name of the firing event.
   * @return {XMouseEvent}
   */
  private mkParams(event: MouseEvent, eventName: string): XMouseEvent {
    const rel: { x: number; y: number } = this.relPos(event);
    const xEvent: XMouseEvent = event as unknown as XMouseEvent;
    xEvent.params = {
      element: this.element,
      name: eventName,
      isTouchEvent: false,
      pos: rel,
      button: event.button, // this.mouseButton,
      leftButton: event.button === 0, // this.mouseButton === 0,
      middleButton: event.button === 1, // this.mouseButton === 1,
      rightButton: event.button === 2, // this.mouseButton === 2,
      mouseDownPos: this.mouseDownPos ?? { x: NaN, y: NaN },
      draggedFrom: this.mouseDragPos ?? { x: NaN, y: NaN },
      wasDragged: this.mouseDownPos != null && (this.mouseDownPos.x != rel.x || this.mouseDownPos.y != rel.y),
      dragAmount: this.mouseDragPos != null ? { x: rel.x - this.mouseDragPos.x, y: rel.y - this.mouseDragPos.y } : { x: 0, y: 0 }
    };
    return xEvent;
  }

  /**
   * Install a new listener.
   * Please note that this mouse handler can only handle one listener per event type.
   *
   * @name listenFor
   * @memberof MouseHandler
   * @instance
   * @private
   * @param {string} eventName - The name of the firing event to listen for.
   * @return {void}
   */
  private listenFor(eventName: string) {
    if (this.installed[eventName]) return;
    // In the new version 1.1.0 has all internal listeners installed by default.
    this.installed[eventName] = true;
  }

  /**
   * Un-install a new listener.
   *
   * @name listenFor
   * @memberof MouseHandler
   * @instance
   * @private
   * @param {string} eventName - The name of the firing event to unlisten for.
   * @return {void}
   */
  private unlistenFor(eventName: string) {
    if (!this.installed[eventName]) return;
    // In the new version 1.1.0 has all internal listeners installed by default.
    delete this.installed[eventName];
  }

  /**
   * Installer function to listen for a specific event: mouse-drag.
   * Pass your callbacks here.
   *
   * Note: this support chaining.
   *
   * @name drag
   * @memberof MouseHandler
   * @instance
   * @param {XMouseCallback} callback - The drag-callback to listen for.
   * @return {MouseHandler} this
   */
  drag(callback: XMouseCallback): MouseHandler {
    if (this.listeners.drag) this.throwAlreadyInstalled("drag");
    this.listeners.drag = callback;
    this.listenFor("mousedown");
    this.listenFor("mousemove");
    this.listenFor("mouseup");
    return this;
  }

  /**
   * Installer function to listen for a specific event: mouse-move.
   * Pass your callbacks here.
   *
   * Note: this support chaining.
   *
   * @name move
   * @memberof MouseHandler
   * @instance
   * @param {XMouseCallback} callback - The move-callback to listen for.
   * @return {MouseHandler} this
   */
  move(callback: (e: XMouseEvent) => void): MouseHandler {
    if (this.listeners.mousemove) this.throwAlreadyInstalled("mousemove");
    this.listenFor("mousemove");
    this.listeners.mousemove = callback;
    return this;
  }

  /**
   * Installer function to listen for a specific event: mouse-up.
   * Pass your callbacks here.
   *
   * Note: this support chaining.
   *
   * @name up
   * @memberof MouseHandler
   * @instance
   * @param {XMouseCallback} callback - The up-callback to listen for.
   * @return {MouseHandler} this
   */
  up(callback: (e: XMouseEvent) => void): MouseHandler {
    if (this.listeners.mouseup) this.throwAlreadyInstalled("mouseup");
    this.listenFor("mouseup");
    this.listeners.mouseup = callback;
    return this;
  }

  /**
   * Installer function to listen for a specific event: mouse-down.
   * Pass your callbacks here.
   *
   * Note: this support chaining.
   *
   * @name down
   * @memberof MouseHandler
   * @instance
   * @param {XMouseCallback} callback - The down-callback to listen for.
   * @return {MouseHandler} this
   */
  down(callback: (e: XMouseEvent) => void): MouseHandler {
    if (this.listeners.mousedown) this.throwAlreadyInstalled("mousedown");
    this.listenFor("mousedown");
    this.listeners.mousedown = callback;
    return this;
  }

  /**
   * Installer function to listen for a specific event: mouse-click.
   * Pass your callbacks here.
   *
   * Note: this support chaining.
   *
   * @name click
   * @memberof MouseHandler
   * @instance
   * @param {XMouseCallback} callback - The click-callback to listen for.
   * @return {MouseHandler} this
   */
  click(callback: (e: XMouseEvent) => void): MouseHandler {
    if (this.listeners.click) this.throwAlreadyInstalled("click");
    this.listenFor("click");
    this.listeners.click = callback;
    return this;
  }

  /**
   * Installer function to listen for a specific event: mouse-wheel.
   * Pass your callbacks here.
   *
   * Note: this support chaining.
   *
   * @name wheel
   * @memberof MouseHandler
   * @instance
   * @param {XWheelCallback} callback - The wheel-callback to listen for.
   * @return {MouseHandler} this
   */
  wheel(callback: (e: XWheelEvent) => void): MouseHandler {
    if (this.listeners.wheel) this.throwAlreadyInstalled("wheel");
    this.listenFor("wheel");
    this.listeners.wheel = callback as XMouseCallback;
    return this;
  }

  /**
   * An internal function to throw events.
   *
   * @name throwAlreadyInstalled
   * @memberof MouseHandler
   * @instance
   * @private
   * @param {string} name - The name of the event.
   * @return {void}
   */
  private throwAlreadyInstalled(name: string) {
    throw `This MouseHandler already has a '${name}' callback. To keep the code simple there is only room for one.`;
  }

  /**
   * Call this when your work is done.
   *
   * The function will un-install all event listeners.
   *
   * @name destroy
   * @memberof MouseHandler
   * @instance
   * @private
   * @return {void}
   */
  destroy() {
    this.unlistenFor("mousedown");
    this.unlistenFor("mousemove");
    this.unlistenFor("moseup");
    this.unlistenFor("click");
    this.unlistenFor("wheel");

    this.element.removeEventListener("mousemove", this.handlers["mousemove"] as EventListener);
    this.element.removeEventListener("mouseup", this.handlers["mousedown"] as EventListener);
    this.element.removeEventListener("mousedown", this.handlers["mousedown"] as EventListener);
    this.element.removeEventListener("click", this.handlers["click"] as EventListener);
    this.element.removeEventListener("wheel", this.handlers["wheel"] as EventListener);
  }
}
