import type {
  CloneOption,
  DefineWorkerOptions,
  Procedure,
} from './types'
import { startWebWorkerModuleRunner } from './runner'
import {
  createMessageEvent,
  debug,
  getFileIdFromUrl,
} from './utils'

export function createWorkerConstructor(
  options?: DefineWorkerOptions,
): typeof Worker {
  const cloneType = () =>
    (options?.clone
      ?? process.env.VITEST_WEB_WORKER_CLONE
      ?? 'native') as CloneOption

  return class Worker extends EventTarget {
    static __VITEST_WEB_WORKER__ = true

    private _vw_workerTarget = new EventTarget()
    private _vw_insideListeners = new Map<
      string,
      EventListenerOrEventListenerObject
    >()

    private _vw_outsideListeners = new Map<
      string,
      EventListenerOrEventListenerObject
    >()

    private _vw_name: string
    private _vw_messageQueue: any[] | null = []

    public onmessage: null | Procedure = null
    public onmessageerror: null | Procedure = null
    public onerror: null | Procedure = null

    constructor(url: URL | string, options?: WorkerOptions) {
      super()

      let selfProxy: typeof globalThis

      // should be in sync with DedicatedWorkerGlobalScope, but without globalThis
      const context = {
        onmessage: null as null | ((e: MessageEvent) => void),
        onmessageerror: null,
        onerror: null,
        onlanguagechange: null,
        onoffline: null,
        ononline: null,
        onrejectionhandled: null,
        onrtctransform: null,
        onunhandledrejection: null,
        origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000',
        crossOriginIsolated: false,
        name: options?.name || '',
        close: () => this.terminate(),
        dispatchEvent: (event: Event) => {
          return this._vw_workerTarget.dispatchEvent(event)
        },
        addEventListener: (...args: any[]) => {
          if (args[1]) {
            this._vw_insideListeners.set(args[0], args[1])
          }
          return this._vw_workerTarget.addEventListener(...args as [any, any])
        },
        removeEventListener: (...args: any[]) => {
          return this._vw_workerTarget.removeEventListener(...args as [any, any])
        },
        postMessage: (...args: any[]) => {
          if (!args.length) {
            throw new SyntaxError(
              '"postMessage" requires at least one argument.',
            )
          }

          debug(
            'posting message %o from the worker %s to the main thread',
            args[0],
            this._vw_name,
          )
          const event = createMessageEvent(args[0], args[1], cloneType())
          this.dispatchEvent(event)
        },
        get self() {
          return selfProxy
        },
      }

      selfProxy = new Proxy(context, {
        get(target, prop, receiver) {
          if (Reflect.has(target, prop)) {
            return Reflect.get(target, prop, receiver)
          }
          return globalThis[prop as 'crypto']
        },
      }) as any

      this._vw_workerTarget.addEventListener('message', (e) => {
        context.onmessage?.(e as MessageEvent)
      })

      this.addEventListener('message', (e) => {
        this.onmessage?.(e)
      })

      this.addEventListener('messageerror', (e) => {
        this.onmessageerror?.(e)
      })

      const fileId = getFileIdFromUrl(url)

      this._vw_name = fileId

      const runner = startWebWorkerModuleRunner(context)
      runner.mocker.resolveId(fileId).then(({ url, id: resolvedId }) => {
        this._vw_name = options?.name ?? url
        debug('initialize worker %s', this._vw_name)

        return runner.import(url).then(() => {
          runner._invalidateSubTreeById([
            resolvedId,
            runner.mocker.getMockPath(resolvedId),
          ])
          const q = this._vw_messageQueue
          this._vw_messageQueue = null
          if (q) {
            q.forEach(
              ([data, transfer]) => this.postMessage(data, transfer),
              this,
            )
          }
          debug('worker %s successfully initialized', this._vw_name)
        })
      }).catch((e) => {
        debug('worker %s failed to initialize: %o', this._vw_name, e)
        const EventConstructor = globalThis.ErrorEvent || globalThis.Event
        const error = new EventConstructor('error', {
          error: e,
          message: e.message,
        })
        this.dispatchEvent(error)
        this.onerror?.(error)
        console.error(e)
      })
    }

    addEventListener(
      type: string,
      callback: EventListenerOrEventListenerObject | null,
      options?: boolean | AddEventListenerOptions,
    ): void {
      if (callback) {
        this._vw_outsideListeners.set(type, callback)
      }
      return super.addEventListener(type, callback, options)
    }

    postMessage(
      ...args: [any, StructuredSerializeOptions | Transferable[] | undefined]
    ): void {
      if (!args.length) {
        throw new SyntaxError('"postMessage" requires at least one argument.')
      }

      const [data, transferOrOptions] = args
      if (this._vw_messageQueue != null) {
        debug(
          'worker %s is not yet initialized, queue message %s',
          this._vw_name,
          data,
        )
        this._vw_messageQueue.push([data, transferOrOptions])
        return
      }

      debug(
        'posting message %o from the main thread to the worker %s',
        data,
        this._vw_name,
      )

      const event = createMessageEvent(data, transferOrOptions, cloneType())
      if (event.type === 'messageerror') {
        this.dispatchEvent(event)
      }
      else {
        this._vw_workerTarget.dispatchEvent(event)
      }
    }

    terminate() {
      debug('terminating worker %s', this._vw_name)
      this._vw_outsideListeners.forEach((fn, type) => {
        this.removeEventListener(type, fn)
      })
      this._vw_insideListeners.forEach((fn, type) => {
        this._vw_workerTarget.removeEventListener(type, fn)
      })
    }
  }
}
