import type { MonacoSetupReturn } from '@slidev/types'
import { createSingletonPromise } from '@antfu/utils'
import { shikiToMonaco } from '@shikijs/monaco'
import { setupTypeAcquisition } from '@typescript/ata'
import * as monaco from 'monaco-editor'
import EditorWorker from 'monaco-editor/editor/editor.worker?worker'
// @ts-expect-error missing types
import { StandaloneServices } from 'monaco-editor/editor/standalone/browser/standaloneServices'

import CssWorker from 'monaco-editor/language/css/css.worker?worker'
import HtmlWorker from 'monaco-editor/language/html/html.worker?worker'
import JsonWorker from 'monaco-editor/language/json/json.worker?worker'
import TsWorker from 'monaco-editor/language/typescript/ts.worker?worker'
// @ts-expect-error missing types
import { ContextViewService } from 'monaco-editor/platform/contextview/browser/contextViewService'
// @ts-expect-error missing types
import { SyncDescriptor } from 'monaco-editor/platform/instantiation/common/descriptors'
import ts from 'typescript'
import { watchEffect } from 'vue'

import configs from '#slidev/configs'
import setups from '#slidev/setups/monaco'
import { isDark } from '../logic/dark'
import { lockShortcuts } from '../state'

window.MonacoEnvironment = {
  getWorker(_, label) {
    if (label === 'json')
      return new JsonWorker()
    if (label === 'css' || label === 'scss' || label === 'less')
      return new CssWorker()
    if (label === 'html' || label === 'handlebars' || label === 'razor')
      return new HtmlWorker()
    if (label === 'typescript' || label === 'javascript')
      return new TsWorker()
    return new EditorWorker()
  },
}

class ContextViewService2 extends ContextViewService {
  showContextView(...args: any) {
    super.showContextView(...args)
    // @ts-expect-error missing types
    const contextView = this.contextView.view as HTMLElement
    contextView.style.left = `calc(${contextView.style.left} / var(--slidev-slide-scale))`
    contextView.style.top = `calc(${contextView.style.top} / var(--slidev-slide-scale))`
    // Reset the scale to 1. Otherwise, the sub-menu will be in the wrong position.
    contextView.style.transform = `scale(calc(1 / var(--slidev-slide-scale)))`
    contextView.style.transformOrigin = '0 0'
  }
}

// Initialize services first, otherwise we can't override them.
StandaloneServices.initialize({
  contextViewService: new SyncDescriptor(ContextViewService2, [], true),
})

const setup = createSingletonPromise(async () => {
  const defaults = monaco.typescript.typescriptDefaults
  defaults.setCompilerOptions({
    ...defaults.getCompilerOptions(),
    strict: true,
    moduleResolution: monaco.typescript.ModuleResolutionKind.NodeJs,
    module: monaco.typescript.ModuleKind.ESNext,
  })

  const ata = configs.monacoTypesSource === 'cdn'
    ? setupTypeAcquisition({
        projectName: 'TypeScript Playground',
        typescript: ts as any, // Version mismatch. No problem found so far.
        logger: console,
        delegate: {
          receivedFile: (code: string, path: string) => {
            defaults.addExtraLib(code, `file://${path}`)
            const uri = monaco.Uri.file(path)
            if (monaco.editor.getModel(uri) === null)
              monaco.editor.createModel(code, 'javascript', uri)
          },
          progress: (downloaded: number, total: number) => {
          // eslint-disable-next-line no-console
            console.debug(`[Typescript ATA] ${downloaded} / ${total}`)
          },
        },
      })
    : () => { }

  const { getEagerHighlighter, languageNames, themeOption } = await (await import('./shiki')).default()

  monaco.languages.register({ id: 'vue' })
  monaco.languages.register({ id: 'html' })
  monaco.languages.register({ id: 'css' })
  monaco.languages.register({ id: 'typescript' })
  monaco.languages.register({ id: 'javascript' })
  for (const lang of languageNames) {
    monaco.languages.register({ id: lang })
  }

  const editorOptions: MonacoSetupReturn['editorOptions'] & object = {}
  for (const setup of setups) {
    const result = await setup(monaco)
    Object.assign(editorOptions, result?.editorOptions)
  }

  // Disable shortcuts when focusing Monaco editor.
  monaco.editor.onDidCreateEditor((editor) => {
    let release: (() => void) | null = null
    editor.onDidFocusEditorWidget(() => {
      release = lockShortcuts()
    })
    editor.onDidBlurEditorWidget(() => {
      release?.()
      release = null
    })
  })

  // Use Shiki to highlight Monaco
  const highlighter = await getEagerHighlighter()
  shikiToMonaco(highlighter, monaco)
  if (typeof themeOption === 'string') {
    monaco.editor.setTheme(themeOption)
  }
  else {
    watchEffect(() => {
      monaco.editor.setTheme(isDark.value
        ? themeOption.dark || 'vitesse-dark'
        : themeOption.light || 'vitesse-light')
    })
  }

  return {
    monaco,
    ata,
    editorOptions,
  }
})

async function _addFile(raw: () => Promise<{ default: string }>, path: string) {
  const uri = monaco.Uri.file(path)
  const code = (await raw()).default
  monaco.typescript.typescriptDefaults.addExtraLib(code, `file:///${path}`)
  monaco.editor.createModel(code, 'javascript', uri)
}

const addFileCache = new Map<string, Promise<void>>()

export async function addFile(raw: () => Promise<{ default: string }>, path: string) {
  if (addFileCache.has(path))
    return addFileCache.get(path)
  const promise = _addFile(raw, path)
  addFileCache.set(path, promise)
  return promise
}

export default setup
