import { action } from 'mobx';
import isObject from 'lodash/isObject';
import noop from 'lodash/noop';

type EventTarget = { addEventListener?: Function; removeEventListener?: Function; attachEvent?: Function; detachEvent?: Function };
type EventListenerOrEventListenerObject = Function | { handleEvent: Function };

export type Handler = [EventListenerOrEventListenerObject, EventListenerOptions | AddEventListenerOptions | boolean, Function];

function on(el: EventTarget, eventName: string, handle: Handler, handles: Handler[]): void {
  if (el.addEventListener) {
    const [fn, options] = handle;
    el.addEventListener(eventName, fn, options);
  } else {
    const delegates: Function[] = [];
    handles.forEach(([, , delegateFn]) => {
      if (el.detachEvent) {
        el.detachEvent(`on${eventName}`, delegateFn);
        delegates.unshift(delegateFn);
      }
    });
    delegates.forEach(delegateFn => {
      if (el.attachEvent) {
        el.attachEvent(`on${eventName}`, delegateFn);
      }
    });
  }
}

function off(el: EventTarget, eventName: string, handle: Handler): void {
  const [fn, options, delegateFn] = handle;
  if (el.removeEventListener) {
    el.removeEventListener(eventName, fn, options);
  } else if (el.detachEvent) {
    el.detachEvent(`on${eventName}`, delegateFn);
  }
}

function isEventListenerOptions(options?: EventListenerOptions | boolean): options is EventListenerOptions {
  return isObject(options);
}

function isAddEventListenerOptions(options?: EventListenerOptions | boolean): options is AddEventListenerOptions {
  return isEventListenerOptions(options) && ('once' in options || 'passive' in options);
}

function getCapture(options: EventListenerOptions | boolean): boolean {
  return isEventListenerOptions(options) ? options.capture || false : options;
}

function isSameHandler(handle: Handler, other: Handler): boolean {
  const [handleFn, handleOption] = handle;
  const [otherFn, otherOption] = other;
  return handleFn === otherFn && getCapture(handleOption) === getCapture(otherOption);
}

function callHandler(events: Handler[], handle: Handler, ...rest): any {
  const [, options, delegateFn] = handle;
  if (isAddEventListenerOptions(options) && options.once) {
    const index = events.indexOf(handle);
    if (index !== -1) {
      events.splice(index, 1);
    }
  }
  return delegateFn(...rest);
}

function delegate(fn: EventListenerOrEventListenerObject): Function {
  if ('handleEvent' in fn) {
    return (...rest) => fn.handleEvent(...rest);
  }
  return (...rest) => fn(...rest);
}

export default class EventManager {
  events: { [eventName: string]: Handler[] } = {};

  el?: EventTarget | undefined | null;

  constructor(el?: EventTarget | undefined | null) {
    this.setTarget(el);
  }

  setTarget(el?: EventTarget | undefined | null): EventManager {
    this.el = el;
    return this;
  }

  addEventListener(eventName: string, fn: EventListenerOrEventListenerObject, options: AddEventListenerOptions | boolean = false): EventManager {
    eventName = eventName.toLowerCase();
    const events: Handler[] = this.events[eventName] || [];
    const index = events.findIndex((handle) => isSameHandler(handle, [fn, options, noop]));
    if (index === -1) {
      const newHandle: Handler = [fn, options, delegate(fn)];
      if (getCapture(options)) {
        const captureIndex = events.findIndex(([, handleOptions]) => !getCapture(handleOptions));
        if (captureIndex === -1) {
          events.push(newHandle);
        } else {
          events.splice(captureIndex, 0, newHandle);
        }
      } else {
        events.push(newHandle);
      }
      this.events[eventName] = events;
      const { el } = this;
      if (el) {
        on(el, eventName, newHandle, events);
      }
    }
    return this;
  }

  removeEventListener(eventName: string, fn?: EventListenerOrEventListenerObject, options: EventListenerOptions | boolean = false): EventManager {
    eventName = eventName.toLowerCase();
    const events: Handler[] = this.events[eventName];
    if (events) {
      const { el } = this;
      if (fn) {
        const index = events.findIndex(handle => isSameHandler(handle, [fn, options, noop]));
        if (index !== -1) {
          if (el) {
            off(el, eventName, events[index]);
          }
          events.splice(index, 1);
        }
      } else {
        this.events[eventName] = el
          ? (this.events[eventName] || []).filter((handle) => {
            off(el, eventName, handle);
            return false;
          })
          : [];
      }
    }
    return this;
  }

  @action
  fireEventSync(eventName: string, ...rest: any[]): boolean {
    const events: Handler[] | undefined = this.events[eventName.toLowerCase()];
    return events ? [...events].every(handle => callHandler(events, handle, ...rest) !== false) : true;
  }

  @action
  fireEvent(eventName: string, ...rest: any[]): Promise<boolean> {
    const events: Handler[] | undefined = this.events[eventName.toLowerCase()];
    return events
      ? Promise.all([...events].map((handle) => callHandler(events, handle, ...rest))).then(all =>
        all.every(result => result !== false),
      )
      : Promise.resolve(true);
  }

  clear(): EventManager {
    if (this.el) {
      Object.keys(this.events).forEach(eventName => this.removeEventListener(eventName));
    }
    this.events = {};
    return this;
  }
}

export function preventDefault(e) {
  e.preventDefault();
}

export function stopPropagation(e) {
  e.stopPropagation();
}

export function stopEvent(e) {
  preventDefault(e);
  stopPropagation(e);
}