/**
 * Copyright 2013 Facebook, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @providesModule ReactEventEmitter
 * @typechecks static-only
 */

"use strict";

var EventConstants = require('EventConstants');
var EventListener = require('EventListener');
var EventPluginHub = require('EventPluginHub');
var ExecutionEnvironment = require('ExecutionEnvironment');
var ReactEventEmitterMixin = require('ReactEventEmitterMixin');
var ViewportMetrics = require('ViewportMetrics');

var invariant = require('invariant');
var isEventSupported = require('isEventSupported');
var merge = require('merge');

/**
 * Summary of `ReactEventEmitter` event handling:
 *
 *  - Top-level delegation is used to trap native browser events. We normalize
 *    and de-duplicate events to account for browser quirks.
 *
 *  - Forward these native events (with the associated top-level type used to
 *    trap it) to `EventPluginHub`, which in turn will ask plugins if they want
 *    to extract any synthetic events.
 *
 *  - The `EventPluginHub` will then process each event by annotating them with
 *    "dispatches", a sequence of listeners and IDs that care about that event.
 *
 *  - The `EventPluginHub` then dispatches the events.
 *
 * Overview of React and the event system:
 *
 *                   .
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .                         +-----------+
 *       +           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.---------+         |                    +------------+
 * |     |      |    .    +----|---------+
 * +-----|------+    .         |      ^        +-----------+
 *       |           .         |      |        |Enter/Leave|
 *       +           .         |      +-------+|Plugin     |
 * +-------------+   .         v               +-----------+
 * | application |   .    +----------+
 * |-------------|   .    | callback |
 * |             |   .    | registry |
 * |             |   .    +----------+
 * +-------------+   .
 *                   .
 *    React Core     .  General Purpose Event Plugin System
 */

/**
 * Traps top-level events by using event bubbling.
 *
 * @param {string} topLevelType Record from `EventConstants`.
 * @param {string} handlerBaseName Event name (e.g. "click").
 * @param {DOMEventTarget} element Element on which to attach listener.
 * @internal
 */
function trapBubbledEvent(topLevelType, handlerBaseName, element) {
  EventListener.listen(
    element,
    handlerBaseName,
    ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback(
      topLevelType
    )
  );
}

/**
 * Traps a top-level event by using event capturing.
 *
 * @param {string} topLevelType Record from `EventConstants`.
 * @param {string} handlerBaseName Event name (e.g. "click").
 * @param {DOMEventTarget} element Element on which to attach listener.
 * @internal
 */
function trapCapturedEvent(topLevelType, handlerBaseName, element) {
  EventListener.capture(
    element,
    handlerBaseName,
    ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback(
      topLevelType
    )
  );
}

/**
 * Listens to window scroll and resize events. We cache scroll values so that
 * application code can access them without triggering reflows.
 *
 * NOTE: Scroll events do not bubble.
 *
 * @private
 * @see http://www.quirksmode.org/dom/events/scroll.html
 */
function registerScrollValueMonitoring() {
  var refresh = ViewportMetrics.refreshScrollValues;
  EventListener.listen(window, 'scroll', refresh);
  EventListener.listen(window, 'resize', refresh);
}

/**
 * `ReactEventEmitter` is used to attach top-level event listeners. For example:
 *
 *   ReactEventEmitter.putListener('myID', 'onClick', myFunction);
 *
 * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
 *
 * @internal
 */
var ReactEventEmitter = merge(ReactEventEmitterMixin, {

  /**
   * React references `ReactEventTopLevelCallback` using this property in order
   * to allow dependency injection.
   */
  TopLevelCallbackCreator: null,

  /**
   * Ensures that top-level event delegation listeners are installed.
   *
   * There are issues with listening to both touch events and mouse events on
   * the top-level, so we make the caller choose which one to listen to. (If
   * there's a touch top-level listeners, anchors don't receive clicks for some
   * reason, and only in some cases).
   *
   * @param {boolean} touchNotMouse Listen to touch events instead of mouse.
   * @param {DOMDocument} contentDocument DOM document to listen on
   */
  ensureListening: function(touchNotMouse, contentDocument) {
    invariant(
      ExecutionEnvironment.canUseDOM,
      'ensureListening(...): Cannot toggle event listening in a Worker ' +
      'thread. This is likely a bug in the framework. Please report ' +
      'immediately.'
    );
    invariant(
      ReactEventEmitter.TopLevelCallbackCreator,
      'ensureListening(...): Cannot be called without a top level callback ' +
      'creator being injected.'
    );
    // Call out to base implementation.
    ReactEventEmitterMixin.ensureListening.call(
      ReactEventEmitter,
      {
        touchNotMouse: touchNotMouse,
        contentDocument: contentDocument
      }
    );
  },

  /**
   * Sets whether or not any created callbacks should be enabled.
   *
   * @param {boolean} enabled True if callbacks should be enabled.
   */
  setEnabled: function(enabled) {
    invariant(
      ExecutionEnvironment.canUseDOM,
      'setEnabled(...): Cannot toggle event listening in a Worker thread. ' +
      'This is likely a bug in the framework. Please report immediately.'
    );
    if (ReactEventEmitter.TopLevelCallbackCreator) {
      ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled);
    }
  },

  /**
   * @return {boolean} True if callbacks are enabled.
   */
  isEnabled: function() {
    return !!(
      ReactEventEmitter.TopLevelCallbackCreator &&
      ReactEventEmitter.TopLevelCallbackCreator.isEnabled()
    );
  },

  /**
   * We listen for bubbled touch events on the document object.
   *
   * Firefox v8.01 (and possibly others) exhibited strange behavior when
   * mounting `onmousemove` events at some node that was not the document
   * element. The symptoms were that if your mouse is not moving over something
   * contained within that mount point (for example on the background) the
   * top-level listeners for `onmousemove` won't be called. However, if you
   * register the `mousemove` on the document object, then it will of course
   * catch all `mousemove`s. This along with iOS quirks, justifies restricting
   * top-level listeners to the document object only, at least for these
   * movement types of events and possibly all events.
   *
   * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
   *
   * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
   * they bubble to document.
   *
   * @param {boolean} touchNotMouse Listen to touch events instead of mouse.
   * @param {DOMDocument} contentDocument Document which owns the container
   * @private
   * @see http://www.quirksmode.org/dom/events/keys.html.
   */
  listenAtTopLevel: function(touchNotMouse, contentDocument) {
    invariant(
      !contentDocument._isListening,
      'listenAtTopLevel(...): Cannot setup top-level listener more than once.'
    );
    var topLevelTypes = EventConstants.topLevelTypes;
    var mountAt = contentDocument;

    registerScrollValueMonitoring();
    trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt);
    trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt);
    trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt);
    if (touchNotMouse) {
      trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt);
      trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt);
      trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt);
      trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt);
    }
    trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt);
    trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt);
    trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt);
    trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt);
    trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt);
    trapBubbledEvent(
      topLevelTypes.topSelectionChange,
      'selectionchange',
      mountAt
    );

    trapBubbledEvent(
      topLevelTypes.topCompositionEnd,
      'compositionend',
      mountAt
    );
    trapBubbledEvent(
      topLevelTypes.topCompositionStart,
      'compositionstart',
      mountAt
    );
    trapBubbledEvent(
      topLevelTypes.topCompositionUpdate,
      'compositionupdate',
      mountAt
    );

    if (isEventSupported('drag')) {
      trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt);
      trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt);
      trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt);
      trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt);
      trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt);
      trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt);
      trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt);
      trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt);
    }

    if (isEventSupported('wheel')) {
      trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
    } else if (isEventSupported('mousewheel')) {
      trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
    } else {
      // Firefox needs to capture a different mouse scroll event.
      // @see http://www.quirksmode.org/dom/events/tests/scroll.html
      trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
    }

    // IE<9 does not support capturing so just trap the bubbled event there.
    if (isEventSupported('scroll', true)) {
      trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
    } else {
      trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window);
    }

    if (isEventSupported('focus', true)) {
      trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
      trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
    } else if (isEventSupported('focusin')) {
      // IE has `focusin` and `focusout` events which bubble.
      // @see
      // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
      trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
      trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
    }

    if (isEventSupported('copy')) {
      trapBubbledEvent(topLevelTypes.topCopy, 'copy', mountAt);
      trapBubbledEvent(topLevelTypes.topCut, 'cut', mountAt);
      trapBubbledEvent(topLevelTypes.topPaste, 'paste', mountAt);
    }
  },

  registrationNames: EventPluginHub.registrationNames,

  putListener: EventPluginHub.putListener,

  getListener: EventPluginHub.getListener,

  deleteListener: EventPluginHub.deleteListener,

  deleteAllListeners: EventPluginHub.deleteAllListeners,

  trapBubbledEvent: trapBubbledEvent,

  trapCapturedEvent: trapCapturedEvent

});


module.exports = ReactEventEmitter;
