import {DOM} from 'aurelia-pal';

export interface EventHandler {
  eventName: string;
  bubbles: boolean;
  capture: boolean;
  dispose: Function;
  handler: Function;
}

/**
 * Dispatches subscribets to and publishes events in the DOM.
 * @param element
 */
export class ElementEvents {
  static defaultListenerOptions: boolean | AddEventListenerOptions = true;

  /** @internal */
  element: EventTarget;

  /** @internal */
  private subscriptions: Record<string, EventHandler[]>;

  constructor(element: EventTarget) {
    this.element = element;
    this.subscriptions = {};
  }

  /** @internal */
  _enqueueHandler(handler: EventHandler): void {
    this.subscriptions[handler.eventName] = this.subscriptions[handler.eventName] || [];
    this.subscriptions[handler.eventName].push(handler);
  }

  /** @internal */
  _dequeueHandler(handler: EventHandler): EventHandler {
    let index;
    let subscriptions = this.subscriptions[handler.eventName];
    if (subscriptions) {
      index = subscriptions.indexOf(handler);
      if (index > -1) {
        subscriptions.splice(index, 1);
      }
    }
    return handler;
  }

  /**
   * Dispatches an Event on the context element.
   * @param eventName
   * @param detail
   * @param bubbles
   * @param cancelable
   */
  publish(eventName: string, detail: object = {}, bubbles = true, cancelable = true) {
    let event = DOM.createCustomEvent(eventName, {cancelable, bubbles, detail});
    this.element.dispatchEvent(event);
  }

  /**
   * Adds and Event Listener on the context element.
   * @return Returns the eventHandler containing a dispose method
   */
  subscribe(eventName: string, handler: Function, captureOrOptions?: boolean | AddEventListenerOptions): EventHandler {
    if (typeof handler === 'function') {
      if (captureOrOptions === undefined) {
        captureOrOptions = ElementEvents.defaultListenerOptions;
      }
      const eventHandler = new EventHandlerImpl(this, eventName, handler, captureOrOptions, false);
      return eventHandler;
    }

    return undefined;
  }

  /**
   * Adds an Event Listener on the context element, that will be disposed on the first trigger.
   * @return Returns the eventHandler containing a dispose method
   */
  subscribeOnce(eventName: string, handler: Function, captureOrOptions?: boolean | AddEventListenerOptions): EventHandler {
    if (typeof handler === 'function') {
      if (captureOrOptions === undefined) {
        captureOrOptions = ElementEvents.defaultListenerOptions;
      }
      const eventHandler = new EventHandlerImpl(this, eventName, handler, captureOrOptions, true);
      return eventHandler;
    }

    return undefined;
  }

  /**
   * Removes all events that are listening to the specified eventName.
   * @param eventName
   */
  dispose(eventName: string): void {
    if (eventName && typeof eventName === 'string') {
      let subscriptions = this.subscriptions[eventName];
      if (subscriptions) {
        while (subscriptions.length) {
          let subscription = subscriptions.pop();
          if (subscription) {
            subscription.dispose();
          }
        }
      }
    } else {
      this.disposeAll();
    }
  }

  /**
   * Removes all event handlers.
   */
  disposeAll() {
    for (let key in this.subscriptions) {
      this.dispose(key);
    }
  }
}

class EventHandlerImpl {
  owner: ElementEvents;
  eventName: string;
  handler: Function;
  capture: any;
  bubbles: boolean;
  captureOrOptions: boolean | EventListenerOptions;
  once: boolean;
  constructor(owner: ElementEvents, eventName: string, handler: Function, captureOrOptions: boolean | EventListenerOptions, once: boolean) {
    this.owner = owner;
    this.eventName = eventName;
    this.handler = handler;
    // For compat with interface
    this.capture = typeof captureOrOptions === 'boolean' ? captureOrOptions : captureOrOptions.capture;
    this.bubbles = !this.capture;
    this.captureOrOptions = captureOrOptions;
    this.once = once;
    owner.element.addEventListener(eventName, this, captureOrOptions);
    owner._enqueueHandler(this);
  }

  handleEvent(e: Event) {
    // To keep `undefined` as context, same as the old way
    const fn = this.handler;
    fn(e);
    if (this.once) {
      this.dispose();
    }
  }

  dispose() {
    this.owner.element.removeEventListener(this.eventName, this, this.captureOrOptions);
    this.owner._dequeueHandler(this);
    this.owner = this.handler = null;
  }
}
