/**
 * @license
 * Copyright 2021 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

// Former goog.module ID: Blockly.browserEvents

import * as Touch from './touch.js';
import * as userAgent from './utils/useragent.js';

/**
 * Blockly opaque event data used to unbind events when using
 * `bind` and `conditionalBind`.
 */
export type Data = [EventTarget, string, (e: Event) => void][];

/**
 * The multiplier for scroll wheel deltas using the line delta mode.
 * See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode
 * for more information on deltaMode.
 */
const LINE_MODE_MULTIPLIER = 40;

/**
 * The multiplier for scroll wheel deltas using the page delta mode.
 * See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode
 * for more information on deltaMode.
 */
const PAGE_MODE_MULTIPLIER = 125;

/**
 * Bind an event handler that can be ignored if it is not part of the active
 * touch stream.
 * Use this for events that either start or continue a multi-part gesture (e.g.
 * mousedown or mousemove, which may be part of a drag or click).
 *
 * @param node Node upon which to listen.
 * @param name Event name to listen to (e.g. 'mousedown').
 * @param thisObject The value of 'this' in the function.
 * @param func Function to call when event is triggered.
 * @param opt_noCaptureIdentifier True if triggering on this event should not
 *     block execution of other event handlers on this touch or other
 *     simultaneous touches.  False by default.
 * @returns Opaque data that can be passed to unbindEvent_.
 */
export function conditionalBind(
  node: EventTarget,
  name: string,
  thisObject: Object | null,
  func: Function,
  opt_noCaptureIdentifier?: boolean,
): Data {
  /**
   *
   * @param e
   */
  function wrapFunc(e: Event) {
    const captureIdentifier = !opt_noCaptureIdentifier;

    if (!(captureIdentifier && !Touch.shouldHandleEvent(e))) {
      if (thisObject) {
        func.call(thisObject, e);
      } else {
        func(e);
      }
    }
  }

  const bindData: Data = [];
  if (name in Touch.TOUCH_MAP) {
    for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
      const type = Touch.TOUCH_MAP[name][i];
      node.addEventListener(type, wrapFunc, false);
      bindData.push([node, type, wrapFunc]);
    }
  } else {
    node.addEventListener(name, wrapFunc, false);
    bindData.push([node, name, wrapFunc]);
  }
  return bindData;
}

/**
 * Bind an event handler that should be called regardless of whether it is part
 * of the active touch stream.
 * Use this for events that are not part of a multi-part gesture (e.g.
 * mouseover for tooltips).
 *
 * @param node Node upon which to listen.
 * @param name Event name to listen to (e.g. 'mousedown').
 * @param thisObject The value of 'this' in the function.
 * @param func Function to call when event is triggered.
 * @returns Opaque data that can be passed to unbindEvent_.
 */
export function bind(
  node: EventTarget,
  name: string,
  thisObject: Object | null,
  func: Function,
): Data {
  /**
   *
   * @param e
   */
  function wrapFunc(e: Event) {
    if (thisObject) {
      func.call(thisObject, e);
    } else {
      func(e);
    }
  }

  const bindData: Data = [];
  if (name in Touch.TOUCH_MAP) {
    for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
      const type = Touch.TOUCH_MAP[name][i];
      node.addEventListener(type, wrapFunc, false);
      bindData.push([node, type, wrapFunc]);
    }
  } else {
    node.addEventListener(name, wrapFunc, false);
    bindData.push([node, name, wrapFunc]);
  }
  return bindData;
}

/**
 * Unbind one or more events event from a function call.
 *
 * @param bindData Opaque data from bindEvent_.
 *     This list is emptied during the course of calling this function.
 * @returns The function call.
 */
export function unbind(bindData: Data): (e: Event) => void {
  // Accessing an element of the last property of the array is unsafe if the
  // bindData is an empty array. But that should never happen because developers
  // should only pass Data from bind or conditionalBind.
  const callback = bindData[bindData.length - 1][2];
  while (bindData.length) {
    const [node, name, func] = bindData.pop()!;
    node.removeEventListener(name, func, false);
  }
  return callback;
}

/**
 * Returns true if this event is targeting a text input widget?
 *
 * @param e An event.
 * @returns True if text input.
 */
export function isTargetInput(e: Event): boolean {
  if (e.target instanceof HTMLElement) {
    if (
      e.target.isContentEditable ||
      e.target.getAttribute('data-is-text-input') === 'true'
    ) {
      return true;
    }

    if (e.target instanceof HTMLInputElement) {
      const target = e.target;
      return (
        target.type === 'text' ||
        target.type === 'number' ||
        target.type === 'email' ||
        target.type === 'password' ||
        target.type === 'search' ||
        target.type === 'tel' ||
        target.type === 'url'
      );
    }

    if (e.target instanceof HTMLTextAreaElement) {
      return true;
    }
  }

  return false;
}

/**
 * Returns true this event is a right-click.
 *
 * @param e Mouse event.
 * @returns True if right-click.
 */
export function isRightButton(e: MouseEvent): boolean {
  if (e.ctrlKey && userAgent.MAC) {
    // Control-clicking on Mac OS X is treated as a right-click.
    // WebKit on Mac OS X fails to change button to 2 (but Gecko does).
    return true;
  }
  return e.button === 2;
}

/**
 * Returns the converted coordinates of the given mouse event.
 * The origin (0,0) is the top-left corner of the Blockly SVG.
 *
 * @param e Mouse event.
 * @param svg SVG element.
 * @param matrix Inverted screen CTM to use.
 * @returns Object with .x and .y properties.
 */
export function mouseToSvg(
  e: MouseEvent,
  svg: SVGSVGElement,
  matrix: SVGMatrix | null,
): SVGPoint {
  const svgPoint = svg.createSVGPoint();
  svgPoint.x = e.clientX;
  svgPoint.y = e.clientY;

  if (!matrix) {
    matrix = svg.getScreenCTM()!.inverse();
  }
  return svgPoint.matrixTransform(matrix);
}

/**
 * Returns the scroll delta of a mouse event in pixel units.
 *
 * @param e Mouse event.
 * @returns Scroll delta object with .x and .y properties.
 */
export function getScrollDeltaPixels(e: WheelEvent): {x: number; y: number} {
  switch (e.deltaMode) {
    case 0x00: // Pixel mode.
    default:
      return {x: e.deltaX, y: e.deltaY};
    case 0x01: // Line mode.
      return {
        x: e.deltaX * LINE_MODE_MULTIPLIER,
        y: e.deltaY * LINE_MODE_MULTIPLIER,
      };
    case 0x02: // Page mode.
      return {
        x: e.deltaX * PAGE_MODE_MULTIPLIER,
        y: e.deltaY * PAGE_MODE_MULTIPLIER,
      };
  }
}