"use strict";

exports.__esModule = true;
var _element = require("../../../helpers/dom/element");
var _function = require("../../../helpers/function");
var _feature = require("../../../helpers/feature");
var _browser = require("../../../helpers/browser");
var _mixed = require("../../../helpers/mixed");
const privatePool = new WeakMap();

/**
 * @class Event
 */
class Event {
  /**
   * @param {FacadeGetter} facadeGetter Gets an instance facade.
   * @param {DomBindings} domBindings Bindings into dom.
   * @param {Settings} wtSettings The walkontable settings.
   * @param {EventManager} eventManager The walkontable event manager.
   * @param {Table} wtTable The table.
   * @param {SelectionManager} selectionManager Selections.
   * @param {Event} [parent=null] The main Event instance.
   */
  constructor(facadeGetter, domBindings, wtSettings, eventManager, wtTable, selectionManager) {
    let parent = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : null;
    this.wtSettings = wtSettings;
    this.domBindings = domBindings;
    this.wtTable = wtTable;
    this.selectionManager = selectionManager;
    this.parent = parent;

    /**
     * Instance of {@link EventManager}.
     *
     * @private
     * @type {EventManager}
     */
    this.eventManager = eventManager;

    /**
     * Should be use only for passing face called external origin methods, like registered event listeners.
     * It provides backward compatibility by getting instance facade.
     *
     * @todo Consider about removing this from Event class, because it make relationship into facade (implicit circular
     *   dependency).
     * @todo Con. Maybe passing listener caller as an ioc from faced resolves this issue. To rethink later.
     *
     * @type {FacadeGetter}
     * @private
     */
    this.facadeGetter = facadeGetter;
    privatePool.set(this, {
      selectedCellBeforeTouchEnd: void 0,
      dblClickTimeout: [null, null],
      dblClickOrigin: [null, null]
    });
    this.registerEvents();
  }

  /**
   * Adds listeners for mouse and touch events.
   *
   * @private
   */
  registerEvents() {
    this.eventManager.addEventListener(this.wtTable.holder, 'contextmenu', event => this.onContextMenu(event));
    this.eventManager.addEventListener(this.wtTable.TABLE, 'mouseover', event => this.onMouseOver(event));
    this.eventManager.addEventListener(this.wtTable.TABLE, 'mouseout', event => this.onMouseOut(event));
    const initTouchEvents = () => {
      this.eventManager.addEventListener(this.wtTable.holder, 'touchstart', event => this.onTouchStart(event));
      this.eventManager.addEventListener(this.wtTable.holder, 'touchend', event => this.onTouchEnd(event));
      if (!this.momentumScrolling) {
        this.momentumScrolling = {};
      }
      this.eventManager.addEventListener(this.wtTable.holder, 'scroll', () => {
        clearTimeout(this.momentumScrolling._timeout);
        if (!this.momentumScrolling.ongoing) {
          this.wtSettings.getSetting('onBeforeTouchScroll');
        }
        this.momentumScrolling.ongoing = true;
        this.momentumScrolling._timeout = setTimeout(() => {
          if (!this.touchApplied) {
            this.momentumScrolling.ongoing = false;
            this.wtSettings.getSetting('onAfterMomentumScroll');
          }
        }, 200);
      });
    };
    const initMouseEvents = () => {
      this.eventManager.addEventListener(this.wtTable.holder, 'mouseup', event => this.onMouseUp(event));
      this.eventManager.addEventListener(this.wtTable.holder, 'mousedown', event => this.onMouseDown(event));
    };
    if ((0, _browser.isMobileBrowser)()) {
      initTouchEvents();
    } else {
      // PC like devices which support both methods (touchscreen and ability to plug-in mouse).
      if ((0, _feature.isTouchSupported)()) {
        initTouchEvents();
      }
      initMouseEvents();
    }
  }

  /**
   * Checks if an element is already selected.
   *
   * @private
   * @param {Element} touchTarget An element to check.
   * @returns {boolean}
   */
  selectedCellWasTouched(touchTarget) {
    const priv = privatePool.get(this);
    const cellUnderFinger = this.parentCell(touchTarget);
    const coordsOfCellUnderFinger = cellUnderFinger.coords;
    if (priv.selectedCellBeforeTouchEnd && coordsOfCellUnderFinger) {
      const [rowTouched, rowSelected] = [coordsOfCellUnderFinger.row, priv.selectedCellBeforeTouchEnd.from.row];
      const [colTouched, colSelected] = [coordsOfCellUnderFinger.col, priv.selectedCellBeforeTouchEnd.from.col];
      return rowTouched === rowSelected && colTouched === colSelected;
    }
    return false;
  }

  /**
   * Gets closest TD or TH element.
   *
   * @private
   * @param {Element} elem An element from the traversing starts.
   * @returns {object} Contains coordinates and reference to TD or TH if it exists. Otherwise it's empty object.
   */
  parentCell(elem) {
    const cell = {};
    const TABLE = this.wtTable.TABLE;
    const TD = (0, _element.closestDown)(elem, ['TD', 'TH'], TABLE);
    if (TD) {
      cell.coords = this.wtTable.getCoords(TD);
      cell.TD = TD;
    } else if ((0, _element.hasClass)(elem, 'wtBorder') && (0, _element.hasClass)(elem, 'current')) {
      cell.coords = this.selectionManager.getFocusSelection().cellRange.highlight;
      cell.TD = this.wtTable.getCell(cell.coords);
    } else if ((0, _element.hasClass)(elem, 'wtBorder') && (0, _element.hasClass)(elem, 'area')) {
      if (this.selectionManager.getAreaSelection().cellRange) {
        cell.coords = this.selectionManager.getAreaSelection().cellRange.to;
        cell.TD = this.wtTable.getCell(cell.coords);
      }
    }
    return cell;
  }

  /**
   * OnMouseDown callback.
   *
   * @private
   * @param {MouseEvent} event The mouse event object.
   */
  onMouseDown(event) {
    const priv = privatePool.get(this);
    const activeElement = this.domBindings.rootDocument.activeElement;
    const getParentNode = (0, _function.partial)(_element.getParent, event.target);
    const realTarget = event.target;

    // ignore non-TD focusable elements from mouse down processing
    // (https://github.com/handsontable/handsontable/issues/3555)
    if (!['TD', 'TH'].includes(activeElement.nodeName) && (realTarget === activeElement || getParentNode(0) === activeElement || getParentNode(1) === activeElement)) {
      return;
    }
    const cell = this.parentCell(realTarget);
    if ((0, _element.hasClass)(realTarget, 'corner')) {
      this.wtSettings.getSetting('onCellCornerMouseDown', event, realTarget);
    } else if (cell.TD && this.wtSettings.has('onCellMouseDown')) {
      this.callListener('onCellMouseDown', event, cell.coords, cell.TD);
    }

    // doubleclick reacts only for left mouse button or from touch events
    if ((event.button === 0 || this.touchApplied) && cell.TD) {
      priv.dblClickOrigin[0] = cell.TD;
      clearTimeout(priv.dblClickTimeout[0]);
      priv.dblClickTimeout[0] = setTimeout(() => {
        priv.dblClickOrigin[0] = null;
      }, 1000);
    }
  }

  /**
   * OnContextMenu callback.
   *
   * @private
   * @param {MouseEvent} event The mouse event object.
   */
  onContextMenu(event) {
    if (this.wtSettings.has('onCellContextMenu')) {
      const cell = this.parentCell(event.target);
      if (cell.TD) {
        this.callListener('onCellContextMenu', event, cell.coords, cell.TD);
      }
    }
  }

  /**
   * OnMouseOver callback.
   *
   * @private
   * @param {MouseEvent} event The mouse event object.
   */
  onMouseOver(event) {
    if (!this.wtSettings.has('onCellMouseOver')) {
      return;
    }
    const table = this.wtTable.TABLE;
    const td = (0, _element.closestDown)(event.target, ['TD', 'TH'], table);
    const parent = this.parent || this;
    if (td && td !== parent.lastMouseOver && (0, _element.isChildOf)(td, table)) {
      parent.lastMouseOver = td;
      this.callListener('onCellMouseOver', event, this.wtTable.getCoords(td), td);
    }
  }

  /**
   * OnMouseOut callback.
   *
   * @private
   * @param {MouseEvent} event The mouse event object.
   */
  onMouseOut(event) {
    if (!this.wtSettings.has('onCellMouseOut')) {
      return;
    }
    const table = this.wtTable.TABLE;
    const lastTD = (0, _element.closestDown)(event.target, ['TD', 'TH'], table);
    const nextTD = (0, _element.closestDown)(event.relatedTarget, ['TD', 'TH'], table);
    const parent = this.parent || this;
    if (lastTD && lastTD !== nextTD && (0, _element.isChildOf)(lastTD, table)) {
      this.callListener('onCellMouseOut', event, this.wtTable.getCoords(lastTD), lastTD);
      if (nextTD === null) {
        parent.lastMouseOver = null;
      }
    }
  }

  /**
   * OnMouseUp callback.
   *
   * @private
   * @param {MouseEvent} event The mouse event object.
   */
  onMouseUp(event) {
    const priv = privatePool.get(this);
    const cell = this.parentCell(event.target);
    if (cell.TD && this.wtSettings.has('onCellMouseUp')) {
      this.callListener('onCellMouseUp', event, cell.coords, cell.TD);
    }

    // if not left mouse button, and the origin event is not comes from touch
    if (event.button !== 0 && !this.touchApplied) {
      return;
    }
    if (cell.TD === priv.dblClickOrigin[0] && cell.TD === priv.dblClickOrigin[1]) {
      if ((0, _element.hasClass)(event.target, 'corner')) {
        this.callListener('onCellCornerDblClick', event, cell.coords, cell.TD);
      } else {
        this.callListener('onCellDblClick', event, cell.coords, cell.TD);
      }
      priv.dblClickOrigin[0] = null;
      priv.dblClickOrigin[1] = null;
    } else if (cell.TD === priv.dblClickOrigin[0]) {
      priv.dblClickOrigin[1] = cell.TD;
      clearTimeout(priv.dblClickTimeout[1]);
      priv.dblClickTimeout[1] = setTimeout(() => {
        priv.dblClickOrigin[1] = null;
      }, 500);
    }
  }

  /**
   * OnTouchStart callback. Simulates mousedown event.
   *
   * @private
   * @param {MouseEvent} event The mouse event object.
   */
  onTouchStart(event) {
    const priv = privatePool.get(this);
    priv.selectedCellBeforeTouchEnd = this.selectionManager.getFocusSelection().cellRange;
    this.touchApplied = true;
    this.onMouseDown(event);
  }

  /**
   * OnTouchEnd callback. Simulates mouseup event.
   *
   * @private
   * @param {MouseEvent} event The mouse event object.
   */
  onTouchEnd(event) {
    var _this$parentCell;
    const target = event.target;
    const parentCellCoords = (_this$parentCell = this.parentCell(target)) === null || _this$parentCell === void 0 ? void 0 : _this$parentCell.coords;
    const isCellsRange = (0, _mixed.isDefined)(parentCellCoords) && parentCellCoords.row >= 0 && parentCellCoords.col >= 0;
    const isEventCancelable = event.cancelable && isCellsRange && this.wtSettings.getSetting('isDataViewInstance');

    // To prevent accidental redirects or other actions that the interactive elements (e.q "A" link) do
    // while the cell is highlighted, all touch events that are triggered on different cells are
    // "preventDefault"'ed. The user can interact with the element (e.q. click on the link that opens
    // a new page) only when the same cell was previously selected (see related PR #7980).
    if (isEventCancelable) {
      const interactiveElements = ['A', 'BUTTON', 'INPUT'];

      // For browsers that use the WebKit as an engine (excluding Safari), there is a bug. The prevent
      // default has to be called all the time. Otherwise, the second tap won't be triggered (probably
      // caused by the native ~300ms delay - https://webkit.org/blog/5610/more-responsive-tapping-on-ios/).
      // To make the interactive elements work, the event target element has to be check. If the element
      // matches the allow-list, the event is not prevented.
      if ((0, _browser.isIOS)() && ((0, _browser.isChromeWebKit)() || (0, _browser.isFirefoxWebKit)()) && this.selectedCellWasTouched(target) && !interactiveElements.includes(target.tagName)) {
        event.preventDefault();
      } else if (!this.selectedCellWasTouched(target)) {
        // For other browsers, prevent default is fired only for the first tap and only when the previous
        // highlighted cell was different.
        event.preventDefault();
      }
    }
    this.onMouseUp(event);
    this.touchApplied = false;
  }

  /**
   * Call listener with backward compatibility.
   *
   * @private
   * @param {string} name Name of listener.
   * @param {MouseEvent} event The event object.
   * @param {CellCoords} coords Coordinates.
   * @param {HTMLElement} target Event target.
   */
  callListener(name, event, coords, target) {
    const listener = this.wtSettings.getSettingPure(name);
    if (listener) {
      listener(event, coords, target, this.facadeGetter());
    }
  }

  /**
   * Clears double-click timeouts and destroys the internal eventManager instance.
   */
  destroy() {
    const priv = privatePool.get(this);
    clearTimeout(priv.dblClickTimeout[0]);
    clearTimeout(priv.dblClickTimeout[1]);
    this.eventManager.destroy();
  }
}
var _default = exports.default = Event;