var _event = require("../core/event");

var addEventListener = _event.addEventListener;
var removeEventListener = _event.removeEventListener;
var normalizeEvent = _event.normalizeEvent;
var getNativeEvent = _event.getNativeEvent;

var zrUtil = require("../core/util");

var Eventful = require("../mixin/Eventful");

var env = require("../core/env");

/* global document */
var TOUCH_CLICK_DELAY = 300;
var globalEventSupported = env.domSupported;

var localNativeListenerNames = function () {
  var mouseHandlerNames = ['click', 'dblclick', 'mousewheel', 'mouseout', 'mouseup', 'mousedown', 'mousemove', 'contextmenu'];
  var touchHandlerNames = ['touchstart', 'touchend', 'touchmove'];
  var pointerEventNameMap = {
    pointerdown: 1,
    pointerup: 1,
    pointermove: 1,
    pointerout: 1
  };
  var pointerHandlerNames = zrUtil.map(mouseHandlerNames, function (name) {
    var nm = name.replace('mouse', 'pointer');
    return pointerEventNameMap.hasOwnProperty(nm) ? nm : name;
  });
  return {
    mouse: mouseHandlerNames,
    touch: touchHandlerNames,
    pointer: pointerHandlerNames
  };
}();

var globalNativeListenerNames = {
  mouse: ['mousemove', 'mouseup'],
  pointer: ['pointermove', 'pointerup']
};

function eventNameFix(name) {
  return name === 'mousewheel' && env.browser.firefox ? 'DOMMouseScroll' : name;
}

function isPointerFromTouch(event) {
  var pointerType = event.pointerType;
  return pointerType === 'pen' || pointerType === 'touch';
} // function useMSGuesture(handlerProxy, event) {
//     return isPointerFromTouch(event) && !!handlerProxy._msGesture;
// }
// function onMSGestureChange(proxy, event) {
//     if (event.translationX || event.translationY) {
//         // mousemove is carried by MSGesture to reduce the sensitivity.
//         proxy.handler.dispatchToElement(event.target, 'mousemove', event);
//     }
//     if (event.scale !== 1) {
//         event.pinchX = event.offsetX;
//         event.pinchY = event.offsetY;
//         event.pinchScale = event.scale;
//         proxy.handler.dispatchToElement(event.target, 'pinch', event);
//     }
// }

/**
 * Prevent mouse event from being dispatched after Touch Events action
 * @see <https://github.com/deltakosh/handjs/blob/master/src/hand.base.js>
 * 1. Mobile browsers dispatch mouse events 300ms after touchend.
 * 2. Chrome for Android dispatch mousedown for long-touch about 650ms
 * Result: Blocking Mouse Events for 700ms.
 *
 * @param {DOMHandlerScope} scope
 */


function setTouchTimer(scope) {
  scope.touching = true;

  if (scope.touchTimer != null) {
    clearTimeout(scope.touchTimer);
    scope.touchTimer = null;
  }

  scope.touchTimer = setTimeout(function () {
    scope.touching = false;
    scope.touchTimer = null;
  }, 700);
} // Mark touch, which is useful in distinguish touch and
// mouse event in upper applicatoin.


function markTouch(event) {
  event && (event.zrByTouch = true);
} // function markTriggeredFromLocal(event) {
//     event && (event.__zrIsFromLocal = true);
// }
// function isTriggeredFromLocal(instance, event) {
//     return !!(event && event.__zrIsFromLocal);
// }


function normalizeGlobalEvent(instance, event) {
  // offsetX, offsetY still need to be calculated. They are necessary in the event
  // handlers of the upper applications. Set `true` to force calculate them.
  return normalizeEvent(instance.dom, new FakeGlobalEvent(instance, event), true);
}
/**
 * Detect whether the given el is in `painterRoot`.
 */


function isLocalEl(instance, el) {
  var elTmp = el;
  var isLocal = false;

  while (elTmp && elTmp.nodeType !== 9 && !(isLocal = elTmp.domBelongToZr || elTmp !== el && elTmp === instance.painterRoot)) {
    elTmp = elTmp.parentNode;
  }

  return isLocal;
}
/**
 * Make a fake event but not change the original event,
 * becuase the global event probably be used by other
 * listeners not belonging to zrender.
 * @class
 */


function FakeGlobalEvent(instance, event) {
  this.type = event.type;
  this.target = this.currentTarget = instance.dom;
  this.pointerType = event.pointerType; // Necessray for the force calculation of zrX, zrY

  this.clientX = event.clientX;
  this.clientY = event.clientY; // Because we do not mount global listeners to touch events,
  // we do not copy `targetTouches` and `changedTouches` here.
}

var fakeGlobalEventProto = FakeGlobalEvent.prototype; // we make the default methods on the event do nothing,
// otherwise it is dangerous. See more details in
// [Drag outside] in `Handler.js`.

fakeGlobalEventProto.stopPropagation = fakeGlobalEventProto.stopImmediatePropagation = fakeGlobalEventProto.preventDefault = zrUtil.noop;
/**
 * Local DOM Handlers
 * @this {HandlerProxy}
 */

var localDOMHandlers = {
  mousedown: function (event) {
    event = normalizeEvent(this.dom, event);
    this._mayPointerCapture = [event.zrX, event.zrY];
    this.trigger('mousedown', event);
  },
  mousemove: function (event) {
    event = normalizeEvent(this.dom, event);
    var downPoint = this._mayPointerCapture;

    if (downPoint && (event.zrX !== downPoint[0] || event.zrY !== downPoint[1])) {
      togglePointerCapture(this, true);
    }

    this.trigger('mousemove', event);
  },
  mouseup: function (event) {
    event = normalizeEvent(this.dom, event);
    togglePointerCapture(this, false);
    this.trigger('mouseup', event);
  },
  mouseout: function (event) {
    event = normalizeEvent(this.dom, event); // Similarly to the browser did on `document` and touch event,
    // `globalout` will be delayed to final pointer cature release.

    if (this._pointerCapturing) {
      event.zrEventControl = 'no_globalout';
    } // There might be some doms created by upper layer application
    // at the same level of painter.getViewportRoot() (e.g., tooltip
    // dom created by echarts), where 'globalout' event should not
    // be triggered when mouse enters these doms. (But 'mouseout'
    // should be triggered at the original hovered element as usual).


    var element = event.toElement || event.relatedTarget;
    event.zrIsToLocalDOM = isLocalEl(this, element);
    this.trigger('mouseout', event);
  },
  touchstart: function (event) {
    // Default mouse behaviour should not be disabled here.
    // For example, page may needs to be slided.
    event = normalizeEvent(this.dom, event);
    markTouch(event);
    this._lastTouchMoment = new Date();
    this.handler.processGesture(event, 'start'); // For consistent event listener for both touch device and mouse device,
    // we simulate "mouseover-->mousedown" in touch device. So we trigger
    // `mousemove` here (to trigger `mouseover` inside), and then trigger
    // `mousedown`.

    localDOMHandlers.mousemove.call(this, event);
    localDOMHandlers.mousedown.call(this, event);
  },
  touchmove: function (event) {
    event = normalizeEvent(this.dom, event);
    markTouch(event);
    this.handler.processGesture(event, 'change'); // Mouse move should always be triggered no matter whether
    // there is gestrue event, because mouse move and pinch may
    // be used at the same time.

    localDOMHandlers.mousemove.call(this, event);
  },
  touchend: function (event) {
    event = normalizeEvent(this.dom, event);
    markTouch(event);
    this.handler.processGesture(event, 'end');
    localDOMHandlers.mouseup.call(this, event); // Do not trigger `mouseout` here, in spite of `mousemove`(`mouseover`) is
    // triggered in `touchstart`. This seems to be illogical, but by this mechanism,
    // we can conveniently implement "hover style" in both PC and touch device just
    // by listening to `mouseover` to add "hover style" and listening to `mouseout`
    // to remove "hover style" on an element, without any additional code for
    // compatibility. (`mouseout` will not be triggered in `touchend`, so "hover
    // style" will remain for user view)
    // click event should always be triggered no matter whether
    // there is gestrue event. System click can not be prevented.

    if (+new Date() - this._lastTouchMoment < TOUCH_CLICK_DELAY) {
      localDOMHandlers.click.call(this, event);
    }
  },
  pointerdown: function (event) {
    localDOMHandlers.mousedown.call(this, event); // if (useMSGuesture(this, event)) {
    //     this._msGesture.addPointer(event.pointerId);
    // }
  },
  pointermove: function (event) {
    // FIXME
    // pointermove is so sensitive that it always triggered when
    // tap(click) on touch screen, which affect some judgement in
    // upper application. So, we dont support mousemove on MS touch
    // device yet.
    if (!isPointerFromTouch(event)) {
      localDOMHandlers.mousemove.call(this, event);
    }
  },
  pointerup: function (event) {
    localDOMHandlers.mouseup.call(this, event);
  },
  pointerout: function (event) {
    // pointerout will be triggered when tap on touch screen
    // (IE11+/Edge on MS Surface) after click event triggered,
    // which is inconsistent with the mousout behavior we defined
    // in touchend. So we unify them.
    // (check localDOMHandlers.touchend for detailed explanation)
    if (!isPointerFromTouch(event)) {
      localDOMHandlers.mouseout.call(this, event);
    }
  }
};
/**
 * Othere DOM UI Event handlers for zr dom.
 * @this {HandlerProxy}
 */

zrUtil.each(['click', 'mousewheel', 'dblclick', 'contextmenu'], function (name) {
  localDOMHandlers[name] = function (event) {
    event = normalizeEvent(this.dom, event);
    this.trigger(name, event);
  };
});
/**
 * DOM UI Event handlers for global page.
 *
 * [Caution]:
 * those handlers should both support in capture phase and bubble phase!
 *
 * @this {HandlerProxy}
 */

var globalDOMHandlers = {
  pointermove: function (event) {
    // FIXME
    // pointermove is so sensitive that it always triggered when
    // tap(click) on touch screen, which affect some judgement in
    // upper application. So, we dont support mousemove on MS touch
    // device yet.
    if (!isPointerFromTouch(event)) {
      globalDOMHandlers.mousemove.call(this, event);
    }
  },
  pointerup: function (event) {
    globalDOMHandlers.mouseup.call(this, event);
  },
  mousemove: function (event) {
    this.trigger('mousemove', event);
  },
  mouseup: function (event) {
    var pointerCaptureReleasing = this._pointerCapturing;
    togglePointerCapture(this, false);
    this.trigger('mouseup', event);

    if (pointerCaptureReleasing) {
      event.zrEventControl = 'only_globalout';
      this.trigger('mouseout', event);
    }
  }
};
/**
 * @param {HandlerProxy} instance
 * @param {DOMHandlerScope} scope
 */

function mountLocalDOMEventListeners(instance, scope) {
  var domHandlers = scope.domHandlers;

  if (env.pointerEventsSupported) {
    // Only IE11+/Edge
    // 1. On devices that both enable touch and mouse (e.g., MS Surface and lenovo X240),
    // IE11+/Edge do not trigger touch event, but trigger pointer event and mouse event
    // at the same time.
    // 2. On MS Surface, it probablely only trigger mousedown but no mouseup when tap on
    // screen, which do not occurs in pointer event.
    // So we use pointer event to both detect touch gesture and mouse behavior.
    zrUtil.each(localNativeListenerNames.pointer, function (nativeEventName) {
      mountSingleDOMEventListener(scope, nativeEventName, function (event) {
        // markTriggeredFromLocal(event);
        domHandlers[nativeEventName].call(instance, event);
      });
    }); // FIXME
    // Note: MS Gesture require CSS touch-action set. But touch-action is not reliable,
    // which does not prevent defuault behavior occasionally (which may cause view port
    // zoomed in but use can not zoom it back). And event.preventDefault() does not work.
    // So we have to not to use MSGesture and not to support touchmove and pinch on MS
    // touch screen. And we only support click behavior on MS touch screen now.
    // MS Gesture Event is only supported on IE11+/Edge and on Windows 8+.
    // We dont support touch on IE on win7.
    // See <https://msdn.microsoft.com/en-us/library/dn433243(v=vs.85).aspx>
    // if (typeof MSGesture === 'function') {
    //     (this._msGesture = new MSGesture()).target = dom; // jshint ignore:line
    //     dom.addEventListener('MSGestureChange', onMSGestureChange);
    // }
  } else {
    if (env.touchEventsSupported) {
      zrUtil.each(localNativeListenerNames.touch, function (nativeEventName) {
        mountSingleDOMEventListener(scope, nativeEventName, function (event) {
          // markTriggeredFromLocal(event);
          domHandlers[nativeEventName].call(instance, event);
          setTouchTimer(scope);
        });
      }); // Handler of 'mouseout' event is needed in touch mode, which will be mounted below.
      // addEventListener(root, 'mouseout', this._mouseoutHandler);
    } // 1. Considering some devices that both enable touch and mouse event (like on MS Surface
    // and lenovo X240, @see #2350), we make mouse event be always listened, otherwise
    // mouse event can not be handle in those devices.
    // 2. On MS Surface, Chrome will trigger both touch event and mouse event. How to prevent
    // mouseevent after touch event triggered, see `setTouchTimer`.


    zrUtil.each(localNativeListenerNames.mouse, function (nativeEventName) {
      mountSingleDOMEventListener(scope, nativeEventName, function (event) {
        event = getNativeEvent(event);

        if (!scope.touching) {
          // markTriggeredFromLocal(event);
          domHandlers[nativeEventName].call(instance, event);
        }
      });
    });
  }
}
/**
 * @param {HandlerProxy} instance
 * @param {DOMHandlerScope} scope
 */


function mountGlobalDOMEventListeners(instance, scope) {
  // Only IE11+/Edge. See the comment in `mountLocalDOMEventListeners`.
  if (env.pointerEventsSupported) {
    zrUtil.each(globalNativeListenerNames.pointer, mount);
  } // Touch event has implemented "drag outside" so we do not mount global listener for touch event.
  // (see https://www.w3.org/TR/touch-events/#the-touchmove-event)
  // We do not consider "both-support-touch-and-mouse device" for this feature (see the comment of
  // `mountLocalDOMEventListeners`) to avoid bugs util some requirements come.
  else if (!env.touchEventsSupported) {
      zrUtil.each(globalNativeListenerNames.mouse, mount);
    }

  function mount(nativeEventName) {
    function nativeEventListener(event) {
      event = getNativeEvent(event); // See the reason in [Drag outside] in `Handler.js`
      // This checking supports both `useCapture` or not.
      // PENDING: if there is performance issue in some devices,
      // we probably can not use `useCapture` and change a easier
      // to judes whether local (mark).

      if (!isLocalEl(instance, event.target)) {
        event = normalizeGlobalEvent(instance, event);
        scope.domHandlers[nativeEventName].call(instance, event);
      }
    }

    mountSingleDOMEventListener(scope, nativeEventName, nativeEventListener, {
      capture: true
    } // See [Drag Outside] in `Handler.js`
    );
  }
}

function mountSingleDOMEventListener(scope, nativeEventName, listener, opt) {
  scope.mounted[nativeEventName] = listener;
  scope.listenerOpts[nativeEventName] = opt;
  addEventListener(scope.domTarget, eventNameFix(nativeEventName), listener, opt);
}

function unmountDOMEventListeners(scope) {
  var mounted = scope.mounted;

  for (var nativeEventName in mounted) {
    if (mounted.hasOwnProperty(nativeEventName)) {
      removeEventListener(scope.domTarget, eventNameFix(nativeEventName), mounted[nativeEventName], scope.listenerOpts[nativeEventName]);
    }
  }

  scope.mounted = {};
}
/**
 * See [Drag Outside] in `Handler.js`.
 * @implement
 * @param {boolean} isPointerCapturing Should never be `null`/`undefined`.
 *        `true`: start to capture pointer if it is not capturing.
 *        `false`: end the capture if it is capturing.
 */


function togglePointerCapture(instance, isPointerCapturing) {
  instance._mayPointerCapture = null;

  if (globalEventSupported && instance._pointerCapturing ^ isPointerCapturing) {
    instance._pointerCapturing = isPointerCapturing;
    var globalHandlerScope = instance._globalHandlerScope;
    isPointerCapturing ? mountGlobalDOMEventListeners(instance, globalHandlerScope) : unmountDOMEventListeners(globalHandlerScope);
  }
}
/**
 * @inner
 * @class
 */


function DOMHandlerScope(domTarget, domHandlers) {
  this.domTarget = domTarget;
  this.domHandlers = domHandlers; // Key: eventName, value: mounted handler funcitons.
  // Used for unmount.

  this.mounted = {};
  this.listenerOpts = {};
  this.touchTimer = null;
  this.touching = false;
}
/**
 * @public
 * @class
 */


function HandlerDomProxy(dom, painterRoot) {
  Eventful.call(this);
  this.dom = dom;
  this.painterRoot = painterRoot;
  this._localHandlerScope = new DOMHandlerScope(dom, localDOMHandlers);

  if (globalEventSupported) {
    this._globalHandlerScope = new DOMHandlerScope(document, globalDOMHandlers);
  }
  /**
   * @type {boolean}
   */


  this._pointerCapturing = false;
  /**
   * @type {Array.<number>} [x, y] or null.
   */

  this._mayPointerCapture = null;
  mountLocalDOMEventListeners(this, this._localHandlerScope);
}

var handlerDomProxyProto = HandlerDomProxy.prototype;

handlerDomProxyProto.dispose = function () {
  unmountDOMEventListeners(this._localHandlerScope);

  if (globalEventSupported) {
    unmountDOMEventListeners(this._globalHandlerScope);
  }
};

handlerDomProxyProto.setCursor = function (cursorStyle) {
  this.dom.style && (this.dom.style.cursor = cursorStyle || 'default');
};

zrUtil.mixin(HandlerDomProxy, Eventful);
var _default = HandlerDomProxy;
module.exports = _default;