import { PluginObject } from 'vue';

declare module 'vue/types/vue' {
  interface Vue {
    // todo fn type
    $offGlobal(event: string, handler?: any): boolean;
    $onGlobal(event: string, handler: any): boolean;
    $awaitGlobal<T = any>(
      event: string,
      timeout?: number,
      abort?: AbortSignal
    ): Promise<T>;
    $awaitGlobal<T = any>(
      event: string,
      abort?: AbortSignal,
      timeout?: number
    ): Promise<T>;
    $emitGlobal<T = any>(event: string, data?: T): void;
  }
}

export default {
  install(Vue) {
    Object.defineProperties(Vue.prototype, {
      __eventBus: {
        value: new Vue(),
      },
      __eventList: {
        value: Object.create(null),
      },
      $onGlobal: {
        enumerable: true,
        value(event: string, func: any) {
          if (!this.__eventList[event]) this.__eventList[event] = [];
          this.__eventList[event].push(func);
          this.__eventBus.$on(event, func);
        },
      },
      $offGlobal: {
        enumerable: true,
        value(event: string, func: any) {
          const list = this.__eventList[event];
          if (!list) return false;
          if (!func) {
            list.forEach((f: any) => this.__eventBus.$off(event, f));
            return delete this.__eventList[event];
          }

          const index = list.lastIndexOf(func);
          if (index === -1) return false;

          this.__eventBus.$off(event, func);

          if (list.length === 1) return delete this.__eventList[event];

          list.splice(index, 1);

          return true;
        },
      },
      $awaitGlobal: {
        enumerable: true,
        value(
          this: Vue,
          event: string,
          ...args: [AbortSignal, number] | [number | AbortSignal]
        ) {
          const { timeout = 60000, abort } = args.reduce(
            (obj, arg) => {
              if (typeof arg === 'object') obj.abort = arg;
              else if (typeof arg === 'number') obj.timeout = arg;
              return obj;
            },
            Object.create(null) as {
              abort?: AbortSignal;
              timeout?: number;
            }
          );

          return new Promise((resolve, reject) => {
            let abortHandler: () => void;
            if (abort) {
              const reason = `Aborted waiting for "${event}"`;
              if (abort.aborted) return reject(reason);
              // eslint-disable-next-line @typescript-eslint/no-use-before-define
              abortHandler = () => handler(reject, reason);
            }
            // eslint-disable-next-line @typescript-eslint/no-use-before-define
            const rejectHandler = (reason: unknown) => handler(reject, reason);
            // eslint-disable-next-line @typescript-eslint/no-use-before-define
            const resolveHandler = (data: unknown) => handler(resolve, data);

            const timer = setTimeout(
              rejectHandler,
              timeout,
              `Timeout waiting for "${event}"`
            );

            this.$onGlobal(event, resolveHandler);

            const handler = (
              action: (value?: unknown) => void,
              data: unknown
            ) => {
              abort?.removeEventListener('abort', abortHandler);
              clearTimeout(timer);
              this.$offGlobal(event, resolveHandler);
              return action(data);
            };
          });
        },
      },
      $emitGlobal: {
        enumerable: true,
        value(event: string, ...data: unknown[]) {
          this.__eventBus.$emit(event, ...data);
        },
      },
    });

    Vue.mixin({
      beforeCreate() {
        Object.defineProperty(this, '__eventList', {
          configurable: true,
          value: Object.create(null),
        });
      },
      beforeDestroy(this: Vue & { __eventList: Array<any> }) {
        for (const event in this.__eventList) {
          this.$offGlobal(event);
        }
      },
    });
  },
} as PluginObject<undefined>;
