'use strict';
/**
 * @module events
 * @memberOf module:buildin
 */

/**
 * @classdesc
 * NodeJS like EventEmitter. See also <a href="http://nodejs.org/api/events.html">NodeJS events documentation</a>
 *
 * Starting from UB@5.23.9 constructor can accept optional `traceAs: string` parameter. Is specified,
 * all event handlers calls will be logged using `console.traceFunc` on `Trace` level.
 *
 * @example
const EventEmitter = require('events').EventEmitter
const myObject = {}
// add EventEmitter to myObject
EventEmitter.call(myObject, 'MyObject') // traceAs = 'muObject'
Object.assign(myObject, EventEmitter.prototype)

// In case object created via constructor function
//
// const util = require('util')
// function MyObject() {
//    EventEmitter.call(this, MyObject)
// }
// util.inherits(MyObject, EventEmitter)
//
// var myObject = new MyObject()

// usage
myObject.on('myEvent', function logSumAndNum(num, str){ console.log(num, 'is', str) })
myObject.emit('myEvent', 1, 'two') // output: 1 is two
// Since we specify a `traceAs` parameter, if `Trace` level is enabled in server logging, will add log line:
// YYYYMMDD HHMMSS Trace  MyObject.on(myEvent)->logSumAndNum

// to prevent tracing of some handlers, for example, a handler that already logs its call, `_skipEmitterTrace:true`
// can be added to handler:
function logSumAndNumWOTrace (num, sum) { console.debug('I trace my call by myself'); console.log(num, 'is (wo trace)', str)  }
logSumAndNumWOTrace._skipEmitterTrace = true
myObject.on('myEvent', logSumAndNumWOTrace)
 *
 * @class EventEmitter
 * @param {string} [traceAs] if specified, all event handlers calls will be logged using `console.traceFunc` on `Trace` level
 */
function EventEmitter(traceAs) {
  EventEmitter.init.call(this, traceAs);
}
module.exports = EventEmitter;

// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;

/**
 * Private collection of events.
 * @private
 */
EventEmitter.prototype._events = undefined;
/**
 * Use set/get MaxListeners instead direct access
 * @private
 */
EventEmitter.prototype._maxListeners = undefined;

// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
EventEmitter.defaultMaxListeners = 10;

/**
 * @private
 */
EventEmitter.init = function(traceAs) {
  if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
    this._events = {};
    this._eventsCount = 0;
    this._traceAs = traceAs
  }

  this._maxListeners = this._maxListeners || undefined;
};

/**
 * Obviously not all Emitters should be limited to 10. This function allows
 * that to be increased. Set to zero for unlimited.
 * @param {Number} n
 */
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
  if (typeof n !== 'number' || n < 0 || isNaN(n))
    throw new TypeError('n must be a positive number');
  this._maxListeners = n;
  return this;
};

function $getMaxListeners(that) {
  if (that._maxListeners === undefined)
    return EventEmitter.defaultMaxListeners;
  return that._maxListeners;
}

/**
 *
 * @return {Number}
 */
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
  return $getMaxListeners(this);
};

// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
function emitNone(type, handler, isFn, self) {
  if (isFn) {
    if (self._traceAs) console.traceFunc(handler, type, self._traceAs)
    handler.call(self);
  } else {
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i) {
      if (self._traceAs) console.traceFunc(listeners[i], type, self._traceAs)
      listeners[i].call(self);
    }
  }
}
function emitOne(type, handler, isFn, self, arg1) {
  if (isFn) {
    if (self._traceAs) console.traceFunc(handler, type, self._traceAs)
    handler.call(self, arg1);
  } else {
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i) {
      if (self._traceAs) console.traceFunc(listeners[i], type, self._traceAs)
      listeners[i].call(self, arg1);
    }
  }
}
function emitTwo(type, handler, isFn, self, arg1, arg2) {
  if (isFn) {
    if (self._traceAs) console.traceFunc(handler, type, self._traceAs)
    handler.call(self, arg1, arg2);
  } else {
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i) {
      if (self._traceAs) console.traceFunc(listeners[i], type, self._traceAs)
      listeners[i].call(self, arg1, arg2);
    }
  }
}
function emitThree(type, handler, isFn, self, arg1, arg2, arg3) {
  if (isFn) {
    if (self._traceAs) console.traceFunc(handler, type, self._traceAs)
    handler.call(self, arg1, arg2, arg3);
  } else {
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i) {
      if (self._traceAs) console.traceFunc(listeners[i], type, self._traceAs)
      listeners[i].call(self, arg1, arg2, arg3);
    }
  }
}

function emitMany(type, handler, isFn, self, args) {
  if (isFn) {
    if (self._traceAs) console.traceFunc(handler, type, self._traceAs)
    handler.apply(self, args);
  } else {
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i) {
      if (self._traceAs) console.traceFunc(listeners[i], type, self._traceAs)
      listeners[i].apply(self, args);
    }
  }
}

/**
 * Execute each of the listeners in order with the supplied arguments.
 * Returns true if event had listeners, false otherwise.
 *
 * @param {String} type Event name
 * @param {...*} eventArgs Arguments, passed to listeners
 * @return {boolean}
 */
EventEmitter.prototype.emit = function emit(type) {
  var er, handler, len, args, i, events;
  var doError = (type === 'error');

  events = this._events;
  if (events)
    doError = (doError && events.error == null);
  else if (!doError)
    return false;

  // If there is no 'error' event listener then throw.
  if (doError) {
    er = arguments[1];

    if (er instanceof Error) {
      throw er; // Unhandled 'error' event
    } else {
      // At least give some kind of context to the user
      var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
      err.context = er;
      throw err;
    }
    return false;
  }

  handler = events[type];

  if (!handler)
    return false;

  var isFn = typeof handler === 'function';
  len = arguments.length;
  switch (len) {
    // fast cases
    case 1:
      emitNone(type, handler, isFn, this);
      break;
    case 2:
      emitOne(type, handler, isFn, this, arguments[1]);
      break;
    case 3:
      emitTwo(type, handler, isFn, this, arguments[1], arguments[2]);
      break;
    case 4:
      emitThree(type, handler, isFn, this, arguments[1], arguments[2], arguments[3]);
      break;
    // slower
    default:
      args = new Array(len - 1);
      for (i = 1; i < len; i++)
        args[i - 1] = arguments[i];
      emitMany(type, handler, isFn, this, args);
  }

  return true;
};

function checkListener (listener) {
  if (typeof listener !== 'function')
    throw new TypeError('listener must be a function');
}

function _addListener(target, type, listener, prepend) {
  var m;
  var events;
  var existing;

  checkListener(listener)

  events = target._events;
  if (!events) {
    events = target._events = {};
    target._eventsCount = 0;
  } else {
    // To avoid recursion in the case that type === "newListener"! Before
    // adding it to the listeners, first emit "newListener".
    if (events.newListener) {
      /** @fires  newListener */
      target.emit('newListener', type,
        listener.listener ? listener.listener : listener);

      // Re-assign `events` because a newListener handler could have caused the
      // this._events to be assigned to a new object
      events = target._events;
    }
    existing = events[type];
  }

  if (!existing) {
    // Optimize the case of one listener. Don't need the extra array object.
    existing = events[type] = listener;
    ++target._eventsCount;
  } else {
    if (typeof existing === 'function') {
      // Adding the second element, need to change to array.
      existing = events[type] = prepend ? [listener, existing] : [existing, listener]
    } else if (prepend) {
      existing.unshift(listener);
    } else {
      existing.push(listener);
    }

    // Check for listener leak
    if (!existing.warned) {
      m = $getMaxListeners(target);
      if (m && m > 0 && existing.length > m) {
        existing.warned = true;
        console.error('(node) warning: possible EventEmitter memory ' +
          'leak detected. %d %s listeners added. ' +
          'Use emitter.setMaxListeners() to increase limit.',
          existing.length, type);
        console.trace();
      }
    }
  }

  return target;
}

/**
 * Adds a listener to the end of the listeners array for the specified event.
 * Will emit `newListener` event on success.
 *
 * Usage sample:
 *
 *      Session.on('login', function () {
 *          console.log('someone connected!');
 *      });
 *
 * Returns emitter, so calls can be chained.
 *
 * @param {String} type Event name
 * @param {Function} listener
 * @return {EventEmitter}
 */
EventEmitter.prototype.addListener = function addListener(type, listener) {
  return _addListener(this, type, listener, false);
};

/**
 * Alias for {@link EventEmitter#addListener addListener}
 * @method
 * @param {String} type Event name
 * @param {Function} listener
 * @return {EventEmitter}
 */
EventEmitter.prototype.on = EventEmitter.prototype.addListener;

/**
 * By default, event listeners are invoked in the order they are added.
 * The emitter.prependOnceListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.
 *
 * @method
 * @param {String} type Event name
 * @param {Function} listener
 * @return {EventEmitter}
 */
EventEmitter.prototype.prependListener =
  function prependListener(type, listener) {
    return _addListener(this, type, listener, true);
  };

function onceWrapper() {
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    if (arguments.length === 0)
      return this.listener.call(this.target);
    return this.listener.apply(this.target, arguments);
  }
}

function _onceWrap(target, type, listener) {
  const state = { fired: false, wrapFn: undefined, target, type, listener };
  const wrapped = onceWrapper.bind(state);
  wrapped.listener = listener;
  state.wrapFn = wrapped;
  return wrapped;
}

/**
 * Adds a one time listener for the event. This listener is invoked only the next time the event is fired, after which it is removed.
 * @param {String} type Event name
 * @param {Function} listener
 * @return {EventEmitter}
 */
EventEmitter.prototype.once = function once(type, listener) {
  checkListener(listener);

  this.on(type, _onceWrap(this, type, listener));
  return this;
};

/**
 * Adds a one-time listener function for the event named eventName to the beginning of the listeners array.
 * The next time eventName is triggered, this listener is removed, and then invoked.
 * @param {String} type Event name
 * @param {Function} listener
 * @return {EventEmitter}
 */
EventEmitter.prototype.prependOnceListener =
  function prependOnceListener(type, listener) {
    checkListener(listener);

    this.prependListener(type, _onceWrap(this, type, listener));
    return this;
  };

/**
 * Remove a listener from the listener array for the specified event.
 * Caution: changes array indices in the listener array behind the listener.
 * Emits a 'removeListener' event if the listener was removed.
 *
 * @param {String} type Event name
 * @param {Function} listener
 */
EventEmitter.prototype.removeListener =
    function removeListener(type, listener) {
      var list, events, position, i;

      if (typeof listener !== 'function')
        throw new TypeError('listener must be a function');

      events = this._events;
      if (!events)
        return this;

      list = events[type];
      if (!list)
        return this;

      if (list === listener || (list.listener && list.listener === listener)) {
        if (--this._eventsCount === 0)
          this._events = {};
        else {
          delete events[type];
          if (events.removeListener)
            /** @fires removeListener */
            this.emit('removeListener', type, listener);
        }
      } else if (typeof list !== 'function') {
        position = -1;

        for (i = list.length; i-- > 0;) {
          if (list[i] === listener ||
              (list[i].listener && list[i].listener === listener)) {
            position = i;
            break;
          }
        }

        if (position < 0)
          return this;

        if (list.length === 1) {
          list[0] = undefined;
          if (--this._eventsCount === 0) {
            this._events = {};
            return this;
          } else {
            delete events[type];
          }
        } else {
          spliceOne(list, position);
        }

        if (events.removeListener)
          this.emit('removeListener', type, listener);
      }

      return this;
    };

/**
 * Removes all listeners, or those of the specified event.
 * It's not a good idea to remove listeners that were added elsewhere in the code,
 * especially when it's on an emitter that you didn't create (e.g. sockets or file streams).
 *
 * Returns emitter, so calls can be chained.
 * @param {String} type Event name
 * @return {EventEmitter}
 */
EventEmitter.prototype.removeAllListeners =
    function removeAllListeners(type) {
      var listeners, events;

      events = this._events;
      if (!events)
        return this;

      // not listening for removeListener, no need to emit
      if (!events.removeListener) {
        if (arguments.length === 0) {
          this._events = {};
          this._eventsCount = 0;
        } else if (events[type]) {
          if (--this._eventsCount === 0)
            this._events = {};
          else
            delete events[type];
        }
        return this;
      }

      // emit removeListener for all listeners on all events
      if (arguments.length === 0) {
        var keys = Object.keys(events);
        for (var i = 0, key; i < keys.length; ++i) {
          key = keys[i];
          if (key === 'removeListener') continue;
          this.removeAllListeners(key);
        }
        this.removeAllListeners('removeListener');
        this._events = {};
        this._eventsCount = 0;
        return this;
      }

      listeners = events[type];

      if (typeof listeners === 'function') {
        this.removeListener(type, listeners);
      } else if (listeners) {
        // LIFO order
        do {
          this.removeListener(type, listeners[listeners.length - 1]);
        } while (listeners[0]);
      }

      return this;
    };

/**
 * Returns an array of listeners for the specified event.
 * @param {String} type Event name
 * @return {Array.<Function>}
 */
EventEmitter.prototype.listeners = function listeners(type) {
  var evlistener;
  var ret;
  var events = this._events;

  if (!events)
    ret = [];
  else {
    evlistener = events[type];
    if (!evlistener)
      ret = [];
    else if (typeof evlistener === 'function')
      ret = [evlistener];
    else
      ret = arrayClone(evlistener, evlistener.length);
  }

  return ret;
};

/**
 * Return the number of listeners for a given event.
 * @param {EventEmitter} emitter
 * @param {String} type
 * @return {Number}
 */
EventEmitter.listenerCount = function(emitter, type) {
  if (typeof emitter.listenerCount === 'function') {
    return emitter.listenerCount(type);
  } else {
    return listenerCount.call(emitter, type);
  }
};

EventEmitter.prototype.listenerCount = listenerCount;
function listenerCount(type) {
  const events = this._events;

  if (events) {
    const evlistener = events[type];

    if (typeof evlistener === 'function') {
      return 1;
    } else if (evlistener) {
      return evlistener.length;
    }
  }

  return 0;
}

// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
  for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
    list[i] = list[k];
  list.pop();
}

function arrayClone(arr, i) {
  var copy = new Array(i);
  while (i--)
    copy[i] = arr[i];
  return copy;
}