<script setup lang="ts">
import type { Extension } from '@codemirror/state';

import type { EditorLanguage } from './types';

import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';

import {
  defaultKeymap,
  history,
  historyKeymap,
  indentWithTab,
} from '@codemirror/commands';
import {
  bracketMatching,
  defaultHighlightStyle,
  foldGutter,
  foldKeymap,
  indentOnInput,
  syntaxHighlighting,
} from '@codemirror/language';
import {
  closeSearchPanel,
  highlightSelectionMatches,
  openSearchPanel,
  searchKeymap,
} from '@codemirror/search';
import { Compartment, EditorState } from '@codemirror/state';
import {
  drawSelection,
  dropCursor,
  placeholder as editorPlaceholder,
  EditorView,
  highlightActiveLine,
  highlightActiveLineGutter,
  highlightSpecialChars,
  keymap,
  lineNumbers,
} from '@codemirror/view';

const props = withDefaults(
  defineProps<{
    autofocus?: boolean;
    disabled?: boolean;
    language?: EditorLanguage;
    lineWrapping?: boolean;
    loading?: boolean;
    modelValue: string;
    placeholder?: string;
    readonly?: boolean;
  }>(),
  {
    autofocus: false,
    disabled: false,
    language: 'text',
    lineWrapping: true,
    loading: false,
    placeholder: '',
    readonly: false,
  },
);

const emit = defineEmits<{
  cursorChange: [{ column: number; line: number }];
  save: [];
  'update:modelValue': [value: string];
}>();

const hostRef = ref<HTMLElement>();
const viewRef = shallowRef<EditorView>();
const languageError = ref('');
const languageLoading = ref(false);
const languageSlot = new Compartment();
const readonlySlot = new Compartment();
const wrappingSlot = new Compartment();
let languageRequest = 0;
let syncingFromOutside = false;

async function loadLanguage(language: EditorLanguage): Promise<Extension> {
  if (language === 'python') {
    const { python } = await import('@codemirror/lang-python');
    return python();
  }
  if (language === 'javascript' || language === 'json') {
    const { javascript } = await import('@codemirror/lang-javascript');
    return javascript({ jsx: language === 'javascript', typescript: true });
  }
  if (language === 'markdown') {
    const { markdown } = await import('@codemirror/lang-markdown');
    return markdown();
  }
  if (language === 'shell') {
    const [{ StreamLanguage }, { shell }] = await Promise.all([
      import('@codemirror/language'),
      import('@codemirror/legacy-modes/mode/shell'),
    ]);
    return StreamLanguage.define(shell);
  }
  return [];
}

function readonlyExtensions(readonly: boolean): Extension[] {
  return [EditorState.readOnly.of(readonly), EditorView.editable.of(!readonly)];
}

function isReadonly() {
  return props.readonly || props.disabled || props.loading;
}

function wrappingExtension(enabled: boolean): Extension {
  return enabled ? EditorView.lineWrapping : [];
}

function createState(doc: string, language: Extension) {
  return EditorState.create({
    doc,
    extensions: [
      lineNumbers(),
      foldGutter(),
      highlightSpecialChars(),
      history(),
      drawSelection(),
      dropCursor(),
      indentOnInput(),
      bracketMatching(),
      highlightActiveLine(),
      highlightActiveLineGutter(),
      highlightSelectionMatches(),
      syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
      editorPlaceholder(props.placeholder),
      keymap.of([
        {
          key: 'Mod-s',
          preventDefault: true,
          run: () => {
            if (isReadonly()) return true;
            emit('save');
            return true;
          },
        },
        {
          key: 'Escape',
          run: (view) => {
            if (closeSearchPanel(view)) return true;
            view.contentDOM.blur();
            hostRef.value?.focus();
            return true;
          },
        },
        indentWithTab,
        ...defaultKeymap,
        ...historyKeymap,
        ...searchKeymap,
        ...foldKeymap,
      ]),
      languageSlot.of(language),
      readonlySlot.of(readonlyExtensions(isReadonly())),
      wrappingSlot.of(wrappingExtension(props.lineWrapping)),
      EditorView.updateListener.of((update) => {
        if (update.docChanged && !syncingFromOutside) {
          emit('update:modelValue', update.state.doc.toString());
        }
        if (update.selectionSet || update.docChanged) {
          const head = update.state.selection.main.head;
          const line = update.state.doc.lineAt(head);
          emit('cursorChange', {
            column: head - line.from + 1,
            line: line.number,
          });
        }
      }),
      EditorView.theme({
        '&': {
          backgroundColor: 'hsl(var(--surface-panel))',
          color: 'hsl(var(--foreground))',
          fontSize: '13px',
          height: '100%',
        },
        '&.cm-focused': { outline: 'none' },
        '.cm-content': { caretColor: 'hsl(var(--primary))', padding: '16px 0' },
        '.cm-cursor, .cm-dropCursor': {
          borderLeftColor: 'hsl(var(--primary))',
        },
        '.cm-focused .cm-selectionBackground, ::selection': {
          backgroundColor: 'hsl(var(--primary) / 0.16)',
        },
        '.cm-gutters': {
          backgroundColor: 'hsl(var(--surface-subtle))',
          borderRight: '1px solid hsl(var(--line-subtle))',
          color: 'hsl(var(--text-muted))',
        },
        '.cm-activeLine, .cm-activeLineGutter': {
          backgroundColor: 'hsl(var(--nav-item-hover) / 0.62)',
        },
        '.cm-foldPlaceholder': {
          backgroundColor: 'hsl(var(--surface-contrast-soft))',
          border: '0',
          color: 'hsl(var(--text-muted))',
        },
        '.cm-search': {
          backgroundColor: 'hsl(var(--surface-elevated))',
          borderBottom: '1px solid hsl(var(--line-subtle))',
          color: 'hsl(var(--foreground))',
          padding: '8px 12px',
        },
      }),
    ],
  });
}

async function mountEditor() {
  if (!hostRef.value) return;
  const request = ++languageRequest;
  languageError.value = '';
  languageLoading.value = true;
  try {
    const language = await loadLanguage(props.language);
    if (!hostRef.value || request !== languageRequest) return;
    viewRef.value?.destroy();
    viewRef.value = new EditorView({
      parent: hostRef.value,
      state: createState(props.modelValue || '', language),
    });
    if (props.autofocus) viewRef.value.focus();
  } catch {
    if (request === languageRequest) languageError.value = '编辑器加载失败';
  } finally {
    if (request === languageRequest) languageLoading.value = false;
  }
}

async function updateLanguage(language: EditorLanguage) {
  const view = viewRef.value;
  if (!view) return;
  const request = ++languageRequest;
  languageError.value = '';
  languageLoading.value = true;
  try {
    const extension = await loadLanguage(language);
    if (!viewRef.value || request !== languageRequest) return;
    viewRef.value.dispatch({ effects: languageSlot.reconfigure(extension) });
  } catch {
    if (request === languageRequest) languageError.value = '语法高亮加载失败';
  } finally {
    if (request === languageRequest) languageLoading.value = false;
  }
}

function handleLanguageChange(language: EditorLanguage) {
  if (viewRef.value) {
    void updateLanguage(language);
    return;
  }
  // 首次语言扩展仍在加载时重新发起挂载；generation 会使旧请求失效。
  void mountEditor();
}

function retryLanguage() {
  if (viewRef.value) void updateLanguage(props.language);
  else void mountEditor();
}

function focus() {
  viewRef.value?.focus();
}

function openSearch() {
  if (viewRef.value) openSearchPanel(viewRef.value);
}

function scrollToLine(lineNumber: number, column = 1) {
  const view = viewRef.value;
  if (!view) return;
  const safeLine = Math.min(Math.max(lineNumber, 1), view.state.doc.lines);
  const line = view.state.doc.line(safeLine);
  const position = Math.min(line.from + Math.max(column - 1, 0), line.to);
  view.dispatch({
    effects: EditorView.scrollIntoView(position, { y: 'center' }),
    selection: { anchor: position },
  });
  view.focus();
}

watch(
  () => props.modelValue,
  (value = '') => {
    const view = viewRef.value;
    if (!view) return;
    const nextValue = value;
    const currentValue = view.state.doc.toString();
    if (nextValue === currentValue) return;
    syncingFromOutside = true;
    view.dispatch({
      changes: { from: 0, insert: nextValue, to: currentValue.length },
    });
    syncingFromOutside = false;
  },
);

watch(() => props.language, handleLanguageChange);

watch(
  () => [props.readonly, props.disabled, props.loading],
  () => {
    viewRef.value?.dispatch({
      effects: readonlySlot.reconfigure(readonlyExtensions(isReadonly())),
    });
  },
);

watch(
  () => props.lineWrapping,
  (value) => {
    viewRef.value?.dispatch({
      effects: wrappingSlot.reconfigure(wrappingExtension(value)),
    });
  },
);

onMounted(mountEditor);
onBeforeUnmount(() => {
  languageRequest++;
  viewRef.value?.destroy();
});

defineExpose({ focus, openSearch, scrollToLine });
</script>

<template>
  <div
    ref="hostRef"
    class="easyflow-code-editor"
    :class="{
      'is-disabled': disabled,
      'is-loading': loading || languageLoading,
    }"
    :aria-busy="loading || languageLoading"
    :aria-disabled="disabled || loading"
    role="region"
    tabindex="-1"
    aria-label="代码编辑器，按 Esc 退出编辑区域"
  >
    <div
      v-if="loading || languageLoading"
      class="easyflow-code-editor__loading"
      aria-live="polite"
    >
      正在加载编辑器…
    </div>
    <div
      v-else-if="languageError"
      class="easyflow-code-editor__error"
      role="alert"
    >
      <span>{{ languageError }}</span>
      <button type="button" @click="retryLanguage">重试</button>
    </div>
  </div>
</template>

<style scoped>
.easyflow-code-editor {
  position: relative;
  min-height: 0;
  overflow: hidden;
  background: hsl(var(--surface-panel));
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  transition:
    border-color var(--motion-duration-fast) var(--motion-ease-standard),
    box-shadow var(--motion-duration-fast) var(--motion-ease-standard);
}

.easyflow-code-editor:focus-within {
  border-color: hsl(var(--primary) / 72%);
  box-shadow: 0 0 0 2px hsl(var(--primary) / 12%);
}

.easyflow-code-editor.is-disabled {
  cursor: not-allowed;
  opacity: 0.62;
}

.easyflow-code-editor__loading,
.easyflow-code-editor__error {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  color: hsl(var(--text-muted));
  pointer-events: auto;
  background: hsl(var(--surface-panel) / 90%);
}

.easyflow-code-editor__error {
  gap: var(--space-2);
  color: hsl(var(--destructive));
}

.easyflow-code-editor__error button {
  min-height: 32px;
  padding: 0 var(--space-3);
  color: hsl(var(--nav-item-active-foreground));
  cursor: pointer;
  background: hsl(var(--nav-item-active));
  border: 0;
  border-radius: var(--radius-control);
}

.easyflow-code-editor__error button:hover {
  background: hsl(var(--nav-tool-hover));
}

.easyflow-code-editor__error button:focus-visible {
  outline: 2px solid hsl(var(--primary) / 72%);
  outline-offset: 2px;
}
</style>
