import * as Collections from '../utils/collections';
import Callback from './callback';
import Metadata from '../channels/metadata';
import CallbackRegistry from './callback_registry';

/** Manages callback bindings and event emitting.
 *
 * @param Function failThrough called when no listeners are bound to an event
 */
export default class Dispatcher {
  callbacks: CallbackRegistry;
  global_callbacks: Function[];
  failThrough: Function;

  constructor(failThrough?: Function) {
    this.callbacks = new CallbackRegistry();
    this.global_callbacks = [];
    this.failThrough = failThrough;
  }

  bind(eventName: string, callback: Function, context?: any) {
    this.callbacks.add(eventName, callback, context);
    return this;
  }

  bind_global(callback: Function) {
    this.global_callbacks.push(callback);
    return this;
  }

  unbind(eventName?: string, callback?: Function, context?: any) {
    this.callbacks.remove(eventName, callback, context);
    return this;
  }

  unbind_global(callback?: Function) {
    if (!callback) {
      this.global_callbacks = [];
      return this;
    }

    this.global_callbacks = Collections.filter(
      this.global_callbacks || [],
      c => c !== callback
    );

    return this;
  }

  unbind_all() {
    this.unbind();
    this.unbind_global();
    return this;
  }

  emit(eventName: string, data?: any, metadata?: Metadata): Dispatcher {
    for (var i = 0; i < this.global_callbacks.length; i++) {
      this.global_callbacks[i](eventName, data);
    }

    var callbacks = this.callbacks.get(eventName);
    var args = [];

    if (metadata) {
      // if there's a metadata argument, we need to call the callback with both
      // data and metadata regardless of whether data is undefined
      args.push(data, metadata);
    } else if (data) {
      // metadata is undefined, so we only need to call the callback with data
      // if data exists
      args.push(data);
    }

    if (callbacks && callbacks.length > 0) {
      for (var i = 0; i < callbacks.length; i++) {
        callbacks[i].fn.apply(callbacks[i].context || global, args);
      }
    } else if (this.failThrough) {
      this.failThrough(eventName, data);
    }

    return this;
  }
}
