import type { SourceMap } from 'magic-string'
import type { Environment, Plugin, Rollup } from 'vite'
import type { Expression, Positioned } from './esmWalker'
import MagicString from 'magic-string'
import { esmWalker } from './esmWalker'

const regexDynamicImport = /import\s*\(/

export interface DynamicImportPluginOptions {
  /**
   * @default `"__vitest_mocker__"`
   */
  globalThisAccessor?: string
  filter?: (id: string, environment: Environment) => boolean
}

export function dynamicImportPlugin(options: DynamicImportPluginOptions = {}): Plugin {
  return {
    name: 'vitest:browser:esm-injector',
    enforce: 'post',
    transform: {
      order: 'post',
      handler(source, id) {
      // TODO: test is not called for static imports
        if (!regexDynamicImport.test(source)) {
          return
        }
        if (options.filter && !options.filter(id, this.environment)) {
          return
        }
        return injectDynamicImport(source, id, this.parse, options)
      },
    },
  }
}

export interface DynamicImportInjectorResult {
  code: string
  map: SourceMap
}

export function injectDynamicImport(
  code: string,
  id: string,
  parse: Rollup.PluginContext['parse'],
  options: DynamicImportPluginOptions = {},
): DynamicImportInjectorResult | undefined {
  if (code.includes('wrapDynamicImport')) {
    return
  }

  const s = new MagicString(code)

  let ast: ReturnType<Rollup.PluginContext['parse']>
  try {
    ast = parse(code)
  }
  catch (err) {
    console.error(`Cannot parse ${id}:\n${(err as any).message}`)
    return
  }

  // 3. convert references to import bindings & import.meta references
  esmWalker(ast, {
    // TODO: make env updatable
    onImportMeta() {
      // s.update(node.start, node.end, viImportMetaKey)
    },
    onDynamicImport(node) {
      const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"'
      const replaceString = `globalThis[${globalThisAccessor}].wrapDynamicImport(async () => import(`
      const importSubstring = code.substring(node.start, node.end)
      const hasIgnore = importSubstring.includes('/* @vite-ignore */')
      s.overwrite(
        node.start,
        (node.source as Positioned<Expression>).start,
        replaceString + (hasIgnore ? '/* @vite-ignore */ ' : ''),
      )
      s.overwrite(node.end - 1, node.end, '))')
    },
  })

  return {
    code: s.toString(),
    map: s.generateMap({ hires: 'boundary', source: id }),
  }
}
