<script lang="ts" setup>

import {

  computed,

  nextTick,

  onMounted,

  onUnmounted,

  ref,

  shallowRef,

  toRef,

  watch,

} from 'vue';



import { usePreferences } from '@vben/preferences';

import { useDebounceFn } from '@vueuse/core';

import { message } from 'ant-design-vue';



import { normalizeRange, iterateRanges } from './commands/FillRangeCommand';

import {

  renderBody,

  renderHeader,

} from './canvas/SpreadsheetRenderer';

import { getSpreadsheetTheme } from './canvas/theme';

import { getDataColScreenX } from './composables/useAutoMeasure';

import { useSpreadsheetEngine } from './composables/useSpreadsheetEngine';

import { useFormulaReferenceMode } from './composables/useFormulaReferenceMode';
import { useSpreadsheetFind } from './composables/useSpreadsheetFind';

import {

  openCellContextMenu,

  openHeaderContextMenu,

  type SpreadsheetMenuActions,

} from './composables/useSpreadsheetContextMenu';

import CellEditorOverlay from './editor/CellEditorOverlay.vue';

import {

  debouncedWriteSpreadsheetLayout,

  readSpreadsheetLayout,

} from './history/layoutLocalStore';

import SpreadsheetToolbar from './toolbar/SpreadsheetToolbar.vue';
import SpreadsheetFormulaBar from './toolbar/SpreadsheetFormulaBar.vue';
import SpreadsheetFindBar from './toolbar/SpreadsheetFindBar.vue';
import HeaderFilterLayer from './header/HeaderFilterLayer.vue';
import SpreadsheetHeaderColorPickerPopover from './header/SpreadsheetHeaderColorPickerPopover.vue';

import { listSpreadsheetFormula, type SpreadsheetFormulaItem } from '#/api/spreadsheet-formula';

import { colIndexToLetters } from './formula/evaluator';

import type {

  SpreadsheetColumnConfig,

  SpreadsheetHeaderConfig,

  SpreadsheetHistoryEntry,

  SpreadsheetLayoutState,

  SpreadsheetTableConfig,

} from './types';

import { HEADER_HEIGHT } from './types';



defineOptions({ name: 'CanvasSpreadsheet' });



const props = withDefaults(

  defineProps<{

    tableConfig: SpreadsheetTableConfig;

    columns: SpreadsheetColumnConfig[];

    data: Record<string, unknown>[];

    rowKeyField?: string;

    loading?: boolean;

    showConfigButton?: boolean;

    workbookName?: string;

  }>(),

  {

    rowKeyField: 'id',

    loading: false,

    showConfigButton: false,

  },

);



const emit = defineEmits<{

  change: [payload: { data: Record<string, unknown>[] }];

  error: [err: unknown];

  refresh: [];

  openConfig: [];

  adminAddColumn: [];

  adminSetColumn: [col: number];

  updateColumnFormula: [payload: { col: number; formula: string }];

}>();



const { isDark } = usePreferences();

const theme = computed(() => getSpreadsheetTheme(isDark.value));

const refreshing = ref(false);



const containerRef = ref<HTMLDivElement | null>(null);

const stickyTopRef = ref<HTMLDivElement | null>(null);

const headerCanvasRef = ref<HTMLCanvasElement | null>(null);

const bodyCanvasRef = ref<HTMLCanvasElement | null>(null);

const editorOverlayRef = ref<InstanceType<typeof CellEditorOverlay> | null>(null);

const bodyWrapRef = ref<HTMLDivElement | null>(null);

const measureCanvasRef = ref<HTMLCanvasElement | null>(null);

const scrollViewportHeight = ref(600);



const localData = ref<Record<string, unknown>[]>([]);

watch(

  () => props.data,

  (v) => {

    localData.value = v.map((r) => ({ ...r }));

  },

  { immediate: true, deep: true },

);



const layoutColWidths = ref<Record<number, number>>({});
const mergedHeaders = ref<SpreadsheetHeaderConfig[]>([]);

function mergeHeadersFromLayout(

  baseHeaders: SpreadsheetHeaderConfig[],

  layout: SpreadsheetLayoutState | null,

): SpreadsheetHeaderConfig[] {

  const layoutMap = new Map(

    layout?.headers?.map((h) => [h.col, h]) ?? [],

  );

  return baseHeaders.map((h) => {

    const override = layoutMap.get(h.col);

    if (!override) return { ...h };

    return {

      ...h,

      headerColor: override.headerColor ?? h.headerColor,

      fixed: override.fixed ?? h.fixed,

    };

  });

}



function applyStoredLayout() {

  const layout = readSpreadsheetLayout(

    props.tableConfig.tableName,

    props.tableConfig.sheetKey,

  );

  layoutColWidths.value = { ...(layout?.colWidths ?? {}) };

  mergedHeaders.value = mergeHeadersFromLayout(

    props.tableConfig.headers,

    layout,

  );

}



function persistLayout() {

  debouncedWriteSpreadsheetLayout(

    props.tableConfig.tableName,

    props.tableConfig.sheetKey,

    {

      colWidths: { ...layoutColWidths.value },

      headers: mergedHeaders.value.map((h) => ({

        col: h.col,

        headerColor: h.headerColor,

        fixed: h.fixed,

      })),

    },

  );

}



function toggleHeaderFixed(col: number) {

  const target = mergedHeaders.value.find((h) => h.col === col);

  const willFix = !target?.fixed;

  mergedHeaders.value = mergedHeaders.value.map((h) => {

    if (willFix) {

      return { ...h, fixed: h.col <= col ? true : h.fixed };

    }

    if (h.col >= col) {

      return { ...h, fixed: false };

    }

    return h;

  });

  persistLayout();

  remeasure();

  scheduleRender();

}



function setHeaderColor(col: number, color?: string) {

  mergedHeaders.value = mergedHeaders.value.map((h) =>

    h.col === col ? { ...h, headerColor: color } : h,

  );

  persistLayout();

  scheduleRender();

}



const headerLayoutActions = {

  toggleFixed: toggleHeaderFixed,

  setHeaderColor,

  isFixed: (col: number) =>

    mergedHeaders.value.find((h) => h.col === col)?.fixed ?? false,

  getHeaderColor: (col: number) =>

    mergedHeaders.value.find((h) => h.col === col)?.headerColor,

};



applyStoredLayout();

const engineState = useSpreadsheetEngine({

  tableConfig: toRef(props, 'tableConfig'),

  mergedHeaders,

  columns: toRef(props, 'columns'),

  data: localData,

  rowKeyField: toRef(props, 'rowKeyField'),

  layoutColWidths,

  scrollViewportHeight,

  isDark,

  onChange: (data) => {

    localData.value = data;

    emit('change', { data });

    scheduleRender();

  },

});



const {

  engine,

  sortedColumns,

  colWidths,

  rowHeight,

  scrollTop,

  scrollLeft,

  viewportWidth,

  viewportHeight,

  filterValues,

  viewState,

  setColumnFilter,

  clearColumnFilter,

  setSort,

  clearSort,

  dirtyKeys,

  editing,

  measureCanvas,

  filteredRowIndices,

  contentSize,

  displayRowCount,

  displayColCount,

  colCount,

  defaultColWidth,

  paddedColWidths,

  fixedCols,

  fixedWidth,

  selectionApi,

  undoRedo,

  colConfig,

  isEditable,

  buildCellContext,

  resolveDisplayValue,

  resolveCellStyle,

  commitCellChange,

  initSnapshot,

  remeasure,

  hitTestCell,

  hitTestHeaderCol,

  hitTestRowNumber,

  hitTestHeaderArea,

} = engineState;



const findApi = useSpreadsheetFind({

  filteredRowIndices,

  columns: sortedColumns,

  rows: localData,

  resolveDisplayValue,

});



const selectionMaxRow = computed(() =>

  Math.max(0, filteredRowIndices.value.length - 1),

);

const selectionMaxCol = computed(() => {
  const cols = sortedColumns.value;
  return cols.length ? Math.max(...cols.map((c) => c.col)) : 0;
});



const fillDragSource = shallowRef<ReturnType<typeof normalizeRange> | null>(

  null,

);

const isDraggingFill = ref(false);

const isSelecting = ref(false);

const selectDragStart = ref<{ x: number; y: number; row: number; col: number } | null>(
  null,
);

const headerDragMode = ref<'row' | 'column' | null>(null);

const bodyCursor = ref<'cell' | 'crosshair'>('cell');

const lastPointerOnBody = ref<{ x: number; y: number } | null>(null);

const editDraftSeed = ref<string | number | null>(null);

const editSelectAllOnFocus = ref(true);

let rafId = 0;

const formulaItems = ref<SpreadsheetFormulaItem[]>([]);

const formulaBarRef = ref<InstanceType<typeof SpreadsheetFormulaBar> | null>(
  null,
);

const formulaRefMode = useFormulaReferenceMode();

const lastMenuClientPos = ref({ x: 0, y: 0 });

const headerColorPickerState = ref({
  open: false,
  col: 0,
  x: 0,
  y: 0,
  color: '',
});

const activeCol = computed(() => selectionApi.activeCell.value?.col ?? null);

const activeRow = computed(() => selectionApi.activeCell.value?.row ?? null);

const formulaBarAddress = computed(() => {
  if (activeCol.value == null || activeRow.value == null) return '';
  return `${colIndexToLetters(activeCol.value)}${activeRow.value + 1}`;
});

const formulaBarColConfig = computed(() =>
  activeCol.value == null ? undefined : colConfig(activeCol.value),
);

const formulaBarEditable = computed(() => {
  const cfg = formulaBarColConfig.value;
  if (!cfg) return false;
  return (
    isEditable(cfg) ||
    (props.showConfigButton && cfg.cellType === 'formula')
  );
});

const formulaBarSourceValue = computed(() => {
  const cfg = formulaBarColConfig.value;
  if (!cfg) return '';
  if (cfg.cellType === 'formula') return cfg.formula ?? '';
  return formulaBarDisplayValue.value;
});

const formulaBarDisplayValue = computed(() => {
  if (activeCol.value == null || activeRow.value == null) return '';
  const cfg = formulaBarColConfig.value;
  if (!cfg) return '';
  const dataRow = filteredRowIndices.value[activeRow.value];
  const row = dataRow === undefined ? undefined : localData.value[dataRow];
  const val = resolveDisplayValue(activeRow.value, cfg, row);
  return val == null ? '' : String(val);
});

async function loadFormulaItems() {
  try {
    formulaItems.value = await listSpreadsheetFormula();
  } catch {
    formulaItems.value = [];
  }
}

function openHeaderColorPicker(col: number) {
  const color = headerLayoutActions.getHeaderColor(col) ?? '';
  headerColorPickerState.value = {
    open: true,
    col,
    x: lastMenuClientPos.value.x,
    y: lastMenuClientPos.value.y,
    color,
  };
}

function onHeaderColorPickerApply(col: number, color: string | undefined) {
  setHeaderColor(col, color);
}

function closeHeaderColorPicker() {
  headerColorPickerState.value = {
    ...headerColorPickerState.value,
    open: false,
  };
}

function buildSpreadsheetMenuActions(col: number): SpreadsheetMenuActions {
  const base: SpreadsheetMenuActions = {
    col,
    layoutActions: headerLayoutActions,
    onOpenHeaderColorPicker: openHeaderColorPicker,
  };
  if (!props.showConfigButton) return base;
  return {
    ...base,
    admin: {
      onOpenConfig: () => emit('openConfig'),
      onAddColumn: () => emit('adminAddColumn'),
      onSetColumn: (c: number) => emit('adminSetColumn', c),
    },
  };
}

async function onFormulaBarCommit(value: string) {
  formulaRefMode.exit();
  if (activeCol.value == null || activeRow.value == null) return;
  const trimmed = value.trim();
  const cfg = formulaBarColConfig.value;
  if (!cfg) return;

  if (trimmed.startsWith('=')) {
    if (props.showConfigButton) {
      emit('updateColumnFormula', { col: activeCol.value, formula: trimmed });
    }
    return;
  }

  if (isEditable(cfg)) {
    let parsed: unknown = trimmed;
    if (cfg.cellType === 'number') {
      const n = Number(trimmed);
      parsed = Number.isFinite(n) ? n : trimmed;
    }
    try {
      await commitCellChange(activeRow.value, activeCol.value, parsed);
      scheduleRender();
    } catch (err) {
      emit('error', err);
    }
  }
}

function onFormulaBarDraftChange(draft: string) {
  formulaRefMode.enterIfNeeded(draft, true, formulaBarEditable.value);
  scheduleRender();
}

function onFormulaBarCaretChange(pos: number) {
  formulaRefMode.setCaret(pos);
}

function isOverlayTarget(target: EventTarget | null) {
  if (!(target instanceof Element)) return false;
  return !!target.closest(
    '.global-context-menu, .ant-dropdown, .ant-popover, .ant-modal, .ant-drawer, .ant-select-dropdown, .filter-panel',
  );
}

function onDocumentPointerDown(e: PointerEvent) {
  const target = e.target as Node | null;
  if (!target) return;
  if (formulaRefMode.active.value) return;
  if (containerRef.value?.contains(target)) return;
  if (isOverlayTarget(e.target)) return;
  selectionApi.clearSelection();
  exitEditMode(false);
}



const editingValue = computed(() => {

  if (!editing.value) return '';

  const ctx = buildCellContext(editing.value.row, editing.value.col);

  return ctx?.fieldValue ?? '';

});



const editingColumn = computed(() =>

  editing.value ? colConfig(editing.value.col) : undefined,

);



function renderOptions() {

  return {

    headers: mergedHeaders.value,

    columns: sortedColumns.value,

    rows: localData.value,

    colWidths: paddedColWidths.value,

    rowHeight: rowHeight.value,

    scrollTop: scrollTop.value,

    scrollLeft: scrollLeft.value,

    viewportWidth: viewportWidth.value,

    viewportHeight: viewportHeight.value,

    displayRowCount: displayRowCount.value,

    colCount: colCount.value,

    displayColCount: displayColCount.value,

    defaultColWidth: defaultColWidth.value,

    defaultTextAlign: props.tableConfig.defaultTextAlign ?? 'left',

    isDark: isDark.value,

    fixedCols: fixedCols.value,

    fixedWidth: fixedWidth.value,

    selectionRanges: selectionApi.getSelectionRanges(),

    activeCell: selectionApi.activeCell.value,

    dirtyKeys: dirtyKeys.value,

    filterValues: filterValues.value,

    filteredRowIndices: filteredRowIndices.value,

    showFillHandle:

      !!selectionApi.activeCell.value &&

      selectionApi.getSelectionRanges().length === 1 &&

      !isDraggingFill.value &&

      !editing.value &&

      !formulaRefMode.active.value,

    theme: theme.value,

    resolveDisplayValue,

    resolveCellStyle,

    referenceCell: formulaRefMode.active.value
      ? formulaRefMode.pickedCell.value
      : null,

    findMatches: findApi.open.value ? findApi.matches.value : undefined,

    findActiveIndex: findApi.open.value ? findApi.activeIndex.value : undefined,

  };

}



function scheduleRender() {

  cancelAnimationFrame(rafId);

  rafId = requestAnimationFrame(() => {

    if (headerCanvasRef.value) renderHeader(headerCanvasRef.value, renderOptions());

    if (bodyCanvasRef.value) renderBody(bodyCanvasRef.value, renderOptions());

  });

}



function updateViewportSize() {

  if (!bodyWrapRef.value || !containerRef.value) return;

  const bw = bodyWrapRef.value.clientWidth;

  const bh = bodyWrapRef.value.clientHeight;

  if (bw <= 0 || bh <= 0) return;

  viewportWidth.value = bw;

  viewportHeight.value = bh;

  const stickyH = stickyTopRef.value?.offsetHeight ?? 0;

  scrollViewportHeight.value = Math.max(

    100,

    containerRef.value.clientHeight - stickyH,

  );

  scheduleRender();

}



function onCanvasWheel(e: WheelEvent) {

  if (!bodyWrapRef.value) return;

  const dx = e.deltaX + (e.shiftKey ? e.deltaY : 0);

  const dy = e.shiftKey ? 0 : e.deltaY;

  bodyWrapRef.value.scrollTop += dy;

  bodyWrapRef.value.scrollLeft += dx;

  e.preventDefault();

  onBodyScroll();

}



async function handleRefresh() {

  if (refreshing.value) return;

  refreshing.value = true;

  try {

    await nextTick();

    if (props.tableConfig.onRefresh) {

      await props.tableConfig.onRefresh();

    }

    emit('refresh');

    scheduleRender();

  } catch (err) {

    emit('error', err);

  } finally {

    refreshing.value = false;

  }

}



function onBodyScroll() {

  if (!bodyWrapRef.value) return;

  scrollTop.value = bodyWrapRef.value.scrollTop;

  scrollLeft.value = bodyWrapRef.value.scrollLeft;

  scheduleRender();

}



function exitEditMode(focusContainer = true) {

  editing.value = null;

  editDraftSeed.value = null;

  editSelectAllOnFocus.value = true;

  scheduleRender();

  if (focusContainer) {

    void nextTick(() => {

      containerRef.value?.focus({ preventScroll: true });

    });

  }

}



async function commitActiveEdit(): Promise<boolean> {

  if (!editing.value) return false;

  const { row, col } = editing.value;

  const value = editorOverlayRef.value?.getDraftValue();

  exitEditMode();

  if (value !== undefined) {

    try {

      await commitCellChange(row, col, value);

    } catch (err) {

      emit('error', err);

    }

  }

  return true;

}



function startEdit(

  viewRow: number,

  col: number,

  options?: { replaceWith?: string; selectAllOnFocus?: boolean },

) {

  const cfg = colConfig(col);

  if (!cfg || !isEditable(cfg)) return;

  if (options?.replaceWith != null) {

    editDraftSeed.value = options.replaceWith;

    editSelectAllOnFocus.value = false;

  } else {

    editDraftSeed.value = null;

    editSelectAllOnFocus.value = options?.selectAllOnFocus ?? true;

  }

  editing.value = { row: viewRow, col };

  selectionApi.selectCell(viewRow, col);

  containerRef.value?.focus({ preventScroll: true });

  scheduleRender();

}



function onFindNext() {

  findApi.goNext();

}



function onFindPrev() {

  findApi.goPrev();

}



function onFindQueryUpdate(v: string) {

  findApi.query.value = v;

  scheduleRender();

}



function getCanvasHit(e: MouseEvent) {

  if (!bodyCanvasRef.value) return null;

  const rect = bodyCanvasRef.value.getBoundingClientRect();

  return hitTestCell(e.clientX - rect.left, e.clientY - rect.top);

}



function onCanvasMouseDown(e: MouseEvent) {

  if (e.button === 2) return;

  if (!bodyCanvasRef.value) return;

  const rect = bodyCanvasRef.value.getBoundingClientRect();

  const x = e.clientX - rect.left;

  const y = e.clientY - rect.top;

  const rowHit = hitTestRowNumber(x, y);

  if (rowHit !== null && !formulaRefMode.active.value) {

    containerRef.value?.focus({ preventScroll: true });

    if (editing.value) {

      void commitActiveEdit().then(() => {

        selectionApi.selectEntireRow(rowHit, selectionMaxCol.value, {

          shift: e.shiftKey,

          ctrl: e.ctrlKey || e.metaKey,

        });

        headerDragMode.value = 'row';

        selectDragStart.value = { x, y, row: rowHit, col: 0 };

        isSelecting.value = false;

        scheduleRender();

      });

      return;

    }

    selectionApi.selectEntireRow(rowHit, selectionMaxCol.value, {

      shift: e.shiftKey,

      ctrl: e.ctrlKey || e.metaKey,

    });

    headerDragMode.value = 'row';

    selectDragStart.value = { x, y, row: rowHit, col: 0 };

    isSelecting.value = false;

    scheduleRender();

    return;

  }

  const hit = hitTestCell(x, y);

  if (formulaRefMode.active.value) {
    e.preventDefault();
    if (!hit) return;
    const ref = `${colIndexToLetters(hit.col)}${hit.row + 1}`;
    formulaBarRef.value?.insertAtCaret(ref);
    formulaRefMode.pickCell(hit.row, hit.col);
    formulaBarRef.value?.focusInput();
    scheduleRender();
    return;
  }

  if (editing.value) {

    if (!hit || (hit.row === editing.value.row && hit.col === editing.value.col)) {

      return;

    }

    void commitActiveEdit().then(() => {

      if (!hit) return;

      containerRef.value?.focus({ preventScroll: true });

      selectionApi.selectCell(hit.row, hit.col);

      selectDragStart.value = {

        x: e.clientX - (bodyCanvasRef.value?.getBoundingClientRect().left ?? 0),

        y: e.clientY - (bodyCanvasRef.value?.getBoundingClientRect().top ?? 0),

        row: hit.row,

        col: hit.col,

      };

      isSelecting.value = false;

      scheduleRender();

    });

    return;

  }

  if (!hit) return;

  containerRef.value?.focus({ preventScroll: true });

  headerDragMode.value = null;

  if (e.shiftKey) {

    selectionApi.extendSelection(hit.row, hit.col);

  } else {

    selectionApi.selectCell(hit.row, hit.col);

    selectDragStart.value = { x, y, row: hit.row, col: hit.col };

    isSelecting.value = false;

  }

  const sel = selectionApi.getPrimaryRange();

  if (sel && isFillHandleHit(x, y, sel)) {

    isDraggingFill.value = true;

    fillDragSource.value = sel;

    bodyCursor.value = 'crosshair';

    selectDragStart.value = null;

  }

  scheduleRender();

}



function onCanvasDblClick(e: MouseEvent) {

  e.preventDefault();

  e.stopPropagation();

  const hit = getCanvasHit(e);

  if (!hit) return;

  selectionApi.selectCell(hit.row, hit.col);

  startEdit(hit.row, hit.col, { selectAllOnFocus: true });

}



function isFillHandleHit(x: number, y: number, _sel: ReturnType<typeof normalizeRange>) {

  const active = selectionApi.activeCell.value;

  if (!active) return false;

  const cellX = getDataColScreenX(

    active.col,

    colWidths.value,

    scrollLeft.value,

    fixedCols.value,

    fixedWidth.value,

  );

  const cellY = active.row * rowHeight.value - scrollTop.value;

  const cw = paddedColWidths.value[active.col] ?? defaultColWidth.value;

  const fh = 8;

  return (

    x >= cellX + cw - fh &&

    x <= cellX + cw &&

    y >= cellY + rowHeight.value - fh &&

    y <= cellY + rowHeight.value

  );

}



function updateBodyCursorAt(x: number, y: number) {

  if (isDraggingFill.value) {

    bodyCursor.value = 'crosshair';

    return;

  }

  const sel = selectionApi.getPrimaryRange();

  const overFillHandle =

    !!selectionApi.activeCell.value &&

    !editing.value &&

    !!sel &&

    isFillHandleHit(x, y, sel);

  bodyCursor.value = overFillHandle ? 'crosshair' : 'cell';

}



function onCanvasMouseMove(e: MouseEvent) {

  if (!bodyCanvasRef.value) return;

  const rect = bodyCanvasRef.value.getBoundingClientRect();

  const x = e.clientX - rect.left;

  const y = e.clientY - rect.top;

  lastPointerOnBody.value = { x, y };

  if (isDraggingFill.value && fillDragSource.value) {

    bodyCursor.value = 'crosshair';

    const hit = hitTestCell(x, y);

    if (!hit) return;

    selectionApi.extendSelection(hit.row, hit.col);

    scheduleRender();

    return;

  }

  if (headerDragMode.value === 'row' && selectDragStart.value && (e.buttons & 1) === 1) {

    const rowHit = hitTestRowNumber(x, y);

    if (rowHit !== null) {

      selectionApi.extendRowSelection(rowHit, selectionMaxCol.value);

      scheduleRender();

    }

    return;

  }

  if (selectDragStart.value && (e.buttons & 1) === 1) {

    const dx = x - selectDragStart.value.x;

    const dy = y - selectDragStart.value.y;

    if (!isSelecting.value && Math.hypot(dx, dy) > 3) {

      isSelecting.value = true;

    }

    if (isSelecting.value) {

      const hit = hitTestCell(x, y);

      if (hit) {

        selectionApi.extendSelection(hit.row, hit.col);

        scheduleRender();

      }

      return;

    }

  }

  updateBodyCursorAt(x, y);

}



function onCanvasMouseLeave() {

  lastPointerOnBody.value = null;

  bodyCursor.value = 'cell';

}



async function onCanvasMouseUp() {

  if (isDraggingFill.value && fillDragSource.value) {

    const target = selectionApi.getPrimaryRange();

    if (target) {

      try {

        await engine.fillRange(fillDragSource.value, target);

      } catch (err) {

        emit('error', err);

      }

    }

  }

  isDraggingFill.value = false;

  fillDragSource.value = null;

  isSelecting.value = false;

  selectDragStart.value = null;

  headerDragMode.value = null;

  scheduleRender();

  if (lastPointerOnBody.value) {

    updateBodyCursorAt(lastPointerOnBody.value.x, lastPointerOnBody.value.y);

  } else {

    bodyCursor.value = 'cell';

  }

}



function onHeaderMouseDown(e: MouseEvent) {

  if (e.button === 2) return;

  if (!headerCanvasRef.value) return;

  if (formulaRefMode.active.value || editing.value) return;

  const rect = headerCanvasRef.value.getBoundingClientRect();

  const x = e.clientX - rect.left;

  const y = e.clientY - rect.top;

  const hit = hitTestHeaderArea(x, y);

  if (!hit) return;

  containerRef.value?.focus({ preventScroll: true });

  const mods = { shift: e.shiftKey, ctrl: e.ctrlKey || e.metaKey };

  if (hit.area === 'corner') {

    selectionApi.selectAll(selectionMaxRow.value, selectionMaxCol.value);

    scheduleRender();

    return;

  }

  if (hit.col === undefined) return;

  selectionApi.selectEntireColumn(hit.col, selectionMaxRow.value, mods);

  headerDragMode.value = 'column';

  selectDragStart.value = { x, y, row: 0, col: hit.col };

  isSelecting.value = false;

  scheduleRender();

}



function onHeaderMouseMove(e: MouseEvent) {

  if (!headerCanvasRef.value) return;

  if (headerDragMode.value !== 'column' || !selectDragStart.value) return;

  if ((e.buttons & 1) !== 1) return;

  const rect = headerCanvasRef.value.getBoundingClientRect();

  const x = e.clientX - rect.left;

  const col = hitTestHeaderCol(x);

  if (col === null) return;

  selectionApi.extendColumnSelection(col, selectionMaxRow.value);

  scheduleRender();

}



function onHeaderContextMenu(e: MouseEvent) {

  if (!headerCanvasRef.value) return;

  const rect = headerCanvasRef.value.getBoundingClientRect();

  const x = e.clientX - rect.left;

  const col = hitTestHeaderCol(x);

  if (col === null) return;

  lastMenuClientPos.value = { x: e.clientX, y: e.clientY };

  const header = mergedHeaders.value.find((h) => h.col === col);

  const ctx = engine.getHeaderContext(col);

  openHeaderContextMenu(

    e,

    ctx,

    header,

    props.tableConfig,

    buildSpreadsheetMenuActions(col),

  );

}



function onBodyContextMenu(e: MouseEvent) {

  if (!bodyCanvasRef.value) return;

  const rect = bodyCanvasRef.value.getBoundingClientRect();

  const hit = hitTestCell(e.clientX - rect.left, e.clientY - rect.top);

  if (!hit) return;

  if (!selectionApi.containsCell(hit.row, hit.col)) {
    selectionApi.selectCell(hit.row, hit.col);
  }

  const ctx = buildCellContext(hit.row, hit.col);

  if (!ctx) return;

  lastMenuClientPos.value = { x: e.clientX, y: e.clientY };

  openCellContextMenu(
    e,
    ctx,
    colConfig(hit.col),
    engine,
    startEdit,
    buildSpreadsheetMenuActions(hit.col),
  );

  scheduleRender();

}



async function onEditorCommit(value: unknown) {

  if (!editing.value) return;

  const { row, col } = editing.value;

  exitEditMode();

  try {

    await commitCellChange(row, col, value);

  } catch (err) {

    emit('error', err);

  }

}



function onEditorCancel() {

  exitEditMode();

}



function isSpreadsheetFocused() {

  const root = containerRef.value;

  if (!root) return false;

  const active = document.activeElement;

  if (!active) return true;

  return root === active || root.contains(active);

}



function isTypingInOtherInput() {

  const active = document.activeElement;

  if (!active) return false;

  const tag = active.tagName;

  if (tag === 'INPUT' || tag === 'TEXTAREA') return true;

  return (active as HTMLElement).isContentEditable;

}



function isSpreadsheetShortcutBlocked() {

  if (isTypingInOtherInput()) return false;

  return !!document.querySelector(

    '.ant-modal-wrap:not(.ant-modal-hidden), .ant-drawer-open',

  );

}



function isPrintableKeyForColumn(key: string, cellType?: string) {

  if (key.length !== 1) return false;

  if (cellType === 'number') {

    return /^[0-9.\-]$/.test(key);

  }

  return key >= ' ';

}



function onKeyDown(e: KeyboardEvent) {

  if (e.defaultPrevented) return;

  if (findApi.open.value && e.key === 'Escape') {

    e.preventDefault();

    findApi.closeFind();

    scheduleRender();

    return;

  }

  if ((e.ctrlKey || e.metaKey) && (e.key === 'f' || e.key === 'F')) {

    const inFindInput = (e.target as HTMLElement)?.closest?.('.spreadsheet-find-bar');

    if (!isSpreadsheetShortcutBlocked() || inFindInput) {

      e.preventDefault();

      findApi.toggleFind();

      scheduleRender();

      return;

    }

  }

  if (isSpreadsheetShortcutBlocked()) return;

  const active = selectionApi.activeCell.value;

  if (editing.value) {
    if (e.key === 'Enter') {
      e.preventDefault();
      void commitActiveEdit();
    }
    return;
  }



  if ((e.ctrlKey || e.metaKey) && e.key === 'z') {

    e.preventDefault();

    if (e.shiftKey) {

      void engine.redo().then(scheduleRender).catch((err) => emit('error', err));

    } else {

      void engine.undo().then(scheduleRender).catch((err) => emit('error', err));

    }

    return;

  }

  if ((e.ctrlKey || e.metaKey) && e.key === 'y') {

    e.preventDefault();

    void engine.redo().then(scheduleRender).catch((err) => emit('error', err));

    return;

  }

  if ((e.ctrlKey || e.metaKey) && e.key === 'c') {

    e.preventDefault();

    if (selectionApi.getSelectionRanges().length) {
      engine.copy();
      message.success('复制成功');
    }

    return;

  }

  if ((e.ctrlKey || e.metaKey) && e.key === 'v') {

    e.preventDefault();

    void engine.paste().then(scheduleRender).catch((err) => emit('error', err));

    return;

  }



  if (!active) return;



  if (e.key === 'Enter' || e.key === 'F2') {

    if (isTypingInOtherInput()) return;

    e.preventDefault();

    startEdit(active.row, active.col, { selectAllOnFocus: true });

    return;

  }



  if (

    active &&

    isSpreadsheetFocused() &&

    !isTypingInOtherInput() &&

    !e.ctrlKey &&

    !e.metaKey &&

    !e.altKey &&

    isPrintableKeyForColumn(e.key, colConfig(active.col)?.cellType)

  ) {

    const cfg = colConfig(active.col);

    if (cfg && isEditable(cfg)) {

      e.preventDefault();

      startEdit(active.row, active.col, { replaceWith: e.key });

      return;

    }

  }



  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {

    e.preventDefault();

    let { row, col } = active;

    const cols = sortedColumns.value.map((c) => c.col);

    const colIdx = cols.indexOf(col);



    if (e.key === 'ArrowUp') row = Math.max(0, row - 1);

    if (e.key === 'ArrowDown') {

      row = Math.min(filteredRowIndices.value.length - 1, row + 1);

    }

    if (e.key === 'ArrowLeft' && colIdx > 0) col = cols[colIdx - 1]!;

    if (e.key === 'ArrowRight' && colIdx < cols.length - 1) {

      col = cols[colIdx + 1]!;

    }

    if (e.key === 'Tab') {

      if (e.shiftKey && colIdx > 0) col = cols[colIdx - 1]!;

      else if (!e.shiftKey && colIdx < cols.length - 1) col = cols[colIdx + 1]!;

    }



    if (e.shiftKey) selectionApi.extendSelection(row, col);

    else selectionApi.selectCell(row, col);

    scrollCellIntoView(row, col);

    scheduleRender();

  }



  if (e.key === 'Delete' || e.key === 'Backspace') {

    if (isTypingInOtherInput()) return;

    const selRanges = selectionApi.getSelectionRanges();

    if (!selRanges.length) return;

    e.preventDefault();

    iterateRanges(selRanges, (row, c) => {

      const cfg = colConfig(c);

      if (cfg && isEditable(cfg)) {

        void commitCellChange(row, c, cfg.cellType === 'number' ? 0 : '');

      }

    });

    scheduleRender();

  }

}



function scrollCellIntoView(viewRow: number, col?: number) {

  if (!bodyWrapRef.value) return;

  const top = viewRow * rowHeight.value;

  const bottom = top + rowHeight.value;

  if (top < scrollTop.value) {

    bodyWrapRef.value.scrollTop = top;

  } else if (bottom > scrollTop.value + viewportHeight.value) {

    bodyWrapRef.value.scrollTop = bottom - viewportHeight.value;

  }

  if (col === undefined) return;

  const cellLeft = getDataColScreenX(

    col,

    paddedColWidths.value,

    scrollLeft.value,

    fixedCols.value,

    fixedWidth.value,

  );

  const cellWidth = paddedColWidths.value[col] ?? defaultColWidth.value;

  const cellRight = cellLeft + cellWidth;

  const viewLeft = scrollLeft.value;

  const viewRight = scrollLeft.value + viewportWidth.value;

  if (cellLeft < viewLeft + fixedWidth.value) {

    bodyWrapRef.value.scrollLeft = Math.max(0, cellLeft - fixedWidth.value);

  } else if (cellRight > viewRight) {

    bodyWrapRef.value.scrollLeft = cellRight - viewportWidth.value;

  }

  onBodyScroll();

}



function onFilterUpdate(_v: Record<number, string>) {

  /* legacy noop – filters use viewState */

}



function onColumnFilterUpdate(col: number, checkedValues: string[] | undefined) {

  debouncedColumnFilterUpdate(col, checkedValues);

}



const debouncedColumnFilterUpdate = useDebounceFn(

  (col: number, checkedValues: string[] | undefined) => {

    setColumnFilter(col, checkedValues);

    scheduleRender();

  },

  150,

);



async function handleRollback(entry: SpreadsheetHistoryEntry) {

  const dataRow = localData.value.findIndex(

    (r) => String(r[props.rowKeyField]) === String(entry.rowKey),

  );

  if (dataRow < 0) return;

  const viewRow = filteredRowIndices.value.indexOf(dataRow);

  const col = sortedColumns.value.find((c) => c.fieldId === entry.fieldId)?.col;

  if (col === undefined || viewRow < 0) return;

  try {

    await commitCellChange(

      viewRow,

      col,

      entry.beforeSnapshot,

      true,

      'rollback',

    );

  } catch (err) {

    emit('error', err);

  }

}



function getData() {

  return localData.value;

}



function scrollToRow(viewRow: number) {

  scrollCellIntoView(viewRow);

}



defineExpose({

  undo: engine.undo,

  redo: engine.redo,

  getData,

  scrollToRow,

});



let resizeObserver: ResizeObserver | null = null;



onMounted(async () => {

  applyStoredLayout();

  measureCanvas.value = measureCanvasRef.value;

  initSnapshot();

  remeasure();

  resizeObserver = new ResizeObserver(updateViewportSize);

  if (bodyWrapRef.value) resizeObserver.observe(bodyWrapRef.value);

  if (containerRef.value) resizeObserver.observe(containerRef.value);

  if (stickyTopRef.value) resizeObserver.observe(stickyTopRef.value);

  window.addEventListener('keydown', onKeyDown);

  window.addEventListener('mouseup', onCanvasMouseUp);

  document.addEventListener('pointerdown', onDocumentPointerDown);

  void loadFormulaItems();

  await nextTick();

  requestAnimationFrame(() => {

    updateViewportSize();

    scheduleRender();

  });

});



onUnmounted(() => {

  resizeObserver?.disconnect();

  window.removeEventListener('keydown', onKeyDown);

  window.removeEventListener('mouseup', onCanvasMouseUp);

  document.removeEventListener('pointerdown', onDocumentPointerDown);

  cancelAnimationFrame(rafId);

});



watch(

  () => findApi.activeMatch.value,

  (m) => {

    if (!findApi.open.value || !m) return;

    selectionApi.selectCell(m.row, m.col);

    scrollCellIntoView(m.row, m.col);

    scheduleRender();

  },

);



watch(

  () => props.tableConfig.headers,

  () => {

    applyStoredLayout();

    remeasure();

    scheduleRender();

  },

  { deep: true },

);



watch(isDark, scheduleRender);



watch(

  () => props.loading,

  (v) => {

    if (!v) {

      void nextTick(() => {

        requestAnimationFrame(updateViewportSize);

      });

    }

  },

);



watch(

  [colWidths, paddedColWidths, filteredRowIndices, viewState, dirtyKeys, displayRowCount, displayColCount, () => selectionApi.ranges.value, () => findApi.matches.value, () => findApi.open.value],

  scheduleRender,

  { deep: true },

);

</script>



<template>

  <div

    ref="containerRef"

    class="canvas-spreadsheet"

    :class="{ 'is-dark': isDark }"

    tabindex="0"

  >

    <SpreadsheetFindBar

      :active-index="findApi.activeIndex.value"

      :is-dark="isDark"

      :match-count="findApi.matchCount.value"

      :open="findApi.open.value"

      :query="findApi.query.value"

      @close="findApi.closeFind(); scheduleRender()"

      @next="onFindNext"

      @prev="onFindPrev"

      @update:query="onFindQueryUpdate"

    />

    <canvas ref="measureCanvasRef" class="measure-canvas" />



    <div ref="stickyTopRef" class="sheet-sticky-top">

      <SpreadsheetToolbar

        :can-redo="undoRedo.canRedo()"

        :can-undo="undoRedo.canUndo()"

        :is-dark="isDark"

        :refreshing="refreshing"

        :show-config-button="showConfigButton"

        :show-formula-library="showConfigButton"

        :table-config="tableConfig"

        :workbook-name="workbookName"

        @formula-library-updated="loadFormulaItems"

        @open-config="emit('openConfig')"

        @redo="void engine.redo().then(scheduleRender).catch((err) => emit('error', err))"

        @refresh="void handleRefresh()"

        @rollback="handleRollback"

        @undo="void engine.undo().then(scheduleRender).catch((err) => emit('error', err))"

      >

        <template #formula-bar>

          <SpreadsheetFormulaBar

            ref="formulaBarRef"

            :cell-address="formulaBarAddress"

            :caret-position="formulaRefMode.caret.value"

            :editable="formulaBarEditable"

            :formulas="formulaItems"

            :is-dark="isDark"

            :reference-mode="formulaRefMode.active.value"

            :source-value="formulaBarSourceValue"

            @caret-change="onFormulaBarCaretChange"

            @commit="onFormulaBarCommit"

            @draft-change="onFormulaBarDraftChange"

          />

        </template>

      </SpreadsheetToolbar>



      <div class="header-stack">

        <canvas

          ref="headerCanvasRef"

          class="header-canvas"

          :style="{ height: `${HEADER_HEIGHT}px` }"

          @contextmenu="onHeaderContextMenu"

          @mousedown="onHeaderMouseDown"

          @mousemove="onHeaderMouseMove"

        />



        <HeaderFilterLayer

          :col-widths="paddedColWidths"

          :columns="sortedColumns"

          :enable-filter="tableConfig.enableFilter"

          :fixed-cols="fixedCols"

          :fixed-width="fixedWidth"

          :headers="mergedHeaders"

          :is-dark="isDark"

          :resolve-display-value="resolveDisplayValue"

          :rows="localData"

          :scroll-left="scrollLeft"

          :view-state="viewState"

          :viewport-width="viewportWidth"

          @clear-filter="(col) => { clearColumnFilter(col); scheduleRender(); }"

          @clear-sort="() => { clearSort(); scheduleRender(); }"

          @sort="(col, dir) => { setSort(col, dir); scheduleRender(); }"

          @update:column-filter="onColumnFilterUpdate"

        />

      </div>

    </div>



    <div class="sheet-body">

      <div

        ref="bodyWrapRef"

        class="body-wrap"

        @scroll="onBodyScroll"

        @wheel="onCanvasWheel"

      >

        <div

          class="body-spacer"

          :style="{

            width: `${contentSize.width}px`,

            height: `${contentSize.height}px`,

          }"

        >

          <div

            class="body-viewport-layer"

            :style="{

              width: `${viewportWidth}px`,

              height: `${viewportHeight}px`,

            }"

          >

            <canvas

              ref="bodyCanvasRef"

              class="body-canvas"

              :style="{

                cursor: bodyCursor,

              }"

              @contextmenu="onBodyContextMenu"

              @dblclick="onCanvasDblClick"

              @mousedown="onCanvasMouseDown"

              @mousemove="onCanvasMouseMove"

              @mouseleave="onCanvasMouseLeave"

            />



            <CellEditorOverlay

              v-if="editing"

              ref="editorOverlayRef"

              :col="editing.col"

              :col-widths="paddedColWidths"

              :column="editingColumn"

              :draft-seed="editDraftSeed"

              :fixed-cols="fixedCols"

              :fixed-width="fixedWidth"

              :is-dark="isDark"

              :row-height="rowHeight"

              :scroll-left="scrollLeft"

              :scroll-top="scrollTop"

              :select-all-on-focus="editSelectAllOnFocus"

              :value="editingValue"

              :view-row="editing.row"

              :visible="true"

              @cancel="onEditorCancel"

              @commit="onEditorCommit"

            />

          </div>

        </div>

      </div>



      <div v-if="loading" class="sheet-loading-overlay" aria-hidden="true">

        <span class="sheet-loading-spinner" />

      </div>

    </div>

    <SpreadsheetHeaderColorPickerPopover
      :col="headerColorPickerState.col"
      :color="headerColorPickerState.color"
      :open="headerColorPickerState.open"
      :x="headerColorPickerState.x"
      :y="headerColorPickerState.y"
      @apply="onHeaderColorPickerApply"
      @close="closeHeaderColorPicker"
    />

  </div>

</template>



<style scoped lang="scss">

.canvas-spreadsheet {

  position: relative;

  display: flex;

  flex-direction: column;

  flex: 1;

  min-height: 0;

  height: 100%;

  outline: none;

  border: 1px solid #e5e6eb;

  border-radius: 8px;

  overflow: hidden;



  &.is-dark {

    border-color: #424242;

  }

}



.measure-canvas {

  position: absolute;

  visibility: hidden;

  pointer-events: none;

  width: 1px;

  height: 1px;

}



.sheet-sticky-top {

  flex-shrink: 0;

  position: sticky;

  top: 0;

  z-index: 5;

  background: #fff;



  .is-dark & {

    background: #141414;

  }

}



.sheet-body {

  position: relative;

  flex: 1;

  min-height: 0;

  display: flex;

  flex-direction: column;

}



.sheet-loading-overlay {

  position: absolute;

  inset: 0;

  z-index: 4;

  display: flex;

  align-items: center;

  justify-content: center;

  pointer-events: none;

  background: rgba(255, 255, 255, 0.45);



  .is-dark & {

    background: rgba(20, 20, 20, 0.45);

  }

}



.sheet-loading-spinner {

  width: 28px;

  height: 28px;

  border: 3px solid #e5e6eb;

  border-top-color: #165dff;

  border-radius: 50%;

  animation: sheet-spin 0.8s linear infinite;



  .is-dark & {

    border-color: #424242;

    border-top-color: #1668dc;

  }

}



@keyframes sheet-spin {

  to {

    transform: rotate(360deg);

  }

}



.header-canvas {

  display: block;

  width: 100%;

  flex-shrink: 0;

  cursor: default;

}



.header-stack {

  position: relative;

  flex-shrink: 0;

}



.body-wrap {

  flex: 1;

  min-height: 0;

  overflow: scroll;

  position: relative;

  scrollbar-gutter: stable;



  &::-webkit-scrollbar {

    width: 8px;

    height: 8px;

  }



  &::-webkit-scrollbar-thumb {

    background: #c9cdd4;

    border-radius: 4px;

  }



  &::-webkit-scrollbar-track {

    background: #f2f3f5;

  }



  .is-dark & {

    &::-webkit-scrollbar-thumb {

      background: #424242;

    }



    &::-webkit-scrollbar-track {

      background: #1f1f1f;

    }

  }

}



.body-spacer {

  position: relative;

  min-width: 100%;

}



.body-viewport-layer {

  position: sticky;

  top: 0;

  left: 0;

  z-index: 2;

}



.body-canvas {

  position: absolute;

  top: 0;

  left: 0;

  display: block;

  width: 100%;

  height: 100%;

}

</style>

