'use strict';

/**
 * Module exports.
 */

exports.EventEmitter = EventEmitter;

/**
 * Object#toString reference.
 */
var objToString = Object.prototype.toString;

/**
 * Check if a value is an array.
 *
 * @api private
 * @param {*} val The value to test.
 * @return {boolean} true if the value is an array, otherwise false.
 */
function isArray (val) {
  return objToString.call(val) === '[object Array]';
}

/**
 * Event emitter constructor.
 *
 * @api public
 */
function EventEmitter () {}

/**
 * Add a listener.
 *
 * @api public
 * @param {string} name Event name.
 * @param {Function} fn Event handler.
 * @return {EventEmitter} Emitter instance.
 */
EventEmitter.prototype.on = function (name, fn) {
  if (!this.$events) {
    this.$events = {};
  }

  if (!this.$events[name]) {
    this.$events[name] = fn;
  } else if (isArray(this.$events[name])) {
    this.$events[name].push(fn);
  } else {
    this.$events[name] = [this.$events[name], fn];
  }

  return this;
};

EventEmitter.prototype.addListener = EventEmitter.prototype.on;

/**
 * Adds a volatile listener.
 *
 * @api public
 * @param {string} name Event name.
 * @param {Function} fn Event handler.
 * @return {EventEmitter} Emitter instance.
 */
EventEmitter.prototype.once = function (name, fn) {
  var self = this;

  function on () {
    self.removeListener(name, on);
    fn.apply(this, arguments);
  }

  on.listener = fn;
  this.on(name, on);

  return this;
};

/**
 * Remove a listener.
 *
 * @api public
 * @param {string} name Event name.
 * @param {Function} fn Event handler.
 * @return {EventEmitter} Emitter instance.
 */
EventEmitter.prototype.removeListener = function (name, fn) {
  if (this.$events && this.$events[name]) {
    var list = this.$events[name];

    if (isArray(list)) {
      var pos = -1;

      for (var i = 0, l = list.length; i < l; i++) {
        if (list[i] === fn || (list[i].listener && list[i].listener === fn)) {
          pos = i;
          break;
        }
      }

      if (pos < 0) {
        return this;
      }

      list.splice(pos, 1);

      if (!list.length) {
        delete this.$events[name];
      }
    } else if (list === fn || (list.listener && list.listener === fn)) {
      delete this.$events[name];
    }
  }

  return this;
};

/**
 * Remove all listeners for an event.
 *
 * @api public
 * @param {string} name Event name.
 * @return {EventEmitter} Emitter instance.
 */
EventEmitter.prototype.removeAllListeners = function (name) {
  if (name === undefined) {
    this.$events = {};
    return this;
  }

  if (this.$events && this.$events[name]) {
    this.$events[name] = null;
  }

  return this;
};

/**
 * Get all listeners for a given event.
 *
 * @api public
 * @param {string} name Event name.
 * @return {EventEmitter} Emitter instance.
 */
EventEmitter.prototype.listeners = function (name) {
  if (!this.$events) {
    this.$events = {};
  }

  if (!this.$events[name]) {
    this.$events[name] = [];
  }

  if (!isArray(this.$events[name])) {
    this.$events[name] = [this.$events[name]];
  }

  return this.$events[name];
};

/**
 * Emit an event.
 *
 * @api public
 * @param {string} name Event name.
 * @return {boolean} true if at least one handler was invoked, else false.
 */
EventEmitter.prototype.emit = function (name) {
  if (!this.$events) {
    return false;
  }

  var handler = this.$events[name];

  if (!handler) {
    return false;
  }

  var args = Array.prototype.slice.call(arguments, 1);

  if (typeof handler === 'function') {
    handler.apply(this, args);
  } else if (isArray(handler)) {
    var listeners = handler.slice();

    for (var i = 0, l = listeners.length; i < l; i++) {
      listeners[i].apply(this, args);
    }
  } else {
    return false;
  }

  return true;
};
