// @author pavel.mash

/**
 * Exports client side EventEmitter
 *
 * @module events
 * @memberOf module:@unitybase/ub-pub
 */

/**
 * NodeJS like EventEmitter for Browser usage. See also <a href="http://nodejs.org/api/events.html">NodeJS events documentation</a>
 *
 * @example
 // adding event emitting to any object:
 var myObject = {},
 var EventEmitter = UB.EventEmitter;
 // add EventEmitter to myObject
 EventEmitter.call(myObject);
 Object.assign(myObject, EventEmitter.prototype);
 // In case object created via constructor function
 var EventEmitter = UB.EventEmitter;
 function MyObject() {
    EventEmitter.call(this);
 }
 MyObject.prototype = _.create(EventEmitter.prototype);
 var myObject = new MyObject();
 myObject instanceof UB.EventEmitter; //true
 // usage:
 myObject.on('myEvent', function(num, str){console.log(num, str) });
 myObject.emit('myEvent', 1, 'two'); // output: 1 "two"
 * @class EventEmitter
 */
function EventEmitter () {
  EventEmitter.init.call(this)
}

/**
 * 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 20 listeners are
// added to it. This is a useful default which helps finding memory leaks.
EventEmitter.defaultMaxListeners = 20

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

  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
}

/**
 *
 * @param that
 */
function $getMaxListeners (that) {
  if (that._maxListeners === undefined) { return EventEmitter.defaultMaxListeners }
  return that._maxListeners
}

/**
 *
 * @returns {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.
/**
 *
 * @param handler
 * @param isFn
 * @param self
 */
function emitNone (handler, isFn, self) {
  if (isFn) { handler.call(self) } else {
    const len = handler.length
    const listeners = arrayClone(handler, len)
    for (let i = 0; i < len; ++i) { listeners[i].call(self) }
  }
}

/**
 *
 * @param handler
 * @param isFn
 * @param self
 * @param arg1
 */
function emitOne (handler, isFn, self, arg1) {
  if (isFn) { handler.call(self, arg1) } else {
    const len = handler.length
    const listeners = arrayClone(handler, len)
    for (let i = 0; i < len; ++i) { listeners[i].call(self, arg1) }
  }
}

/**
 *
 * @param handler
 * @param isFn
 * @param self
 * @param arg1
 * @param arg2
 */
function emitTwo (handler, isFn, self, arg1, arg2) {
  if (isFn) { handler.call(self, arg1, arg2) } else {
    const len = handler.length
    const listeners = arrayClone(handler, len)
    for (let i = 0; i < len; ++i) { listeners[i].call(self, arg1, arg2) }
  }
}

/**
 *
 * @param handler
 * @param isFn
 * @param self
 * @param arg1
 * @param arg2
 * @param arg3
 */
function emitThree (handler, isFn, self, arg1, arg2, arg3) {
  if (isFn) { handler.call(self, arg1, arg2, arg3) } else {
    const len = handler.length
    const listeners = arrayClone(handler, len)
    for (let i = 0; i < len; ++i) { listeners[i].call(self, arg1, arg2, arg3) }
  }
}

/**
 *
 * @param handler
 * @param isFn
 * @param self
 * @param args
 */
function emitMany (handler, isFn, self, args) {
  if (isFn) { handler.apply(self, args) } else {
    const len = handler.length
    const listeners = arrayClone(handler, len)
    for (let i = 0; i < len; ++i) { 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
 * @returns {boolean}
 */
EventEmitter.prototype.emit = function emit (type) {
  let er, args, i
  let doError = (type === 'error')

  const 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
      const err = new Error('Uncaught, unspecified "error" event. (' + er + ')')
      err.context = er
      throw err
    }
  }

  const handler = events[type]

  if (!handler) { return false }

  const isFn = typeof handler === 'function'
  const len = arguments.length
  switch (len) {
    // fast cases
    case 1:
      emitNone(handler, isFn, this)
      break
    case 2:
      emitOne(handler, isFn, this, arguments[1])
      break
    case 3:
      emitTwo(handler, isFn, this, arguments[1], arguments[2])
      break
    case 4:
      emitThree(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(handler, isFn, this, args)
  }

  return true
}

/**
 *
 * @param listener
 */
function checkListener (listener) {
  if (typeof listener !== 'function') { throw new TypeError('listener must be a function') }
}

/**
 *
 * @param target
 * @param type
 * @param listener
 * @param prepend
 */
function _addListener (target, type, listener, prepend) {
  let m
  let events
  let 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
 * @returns {EventEmitter}
 */
EventEmitter.prototype.addListener = function addListener (type, listener) {
  return _addListener(this, type, listener, false)
}

/**
 * Alias for {@link EventEmitter#addListener addListener}
 *
 * @function
 * @param {string} type Event name
 * @param {Function} listener
 * @returns {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.
 *
 * @function
 * @param {string} type Event name
 * @param {Function} listener
 * @returns {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)
  }
}

/**
 *
 * @param target
 * @param type
 * @param listener
 */
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
 * @returns {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
 * @returns {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) {
    let position, i

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

    const events = this._events
    if (!events) { return this }

    const 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) {
          /** @event 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
 * @returns {EventEmitter}
 */
EventEmitter.prototype.removeAllListeners =
  function removeAllListeners (type) {
    const 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) {
      const keys = Object.keys(events)
      let i = 0; let key
      for (; i < keys.length; ++i) {
        key = keys[i]
        if (key === 'removeListener') continue
        this.removeAllListeners(key)
      }
      this.removeAllListeners('removeListener')
      this._events = {}
      this._eventsCount = 0
      return this
    }

    const 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
 * @returns {Array.<Function>}
 */
EventEmitter.prototype.listeners = function listeners (type) {
  let evlistener
  let ret
  const 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
 * @returns {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

/**
 *
 * @param type
 */
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().
/**
 *
 * @param list
 * @param index
 */
function spliceOne (list, index) {
  let i = index; let k = i + 1
  const n = list.length
  for (; k < n; i += 1, k += 1) { list[i] = list[k] }
  list.pop()
}

/**
 *
 * @param arr
 * @param i
 */
function arrayClone (arr, i) {
  const copy = new Array(i)
  while (i--) { copy[i] = arr[i] }
  return copy
}

module.exports = EventEmitter