/**
 * 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 EventPluginHub
 */

"use strict";

var AbstractEvent = require('AbstractEvent');
var CallbackRegistry = require('CallbackRegistry');
var EventPluginUtils = require('EventPluginUtils');
var EventPropagators = require('EventPropagators');
var ExecutionEnvironment = require('ExecutionEnvironment');

var accumulate = require('accumulate');
var forEachAccumulated = require('forEachAccumulated');
var keyMirror = require('keyMirror');
var merge = require('merge');
var throwIf = require('throwIf');

var deleteListener = CallbackRegistry.deleteListener;

var ERRORS = keyMirror({
  DOUBLE_REGISTER: null,
  DOUBLE_ENQUEUE: null,
  DEPENDENCY_ERROR: null
});

if (__DEV__) {
  ERRORS.DOUBLE_REGISTER =
    'You\'ve included an event plugin that extracts an ' +
    'event type with the exact same or identity as an event that ' +
    'had previously been injected - or, one of the registration names ' +
    'used by an plugin has already been used.';
  ERRORS.DOUBLE_ENQUEUE =
    'During the processing of events, more events were enqueued. This ' +
    'is strange and should not happen. Please report immediately. ';
  ERRORS.DEPENDENCY_ERROR =
    'You have either attempted to load an event plugin that has no ' +
    'entry in EventPluginOrder, or have attempted to extract events ' +
    'when some critical dependencies have not yet been injected.';
}


/**
 * EventPluginHub: To see a diagram and explanation of the overall architecture
 * of the plugin hub system, @see ReactEvents.js
 */

/**
 * Injected Dependencies:
 */
var injection = {
  /**
   * [required] Dependency of `EventPropagators`.
   */
  injectInstanceHandle: function(InjectedInstanceHandle) {
    EventPropagators.injection.injectInstanceHandle(InjectedInstanceHandle);
  },

  /**
   * `EventPluginOrder`: [optional] Provides deterministic ordering of
   * `EventPlugin`s. Ordering decoupled from the injection of actual
   * plugins so that there is always a deterministic and permanent ordering
   * regardless of the plugins that happen to become packaged, or applications
   * that happen to inject on-the-fly event plugins.
   */
  EventPluginOrder: null,
  injectEventPluginOrder: function(InjectedEventPluginOrder) {
    injection.EventPluginOrder = InjectedEventPluginOrder;
    injection._recomputePluginsList();
  },

  /**
   * `EventPlugins`: [optional][lazy-loadable] Plugins that must be listed in
   * the `EventPluginOrder` injected dependency. The list of plugins may grow as
   * custom plugins are injected into the system at page as part of the
   * initialization process, or even after the initialization process (on-the-
   * fly). Plugins injected into the hub have an opportunity to infer abstract
   * events when top level events are streamed through the `EventPluginHub`.
   */
  plugins: [],
  injectEventPluginsByName: function(pluginsByName) {
    injection.pluginsByName = merge(injection.pluginsByName, pluginsByName);
    injection._recomputePluginsList();
  },

  /**
   * A reference of all injected plugins by their name. Both plugins and
   * `EventPluginOrder` can be injected on-the-fly. Any time either dependency
   * is (re)injected, the resulting list of plugins in correct order is
   * recomputed.
   */
  pluginsByName: {},
  _recomputePluginsList: function() {
    var injectPluginByName = function(name, PluginModule) {
      var pluginIndex = injection.EventPluginOrder.indexOf(name);
      throwIf(pluginIndex === -1, ERRORS.DEPENDENCY_ERROR + name);
      if (!injection.plugins[pluginIndex]) {
        injection.plugins[pluginIndex] = PluginModule;
        for (var eventName in PluginModule.abstractEventTypes) {
          var eventType = PluginModule.abstractEventTypes[eventName];
          recordAllRegistrationNames(eventType, PluginModule);
        }
      }
    };
    if (injection.EventPluginOrder) {  // Else, do when plugin order injected
      var injectedPluginsByName = injection.pluginsByName;
      for (var name in injectedPluginsByName) {
        injectPluginByName(name, injectedPluginsByName[name]);
      }
    }
  }
};


/**
 * `renderedTarget`: We'll refer to the concept of a "rendered target". Any
 * framework code that uses the `EventPluginHub` will stream events to the
 * `EventPluginHub`. In order for registered plugins to make sense of the native
 * events, it helps to allow plugins to not only have access to the native
 * `Event` object but also the notion of a "rendered target", which is the
 * conceptual target `Element` of the `Event` that the framework (user of
 * `EventPluginHub`) deems as being the "important" target.
 */
/**
 * Keeps track of all valid "registration names" (`onClick` etc). We expose
 * these structures to other modules, who will always see updates that we apply
 * to these structures. They should never have a desire to mutate these.
 */
var registrationNames = {};

/**
 * This prevents the need for clients to call `Object.keys(registrationNames)`
 * every time they want to loop through the possible registration names.
 */
var registrationNamesArr = [];

/**
 * Internal queue of events that have accumulated their dispatches and are
 * waiting to have their dispatches executed.
 */
var abstractEventQueue = [];

/**
 * Records all the registration names that the event plugin makes available
 * to the general event system. These are things like
 * `onClick`/`onClickCapture`.
 */
function recordAllRegistrationNames(eventType, PluginModule) {
  var phaseName;
  var phasedRegistrationNames = eventType.phasedRegistrationNames;
  if (phasedRegistrationNames) {
    for (phaseName in phasedRegistrationNames) {
      if (!phasedRegistrationNames.hasOwnProperty(phaseName)) {
        continue;
      }
      if (__DEV__) {
        throwIf(
          registrationNames[phasedRegistrationNames[phaseName]],
          ERRORS.DOUBLE_REGISTER
        );
      }
      registrationNames[phasedRegistrationNames[phaseName]] = PluginModule;
      registrationNamesArr.push(phasedRegistrationNames[phaseName]);
    }
  } else if (eventType.registrationName) {
    if (__DEV__) {
      throwIf(
        registrationNames[eventType.registrationName],
        ERRORS.DOUBLE_REGISTER
      );
    }
    registrationNames[eventType.registrationName] = PluginModule;
    registrationNamesArr.push(eventType.registrationName);
  }
}

/**
 * A hacky way to reverse engineer which event plugin module created an
 * AbstractEvent.
 * @param {AbstractEvent} abstractEvent to look at
 */
function getPluginModuleForAbstractEvent(abstractEvent) {
  if (abstractEvent.type.registrationName) {
    return registrationNames[abstractEvent.type.registrationName];
  } else {
    for (var phase in abstractEvent.type.phasedRegistrationNames) {
      if (!abstractEvent.type.phasedRegistrationNames.hasOwnProperty(phase)) {
        continue;
      }
      var PluginModule = registrationNames[
        abstractEvent.type.phasedRegistrationNames[phase]
      ];
      if (PluginModule) {
        return PluginModule;
      }
    }
  }
  return null;
}

var deleteAllListeners = function(domID) {
  var ii;
  for (ii = 0; ii < registrationNamesArr.length; ii++) {
    deleteListener(domID, registrationNamesArr[ii]);
  }
};

/**
 * Accepts the stream of top level native events, and gives every registered
 * plugin an opportunity to extract `AbstractEvent`s with annotated dispatches.
 *
 * @param {Enum} topLevelType Record from `EventConstants`.
 * @param {Event} nativeEvent A Standard Event with fixed `target` property.
 * @param {Element} renderedTarget Element of interest to the framework, usually
 *  the same as `nativeEvent.target` but occasionally an element immediately
 *  above `nativeEvent.target` (the first DOM node recognized as one "rendered"
 *  by the framework at hand.)
 * @param {string} renderedTargetID string ID of `renderedTarget`.
 */
var extractAbstractEvents =
  function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) {
    var abstractEvents;
    var plugins = injection.plugins;
    var len = plugins.length;
    for (var i = 0; i < len; i++) {
      // Not every plugin in the ordering may be loaded at runtime.
      var possiblePlugin = plugins[i];
      var extractedAbstractEvents =
        possiblePlugin &&
        possiblePlugin.extractAbstractEvents(
          topLevelType,
          nativeEvent,
          renderedTargetID,
          renderedTarget
        );
      if (extractedAbstractEvents) {
        abstractEvents = accumulate(abstractEvents, extractedAbstractEvents);
      }
    }
    return abstractEvents;
  };

var enqueueAbstractEvents = function(abstractEvents) {
  if (abstractEvents) {
    abstractEventQueue = accumulate(abstractEventQueue, abstractEvents);
  }
};

/**
 * Executes a single abstract event dispatch. Returns a value, but this return
 * value doesn't make much sense when executing dispatches for a list of a
 * events. However, if a plugin executes a single dispatch, mostly bypassing
 * `EventPluginHub`, it can execute dispatches directly and assign special
 * meaning to the return value. So this will return the result of executing the
 * dispatch, though for most use cases, it gets dropped.
 */
var executeDispatchesAndRelease = function(abstractEvent) {
  if (abstractEvent) {
    var PluginModule = getPluginModuleForAbstractEvent(abstractEvent);
    var pluginExecuteDispatch = PluginModule && PluginModule.executeDispatch;
    EventPluginUtils.executeDispatchesInOrder(
      abstractEvent,
      pluginExecuteDispatch || EventPluginUtils.executeDispatch
    );
    AbstractEvent.release(abstractEvent);
  }
};

/**
 * Sets `abstractEventQueue` to null before processing it, so that we can tell
 * if in the process of processing events, more were enqueued. We throw if we
 * find that any were enqueued though this use case could be supported in the
 * future. For now, throwing an error as it's something we don't expect to
 * occur.
 */
var processAbstractEventQueue = function() {
  var processingAbstractEventQueue = abstractEventQueue;
  abstractEventQueue = null;
  forEachAccumulated(processingAbstractEventQueue, executeDispatchesAndRelease);
  if (__DEV__) {
    throwIf(abstractEventQueue, ERRORS.DOUBLE_ENQUEUE);
  }
};

/**
 * Provides a unified interface for an arbitrary and dynamic set of event
 * plugins. Loosely, a hub, where several "event plugins" may act as a single
 * event plugin behind the facade of `EventPluginHub`. Each event plugin injects
 * themselves into the HUB, and will immediately become operable upon injection.
 *
 * @constructor EventPluginHub
 */
var EventPluginHub = {
  registrationNames: registrationNames,
  registrationNamesArr: registrationNamesArr,
  putListener: CallbackRegistry.putListener,
  getListener: CallbackRegistry.getListener,
  deleteAllListeners: deleteAllListeners,
  extractAbstractEvents: extractAbstractEvents,
  enqueueAbstractEvents: enqueueAbstractEvents,
  processAbstractEventQueue: processAbstractEventQueue,
  injection: injection
};


if (ExecutionEnvironment.canUseDOM) {
  window.EventPluginHub = EventPluginHub;
}

module.exports = EventPluginHub;
