<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, shallowRef, watch} from 'vue';

import {defaultKeymap, history, historyKeymap, indentWithTab} from '@codemirror/commands';
import {javascript} from '@codemirror/lang-javascript';
import {python} from '@codemirror/lang-python';
import {
  bracketMatching,
  defaultHighlightStyle,
  indentOnInput,
  StreamLanguage,
  syntaxHighlighting
} from '@codemirror/language';
import {shell} from '@codemirror/legacy-modes/mode/shell';
import {EditorState, type Extension} from '@codemirror/state';
import {
  drawSelection,
  EditorView,
  highlightActiveLine,
  keymap,
  lineNumbers
} from '@codemirror/view';

type EditorLanguage = 'javascript' | 'markdown' | 'python' | 'shell' | 'text';

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

const emit = defineEmits<{
  (event: 'update:modelValue', value: string): void;
}>();

const hostRef = ref<HTMLElement>();
const viewRef = shallowRef<EditorView>();
let syncingFromOutside = false;

function languageExtension(): Extension[] {
  if (props.language === 'python') {
    return [python()];
  }
  if (props.language === 'javascript') {
    return [javascript({ jsx: true, typescript: true })];
  }
  if (props.language === 'shell') {
    return [StreamLanguage.define(shell)];
  }
  return [];
}

function createState(doc: string) {
  return EditorState.create({
    doc,
    extensions: [
      lineNumbers(),
      history(),
      drawSelection(),
      highlightActiveLine(),
      indentOnInput(),
      bracketMatching(),
      syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
      keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
      EditorView.editable.of(!props.readonly),
      EditorState.readOnly.of(props.readonly),
      EditorView.lineWrapping,
      EditorView.updateListener.of((update) => {
        if (!update.docChanged || syncingFromOutside) return;
        emit('update:modelValue', update.state.doc.toString());
      }),
      EditorView.theme({
        '&': {
          height: '100%',
          fontSize: '13px',
          backgroundColor: 'var(--el-bg-color)',
          color: 'var(--el-text-color-primary)',
        },
        '.cm-scroller': {
          fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
        },
        '.cm-gutters': {
          backgroundColor: 'var(--el-fill-color-lighter)',
          borderRight: '1px solid var(--el-border-color-lighter)',
          color: 'var(--el-text-color-secondary)',
        },
        '.cm-activeLine': {
          backgroundColor: 'var(--el-fill-color-light)',
        },
        '.cm-activeLineGutter': {
          backgroundColor: 'var(--el-fill-color-light)',
        },
      }),
      ...languageExtension(),
    ],
  });
}

function mountEditor() {
  if (!hostRef.value) return;
  viewRef.value = new EditorView({
    state: createState(props.modelValue || ''),
    parent: hostRef.value,
  });
}

function recreateEditor() {
  const currentDoc = viewRef.value?.state.doc.toString() ?? props.modelValue ?? '';
  viewRef.value?.destroy();
  viewRef.value = undefined;
  if (!hostRef.value) return;
  viewRef.value = new EditorView({
    state: createState(currentDoc),
    parent: hostRef.value,
  });
}

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, to: currentValue.length, insert: nextValue },
    });
    syncingFromOutside = false;
  },
);

watch(
  () => [props.language, props.readonly],
  () => recreateEditor(),
);

onMounted(mountEditor);

onBeforeUnmount(() => {
  viewRef.value?.destroy();
});
</script>

<template>
  <div ref="hostRef" class="code-mirror-editor"></div>
</template>

<style scoped>
.code-mirror-editor {
  height: 100%;
  min-height: 0;
  overflow: hidden;
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 8px;
}
</style>
