/**
 *  Event caller/listener system. Intended to be extended by classes which require
 *  an event callback system.
 */
export class EventCaller{
  constructor(){
    this.__listeners = {};
  }

  /**
   *  @type {number}
   */
  get totalListeners(){
    var count = 0;
    for (key in Object.keys(this.__listeners)){
      count += this.__listeners[key].length
    }
    return count;
  }

  /**
   *  @type {Array[string]}
   */
  get watchedEvents(){
    return Object.keys(this.__listeners);
  }

  /**
   *  Returns true if the given callback is listening for the given event, and false otherwise.
   *  @param {string} eventName - The name of the event to check.
   *  @param {Function} callback - The function/method to check for
   *  @param {Object} [owner=null] - The object to use as "this" when calling the callback function.
   *  @returns {bool}
   */
  is_listening(eventName, callback, owner=null){
    if (typeof(eventName) !== 'string')
      throw new TypeError("Expected eventName to be string.");
    if (eventName.length <= 0)
      throw new ValueError("Argument eventName cannot be a zero-length string.");

    if (this.__listeners.hasOwnProperty(eventName)){
      if (typeof(callback) !== 'function')
        throw new TypeError("Expected callback argument to be a function or class method.");
      for (var i=0; i < this.__listeners[eventName].length; i++){
        if (this.__listeners[eventName][i][0] === callback && 
          this.__listeners[eventName][i][1] === owner){
          return true;
        }
      }
    }
    return false;
  }

  /**
   *  Sets the given callback to listen for the given event.
   *  If [once] is true, the callback will be cleared from the listeners list after the named
   *  event has been emitted.
   *  @param {string} eventName - The name of the event to listen for.
   *  @param {Function} callback - The function to call when the given event is emitted.
   *  @param {Object} [owner=null] - The owner to use when calling the callback.
   *  @param {bool} [once=false] - If true, the callback will be cleared after the next emit of the given event.
   *  @returns {this}
   */
  listen(eventName, callback, owner=null, once=false){
    if (typeof(callback) !== 'function')
      throw new TypeError("Expected callback argument to be a function or class method.");
    try{
      if (!this.is_listening(eventName, callback, owner)){
        if (!this.__listeners.hasOwnProperty(eventName)){
          this.__listeners[eventName] = [];
        }
        this.__listeners[eventName].push([callback, owner, once]);
      }
    } catch (e) {
      throw e;
    }
    return this;
  }

  /**
   *  Removes the given callback from the given event.
   *  NOTE: If the listener callback was set with it's owner, that owner must be included
   *  to remove the listener callback successfully.
   *  @param {string} eventName - The name of the event to remove from.
   *  @param {Function} callback - The function/method to remove.
   *  @param {Object} [owner=null] - The owner of the callback.
   *  @returns {this}
   */
  unlisten(eventName, callback, owner=null){
    if (typeof(eventName) !== 'string')
      throw new TypeError("Expected eventName to be string.");
    if (eventName.length <= 0)
      throw new ValueError("Argument eventName cannot be a zero-length string.");

    if (this.__listeners.hasOwnProperty(eventName)){
      if (typeof(callback) !== 'function')
        throw new TypeError("Expected callback argument to be a function or class method.");
      for (var i=0; i < this.__listeners[eventName].length; i++){
        if (this.__listeners[eventName][i][0] === callback && 
          this.__listeners[eventName][i][1] === owner){
          this.__listeners[eventName].splice(i, 1);
          if (this.__listeners[eventName].length <= 0)
            delete this.__listeners[eventName];
          break;
        }
      }
    }
    return this;
  }

  /**
   *  Removes all listeners from the given event.
   *  @param {string} eventName - The name of the event to clear listeners from.
   *  @returns {this}
   */
  unlisten_event(eventName){
    if (typeof(eventName) !== 'string')
      throw new TypeError("Expected eventName to be string.");
    if (eventName.length <= 0)
      throw new ValueError("Argument eventName cannot be a zero-length string.");
    if (this.__listener.hasOwnProperty(eventName))
      delete this.__listener[eventName];
    return this;
  }

  /**
   *  Removes all listeners.
   *  @returns {this}
   */
  unlisten_all(){
    // NOTE: Perhaps it's better to loop through and delete each property? This should do for now though.
    this.__listeners = {};
    return this;
  }


  /**
   *  Returns the count of listeners attached to the given event.
   *  @param {string} eventName - The name of the event to get the count of.
   *  @returns {number}
   */
  event_listener_count(eventName){
    if (typeof(eventName) !== 'string')
      throw new TypeError("Expected eventName to be string.");
    if (eventName.length <= 0)
      throw new ValueError("Argument eventName cannot be a zero-length string.");
    if (this.__listener.hasOwnProperty(eventName)){
      return this.__listener[eventName].length;
    }
    return 0;
  }


  /**
   *  Emits the given event, calling all listener callbacks attached to the event and passing each
   *  the arguments (if any) given.
   *  NOTE: All listeners of the given event designated to only listen once will be removed after
   *  this call.
   *  @param {string} eventName - The name of the event to emit.
   *  @param {...*} args - The arguments to pass to every listener of this event.
   *  @returns {this}
   */
  emit(){
    var args = Array.from(arguments);
    if (args.length <= 0)
      throw new Error("Missing required eventName argument.");
    var eventName = args[0];
    args = args.slice(1);

    if (typeof(eventName) !== 'string')
      throw new TypeError("Expected eventName to be string.");
    if (eventName.length <= 0)
      throw new ValueError("Argument eventName cannot be a zero-length string.");

    var once = [];
    if (this.__listeners.hasOwnProperty(eventName)){
      for (var i=0; i < this.__listeners[eventName].length; i++){
        var cb = this.__listeners[eventName][i][0];
        var own = this.__listeners[eventName][i][1];
        if (this.__listeners[eventName][i][2] === true)
          once.push([cb, own]);
        cb.apply(own, args);
      }

      if (once.length > 0){
        for (var i=0; i < once.length; i++){
          this.unlisten(eventName, once[i][0], once[i][1]);
        }
      }
    }

    return this;
  }
}


const GlobalEvent = new EventCaller();
export default GlobalEvent;