<script setup lang="ts">
import { NSpin } from 'naive-ui'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'

import { extOf } from '@/components/objectstorage/codeHighlight'

/** 文本编辑器:CodeMirror 6,行号 / 语法高亮 / 搜索;
 *  核心与语言包全部动态 import 独立 chunk,不进编辑不下载。
 *  须放在定高容器内(内部自滚动)。 */
const props = defineProps<{
  modelValue: string
  objectName: string
  disabled?: boolean
}>()
const emit = defineEmits<{ 'update:modelValue': [string] }>()

type CmExtension = import('@codemirror/state').Extension
type CmEditorView = typeof import('@codemirror/view').EditorView

const host = ref<HTMLElement | null>(null)
const booting = ref(true)
const bootError = ref('')

let view: import('@codemirror/view').EditorView | null = null
let EV: CmEditorView | null = null
let editableComp: import('@codemirror/state').Compartment | null = null

onMounted(() => void boot())
onBeforeUnmount(() => view?.destroy())

async function boot() {
  try {
    const [{ basicSetup }, { EditorView }, { Compartment }, lang, theme] = await Promise.all([
      import('codemirror'),
      import('@codemirror/view'),
      import('@codemirror/state'),
      loadLanguage(extOf(props.objectName)),
      themeExtensions(),
    ])
    EV = EditorView
    editableComp = new Compartment()
    view = new EditorView({
      parent: host.value!,
      doc: props.modelValue,
      extensions: [
        basicSetup,
        EditorView.lineWrapping,
        ...(lang ? [lang] : []),
        ...theme,
        editableComp.of(EditorView.editable.of(!props.disabled)),
        EditorView.updateListener.of((u) => {
          if (u.docChanged) emit('update:modelValue', u.state.doc.toString())
        }),
      ],
    })
  } catch (e) {
    bootError.value = e instanceof Error ? e.message : '编辑器加载失败'
  } finally {
    booting.value = false
  }
}

/** 主题与语法配色引用设计 token(明暗自适应),语义与全局 .code-hl 一致 */
async function themeExtensions(): Promise<CmExtension[]> {
  const [{ HighlightStyle, syntaxHighlighting }, { tags: t }, { EditorView }] = await Promise.all([
    import('@codemirror/language'),
    import('@lezer/highlight'),
    import('@codemirror/view'),
  ])
  const hl = HighlightStyle.define([
    { tag: [t.keyword, t.operatorKeyword, t.modifier], color: 'var(--color-warn)' },
    { tag: [t.string, t.special(t.string), t.regexp], color: 'var(--color-ok)' },
    { tag: [t.number, t.bool, t.null, t.atom], color: 'var(--color-accent)' },
    { tag: [t.comment, t.meta], color: 'var(--color-ink-3)', fontStyle: 'italic' },
    { tag: [t.propertyName, t.attributeName], color: 'var(--color-info)' },
    { tag: [t.typeName, t.className, t.tagName], color: 'var(--color-accent)' },
    { tag: t.heading, color: 'var(--color-ink)', fontWeight: '600' },
    { tag: t.link, color: 'var(--color-accent)' },
  ])
  const theme = EditorView.theme({
    '&': { height: '100%', fontSize: '12px', backgroundColor: 'transparent' },
    '.cm-scroller': {
      fontFamily: "ui-monospace, 'SF Mono', Menlo, Consolas, monospace",
      lineHeight: '1.65',
      overflow: 'auto',
    },
    '.cm-gutters': {
      backgroundColor: 'var(--color-wash)',
      color: 'var(--color-ink-3)',
      border: 'none',
    },
    '.cm-activeLineGutter': { backgroundColor: 'var(--color-wash)' },
    '.cm-activeLine': { backgroundColor: 'color-mix(in srgb, var(--color-wash) 55%, transparent)' },
    '&.cm-focused': { outline: 'none' },
    '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
      backgroundColor: 'color-mix(in srgb, var(--color-accent) 18%, transparent)',
    },
  })
  // 不带 fallback:作为正式高亮器压过 basicSetup 内置的 defaultHighlightStyle(其为 fallback)
  return [theme, syntaxHighlighting(hl)]
}

/** 扩展名 → CodeMirror 语言扩展;不在表内按纯文本编辑 */
const CM_LANG: Record<string, () => Promise<CmExtension>> = {
  json: () => import('@codemirror/lang-json').then((m) => m.json()),
  yaml: () => import('@codemirror/lang-yaml').then((m) => m.yaml()),
  yml: () => import('@codemirror/lang-yaml').then((m) => m.yaml()),
  py: () => import('@codemirror/lang-python').then((m) => m.python()),
  js: () => import('@codemirror/lang-javascript').then((m) => m.javascript()),
  ts: () => import('@codemirror/lang-javascript').then((m) => m.javascript({ typescript: true })),
  xml: () => import('@codemirror/lang-xml').then((m) => m.xml()),
  svg: () => import('@codemirror/lang-xml').then((m) => m.xml()),
  html: () => import('@codemirror/lang-html').then((m) => m.html()),
  htm: () => import('@codemirror/lang-html').then((m) => m.html()),
  css: () => import('@codemirror/lang-css').then((m) => m.css()),
  md: () => import('@codemirror/lang-markdown').then((m) => m.markdown()),
  sql: () => import('@codemirror/lang-sql').then((m) => m.sql()),
  go: () => import('@codemirror/lang-go').then((m) => m.go()),
  sh: () => import('@codemirror/legacy-modes/mode/shell').then((m) => streamOf(m.shell)),
  ini: () => import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
  conf: () =>
    import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
  cfg: () => import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
  env: () => import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
  properties: () =>
    import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
  toml: () => import('@codemirror/legacy-modes/mode/toml').then((m) => streamOf(m.toml)),
}

async function streamOf<T>(def: import('@codemirror/language').StreamParser<T>) {
  const { StreamLanguage } = await import('@codemirror/language')
  return StreamLanguage.define(def)
}

async function loadLanguage(ext: string): Promise<CmExtension | null> {
  const load = CM_LANG[ext]
  if (!load) return null
  try {
    return await load()
  } catch {
    return null
  }
}

watch(
  () => props.modelValue,
  (v) => {
    // 外部改写(如美化格式)同步进编辑器;自身输入产生的 emit 值相等,跳过
    if (view && v !== view.state.doc.toString())
      view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: v } })
  },
)

watch(
  () => props.disabled,
  (d) => {
    if (view && editableComp && EV)
      view.dispatch({ effects: editableComp.reconfigure(EV.editable.of(!d)) })
  },
)
</script>

<template>
  <div class="flex h-full min-h-0 flex-col">
    <div v-if="booting" class="flex flex-1 items-center justify-center">
      <NSpin size="small" />
    </div>
    <div v-else-if="bootError" class="flex flex-1 items-center justify-center text-xs text-err">
      {{ bootError }}
    </div>
    <div ref="host" class="cm-host min-h-0 flex-1" :class="booting || bootError ? 'hidden' : ''"></div>
  </div>
</template>

<style scoped>
.cm-host :deep(.cm-editor) {
  height: 100%;
}
</style>
