import { css } from "@emotion/css"
import * as monaco from "monaco-editor"
import Editor from "@monaco-editor/react"
import { useAutoCallback } from "hooks.macro"
import { makeVar, useReactiveVar } from "@apollo/client"
import { memo, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { defineMonacoThemes } from "./themes"
import { useDisposables } from "~/shared/hooks/useDisposables"
import type { Morocco } from "./monaco.d"
import { compilerErrorVars } from "../hooks/useSaveFile"
import { useMonacoOptions, useMonacoTheme } from "./useOptions"

type Editor = monaco.editor.IStandaloneCodeEditor

const _import = <K extends keyof Morocco.ModuleMap>(name: K) =>
  new Promise<Morocco.ModuleMap[K]>(resolve =>
    (globalThis as any).require([name], resolve),
  )

function decorateMonaco({ editor }: typeof monaco) {
  defineMonacoThemes({ editor })
}

interface MonacoProps {
  editorRef: React.MutableRefObject<Editor | undefined>
  fileName: string
  content: string
  readOnly?: boolean
  onContentChange(content?: string): void
}

export const cursorPosition = makeVar<monaco.IPosition | undefined>(undefined)
export const topLevelSymbols = makeVar<readonly monaco.languages.DocumentSymbol[]>([])
export const editorMarkers = makeVar<readonly monaco.Uri[]>([])

export const Monaco = memo<MonacoProps>(props => {
  const { i18n } = useTranslation()
  const { content, onContentChange, fileName, editorRef, readOnly } = props
  const theme = useMonacoTheme()
  const registerDisposable = useDisposables()
  const compilerErrors = useReactiveVar(compilerErrorVars)

  useEffect(() => {
    const editor = editorRef.current
    if (!editor) return

    monaco.editor.setModelMarkers(
      editor.getModel()!,
      "compiler",
      compilerErrors.map(e => ({
        message: e.message.split("\n")[0],
        severity: monaco.MarkerSeverity.Error,
        startLineNumber: e.span.start.line + 1,
        startColumn: e.span.start.column + 1,
        endLineNumber: e.span.end.line + 1,
        endColumn: e.span.end.column + 1,
        source: "compiler",
        code: "error",
      })),
    )
  }, [compilerErrors, editorRef])

  useEffect(() => {
    topLevelSymbols([])
  }, [fileName])

  const onEditorReady = useAutoCallback(async (editor: Editor) => {
    if (process.env.NODE_ENV === "development") {
      Object.assign(globalThis, { editor })
    }

    const { StandaloneServices } = await _import(
      "vs/editor/standalone/browser/standaloneServices",
    )
    const { ILanguageFeaturesService } = await _import(
      "vs/editor/common/services/languageFeatures",
    )
    const { OutlineModel } = await _import(
      "vs/editor/contrib/documentSymbols/browser/outlineModel",
    )
    const { documentSymbolProvider } = StandaloneServices.get(ILanguageFeaturesService)

    registerDisposable(
      editor.onDidChangeCursorPosition(async e => {
        const outline = await OutlineModel.create(
          documentSymbolProvider,
          editor.getModel(),
        )
        cursorPosition(e.position)
        topLevelSymbols(outline.getTopLevelSymbols())
      }),
      monaco.editor.onDidChangeMarkers(editorMarkers),
    )
  })

  const options = useMonacoOptions({ readOnly })

  return (
    <Editor
      className={editorClassName}
      theme={theme}
      defaultValue={content}
      path={fileName}
      options={options}
      onChange={onContentChange}
      onMount={editor => {
        editorRef.current = editor
        editor.getContainerDomNode().setAttribute("lang", getMonacoLangTag(i18n.language))
        void onEditorReady(editor)
      }}
      beforeMount={decorateMonaco}
    />
  )
})

function getMonacoLangTag(language: string) {
  switch (language) {
    case "zh-HK":
    case "zh-TW":
      return "zh-hant"
    case "zh-CN":
      return "zh-Hans"
    default:
      return language
  }
}

const editorClassName = css`
  .monaco-scrollable-element > .scrollbar > .slider {
    border-radius: 10px;
  }
  .monaco-editor .monaco-hover {
    border-radius: 5px;
  }
`
