<script setup lang="ts">
import {
  computed,
  nextTick,
  ref,
  watch,
  type ComponentPublicInstance,
} from "vue";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeSanitize from "rehype-sanitize";

type DocumentMode = "preview" | "edit" | "mixed";

interface MarkdownAstNode {
  position?: {
    start?: { offset?: number };
    end?: { offset?: number };
  };
}

interface MarkdownBlock {
  id: string;
  source: string;
  html: string;
  startOffset: number;
  endOffset: number;
}

const props = withDefaults(
  defineProps<{
    modelValue?: string;
    placeholder?: string;
    documentMode?: DocumentMode;
    previewOnly?: boolean;
  }>(),
  {
    modelValue: "",
    placeholder: "开始写作...",
    documentMode: undefined,
    previewOnly: false,
  },
);

const emit = defineEmits<{
  "update:modelValue": [value: string];
  "save-shortcut": [];
}>();

// ── Unified processors ─────────────────────────────────────────────

/** Full pipeline: markdown → HTML */
const md2html = unified()
  .use(remarkParse)
  .use(remarkGfm)
  .use(remarkRehype, { allowDangerousHtml: false })
  .use(rehypeSanitize)
  .use(rehypeStringify);

const mdParser = unified().use(remarkParse).use(remarkGfm);

const activeDocumentMode = computed<DocumentMode>(() => {
  return props.documentMode ?? (props.previewOnly ? "preview" : "edit");
});

// ── Rendering ──────────────────────────────────────────────────────

const renderedHtml = ref("");
const renderError = ref(false);
const markdownBlocks = ref<MarkdownBlock[]>([]);

const editorTextareaRef = ref<HTMLTextAreaElement>();
const scrollRef = ref<HTMLDivElement>();
const blockTextareaRef = ref<HTMLTextAreaElement | null>(null);

const EMPTY_BLOCK_ID = "__empty__";
const editingBlockId = ref<string | null>(null);
const editingSource = ref("");
const editingBaseValue = ref("");
const editingRange = ref<{ startOffset: number; endOffset: number } | null>(
  null,
);
const pendingLocalModelUpdate = ref(false);
const isEditingEmptyDocument = computed(
  () => editingBlockId.value === EMPTY_BLOCK_ID,
);

function renderFullHtml(md: string) {
  try {
    const file = md2html.processSync(md);
    renderedHtml.value = String(file.value);
    renderError.value = false;
  } catch {
    renderedHtml.value = "";
    renderError.value = true;
  }
}

function escapeHtml(value: string) {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function renderBlockHtml(source: string) {
  try {
    const file = md2html.processSync(source);
    return String(file.value);
  } catch {
    return `<pre><code>${escapeHtml(source)}</code></pre>`;
  }
}

function parseBlocks(md: string) {
  if (!md.trim()) {
    markdownBlocks.value = [];
    return;
  }

  try {
    const tree = mdParser.parse(md) as { children?: MarkdownAstNode[] };
    const blocks = (tree.children ?? [])
      .map((node, index): MarkdownBlock | null => {
        const startOffset = node.position?.start?.offset;
        const endOffset = node.position?.end?.offset;

        if (
          typeof startOffset !== "number" ||
          typeof endOffset !== "number" ||
          endOffset < startOffset
        ) {
          return null;
        }

        const source = md.slice(startOffset, endOffset);
        return {
          id: `${index}-${startOffset}-${endOffset}`,
          source,
          html: renderBlockHtml(source),
          startOffset,
          endOffset,
        };
      })
      .filter((block): block is MarkdownBlock => block !== null);

    markdownBlocks.value = blocks;
  } catch {
    markdownBlocks.value = [
      {
        id: "fallback-0",
        source: md,
        html: renderBlockHtml(md),
        startOffset: 0,
        endOffset: md.length,
      },
    ];
  }
}

watch(
  () => props.modelValue,
  (val) => {
    const shouldResetScroll = !pendingLocalModelUpdate.value;
    pendingLocalModelUpdate.value = false;

    renderFullHtml(val);
    parseBlocks(val);
    if (editingBlockId.value && val !== editingBaseValue.value) {
      cancelBlockEdit();
    }

    nextTick(() => {
      resizeEditorTextarea();
      // 外部切换文件时重置滚动位置；本组件内编辑时保留当前位置。
      if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0;
    });
  },
  { immediate: true },
);

// ── Editor textarea handlers ───────────────────────────────────────

function setBlockTextareaRef(el: Element | ComponentPublicInstance | null) {
  blockTextareaRef.value = el instanceof HTMLTextAreaElement ? el : null;
}

function resizeTextarea(ta: HTMLTextAreaElement) {
  ta.style.height = "auto";
  ta.style.height = `${ta.scrollHeight}px`;
}

function resizeEditorTextarea() {
  const ta = editorTextareaRef.value;
  if (!ta) return;
  resizeTextarea(ta);
}

function focusBlockEditor() {
  void nextTick(() => {
    const ta = blockTextareaRef.value;
    if (!ta) return;
    resizeTextarea(ta);
    ta.focus();
    ta.selectionStart = ta.selectionEnd = ta.value.length;
  });
}

function emitModelValue(value: string) {
  pendingLocalModelUpdate.value = true;
  emit("update:modelValue", value);
}

function applyFullEditorValue(value: string) {
  emitModelValue(value);
  renderFullHtml(value);
  parseBlocks(value);
}

function onEditorInput(e: Event) {
  const ta = e.target as HTMLTextAreaElement;
  const val = ta.value;
  applyFullEditorValue(val);
  resizeTextarea(ta);
}

function tryAutoPairForValue(
  ta: HTMLTextAreaElement,
  key: string,
  value: string,
  onChange: (nextValue: string) => void,
): boolean {
  const pairs: Record<string, string> = {
    "(": ")",
    "[": "]",
    "{": "}",
    '"': '"',
    "'": "'",
    "`": "`",
  };
  const close = pairs[key];
  if (!close) return false;

  const start = ta.selectionStart;
  const end = ta.selectionEnd;
  const selected = value.substring(start, end);
  const newVal =
    value.substring(0, start) +
    key +
    selected +
    close +
    value.substring(end);

  onChange(newVal);

  void nextTick(() => {
    ta.selectionStart = start + 1;
    ta.selectionEnd = start + 1 + selected.length;
    ta.focus();
  });
  return true;
}

function tryAutoPair(ta: HTMLTextAreaElement, key: string): boolean {
  return tryAutoPairForValue(ta, key, props.modelValue, applyFullEditorValue);
}

function onEditorKeydown(e: KeyboardEvent) {
  if ((e.ctrlKey || e.metaKey) && e.key === "s") {
    e.preventDefault();
    emit("save-shortcut");
    return;
  }

  const ta = e.target as HTMLTextAreaElement;

  if (e.key === "Tab") {
    e.preventDefault();
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    const newVal =
      props.modelValue.substring(0, start) +
      "  " +
      props.modelValue.substring(end);
    applyFullEditorValue(newVal);
    void nextTick(() => {
      ta.selectionStart = ta.selectionEnd = start + 2;
      ta.focus();
    });
    return;
  }

  if (tryAutoPair(ta, e.key)) {
    e.preventDefault();
    return;
  }
}

// ── Mixed mode block editing ───────────────────────────────────────

function startBlockEdit(block: MarkdownBlock) {
  if (activeDocumentMode.value !== "mixed") return;
  if (editingBlockId.value && editingBlockId.value !== block.id) {
    finalizeBlockEdit();
  }

  editingBlockId.value = block.id;
  editingSource.value = block.source;
  editingBaseValue.value = props.modelValue;
  editingRange.value = {
    startOffset: block.startOffset,
    endOffset: block.endOffset,
  };
  focusBlockEditor();
}

function startEmptyBlockEdit() {
  if (activeDocumentMode.value !== "mixed") return;
  editingBlockId.value = EMPTY_BLOCK_ID;
  editingSource.value = props.modelValue;
  editingBaseValue.value = props.modelValue;
  editingRange.value = {
    startOffset: 0,
    endOffset: props.modelValue.length,
  };
  focusBlockEditor();
}

function cancelBlockEdit() {
  editingBlockId.value = null;
  editingSource.value = "";
  editingBaseValue.value = "";
  editingRange.value = null;
}

function finalizeBlockEdit() {
  const range = editingRange.value;
  if (!editingBlockId.value || !range) {
    cancelBlockEdit();
    return;
  }

  const nextValue =
    props.modelValue.slice(0, range.startOffset) +
    editingSource.value +
    props.modelValue.slice(range.endOffset);

  cancelBlockEdit();

  if (nextValue !== props.modelValue) {
    emitModelValue(nextValue);
    renderFullHtml(nextValue);
    parseBlocks(nextValue);
  }
}

function onBlockInput(e: Event) {
  const ta = e.target as HTMLTextAreaElement;
  editingSource.value = ta.value;
  resizeTextarea(ta);
}

function onBlockKeydown(e: KeyboardEvent) {
  const ta = e.target as HTMLTextAreaElement;

  if ((e.ctrlKey || e.metaKey) && e.key === "s") {
    e.preventDefault();
    finalizeBlockEdit();
    emit("save-shortcut");
    return;
  }

  if (e.key === "Escape") {
    e.preventDefault();
    cancelBlockEdit();
    return;
  }

  if (e.key === "Tab") {
    e.preventDefault();
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    editingSource.value =
      editingSource.value.substring(0, start) +
      "  " +
      editingSource.value.substring(end);
    void nextTick(() => {
      ta.selectionStart = ta.selectionEnd = start + 2;
      resizeTextarea(ta);
      ta.focus();
    });
    return;
  }

  if (
    tryAutoPairForValue(ta, e.key, editingSource.value, (nextValue) => {
      editingSource.value = nextValue;
      void nextTick(() => resizeTextarea(ta));
    })
  ) {
    e.preventDefault();
  }
}

watch(activeDocumentMode, (mode, oldMode) => {
  if (oldMode === "mixed" && mode !== "mixed" && editingBlockId.value) {
    finalizeBlockEdit();
  }
  if (mode === "edit") {
    void nextTick(resizeEditorTextarea);
  }
});
</script>

<template>
  <div
    ref="scrollRef"
    class="flex-1 w-full overflow-auto"
    @wheel.passive
  >
    <div
      class="mx-auto px-8 pb-40 pt-8 h-full"
      :style="{
        maxWidth: `calc(700px * var(--editor-zoom, 1))`,
        fontSize: `calc(1rem * var(--editor-zoom, 1))`,
      }"
    >
      <!-- Editor mode -->
      <textarea
        v-if="activeDocumentMode === 'edit'"
        ref="editorTextareaRef"
        :value="props.modelValue"
        :placeholder="props.placeholder"
        class="
          markdown-source-editor
          w-full min-h-[60vh] resize-none overflow-hidden bg-transparent outline-none
          font-serif text-[#38342e] leading-relaxed
          placeholder:text-[#b8b3a8]
        "
        @input="onEditorInput"
        @keydown="onEditorKeydown"
      />

      <!-- Preview mode -->
      <div v-else-if="activeDocumentMode === 'preview'" class="min-h-[60vh]">
        <div v-if="renderError" class="text-red-500 text-sm">
          ⚠ Markdown 解析错误
        </div>
        <div v-else class="markdown-preview" v-html="renderedHtml" />
      </div>

      <!-- Mixed mode -->
      <div v-else class="min-h-[60vh] markdown-preview markdown-mixed">
        <div v-if="renderError" class="text-red-500 text-sm">
          ⚠ Markdown 解析错误
        </div>
        <textarea
          v-else-if="isEditingEmptyDocument"
          :ref="setBlockTextareaRef"
          :value="editingSource"
          :placeholder="props.placeholder"
          rows="1"
          class="markdown-block-editor"
          @input="onBlockInput"
          @keydown="onBlockKeydown"
          @blur="finalizeBlockEdit"
        />
        <div
          v-else-if="!props.modelValue.trim()"
          class="markdown-empty-placeholder"
          role="button"
          tabindex="0"
          @click="startEmptyBlockEdit"
          @keydown.enter.prevent="startEmptyBlockEdit"
          @keydown.space.prevent="startEmptyBlockEdit"
        >
          {{ props.placeholder }}
        </div>
        <template v-else>
          <div
            v-for="block in markdownBlocks"
            :key="block.id"
            class="markdown-block"
            :class="{ 'markdown-block-editing': editingBlockId === block.id }"
          >
            <textarea
              v-if="editingBlockId === block.id"
              :ref="setBlockTextareaRef"
              :value="editingSource"
              rows="1"
              class="markdown-block-editor"
              @input="onBlockInput"
              @keydown="onBlockKeydown"
              @blur="finalizeBlockEdit"
            />
            <div
              v-else
              class="markdown-block-preview"
              role="button"
              tabindex="0"
              v-html="block.html"
              @click="startBlockEdit(block)"
              @keydown.enter.prevent="startBlockEdit(block)"
              @keydown.space.prevent="startBlockEdit(block)"
            />
          </div>
        </template>
      </div>
    </div>
  </div>
</template>

<style>
/* ── Markdown Preview Styles (warm palette) ─────────────────────── */

.markdown-preview {
  font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
  line-height: 1.625;
  color: #38342e;
  word-break: break-word;
}

.markdown-preview > *:first-child { margin-top: 0; }

.markdown-source-editor {
  display: block;
  height: auto;
}

.markdown-mixed > .markdown-block:first-child .markdown-block-preview > *:first-child {
  margin-top: 0;
}

.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
  font-weight: 700;
  color: #2d2a26;
  line-height: 1.3;
  margin-top: 1.5em;
  margin-bottom: 0.5em;
}
.markdown-preview h1 { font-size: 1.5em; }
.markdown-preview h2 { font-size: 1.25em; }
.markdown-preview h3 { font-size: 1.125em; }
.markdown-preview h4 { font-size: 1em; }

.markdown-preview p { margin: 0.5em 0; }

.markdown-preview strong { font-weight: 700; color: #2d2a26; }
.markdown-preview em { font-style: italic; }
.markdown-preview del { text-decoration: line-through; color: #8c877d; }

.markdown-preview code {
  background-color: #f4f1ea;
  color: #bf6a3b;
  border-radius: 0.25rem;
  padding: 0.125rem 0.375rem;
  font-size: 0.875em;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  word-break: break-all;
}

.markdown-preview pre {
  background-color: #f4f1ea;
  border-radius: 0.375rem;
  padding: 1rem;
  overflow-x: auto;
  margin: 0.75em 0;
}
.markdown-preview pre code {
  background: none;
  padding: 0;
  color: #38342e;
  font-size: 0.875em;
  border-radius: 0;
  word-break: normal;
}

.markdown-preview a { color: #bf6a3b; text-decoration: underline; }

.markdown-preview blockquote {
  border-left: 4px solid #bf6a3b;
  padding-left: 1rem;
  margin: 0.75em 0;
  color: #8c877d;
  font-style: italic;
}
.markdown-preview blockquote p { margin: 0.25em 0; }

.markdown-preview ul { list-style-type: disc; padding-left: 1.5em; margin: 0.5em 0; }
.markdown-preview ol { list-style-type: decimal; padding-left: 1.5em; margin: 0.5em 0; }
.markdown-preview li { margin: 0.25em 0; }

.markdown-preview li input[type="checkbox"] {
  margin-right: 0.375rem;
  accent-color: #bf6a3b;
}

.markdown-preview hr {
  border: none;
  border-top: 1px solid #e0dbcf;
  margin: 1.5em 0;
}

.markdown-preview table {
  border-collapse: collapse;
  width: 100%;
  margin: 0.75em 0;
}
.markdown-preview th,
.markdown-preview td {
  border: 1px solid #e0dbcf;
  padding: 0.5rem 0.75rem;
  text-align: left;
}
.markdown-preview th {
  background-color: #f4f1ea;
  font-weight: 700;
  color: #2d2a26;
}
.markdown-preview tr:nth-child(even) td { background-color: #faf9f6; }

.markdown-preview img {
  max-width: 100%;
  height: auto;
  border-radius: 0.25rem;
  margin: 0.5em 0;
}

/* ── Mixed mode block editing ────────────────────────────────────── */

.markdown-block {
  display: contents;
}

.markdown-block-preview {
  display: contents;
  outline: none;
}

.markdown-block:not(.markdown-block-editing) > .markdown-block-preview > * {
  transition:
    background-color 0.15s ease,
    box-shadow 0.15s ease;
}

.markdown-block:not(.markdown-block-editing):hover > .markdown-block-preview > * {
  background-color: #f4f1ea;
  cursor: text;
}

.markdown-block-editing {
  display: block;
}

.markdown-block-editing,
.markdown-block-editing:hover {
  background-color: transparent;
}

.markdown-block-preview:focus-visible > * {
  outline: 2px solid #d6aa8e;
  outline-offset: 2px;
  border-radius: 0.25rem;
}

.markdown-empty-placeholder:focus-visible {
  outline: 2px solid #d6aa8e;
  outline-offset: 2px;
  border-radius: 0.25rem;
}

.markdown-block-editor {
  display: block;
  width: 100%;
  min-height: calc((1em * 1.6) + 1.5rem + 2px);
  resize: none;
  overflow: hidden;
  border: 1px solid #d6aa8e;
  border-radius: 0.375rem;
  background-color: #fffdfa;
  color: #38342e;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 0.92em;
  line-height: 1.6;
  margin: 0.375rem 0;
  padding: 0.75rem 0.875rem;
  outline: none;
  box-shadow: 0 0 0 3px rgba(191, 106, 59, 0.08);
}

.markdown-block-editor::placeholder,
.markdown-empty-placeholder {
  color: #b8b3a8;
}

.markdown-empty-placeholder {
  min-height: 12rem;
  padding-top: 0.25rem;
  cursor: text;
  outline: none;
}
</style>
