<script setup lang="ts">
import {
  AlignCenterOutlined,
  AlignLeftOutlined,
  AlignRightOutlined,
  BoldOutlined,
  CodeOutlined,
  DeleteOutlined,
  InsertRowAboveOutlined,
  InsertRowBelowOutlined,
  InsertRowLeftOutlined,
  InsertRowRightOutlined,
  ItalicOutlined,
  StrikethroughOutlined,
  OrderedListOutlined,
  MinusOutlined,
  PictureOutlined,
  RedoOutlined,
  TableOutlined,
  UndoOutlined,
  UnorderedListOutlined,
} from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import { imageBlockSchema } from "@milkdown/kit/component/image-block";
import { commandsCtx, editorViewCtx } from "@milkdown/kit/core";
import { redoCommand, undoCommand } from "@milkdown/kit/plugin/history";
import { NodeSelection, type Selection } from "@milkdown/kit/prose/state";
import {
  addBlockTypeCommand,
  insertHrCommand,
  toggleEmphasisCommand,
  toggleStrongCommand,
  toggleInlineCodeCommand,
  wrapInBlockquoteCommand,
  wrapInBulletListCommand,
  wrapInHeadingCommand,
  wrapInOrderedListCommand,
} from "@milkdown/kit/preset/commonmark";
import {
  addColAfterCommand,
  addColBeforeCommand,
  addRowAfterCommand,
  addRowBeforeCommand,
  deleteSelectedCellsCommand,
  insertTableCommand,
  selectColCommand,
  selectRowCommand,
  setAlignCommand,
  toggleStrikethroughCommand,
} from "@milkdown/kit/preset/gfm";
import type { Ctx } from "@milkdown/kit/ctx";
import { callCommand, replaceAll } from "@milkdown/kit/utils";
import { Crepe, CrepeFeature } from "@milkdown/crepe";
import "@milkdown/crepe/theme/common/style.css";
import "@milkdown/crepe/theme/frame.css";
import { Milkdown, useEditor, useInstance } from "@milkdown/vue";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";

import { formatError } from "@/lib/errors";

const props = defineProps<{
  title: string;
  modelValue: string;
  disabled?: boolean;
  uploadImage?: (file: File) => Promise<string>;
}>();

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

const { t } = useI18n();
const crepeRef = ref<Crepe | null>(null);
const syncingExternalMarkdown = ref(false);
const surfaceRef = ref<HTMLElement | null>(null);
const tablePickerRef = ref<HTMLElement | null>(null);
const tableGridRef = ref<HTMLElement | null>(null);
const tableContextMenuRef = ref<HTMLElement | null>(null);
const imageContextMenuRef = ref<HTMLElement | null>(null);
const imageInputRef = ref<HTMLInputElement | null>(null);

const TABLE_PICKER_CELL_SIZE = 22;
const TABLE_PICKER_GAP = 6;
const TABLE_PICKER_INITIAL_ROWS = 4;
const TABLE_PICKER_INITIAL_COLS = 10;
const TABLE_PICKER_MAX_ROWS = 15;
const TABLE_PICKER_MAX_COLS = 15;
const TABLE_PICKER_EXPAND_STEP = 2;

type TableContextMenuAction =
  | "add-row-before"
  | "add-row-after"
  | "delete-row"
  | "add-col-before"
  | "add-col-after"
  | "delete-col"
  | "align-left"
  | "align-center"
  | "align-right"
  | "delete-table";

type ImageResizeHandle = "nw" | "ne" | "sw" | "se";

type ImageContextMenuAction = "replace-image" | "toggle-caption" | "reset-size" | "delete-image";

type TableContextMenuState = {
  open: boolean;
  x: number;
  y: number;
  rowIndex: number;
  colIndex: number;
  tableStart: number;
  tableInnerPos: number;
  tableNodeSize: number;
};

type SelectedImageState = {
  open: boolean;
  x: number;
  y: number;
  width: number;
  height: number;
  nodePos: number;
  nodeSize: number;
  src: string;
};

type ImageContextMenuState = {
  open: boolean;
  x: number;
  y: number;
  nodePos: number;
  nodeSize: number;
  src: string;
};

type SelectedImageSnapshot = {
  host: HTMLElement;
  wrapper: HTMLElement;
  image: HTMLImageElement;
  rect: DOMRect;
  nodePos: number;
  nodeSize: number;
  src: string;
};

function createDefaultTableContextMenuState(): TableContextMenuState {
  return {
    open: false,
    x: 0,
    y: 0,
    rowIndex: 0,
    colIndex: 0,
    tableStart: 0,
    tableInnerPos: 0,
    tableNodeSize: 0,
  };
}

function createDefaultSelectedImageState(): SelectedImageState {
  return {
    open: false,
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    nodePos: 0,
    nodeSize: 0,
    src: "",
  };
}

function createDefaultImageContextMenuState(): ImageContextMenuState {
  return {
    open: false,
    x: 0,
    y: 0,
    nodePos: 0,
    nodeSize: 0,
    src: "",
  };
}

const { loading } = useEditor((root) => {
  const crepe = new Crepe({
    root,
    defaultValue: props.modelValue || "",
    features: {
      [CrepeFeature.TopBar]: false,
      [CrepeFeature.Toolbar]: true,
      [CrepeFeature.Latex]: false,
      [CrepeFeature.BlockEdit]: true,
      [CrepeFeature.Placeholder]: false,
    },
    featureConfigs: {
      [CrepeFeature.ImageBlock]: {
        onUpload: props.uploadImage,
        proxyDomURL: (url) => url,
        maxWidth: 960,
        maxHeight: 720,
        blockCaptionPlaceholderText: "Write image caption",
      },
    },
  });

  crepe.setReadonly(!!props.disabled);
  crepe.on((listener) => {
    listener.markdownUpdated((_ctx, markdown) => {
      if (syncingExternalMarkdown.value) {
        return;
      }

      emit("update:modelValue", markdown);
    });
    listener.selectionUpdated((_ctx, selection) => {
      scheduleSelectedImageSync(selection);
    });
  });

  crepeRef.value = crepe;
  return crepe;
});

const [instanceLoading, getEditor] = useInstance();
const editorDisabled = computed(() => props.disabled || instanceLoading.value || loading.value);
const tablePickerOpen = ref(false);
const tablePickerRows = ref(TABLE_PICKER_INITIAL_ROWS);
const tablePickerCols = ref(TABLE_PICKER_INITIAL_COLS);
const tablePreviewRow = ref(0);
const tablePreviewCol = ref(0);
const tablePointerActive = ref(false);
const imageUploading = ref(false);
const imageInputAction = ref<"insert" | "replace">("insert");
const tableContextMenu = ref<TableContextMenuState>(createDefaultTableContextMenuState());
const selectedImage = ref<SelectedImageState>(createDefaultSelectedImageState());
const imageContextMenu = ref<ImageContextMenuState>(createDefaultImageContextMenuState());

let selectedImageHostElement: HTMLElement | null = null;
let selectedImageWrapperElement: HTMLElement | null = null;
let selectedImageElement: HTMLImageElement | null = null;
let selectedImageSyncFrame = 0;
let imageDragActive = false;
let selectedImageResizeObserver: ResizeObserver | null = null;
let selectedImageLoadHandler: (() => void) | null = null;

const tablePickerGridStyle = computed(() => ({
  gridTemplateColumns: `repeat(${tablePickerCols.value}, ${TABLE_PICKER_CELL_SIZE}px)`,
}));

const tablePickerCells = computed(() => {
  const cells: Array<{ key: string; row: number; col: number }> = [];

  for (let row = 1; row <= tablePickerRows.value; row += 1) {
    for (let col = 1; col <= tablePickerCols.value; col += 1) {
      cells.push({ key: `${row}-${col}`, row, col });
    }
  }

  return cells;
});

const tableSelectionLabel = computed(() => {
  if (!tablePreviewRow.value || !tablePreviewCol.value) {
    return t("article.editor.tablePicker.empty");
  }

  return `${tablePreviewRow.value} × ${tablePreviewCol.value}`;
});

const tableContextMenuStyle = computed(() => ({
  left: `${tableContextMenu.value.x}px`,
  top: `${tableContextMenu.value.y}px`,
}));

const selectedImageFrameStyle = computed(() => ({
  left: `${selectedImage.value.x}px`,
  top: `${selectedImage.value.y}px`,
  width: `${selectedImage.value.width}px`,
  height: `${selectedImage.value.height}px`,
}));

const imageContextMenuStyle = computed(() => ({
  left: `${imageContextMenu.value.x}px`,
  top: `${imageContextMenu.value.y}px`,
}));

const tableContextHeaderRow = computed(() => tableContextMenu.value.rowIndex === 0);

function syncMarkdownFromProps(nextValue: string): void {
  const editor = getEditor();
  const crepe = crepeRef.value;
  if (!editor || !crepe) {
    return;
  }

  const resolvedValue = nextValue || "";
  if (crepe.getMarkdown() === resolvedValue) {
    return;
  }

  clearSelectedImageState();
  closeImageContextMenu();
  syncingExternalMarkdown.value = true;
  editor.action(replaceAll(resolvedValue, true));
  queueMicrotask(() => {
    syncingExternalMarkdown.value = false;
  });
}

watch(
  () => props.disabled,
  (value) => {
    crepeRef.value?.setReadonly(!!value);
    if (value) {
      closeTablePicker();
      closeTableContextMenu();
      clearSelectedImageState();
      closeImageContextMenu();
    }
  },
  { immediate: true },
);

watch(
  [() => props.modelValue, loading, instanceLoading],
  ([value, editorLoading, currentInstanceLoading]) => {
    if (editorLoading || currentInstanceLoading) {
      return;
    }

    syncMarkdownFromProps(value);
  },
  { immediate: true },
);

onBeforeUnmount(() => {
  if (selectedImageSyncFrame) {
    cancelAnimationFrame(selectedImageSyncFrame);
    selectedImageSyncFrame = 0;
  }
  detachSelectedImageDomSync();
  window.removeEventListener("pointerdown", handleWindowPointerDown);
  window.removeEventListener("pointermove", handleWindowPointerMove);
  window.removeEventListener("pointerup", handleWindowPointerUp);
  window.removeEventListener("resize", handleWindowResize);
  crepeRef.value = null;
});

onMounted(() => {
  window.addEventListener("pointerdown", handleWindowPointerDown);
  window.addEventListener("pointermove", handleWindowPointerMove);
  window.addEventListener("pointerup", handleWindowPointerUp);
  window.addEventListener("resize", handleWindowResize);
});

function runCommand(command: { key: unknown }, payload?: unknown): void {
  if (editorDisabled.value) {
    return;
  }

  const editor = getEditor();
  if (!editor) {
    return;
  }

  editor.action(callCommand(command.key as never, payload as never));
}

function runEditorAction(action: (ctx: Ctx) => void): void {
  if (editorDisabled.value) {
    return;
  }

  const editor = getEditor();
  if (!editor) {
    return;
  }

  editor.action(action);
}

function insertImageBlockAtCursor(src: string): void {
  const resolvedSrc = src.trim();
  if (!resolvedSrc) {
    return;
  }

  runEditorAction((ctx) => {
    const commands = ctx.get(commandsCtx);
    commands.call(addBlockTypeCommand.key, {
      nodeType: imageBlockSchema.type(ctx),
      attrs: {
        src: resolvedSrc,
        caption: "",
        ratio: 1,
      },
    });

    requestAnimationFrame(() => {
      ctx.get(editorViewCtx).focus();
    });
  });
}

function insertImage(): void {
  if (editorDisabled.value) {
    return;
  }

  if (props.uploadImage) {
    imageInputAction.value = "insert";
    imageInputRef.value?.click();
    return;
  }

  const src = window.prompt(t("article.editor.imagePrompt"));
  if (!src?.trim()) {
    return;
  }

  insertImageBlockAtCursor(src);
}

async function handleImageFileChange(event: Event): Promise<void> {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  input.value = "";
  const action = imageInputAction.value;
  imageInputAction.value = "insert";

  if (!file || !props.uploadImage || editorDisabled.value || imageUploading.value) {
    return;
  }

  imageUploading.value = true;
  try {
    const src = (await props.uploadImage(file)).trim();
    if (!src) {
      throw new Error("article_image_url_unavailable");
    }

    if (action === "replace" && selectedImage.value.open) {
      updateSelectedImageAttribute("src", src);
      return;
    }

    insertImageBlockAtCursor(src);
  } catch (error) {
    message.error(formatError(error));
  } finally {
    imageUploading.value = false;
  }
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}

function resetTablePicker(): void {
  tablePickerRows.value = TABLE_PICKER_INITIAL_ROWS;
  tablePickerCols.value = TABLE_PICKER_INITIAL_COLS;
  tablePreviewRow.value = 0;
  tablePreviewCol.value = 0;
  tablePointerActive.value = false;
}

function closeTablePicker(): void {
  tablePickerOpen.value = false;
  resetTablePicker();
}

function closeTableContextMenu(): void {
  tableContextMenu.value = createDefaultTableContextMenuState();
}

function resolveSurfaceRelativeRect(rect: DOMRect): { x: number; y: number; width: number; height: number } | null {
  const surface = surfaceRef.value;
  if (!surface) {
    return null;
  }

  const surfaceRect = surface.getBoundingClientRect();

  return {
    x: rect.left - surfaceRect.left + surface.scrollLeft,
    y: rect.top - surfaceRect.top + surface.scrollTop,
    width: rect.width,
    height: rect.height,
  };
}

function detachSelectedImageDomSync(): void {
  selectedImageResizeObserver?.disconnect();
  selectedImageResizeObserver = null;

  if (selectedImageElement && selectedImageLoadHandler) {
    selectedImageElement.removeEventListener("load", selectedImageLoadHandler);
  }
  selectedImageLoadHandler = null;
}

function queueSelectedImageBoundsSync(): void {
  requestAnimationFrame(() => {
    syncSelectedImageBoundsFromDom();
    requestAnimationFrame(() => {
      syncSelectedImageBoundsFromDom();
      adjustImageContextMenuPosition();
    });
  });
}

function attachSelectedImageDomSync(): void {
  detachSelectedImageDomSync();

  if (!selectedImageElement) {
    return;
  }

  selectedImageLoadHandler = () => {
    queueSelectedImageBoundsSync();
  };
  selectedImageElement.addEventListener("load", selectedImageLoadHandler);

  if (typeof ResizeObserver !== "undefined") {
    selectedImageResizeObserver = new ResizeObserver(() => {
      syncSelectedImageBoundsFromDom();
      adjustImageContextMenuPosition();
    });
    selectedImageResizeObserver.observe(selectedImageElement);
  }

  queueSelectedImageBoundsSync();
}

function clearSelectedImageState(): void {
  detachSelectedImageDomSync();
  selectedImage.value = createDefaultSelectedImageState();
  selectedImageHostElement = null;
  selectedImageWrapperElement = null;
  selectedImageElement = null;
}

function closeImageContextMenu(): void {
  imageContextMenu.value = createDefaultImageContextMenuState();
}

function resolveSelectedImageSnapshot(targetSelection?: Selection | null): SelectedImageSnapshot | null {
  const editor = getEditor();
  if (!editor) {
    return null;
  }

  let snapshot: SelectedImageSnapshot | null = null;

  editor.action((ctx) => {
    const view = ctx.get(editorViewCtx);
    const selection = targetSelection ?? view.state.selection;
    if (!(selection instanceof NodeSelection) || selection.node.type.name !== "image-block") {
      return;
    }

    const nodeDom = view.nodeDOM(selection.from);
    const host =
      nodeDom instanceof HTMLElement && nodeDom.classList.contains("milkdown-image-block")
        ? nodeDom
        : (view.dom.querySelector(".milkdown-image-block.selected") as HTMLElement | null);

    if (!host) {
      return;
    }

    const wrapper = host.querySelector(".image-wrapper");
    const image = wrapper?.querySelector("img");
    if (!(wrapper instanceof HTMLElement) || !(image instanceof HTMLImageElement)) {
      return;
    }

    const rect = image.getBoundingClientRect();
    if (!rect.width || !rect.height) {
      return;
    }

    snapshot = {
      host,
      wrapper,
      image,
      rect,
      nodePos: selection.from,
      nodeSize: selection.node.nodeSize,
      src: String(selection.node.attrs.src ?? ""),
    };
  });

  return snapshot;
}

function applySelectedImageSnapshot(snapshot: SelectedImageSnapshot | null): void {
  if (!snapshot) {
    clearSelectedImageState();
    closeImageContextMenu();
    return;
  }

  selectedImageHostElement = snapshot.host;
  selectedImageWrapperElement = snapshot.wrapper;
  selectedImageElement = snapshot.image;
  const surfaceRect = resolveSurfaceRelativeRect(snapshot.rect);
  if (!surfaceRect) {
    clearSelectedImageState();
    closeImageContextMenu();
    return;
  }

  selectedImage.value = {
    open: true,
    x: surfaceRect.x,
    y: surfaceRect.y,
    width: surfaceRect.width,
    height: surfaceRect.height,
    nodePos: snapshot.nodePos,
    nodeSize: snapshot.nodeSize,
    src: snapshot.src,
  };
  attachSelectedImageDomSync();

  if (imageContextMenu.value.open && imageContextMenu.value.nodePos !== snapshot.nodePos) {
    closeImageContextMenu();
  }
}

function syncSelectedImageBoundsFromDom(): void {
  const frameElement = selectedImageElement ?? selectedImageWrapperElement;
  if (!selectedImage.value.open || !frameElement) {
    return;
  }

  if (!document.body.contains(frameElement)) {
    clearSelectedImageState();
    closeImageContextMenu();
    return;
  }

  const rect = frameElement.getBoundingClientRect();
  if (!rect.width || !rect.height) {
    return;
  }

  const surfaceRect = resolveSurfaceRelativeRect(rect);
  if (!surfaceRect) {
    return;
  }

  selectedImage.value = {
    ...selectedImage.value,
    x: surfaceRect.x,
    y: surfaceRect.y,
    width: surfaceRect.width,
    height: surfaceRect.height,
  };
}

function scheduleSelectedImageSync(selection?: Selection | null): void {
  if (imageDragActive) {
    return;
  }

  if (selectedImageSyncFrame) {
    cancelAnimationFrame(selectedImageSyncFrame);
  }

  selectedImageSyncFrame = requestAnimationFrame(() => {
    selectedImageSyncFrame = 0;
    applySelectedImageSnapshot(resolveSelectedImageSnapshot(selection));
  });
}

function queueSelectedImageSync(): void {
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      scheduleSelectedImageSync();
    });
  });
}

function ensureTablePickerCapacity(row: number, col: number): void {
  while (row > tablePickerRows.value && tablePickerRows.value < TABLE_PICKER_MAX_ROWS) {
    tablePickerRows.value = Math.min(TABLE_PICKER_MAX_ROWS, tablePickerRows.value + TABLE_PICKER_EXPAND_STEP);
  }

  while (col > tablePickerCols.value && tablePickerCols.value < TABLE_PICKER_MAX_COLS) {
    tablePickerCols.value = Math.min(TABLE_PICKER_MAX_COLS, tablePickerCols.value + TABLE_PICKER_EXPAND_STEP);
  }

  if (row === tablePickerRows.value && tablePickerRows.value < TABLE_PICKER_MAX_ROWS) {
    tablePickerRows.value = Math.min(TABLE_PICKER_MAX_ROWS, tablePickerRows.value + TABLE_PICKER_EXPAND_STEP);
  }

  if (col === tablePickerCols.value && tablePickerCols.value < TABLE_PICKER_MAX_COLS) {
    tablePickerCols.value = Math.min(TABLE_PICKER_MAX_COLS, tablePickerCols.value + TABLE_PICKER_EXPAND_STEP);
  }
}

function updateTablePreview(row: number, col: number): void {
  const nextRow = clamp(row, 1, TABLE_PICKER_MAX_ROWS);
  const nextCol = clamp(col, 1, TABLE_PICKER_MAX_COLS);

  ensureTablePickerCapacity(nextRow, nextCol);

  tablePreviewRow.value = nextRow;
  tablePreviewCol.value = nextCol;
}

function resolveTableSizeFromPointer(clientX: number, clientY: number): { row: number; col: number } | null {
  const grid = tableGridRef.value;
  if (!grid) {
    return null;
  }

  const rect = grid.getBoundingClientRect();
  const stepX = TABLE_PICKER_CELL_SIZE + TABLE_PICKER_GAP;
  const stepY = TABLE_PICKER_CELL_SIZE + TABLE_PICKER_GAP;
  const relativeX = clientX - rect.left;
  const relativeY = clientY - rect.top;

  return {
    row: clamp(Math.ceil((relativeY + TABLE_PICKER_GAP) / stepY), 1, TABLE_PICKER_MAX_ROWS),
    col: clamp(Math.ceil((relativeX + TABLE_PICKER_GAP) / stepX), 1, TABLE_PICKER_MAX_COLS),
  };
}

function insertTableFromSelection(row: number, col: number): void {
  runCommand(insertTableCommand, { row, col });
  closeTablePicker();
}

function toggleTablePicker(): void {
  if (editorDisabled.value) {
    return;
  }

  if (tablePickerOpen.value) {
    closeTablePicker();
    return;
  }

  closeTableContextMenu();
  resetTablePicker();
  tablePickerOpen.value = true;
}

function beginTableSelection(row: number, col: number): void {
  if (editorDisabled.value) {
    return;
  }

  tablePointerActive.value = true;
  updateTablePreview(row, col);
}

function handleWindowPointerDown(event: PointerEvent): void {
  if (!tablePickerOpen.value && !tableContextMenu.value.open && !imageContextMenu.value.open) {
    return;
  }

  const target = event.target;
  if (target instanceof Node) {
    if (tablePickerOpen.value && tablePickerRef.value?.contains(target)) {
      return;
    }

    if (tableContextMenu.value.open && tableContextMenuRef.value?.contains(target)) {
      return;
    }

    if (imageContextMenu.value.open && imageContextMenuRef.value?.contains(target)) {
      return;
    }
  }

  if (tablePickerOpen.value) {
    closeTablePicker();
  }

  if (tableContextMenu.value.open) {
    closeTableContextMenu();
  }

  if (imageContextMenu.value.open) {
    closeImageContextMenu();
  }
}

function handleWindowPointerMove(event: PointerEvent): void {
  if (!tablePickerOpen.value || !tablePointerActive.value) {
    return;
  }

  const nextSize = resolveTableSizeFromPointer(event.clientX, event.clientY);
  if (!nextSize) {
    return;
  }

  updateTablePreview(nextSize.row, nextSize.col);
}

function handleWindowPointerUp(): void {
  if (!tablePointerActive.value) {
    return;
  }

  tablePointerActive.value = false;

  if (!tablePreviewRow.value || !tablePreviewCol.value) {
    closeTablePicker();
    return;
  }

  insertTableFromSelection(tablePreviewRow.value, tablePreviewCol.value);
}

function resolveTableContextFromPointer(clientX: number, clientY: number): Omit<TableContextMenuState, "open" | "x" | "y"> | null {
  const editor = getEditor();
  if (!editor) {
    return null;
  }

  let context: Omit<TableContextMenuState, "open" | "x" | "y"> | null = null;

  editor.action((ctx) => {
    const view = ctx.get(editorViewCtx);
    const position = view.posAtCoords({ left: clientX, top: clientY });
    if (!position) {
      return;
    }

    const resolvedPosition = view.state.doc.resolve(position.inside > -1 ? position.inside : position.pos);

    let tableDepth = -1;
    let rowDepth = -1;
    for (let depth = resolvedPosition.depth; depth > 0; depth -= 1) {
      const nodeName = resolvedPosition.node(depth).type.name;
      if (tableDepth < 0 && nodeName === "table") {
        tableDepth = depth;
      }
      if (rowDepth < 0 && (nodeName === "table_row" || nodeName === "table_header_row")) {
        rowDepth = depth;
      }
    }

    if (tableDepth < 0 || rowDepth < 0) {
      return;
    }

    const tableStart = resolvedPosition.before(tableDepth);
    const tableNode = resolvedPosition.node(tableDepth);

    context = {
      rowIndex: resolvedPosition.index(tableDepth),
      colIndex: resolvedPosition.index(rowDepth),
      tableStart,
      tableInnerPos: tableStart + 1,
      tableNodeSize: tableNode.nodeSize,
    };
  });

  return context;
}

function adjustTableContextMenuPosition(): void {
  const menu = tableContextMenuRef.value;
  if (!menu || !tableContextMenu.value.open) {
    return;
  }

  const offset = 16;
  const maxX = Math.max(offset, window.innerWidth - menu.offsetWidth - offset);
  const maxY = Math.max(offset, window.innerHeight - menu.offsetHeight - offset);

  tableContextMenu.value = {
    ...tableContextMenu.value,
    x: clamp(tableContextMenu.value.x, offset, maxX),
    y: clamp(tableContextMenu.value.y, offset, maxY),
  };
}

function adjustImageContextMenuPosition(): void {
  const menu = imageContextMenuRef.value;
  if (!menu || !imageContextMenu.value.open) {
    return;
  }

  const offset = 16;
  const maxX = Math.max(offset, window.innerWidth - menu.offsetWidth - offset);
  const maxY = Math.max(offset, window.innerHeight - menu.offsetHeight - offset);

  imageContextMenu.value = {
    ...imageContextMenu.value,
    x: clamp(imageContextMenu.value.x, offset, maxX),
    y: clamp(imageContextMenu.value.y, offset, maxY),
  };
}

function openTableContextMenu(event: MouseEvent): void {
  const context = resolveTableContextFromPointer(event.clientX, event.clientY);
  if (!context) {
    closeTableContextMenu();
    return;
  }

  closeTablePicker();
  tableContextMenu.value = {
    open: true,
    x: event.clientX,
    y: event.clientY,
    ...context,
  };

  void nextTick(() => {
    adjustTableContextMenuPosition();
  });
}

function resolveImageContextFromTarget(target: EventTarget | null): SelectedImageSnapshot | null {
  if (!(target instanceof HTMLElement)) {
    return null;
  }

  const host = target.closest(".milkdown-image-block");
  if (!(host instanceof HTMLElement)) {
    return null;
  }

  const editor = getEditor();
  if (!editor) {
    return null;
  }

  let snapshot: SelectedImageSnapshot | null = null;

  editor.action((ctx) => {
    const view = ctx.get(editorViewCtx);
    const nodePos = view.posAtDOM(host, 0);
    const node = view.state.doc.nodeAt(nodePos);
    if (!node || node.type.name !== "image-block") {
      return;
    }

    const wrapper = host.querySelector(".image-wrapper");
    const image = wrapper?.querySelector("img");
    if (!(wrapper instanceof HTMLElement) || !(image instanceof HTMLImageElement)) {
      return;
    }

    const selection = NodeSelection.create(view.state.doc, nodePos);
    if (!view.state.selection.eq(selection)) {
      view.dispatch(view.state.tr.setSelection(selection).scrollIntoView());
    }

    snapshot = {
      host,
      wrapper,
      image,
      rect: image.getBoundingClientRect(),
      nodePos,
      nodeSize: node.nodeSize,
      src: String(node.attrs.src ?? ""),
    };
  });

  return snapshot;
}

function openImageContextMenu(event: MouseEvent): void {
  const snapshot = resolveImageContextFromTarget(event.target);
  if (!snapshot) {
    closeImageContextMenu();
    return;
  }

  closeTablePicker();
  closeTableContextMenu();
  applySelectedImageSnapshot(snapshot);
  imageContextMenu.value = {
    open: true,
    x: event.clientX,
    y: event.clientY,
    nodePos: snapshot.nodePos,
    nodeSize: snapshot.nodeSize,
    src: snapshot.src,
  };

  void nextTick(() => {
    adjustImageContextMenuPosition();
  });
}

function handleEditorDragStart(event: DragEvent): void {
  const target = event.target;
  if (!(target instanceof HTMLElement) || !target.closest(".milkdown-image-block")) {
    return;
  }

  imageDragActive = true;
  if (selectedImageSyncFrame) {
    cancelAnimationFrame(selectedImageSyncFrame);
    selectedImageSyncFrame = 0;
  }
  closeImageContextMenu();
  clearSelectedImageState();
}

function finishEditorImageDrag(): void {
  if (!imageDragActive) {
    return;
  }

  imageDragActive = false;
  queueSelectedImageSync();
}

function handleEditorDragEnd(): void {
  finishEditorImageDrag();
}

function handleEditorDrop(): void {
  finishEditorImageDrag();
}

function updateSelectedImageAttribute(attr: "src" | "ratio", value: string | number): void {
  if (!selectedImage.value.open) {
    return;
  }

  const { nodePos } = selectedImage.value;
  runEditorAction((ctx) => {
    const view = ctx.get(editorViewCtx);
    view.dispatch(view.state.tr.setNodeAttribute(nodePos, attr, value));
    requestAnimationFrame(() => {
      view.focus();
      scheduleSelectedImageSync();
    });
  });
}

function deleteSelectedImage(): void {
  if (!selectedImage.value.open) {
    return;
  }

  const { nodePos, nodeSize } = selectedImage.value;
  closeImageContextMenu();
  clearSelectedImageState();
  runEditorAction((ctx) => {
    const view = ctx.get(editorViewCtx);
    view.dispatch(view.state.tr.delete(nodePos, nodePos + nodeSize).scrollIntoView());
    requestAnimationFrame(() => {
      view.focus();
    });
  });
}

function toggleSelectedImageCaption(): void {
  if (!selectedImageHostElement) {
    return;
  }

  closeImageContextMenu();
  const toggle = selectedImageHostElement.querySelector(".operation .operation-item");
  if (toggle instanceof HTMLElement) {
    toggle.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }));
  }
}

function replaceSelectedImage(): void {
  if (!selectedImage.value.open) {
    return;
  }

  closeImageContextMenu();

  if (props.uploadImage) {
    imageInputAction.value = "replace";
    imageInputRef.value?.click();
    return;
  }

  const src = window.prompt(t("article.editor.imagePrompt"), selectedImage.value.src);
  if (!src?.trim()) {
    return;
  }

  updateSelectedImageAttribute("src", src.trim());
}

function runImageContextAction(action: ImageContextMenuAction): void {
  switch (action) {
    case "replace-image":
      replaceSelectedImage();
      break;
    case "toggle-caption":
      toggleSelectedImageCaption();
      break;
    case "reset-size":
      closeImageContextMenu();
      updateSelectedImageAttribute("ratio", 1);
      break;
    case "delete-image":
      deleteSelectedImage();
      break;
  }
}

function beginSelectedImageResize(event: PointerEvent, handle: ImageResizeHandle): void {
  if (editorDisabled.value || !selectedImageElement || !selectedImage.value.open) {
    return;
  }

  event.preventDefault();
  event.stopPropagation();
  closeImageContextMenu();

  const image = selectedImageElement;
  const startRect = image.getBoundingClientRect();
  const startWidth = startRect.width;
  const startHeight = startRect.height;
  const startDiagonal = Math.hypot(startWidth, startHeight);
  if (!startDiagonal) {
    return;
  }

  const originHeight = Number(image.dataset.origin || startHeight);
  const minScale = Math.max(100 / Math.max(startWidth, 1), 100 / Math.max(startHeight, 1));
  const maxScale = Math.min(960 / Math.max(startWidth, 1), 720 / Math.max(startHeight, 1));
  const anchorX = handle.includes("w") ? startRect.right : startRect.left;
  const anchorY = handle.includes("n") ? startRect.bottom : startRect.top;
  const body = document.body;

  const onPointerMove = (moveEvent: PointerEvent) => {
    moveEvent.preventDefault();

    const currentDistance = Math.hypot(moveEvent.clientX - anchorX, moveEvent.clientY - anchorY);
    const scale = clamp(currentDistance / startDiagonal, minScale, maxScale);
    const nextHeight = Number((startHeight * scale).toFixed(2));

    image.dataset.height = String(nextHeight);
    image.style.height = `${nextHeight}px`;
    syncSelectedImageBoundsFromDom();
  };

  const onPointerUp = () => {
    window.removeEventListener("pointermove", onPointerMove);
    window.removeEventListener("pointerup", onPointerUp);
    body.style.cursor = "";

    const currentHeight = Number(image.dataset.height || image.getBoundingClientRect().height);
    const ratio = Number.parseFloat((currentHeight / Math.max(originHeight, 1)).toFixed(2));
    if (Number.isNaN(ratio) || ratio <= 0) {
      scheduleSelectedImageSync();
      return;
    }

    updateSelectedImageAttribute("ratio", ratio);
  };

  body.style.cursor = handle === "nw" || handle === "se" ? "nwse-resize" : "nesw-resize";
  window.addEventListener("pointermove", onPointerMove);
  window.addEventListener("pointerup", onPointerUp);
}

function handleEditorContextMenu(event: MouseEvent): void {
  if (editorDisabled.value) {
    closeTableContextMenu();
    closeImageContextMenu();
    clearSelectedImageState();
    return;
  }

  const target = event.target;
  if (!(target instanceof HTMLElement)) {
    closeTableContextMenu();
    closeImageContextMenu();
    clearSelectedImageState();
    return;
  }

  if (target.closest(".milkdown-image-block")) {
    event.preventDefault();
    openImageContextMenu(event);
    return;
  }

  if (!target.closest(".milkdown-table-block") || !target.closest("td, th")) {
    closeTableContextMenu();
    closeImageContextMenu();
    clearSelectedImageState();
    return;
  }

  event.preventDefault();
  clearSelectedImageState();
  closeImageContextMenu();
  openTableContextMenu(event);
}

function handleSurfaceScroll(): void {
  closeTableContextMenu();
  closeImageContextMenu();
  syncSelectedImageBoundsFromDom();
}

function handleWindowResize(): void {
  adjustTableContextMenuPosition();
  adjustImageContextMenuPosition();
  syncSelectedImageBoundsFromDom();
}

function runTableContextAction(action: TableContextMenuAction): void {
  if (!tableContextMenu.value.open || editorDisabled.value) {
    return;
  }

  const context = { ...tableContextMenu.value };
  closeTableContextMenu();

  runEditorAction((ctx) => {
    const commands = ctx.get(commandsCtx);
    const view = ctx.get(editorViewCtx);

    const selectRow = () =>
      commands.call(selectRowCommand.key, {
        index: context.rowIndex,
        pos: context.tableInnerPos,
      });

    const selectCol = () =>
      commands.call(selectColCommand.key, {
        index: context.colIndex,
        pos: context.tableInnerPos,
      });

    switch (action) {
      case "add-row-before":
        if (context.rowIndex === 0) {
          break;
        }
        selectRow();
        commands.call(addRowBeforeCommand.key);
        break;
      case "add-row-after":
        selectRow();
        commands.call(addRowAfterCommand.key);
        break;
      case "delete-row":
        if (context.rowIndex === 0) {
          break;
        }
        selectRow();
        commands.call(deleteSelectedCellsCommand.key);
        break;
      case "add-col-before":
        selectCol();
        commands.call(addColBeforeCommand.key);
        break;
      case "add-col-after":
        selectCol();
        commands.call(addColAfterCommand.key);
        break;
      case "delete-col":
        selectCol();
        commands.call(deleteSelectedCellsCommand.key);
        break;
      case "align-left":
        selectCol();
        commands.call(setAlignCommand.key, "left");
        break;
      case "align-center":
        selectCol();
        commands.call(setAlignCommand.key, "center");
        break;
      case "align-right":
        selectCol();
        commands.call(setAlignCommand.key, "right");
        break;
      case "delete-table":
        view.dispatch(
          view.state.tr
            .delete(context.tableStart, context.tableStart + context.tableNodeSize)
            .scrollIntoView(),
        );
        break;
    }

    requestAnimationFrame(() => {
      view.focus();
    });
  });
}
</script>

<template>
  <div class="article-editor-canvas" :class="{ 'article-editor-canvas--disabled': disabled }">
    <div class="article-editor-canvas__toolbar">
      <a-button size="small" type="text" @click="runCommand(undoCommand)">
        <template #icon><IconFont type="icon-Undo" /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(redoCommand)">
        <template #icon><IconFont type="icon-redo" /></template>
      </a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 1)">H1</a-button>
      <a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 2)">H2</a-button>
      <a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 3)">H3</a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" @click="runCommand(toggleStrongCommand)">
        <template #icon><BoldOutlined /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(toggleEmphasisCommand)">
        <template #icon><ItalicOutlined /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(toggleStrikethroughCommand)">
        <template #icon><StrikethroughOutlined /></template>
      </a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" style="font-size: 16px; font-weight: bold; line-height: 1" @click="runCommand(wrapInBlockquoteCommand)">
        "
      </a-button>
      <a-button size="small" type="text" @click="runCommand(insertHrCommand)">
        <template #icon><MinusOutlined /></template>
      </a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" @click="runCommand(wrapInBulletListCommand)">
        <template #icon><UnorderedListOutlined /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(wrapInOrderedListCommand)">
        <template #icon><OrderedListOutlined /></template>
      </a-button>

      <div ref="tablePickerRef" class="article-editor-canvas__table-trigger">
        <a-button
          size="small"
          type="text"
          class="article-editor-canvas__toolbar-button"
          :class="{ 'article-editor-canvas__toolbar-button--active': tablePickerOpen }"
          @click="toggleTablePicker"
        >
          <template #icon><TableOutlined /></template>
        </a-button>

        <div v-if="tablePickerOpen" class="article-editor-canvas__table-panel" @pointerdown.stop>
          <div class="article-editor-canvas__table-panel-status">{{ tableSelectionLabel }}</div>
          <div
            ref="tableGridRef"
            class="article-editor-canvas__table-grid"
            :style="tablePickerGridStyle"
          >
            <button
              v-for="cell in tablePickerCells"
              :key="cell.key"
              type="button"
              class="article-editor-canvas__table-cell"
              :class="{ 'article-editor-canvas__table-cell--active': cell.row <= tablePreviewRow && cell.col <= tablePreviewCol }"
              @mouseenter="updateTablePreview(cell.row, cell.col)"
              @focus="updateTablePreview(cell.row, cell.col)"
              @pointerdown.prevent.stop="beginTableSelection(cell.row, cell.col)"
              @keydown.enter.prevent="insertTableFromSelection(cell.row, cell.col)"
              @keydown.space.prevent="insertTableFromSelection(cell.row, cell.col)"
            ></button>
          </div>
          <div class="article-editor-canvas__table-panel-hint">{{ t("article.editor.tablePicker.hint") }}</div>
        </div>
      </div>

      <span class="article-editor-canvas__divider"></span>
      <input
        ref="imageInputRef"
        type="file"
        accept="image/png,image/jpeg,image/gif,image/webp"
        hidden
        @change="handleImageFileChange"
      />
      <a-button
        size="small"
        type="text"
        :loading="imageUploading"
        :disabled="imageUploading"
        @mousedown.prevent
        @click="insertImage"
      >
        <template #icon><PictureOutlined /></template>
      </a-button>
    </div>

    <div
      ref="surfaceRef"
      class="article-editor-canvas__surface"
      @contextmenu.capture="handleEditorContextMenu"
      @dragstart.capture="handleEditorDragStart"
      @dragend.capture="handleEditorDragEnd"
      @drop.capture="handleEditorDrop"
      @scroll="handleSurfaceScroll"
    >
      <div class="article-editor-canvas__title-row">
        <a-input
          :value="title"
          size="large"
          :disabled="disabled"
          :bordered="false"
          class="article-editor-canvas__title-input"
          :placeholder="t('article.editor.titlePlaceholder')"
          @update:value="emit('update:title', String($event ?? ''))"
        />
      </div>

      <Milkdown />

      <div
        v-if="selectedImage.open"
        class="article-editor-canvas__image-selection"
        :style="selectedImageFrameStyle"
      >
        <button
          type="button"
          class="article-editor-canvas__image-handle article-editor-canvas__image-handle--nw"
          @pointerdown="beginSelectedImageResize($event, 'nw')"
        ></button>
        <button
          type="button"
          class="article-editor-canvas__image-handle article-editor-canvas__image-handle--ne"
          @pointerdown="beginSelectedImageResize($event, 'ne')"
        ></button>
        <button
          type="button"
          class="article-editor-canvas__image-handle article-editor-canvas__image-handle--sw"
          @pointerdown="beginSelectedImageResize($event, 'sw')"
        ></button>
        <button
          type="button"
          class="article-editor-canvas__image-handle article-editor-canvas__image-handle--se"
          @pointerdown="beginSelectedImageResize($event, 'se')"
        ></button>
      </div>

      <div v-if="loading" class="article-editor-canvas__loading">
        <a-skeleton active :paragraph="{ rows: 10 }" />
      </div>
      <div v-if="disabled" class="article-editor-canvas__mask">
        {{ t("article.editor.messages.locked") }}
      </div>
    </div>

    <Teleport to="body">
      <div
        v-if="tableContextMenu.open"
        ref="tableContextMenuRef"
        class="article-editor-canvas__table-context-menu"
        :style="tableContextMenuStyle"
        @pointerdown.stop
        @contextmenu.prevent
      >
        <div class="article-editor-canvas__table-context-row article-editor-canvas__table-context-row--top">
          <button
            type="button"
            class="article-editor-canvas__table-context-action"
            :disabled="tableContextHeaderRow"
            @click="runTableContextAction('add-row-before')"
          >
            <InsertRowAboveOutlined />
            <span>{{ t("article.editor.tableMenu.addRowBefore") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action"
            @click="runTableContextAction('add-row-after')"
          >
            <InsertRowBelowOutlined />
            <span>{{ t("article.editor.tableMenu.addRowAfter") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action article-editor-canvas__table-context-action--danger"
            :disabled="tableContextHeaderRow"
            @click="runTableContextAction('delete-row')"
          >
            <DeleteOutlined />
            <span>{{ t("article.editor.tableMenu.deleteRow") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action article-editor-canvas__table-context-action--danger"
            @click="runTableContextAction('delete-table')"
          >
            <TableOutlined />
            <span>{{ t("article.editor.tableMenu.deleteTable") }}</span>
          </button>
        </div>

        <div class="article-editor-canvas__table-context-divider"></div>

        <div class="article-editor-canvas__table-context-row article-editor-canvas__table-context-row--bottom">
          <button
            type="button"
            class="article-editor-canvas__table-context-action"
            @click="runTableContextAction('add-col-before')"
          >
            <InsertRowLeftOutlined />
            <span>{{ t("article.editor.tableMenu.addColBefore") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action"
            @click="runTableContextAction('add-col-after')"
          >
            <InsertRowRightOutlined />
            <span>{{ t("article.editor.tableMenu.addColAfter") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action article-editor-canvas__table-context-action--danger"
            @click="runTableContextAction('delete-col')"
          >
            <DeleteOutlined />
            <span>{{ t("article.editor.tableMenu.deleteCol") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action"
            @click="runTableContextAction('align-left')"
          >
            <AlignLeftOutlined />
            <span>{{ t("article.editor.tableMenu.alignLeft") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action"
            @click="runTableContextAction('align-center')"
          >
            <AlignCenterOutlined />
            <span>{{ t("article.editor.tableMenu.alignCenter") }}</span>
          </button>
          <button
            type="button"
            class="article-editor-canvas__table-context-action"
            @click="runTableContextAction('align-right')"
          >
            <AlignRightOutlined />
            <span>{{ t("article.editor.tableMenu.alignRight") }}</span>
          </button>
        </div>
      </div>
    </Teleport>

    <Teleport to="body">
      <div
        v-if="imageContextMenu.open"
        ref="imageContextMenuRef"
        class="article-editor-canvas__image-context-menu"
        :style="imageContextMenuStyle"
        @pointerdown.stop
        @contextmenu.prevent
      >
        <button
          type="button"
          class="article-editor-canvas__image-context-action"
          @click="runImageContextAction('replace-image')"
        >
          <PictureOutlined />
          <span>{{ t("article.editor.imageMenu.replace") }}</span>
        </button>
        <button
          type="button"
          class="article-editor-canvas__image-context-action"
          @click="runImageContextAction('toggle-caption')"
        >
          <CodeOutlined />
          <span>{{ t("article.editor.imageMenu.caption") }}</span>
        </button>
        <button
          type="button"
          class="article-editor-canvas__image-context-action"
          @click="runImageContextAction('reset-size')"
        >
          <RedoOutlined />
          <span>{{ t("article.editor.imageMenu.resetSize") }}</span>
        </button>
        <button
          type="button"
          class="article-editor-canvas__image-context-action article-editor-canvas__image-context-action--danger"
          @click="runImageContextAction('delete-image')"
        >
          <DeleteOutlined />
          <span>{{ t("article.editor.imageMenu.delete") }}</span>
        </button>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.article-editor-canvas {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  border: 1px solid #edf2fa;
  border-radius: 24px;
  overflow: hidden;
  background:
    radial-gradient(circle at top right, rgba(64, 110, 255, 0.08), transparent 20%),
    linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
}

.article-editor-canvas__toolbar {
  position: relative;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 14px 18px;
  border-bottom: 1px solid #eef2f8;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(12px);
}

.article-editor-canvas__divider {
  width: 1px;
  height: 20px;
  margin: 0 2px;
  background: #e5ebf5;
}

.article-editor-canvas__table-trigger {
  position: relative;
  display: flex;
  align-items: center;
}

.article-editor-canvas__toolbar :deep(.article-editor-canvas__toolbar-button--active) {
  color: #245bdb;
  background: rgba(47, 107, 255, 0.1);
}

.article-editor-canvas__table-panel {
  position: absolute;
  top: calc(100% + 12px);
  left: -18px;
  z-index: 24;
  width: max-content;
  max-width: min(calc(100vw - 48px), 520px);
  padding: 18px;
  border: 1px solid #e3e9f4;
  border-radius: 20px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 255, 0.98) 100%);
  box-shadow:
    0 18px 40px rgba(15, 23, 42, 0.14),
    0 2px 8px rgba(15, 23, 42, 0.06);
  backdrop-filter: blur(16px);
}

.article-editor-canvas__table-panel-status {
  margin-bottom: 14px;
  color: #101828;
  font-size: 14px;
  font-weight: 700;
  text-align: center;
}

.article-editor-canvas__table-grid {
  display: grid;
  gap: 6px;
  justify-content: start;
}

.article-editor-canvas__table-cell {
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid #d5dbe7;
  border-radius: 4px;
  background: #ffffff;
  cursor: crosshair;
  transition:
    border-color 0.14s ease,
    background-color 0.14s ease,
    transform 0.14s ease,
    box-shadow 0.14s ease;
}

.article-editor-canvas__table-cell:hover,
.article-editor-canvas__table-cell--active {
  border-color: #2f6bff;
  background: rgba(47, 107, 255, 0.14);
  box-shadow: inset 0 0 0 1px rgba(47, 107, 255, 0.08);
}

.article-editor-canvas__table-cell:focus-visible {
  outline: 2px solid rgba(47, 107, 255, 0.36);
  outline-offset: 2px;
}

.article-editor-canvas__table-panel-hint {
  margin-top: 12px;
  color: #667085;
  font-size: 12px;
  line-height: 1.4;
}

.article-editor-canvas__table-context-menu {
  position: fixed;
  z-index: 1200;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 620px;
  max-width: 720px;
  padding: 16px;
  border: 1px solid #edf2fa;
  border-radius: 16px;
  background: #ffffff;
  box-shadow:
    0 20px 48px rgba(15, 23, 42, 0.08),
    0 6px 16px rgba(15, 23, 42, 0.04);
}

.article-editor-canvas__table-context-row--top {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}

.article-editor-canvas__table-context-row--bottom {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 12px;
}

.article-editor-canvas__table-context-divider {
  height: 1px;
  background: #f0f3f8;
}

.article-editor-canvas__table-context-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 86px;
  padding: 12px 10px;
  border: 1px solid transparent;
  border-radius: 12px;
  background: #f7f9fa;
  color: #3f4a5c;
  cursor: pointer;
  transition: all 0.2s ease;
}

.article-editor-canvas__table-context-action span {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
}

.article-editor-canvas__table-context-action :deep(svg) {
  font-size: 22px;
  color: #5c6b81;
  transition: color 0.2s ease;
}

.article-editor-canvas__table-context-action:hover:not(:disabled) {
  background: #eff4ff;
  border-color: #c2d6ff;
  color: #245bdb;
}

.article-editor-canvas__table-context-action:hover:not(:disabled) :deep(svg) {
  color: #245bdb;
}

.article-editor-canvas__table-context-action--danger:hover:not(:disabled) {
  background: #fff1f1;
  border-color: #ffccc7;
  color: #cf1322;
}

.article-editor-canvas__table-context-action--danger:hover:not(:disabled) :deep(svg) {
  color: #cf1322;
}

.article-editor-canvas__table-context-action:disabled {
  opacity: 0.46;
  cursor: not-allowed;
}

.article-editor-canvas__image-selection {
  position: absolute;
  z-index: 1;
  box-sizing: border-box;
  border: 2px solid #2f6bff;
  border-radius: 18px;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.9),
    0 14px 28px rgba(47, 107, 255, 0.12);
  pointer-events: none;
}

.article-editor-canvas__image-handle {
  position: absolute;
  width: 14px;
  height: 14px;
  border: 2px solid #ffffff;
  border-radius: 999px;
  background: #2f6bff;
  box-shadow: 0 4px 10px rgba(47, 107, 255, 0.28);
  pointer-events: auto;
}

.article-editor-canvas__image-handle--nw {
  top: 0;
  left: 0;
  cursor: nwse-resize;
  transform: translate(-50%, -50%);
}

.article-editor-canvas__image-handle--ne {
  top: 0;
  right: 0;
  cursor: nesw-resize;
  transform: translate(50%, -50%);
}

.article-editor-canvas__image-handle--sw {
  bottom: 0;
  left: 0;
  cursor: nesw-resize;
  transform: translate(-50%, 50%);
}

.article-editor-canvas__image-handle--se {
  right: 0;
  bottom: 0;
  cursor: nwse-resize;
  transform: translate(50%, 50%);
}

.article-editor-canvas__image-context-menu {
  position: fixed;
  z-index: 1200;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  min-width: 300px;
  max-width: min(360px, calc(100vw - 32px));
  padding: 12px;
  border: 1px solid #e8eef8;
  border-radius: 16px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 251, 255, 0.98) 100%);
  box-shadow:
    0 20px 48px rgba(15, 23, 42, 0.1),
    0 6px 18px rgba(15, 23, 42, 0.06);
  backdrop-filter: blur(14px);
}

.article-editor-canvas__image-context-action {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: 12px;
  background: #f7f9fc;
  color: #344054;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition:
    border-color 0.18s ease,
    background-color 0.18s ease,
    color 0.18s ease,
    transform 0.18s ease;
}

.article-editor-canvas__image-context-action :deep(svg) {
  font-size: 18px;
  color: #52607a;
  transition: color 0.18s ease;
}

.article-editor-canvas__image-context-action:hover {
  border-color: #c8d8ff;
  background: #edf4ff;
  color: #245bdb;
  transform: translateY(-1px);
}

.article-editor-canvas__image-context-action:hover :deep(svg) {
  color: #245bdb;
}

.article-editor-canvas__image-context-action--danger:hover {
  border-color: #ffd2cf;
  background: #fff1f0;
  color: #cf1322;
}

.article-editor-canvas__image-context-action--danger:hover :deep(svg) {
  color: #cf1322;
}

.article-editor-canvas__surface {
  position: relative;
  z-index: 1;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.article-editor-canvas__title-row {
  position: relative;
  z-index: 0;
  padding: 18px 28px 0;
}

.article-editor-canvas__title-row :deep(.ant-input) {
  height: 68px;
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  color: #101828;
  font-size: 30px !important;
  font-weight: 800;
  line-height: 1.15;
}

.article-editor-canvas__title-row :deep(.ant-input::placeholder) {
  color: #98a2b3;
}

.article-editor-canvas__loading {
  position: absolute;
  inset: 0;
  z-index: 2;
  padding: 24px;
  background: rgba(255, 255, 255, 0.92);
}

/* Removed height 100% blocks to allow natural vertical content growth and scrolling */

.article-editor-canvas__surface :deep(.ProseMirror) {
  min-height: 640px;
  padding: 14px 36px 80px;
  background: transparent;
}

.article-editor-canvas__surface :deep(.ProseMirror:focus) {
  outline: none;
}

.article-editor-canvas__surface :deep(.milkdown-image-block > .image-wrapper) {
  margin: 0 auto;
}

.article-editor-canvas__surface :deep(.milkdown-image-block.selected > .image-wrapper::before) {
  background: transparent;
  border: 0;
}

.article-editor-canvas__surface :deep(.milkdown-image-block img) {
  max-width: min(100%, 960px);
  margin: 0 auto;
}

.article-editor-canvas__surface :deep(.milkdown-image-block > .image-wrapper .operation) {
  opacity: 0 !important;
  pointer-events: none;
}

.article-editor-canvas__surface :deep(.milkdown-image-block > .image-wrapper .image-resize-handle) {
  opacity: 0 !important;
  pointer-events: none;
}

.article-editor-canvas__mask {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 120px;
  color: #475467;
  font-size: 14px;
  background: rgba(255, 255, 255, 0.58);
  backdrop-filter: blur(3px);
}

.article-editor-canvas--disabled .article-editor-canvas__toolbar {
  opacity: 0.56;
}

.article-editor-canvas--disabled .article-editor-canvas__title-row {
  opacity: 0.72;
}
</style>
