<template>
  <div class="magic-code-editor">
    <Teleport to="body" :disabled="!fullScreen">
      <div :class="{ 'magic-code-editor-wrapper': true, 'full-screen': fullScreen }" :style="{ height: computeHeight }">
        <TMagicButton
          v-if="!disabledFullScreen"
          class="magic-code-editor-full-screen-icon"
          circle
          size="small"
          @click="fullScreenHandler"
          ><MIcon :icon="FullScreen"></MIcon
        ></TMagicButton>
        <div ref="codeEditor" class="magic-code-editor-content"></div>
      </div>
    </Teleport>
  </div>
</template>

<script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
import { FullScreen } from '@element-plus/icons-vue';
import { throttle } from 'lodash-es';
import type * as Monaco from 'monaco-editor';

import { TMagicButton } from '@tmagic/design';

import MIcon from '@editor/components/Icon.vue';
import { getEditorConfig } from '@editor/utils/config';
import { serializeConfig } from '@editor/utils/editor';
import loadMonaco from '@editor/utils/monaco-editor';

defineOptions({
  name: 'MEditorCodeEditor',
});

const props = withDefaults(
  defineProps<{
    initValues?: any;
    modifiedValues?: any;
    type?: 'diff';
    language?: string;
    options?: Monaco.editor.IStandaloneEditorConstructionOptions;
    height?: string;
    autoSave?: boolean;
    parse?: boolean;
    disabledFullScreen?: boolean;
    autosize?: {
      minRows?: number;
      maxRows?: number;
    };
    editorCustomType?: string;
  }>(),
  {
    initValues: '',
    autoSave: true,
    language: 'javascript',
    options: () => ({
      tabSize: 2,
    }),
    parse: false,
    disabledFullScreen: false,
  },
);

const emit = defineEmits(['initd', 'save']);

const autoHeight = ref<string>('');
let cachedExtraHeight: number | null = null;

const computeHeight = computed(() => {
  if (fullScreen.value) {
    return '100%';
  }

  if (props.height) {
    return props.height;
  }

  if (props.autosize) {
    return autoHeight.value;
  }

  return '100%';
});

const calculateExtraHeight = (): number => {
  let extraHeight = 10; // 默认值

  if (vsEditor && codeEditorEl.value) {
    try {
      // 获取编辑器容器的总高度和内容区域高度
      const editorElement = codeEditorEl.value.querySelector('.monaco-editor');
      const scrollableElement = codeEditorEl.value.querySelector('.monaco-scrollable-element');

      if (editorElement && scrollableElement) {
        const editorRect = editorElement.getBoundingClientRect();
        const scrollableRect = scrollableElement.getBoundingClientRect();

        // 计算编辑器的边框、内边距等额外高度
        extraHeight = Math.max(editorRect.height - scrollableRect.height, 0);

        // 如果无法获取到有效的差值，使用编辑器配置中的相关选项
        if (extraHeight === 0 && monaco) {
          const editorOptions = vsEditor.getOptions();
          const scrollBeyondLastLine = editorOptions.get(monaco.editor.EditorOption.scrollBeyondLastLine);
          const padding = editorOptions.get(monaco.editor.EditorOption.padding);
          const lineHeight = editorOptions.get(monaco.editor.EditorOption.lineHeight) || 20;

          extraHeight = (scrollBeyondLastLine ? lineHeight : 0) + (padding?.top || 0) + (padding?.bottom || 0) + 10; // 基础边距
        }
      }
    } catch (error) {
      // 如果获取失败，保持默认值
      console.warn('Failed to calculate editor extra height:', error);
    }
  }

  return extraHeight;
};

const setAutoHeight = (v = '') => {
  let lines = Math.max(v.split('\n').length, props.autosize?.minRows || 1);
  if (v) {
    if (props.autosize?.maxRows) {
      lines = Math.min(lines, props.autosize.maxRows);
    }
  }

  // 获取编辑器实际行高，如果编辑器还未初始化则使用默认值
  let lineHeight = 20;
  if (vsEditor && monaco) {
    const editorOptions = vsEditor.getOptions();
    lineHeight = editorOptions.get(monaco.editor.EditorOption.lineHeight) || 20;
  }

  // 获取缓存的额外高度，如果没有缓存则计算并缓存
  if (cachedExtraHeight === null) {
    cachedExtraHeight = calculateExtraHeight();
  }

  const newHeight = `${lines * lineHeight + cachedExtraHeight}px`;

  // 只有当高度真正改变时才更新
  if (autoHeight.value !== newHeight) {
    autoHeight.value = newHeight;

    // 高度变化后需要重新布局编辑器
    nextTick(() => {
      vsEditor?.layout();

      // 确保内容在可视区域内，滚动到顶部
      if (vsEditor) {
        vsEditor.setScrollTop(0);
        vsEditor.revealLine(1);
      }
    });
  }
};

const toString = (v: string | any, language: string): string => {
  let value: string;
  if (typeof v !== 'string') {
    if (language === 'json') {
      value = JSON.stringify(v, null, 2);
    } else {
      value = serializeConfig(v);
    }
  } else {
    value = v;
  }
  if (language === 'javascript' && value.startsWith('{') && value.endsWith('}')) {
    value = `(${value})`;
  }
  return value;
};

const parseCode = (v: string | any, language: string): any => {
  if (typeof v !== 'string') {
    return v;
  }

  if (language === 'json') {
    return JSON.parse(v);
  }

  return getEditorConfig('parseDSL')(v);
};

let monaco: typeof import('monaco-editor') | null = null;
let vsEditor: Monaco.editor.IStandaloneCodeEditor | null = null;
let vsDiffEditor: Monaco.editor.IStandaloneDiffEditor | null = null;

const values = ref('');
const loading = ref(false);
const codeEditorEl = useTemplateRef<HTMLDivElement>('codeEditor');

const resizeObserver = new globalThis.ResizeObserver(
  throttle((): void => {
    vsEditor?.layout();
    vsDiffEditor?.layout();
  }, 300),
);

const setEditorValue = (v: string | any, m: string | any) => {
  values.value = toString(v, props.language.toLocaleLowerCase());

  setAutoHeight(values.value);

  if (!monaco) return;

  if (props.type === 'diff') {
    const originalModel = monaco.editor.createModel(values.value, 'text/javascript');
    const modifiedModel = monaco.editor.createModel(toString(m, props.language), 'text/javascript');
    // 保存视图状态（光标、选区、滚动、折叠等）
    const viewState = vsDiffEditor?.saveViewState();
    const result = vsDiffEditor?.setModel({
      original: originalModel,
      modified: modifiedModel,
    });
    // setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0，这里也放到 nextTick 中
    // 利用 Vue nextTick 队列的 FIFO 特性，保证恢复在重置之后执行
    if (viewState) {
      nextTick(() => {
        vsDiffEditor?.restoreViewState(viewState);
        vsDiffEditor?.focus();
      });
    }
    return result;
  }
  // 保存视图状态（光标、选区、滚动、折叠等）
  const viewState = vsEditor?.saveViewState();
  const result = vsEditor?.setValue(values.value);
  // setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0，这里也放到 nextTick 中
  // 利用 Vue nextTick 队列的 FIFO 特性，保证恢复在重置之后执行
  if (viewState) {
    nextTick(() => {
      vsEditor?.restoreViewState(viewState);
      vsEditor?.focus();
    });
  }
  return result;
};

const getEditorValue = () =>
  (props.type === 'diff' ? vsDiffEditor?.getModifiedEditor().getValue() : vsEditor?.getValue()) || '';

const handleKeyDown = (e: KeyboardEvent) => {
  if (e.keyCode === 83 && (navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey)) {
    e.preventDefault();
    e.stopPropagation();
    const newValue = getEditorValue();
    values.value = newValue;
    emit('save', props.parse ? parseCode(newValue, props.language) : newValue);
  }
};

const init = async () => {
  if (!codeEditorEl.value) return;

  if (codeEditorEl.value.clientHeight === 0) {
    await nextTick();
  }

  // 重置缓存的额外高度，因为编辑器重新初始化
  cachedExtraHeight = null;

  monaco = await loadMonaco();

  const options = {
    value: values.value,
    language: props.language,
    theme: 'vs-dark',
    editorCustomType: props.editorCustomType,
    ...props.options,
  };

  if (props.type === 'diff') {
    vsDiffEditor = await getEditorConfig('customCreateMonacoDiffEditor')(monaco!, codeEditorEl.value, options);

    // 监听diff编辑器内容变化
    vsDiffEditor.getModifiedEditor().onDidChangeModelContent(() => {
      // 如果使用 autosize，内容变化时重新计算高度
      if (props.autosize) {
        setAutoHeight(getEditorValue());
      }
    });
  } else {
    vsEditor = await getEditorConfig('customCreateMonacoEditor')(monaco!, codeEditorEl.value, options);

    // 监听编辑器内容变化
    vsEditor.onDidChangeModelContent(() => {
      // 如果使用 autosize，内容变化时重新计算高度
      if (props.autosize) {
        setAutoHeight(getEditorValue());
      }
    });
  }

  setEditorValue(props.initValues, props.modifiedValues);

  emit('initd', vsEditor);
  codeEditorEl.value.addEventListener('keydown', handleKeyDown);

  if (props.type !== 'diff' && props.autoSave) {
    vsEditor?.onDidBlurEditorWidget(() => {
      const newValue = getEditorValue();
      if (values.value !== newValue) {
        values.value = newValue;
        emit('save', props.parse ? parseCode(newValue, props.language) : newValue);
      }
    });
  }

  resizeObserver.observe(codeEditorEl.value);
};

watch(
  () => props.initValues,
  (v, preV) => {
    if (v !== preV) {
      setEditorValue(props.initValues, props.modifiedValues);
    }
  },
  {
    deep: true,
    immediate: true,
  },
);

// diff 模式下，对比的"当前值"（modifiedValues）也可能在外部变化（例如 lastValues 不变、当前 model 变了），
// 此时同样需要重新设置编辑器值，否则右侧编辑器内容会停留在初始化时的快照。
watch(
  () => props.modifiedValues,
  (v, preV) => {
    if (props.type !== 'diff') return;
    if (v !== preV) {
      setEditorValue(props.initValues, props.modifiedValues);
    }
  },
  {
    deep: true,
  },
);

watch(
  () => props.options,
  (v) => {
    vsEditor?.updateOptions(v);
    vsDiffEditor?.updateOptions(v);
  },
  {
    deep: true,
  },
);

onMounted(async () => {
  loading.value = true;

  await init();

  loading.value = false;
});

onBeforeUnmount(() => {
  resizeObserver.disconnect();

  vsEditor?.dispose();
  vsDiffEditor?.dispose();

  vsEditor = null;
  vsDiffEditor = null;
  monaco = null;

  // 重置缓存
  cachedExtraHeight = null;
});
onUnmounted(() => {
  codeEditorEl.value?.removeEventListener('keydown', handleKeyDown);
});
const fullScreen = ref(false);
const fullScreenHandler = () => {
  fullScreen.value = !fullScreen.value;
  setTimeout(() => {
    vsEditor?.focus();
    vsEditor?.layout();
    vsDiffEditor?.focus();
    vsDiffEditor?.layout();
  });
};

defineExpose({
  values,

  getEditor() {
    return vsEditor || vsDiffEditor;
  },

  getVsEditor() {
    return vsEditor;
  },

  getVsDiffEditor() {
    return vsDiffEditor;
  },

  setEditorValue,
  getEditorValue,

  focus() {
    vsEditor?.focus();
    vsDiffEditor?.focus();
  },
});
</script>
