<template>
  <div class="free-table-element" :data-free-table-id="element.id">
    <!-- 选中时左上角四向箭头：与画布整体拖动一致，不在此阻止 pointerdown，事件冒泡到 ElementWrapper -->
    <button
      v-if="isElementSelected"
      type="button"
      class="free-table-move-handle"
      title="拖动移动整个表格"
      aria-label="拖动移动整个表格"
      @click.stop
    >
      <svg
        class="free-table-move-icon"
        viewBox="0 0 16 16"
        width="11"
        height="11"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        aria-hidden="true"
      >
        <path
          d="M8 1.5v3M8 11.5v3M1.5 8h3M11.5 8h3"
          stroke="currentColor"
          stroke-width="1.35"
          stroke-linecap="round"
        />
        <path
          fill="currentColor"
          d="M8 2.35 6.15 4.55h3.7L8 2.35zm0 11.3 1.85-2.2H6.15L8 13.65zM2.35 8l2.2 1.85V6.15L2.35 8zm11.3 0-2.2-1.85v3.7L13.65 8z"
        />
      </svg>
    </button>
    <div class="free-table-surface" @pointerdown.stop>
      <table>
        <colgroup>
          <col v-for="(cw, ci) in colWidthsMm" :key="`col_${ci}`" :style="{ width: `${cw}mm` }" />
        </colgroup>
        <tbody>
          <tr v-for="r in rowCount" :key="`tr_${r - 1}`" :style="{ height: `${rowHeightsMm[r - 1] ?? 6}mm` }">
            <td
              v-for="cell in anchorsForRow(r - 1)"
              :key="`td_${cell.row}_${cell.col}`"
              class="free-table-cell"
              :class="{
                'is-selected': isAnchorSelected(cell),
                'is-merge-range': isAnchorInMergeRange(cell),
              }"
              :rowspan="Math.max(1, Number(cell.rowspan || 1))"
              :colspan="Math.max(1, Number(cell.colspan || 1))"
              :data-ft-row="cell.row"
              :data-ft-col="cell.col"
              :style="cellStyle(cell)"
              @pointerdown.stop="handleSelectCell(cell, $event)"
              @dblclick.stop="handleCellDblClick(cell)"
            >
              <span
                class="cell-move-handle"
                title="拖动交换单元格内容"
                @pointerdown.stop.prevent="startCellSwapDrag($event, cell.row, cell.col)"
              >
                <span class="cell-move-handle-icon" aria-hidden="true">≡</span>
              </span>
              <span v-if="resolveCellContentType(cell) === 'text'" class="cell-body cell-body--text">{{ resolveCellTextForAnchor(cell) }}</span>
              <span
                v-else-if="resolveCellContentType(cell) === 'number' || resolveCellContentType(cell) === 'amount'"
                class="cell-body cell-body--numeric"
              >
                {{ formatFreeCellNumeric(cell) }}
              </span>
              <img
                v-else-if="resolveCellContentType(cell) === 'image'"
                class="table-media-free"
                alt=""
                :src="resolveFreeCellImageSrc(cell)"
                :style="freeCellMediaStyle(cell, 'image')"
              />
              <img
                v-else-if="resolveCellContentType(cell) === 'qrcode'"
                class="table-media-free"
                alt=""
                :src="resolveFreeCellQrcodeSrc(cell)"
                :style="freeCellMediaStyle(cell, 'qrcode')"
              />
              <img
                v-else-if="resolveCellContentType(cell) === 'barcode'"
                class="table-media-free"
                alt=""
                :src="resolveFreeCellBarcodeSrc(cell)"
                :style="freeCellMediaStyle(cell, 'barcode')"
              />
            </td>
          </tr>
        </tbody>
      </table>
      <div v-if="isElementSelected" class="free-table-track-layer" aria-hidden="true">
        <div
          v-for="(pct, i) in colGripPositionsPct"
          :key="`cg_${i}`"
          class="track-grip track-grip--col"
          :style="{ left: `${pct}%` }"
          title="拖动调整列宽"
          @pointerdown.stop.prevent="onColGripPointerDown(i, $event)"
        />
        <div
          v-for="(pct, i) in rowGripPositionsPct"
          :key="`rg_${i}`"
          class="track-grip track-grip--row"
          :style="{ top: `${pct}%` }"
          title="拖动调整行高"
          @pointerdown.stop.prevent="onRowGripPointerDown(i, $event)"
        />
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
  import { computed, ref } from 'vue';
  import QRCode from 'qrcode';
  import { getValueByPath } from '../../core/tableBuilder';
  import { normalizeFreeTableAnchors } from '../../core/freeTableGrid';
  import { resolveFreeTableCellBorderSides } from '../../core/freeTableBorders';
  import { lineStyleKeyToCssBorderStyle, resolveFreeTableCellLineStyleKeys } from '../../core/freeTableLineStyles';
  import { redistributeColEdge, redistributeRowEdge, resolveFreeTableColWidthsMm, resolveFreeTableRowHeightsMm } from '../../core/freeTableTracks';
  import type { NativeFreeTableCell, NativeFreeTableElement } from '../../core/types';

  const PX_PER_MM = 3.7795275591;

  const qrCodeCache = ref<Record<string, string>>({});
  const barcodeCache = ref<Record<string, string>>({});

  const props = withDefaults(
    defineProps<{
      element: NativeFreeTableElement;
      previewData: Record<string, any>;
      selectedCell?: { row: number; col: number } | null;
      /** 与 selectedCell 配合：Shift 选取的第二角（网格坐标，左上角锚点） */
      mergeRangeCorner?: { row: number; col: number } | null;
      isElementSelected?: boolean;
      /** 画布缩放，用于把指针位移换算为 mm */
      scale?: number;
    }>(),
    { scale: 1 },
  );

  const emit = defineEmits<{
    (e: 'select-cell', payload: { row: number; col: number; shiftKey?: boolean }): void;
    (e: 'swap-cells', payload: { fromRow: number; fromCol: number; toRow: number; toCol: number }): void;
    /** 双击单元格：打开仅针对本表的单元格编辑弹窗 */
    (e: 'edit-cell', payload: { row: number; col: number }): void;
    /** 列宽/行高变更（mm） */
    (e: 'update-tracks', payload: { colWidths?: number[]; rowHeights?: number[] }): void;
  }>();

  const rowCount = computed(() => Math.max(1, Number(props.element?.rowCount || 1)));
  const colCount = computed(() => Math.max(1, Number(props.element?.colCount || 1)));

  const colWidthsMm = computed(() => resolveFreeTableColWidthsMm(props.element));
  const rowHeightsMm = computed(() => resolveFreeTableRowHeightsMm(props.element));

  const colGripPositionsPct = computed(() => {
    const w = Math.max(0.01, Number(props.element?.w) || 0.01);
    let x = 0;
    const arr = colWidthsMm.value;
    const out: number[] = [];
    for (let i = 0; i < arr.length - 1; i += 1) {
      x += arr[i];
      out.push((x / w) * 100);
    }
    return out;
  });

  const rowGripPositionsPct = computed(() => {
    const h = Math.max(0.01, Number(props.element?.h) || 0.01);
    let y = 0;
    const arr = rowHeightsMm.value;
    const out: number[] = [];
    for (let i = 0; i < arr.length - 1; i += 1) {
      y += arr[i];
      out.push((y / h) * 100);
    }
    return out;
  });

  const anchorsNormalized = computed(() =>
    normalizeFreeTableAnchors(rowCount.value, colCount.value, props.element?.cells || []),
  );

  const mergeRect = computed(() => {
    if (!props.selectedCell || !props.mergeRangeCorner) return null;
    const a = props.selectedCell;
    const b = props.mergeRangeCorner;
    return {
      r0: Math.min(a.row, b.row),
      r1: Math.max(a.row, b.row),
      c0: Math.min(a.col, b.col),
      c1: Math.max(a.col, b.col),
    };
  });

  function anchorsForRow(r: number) {
    return anchorsNormalized.value.filter((c) => c.row === r).sort((a, b) => a.col - b.col);
  }

  function resolveCellContentType(cell: NativeFreeTableCell) {
    return String((cell as any)?.contentType || 'text');
  }

  function resolveCellRawString(cell: NativeFreeTableCell) {
    const bindField = String(cell?.bindField || '').trim();
    if (bindField) {
      const bound = getValueByPath(props.previewData || {}, bindField);
      if (bound !== undefined && bound !== null && String(bound).trim()) {
        return String(bound);
      }
    }
    return String(cell?.text || '').trim();
  }

  function resolveCellTextForAnchor(cell: NativeFreeTableCell) {
    const t = resolveCellContentType(cell);
    if (t === 'number' || t === 'amount') {
      return formatFreeCellNumeric(cell);
    }
    const raw = resolveCellRawString(cell);
    return raw || ' ';
  }

  function formatFreeCellNumeric(cell: NativeFreeTableCell) {
    const raw = resolveCellRawString(cell);
    const numeric = Number(raw);
    if (!Number.isFinite(numeric)) {
      return raw || ' ';
    }
    const decimals = Math.max(0, Math.min(6, Number((cell as any)?.decimalPlaces ?? 2)));
    const finalValue =
      (cell as any)?.roundHalfUp === false
        ? Math.trunc(numeric * 10 ** decimals) / 10 ** decimals
        : Number(numeric.toFixed(decimals));
    const formatted = finalValue.toLocaleString(undefined, {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals,
    });
    if (resolveCellContentType(cell) === 'amount') {
      const symbol = (cell as any)?.amountType === 'USD' ? '$' : (cell as any)?.amountType === 'EUR' ? 'EUR ' : '¥';
      return `${symbol}${formatted}`;
    }
    return formatted;
  }

  function resolveFreeCellImageSrc(cell: NativeFreeTableCell) {
    const v = resolveCellRawString(cell);
    if (v) {
      return v;
    }
    return `https://via.placeholder.com/180x80.png?text=${encodeURIComponent('Image')}`;
  }

  function freeCellMediaStyle(cell: NativeFreeTableCell, type: 'image' | 'qrcode' | 'barcode') {
    const fillCell = (cell as any)?.fillCell !== false;
    const scale = Math.max(10, Math.min(100, Number((cell as any)?.contentScale || 100)));
    const percent = `${scale}%`;
    const base = {
      display: 'block',
      margin: '0 auto',
      maxWidth: '100%',
      maxHeight: '100%',
      objectFit: (type === 'image' ? (cell as any)?.imageFit || 'contain' : 'contain') as string,
    } as Record<string, any>;
    if (fillCell) {
      base.width = '100%';
      base.height = '100%';
    } else {
      base.width = percent;
      base.height = type === 'barcode' ? `${Math.max(20, scale * 0.6)}%` : percent;
    }
    return base;
  }

  function resolveFreeCellQrcodeSrc(cell: NativeFreeTableCell) {
    const value = String(resolveCellRawString(cell) || 'qrcode_empty');
    const key = `${(cell as any)?.qrLevel || 'M'}|${(cell as any)?.qrRenderType || 'image/png'}|${value}`;
    if (qrCodeCache.value[key]) {
      return qrCodeCache.value[key];
    }
    void QRCode.toDataURL(value, {
      errorCorrectionLevel: (cell as any)?.qrLevel || 'M',
      margin: 0,
      type: (cell as any)?.qrRenderType || 'image/png',
      width: 200,
    })
      .then((url) => {
        qrCodeCache.value = { ...qrCodeCache.value, [key]: url };
      })
      .catch(() => {});
    return '';
  }

  async function buildBarcodeDataUrl(value: string, format: string) {
    const mod: any = await import('jsbarcode');
    const JsBarcode = mod.default || mod;
    const canvas = document.createElement('canvas');
    JsBarcode(canvas, value || '00000000', {
      format: format || 'CODE128',
      displayValue: false,
      margin: 0,
      width: 2,
      height: 70,
    });
    return canvas.toDataURL('image/png');
  }

  function resolveFreeCellBarcodeSrc(cell: NativeFreeTableCell) {
    const value = String(resolveCellRawString(cell) || '00000000');
    const format = String((cell as any)?.barcodeFormat || 'CODE128');
    const key = `${format}|${value}`;
    if (barcodeCache.value[key]) {
      return barcodeCache.value[key];
    }
    void buildBarcodeDataUrl(value, format)
      .then((url) => {
        barcodeCache.value = { ...barcodeCache.value, [key]: url };
      })
      .catch(() => {});
    return '';
  }

  function cellStyle(cell: NativeFreeTableCell) {
    const rs = Math.max(1, Number(cell?.rowspan || 1));
    const cs = Math.max(1, Number(cell?.colspan || 1));
    const bw = Math.max(1, Number(props.element?.borderWidth || 1));
    const bc = props.element?.borderColor || '#d9d9d9';
    const sides = resolveFreeTableCellBorderSides(
      props.element,
      anchorsNormalized.value,
      cell,
      cell.row,
      cell.col,
      rs,
      cs,
      rowCount.value,
      colCount.value,
    );
    const lineKeys = resolveFreeTableCellLineStyleKeys(
      props.element,
      cell.row,
      cell.col,
      rs,
      cs,
      rowCount.value,
      colCount.value,
    );
    const nowrap = (cell as any).autoWrap === false;
    return {
      boxSizing: 'border-box',
      textAlign: cell?.align || 'left',
      verticalAlign: cell?.verticalAlign || 'middle',
      fontSize: `${Number(cell?.fontSize || 12)}px`,
      color: cell?.color || '#111111',
      backgroundColor: cell?.backgroundColor || '#ffffff',
      whiteSpace: nowrap ? 'nowrap' : 'pre-wrap',
      wordBreak: nowrap ? 'normal' : 'break-all',
      borderTop: sides.top ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.top)} ${bc}` : 'none',
      borderRight: sides.right ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.right)} ${bc}` : 'none',
      borderBottom: sides.bottom ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.bottom)} ${bc}` : 'none',
      borderLeft: sides.left ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.left)} ${bc}` : 'none',
    };
  }

  function handleSelectCell(cell: NativeFreeTableCell, e: PointerEvent) {
    emit('select-cell', {
      row: cell.row,
      col: cell.col,
      shiftKey: e.shiftKey === true,
    });
  }

  function handleCellDblClick(cell: NativeFreeTableCell) {
    emit('edit-cell', { row: cell.row, col: cell.col });
  }

  function isAnchorSelected(cell: NativeFreeTableCell) {
    if (!props.isElementSelected || !props.selectedCell) return false;
    return props.selectedCell.row === cell.row && props.selectedCell.col === cell.col;
  }

  function isAnchorInMergeRange(cell: NativeFreeTableCell) {
    const rect = mergeRect.value;
    if (!rect) return false;
    const rs = Math.max(1, Number(cell.rowspan || 1));
    const cs = Math.max(1, Number(cell.colspan || 1));
    const a0 = cell.row;
    const a1 = cell.row + rs - 1;
    const b0 = cell.col;
    const b1 = cell.col + cs - 1;
    return !(a1 < rect.r0 || a0 > rect.r1 || b1 < rect.c0 || b0 > rect.c1);
  }

  function onColGripPointerDown(edge: number, e: PointerEvent) {
    if (e.button !== 0) {
      return;
    }
    const el = props.element;
    const base = [...resolveFreeTableColWidthsMm(el)];
    const totalW = Math.max(0.01, Number(el.w) || 0.01);
    const startX = e.clientX;
    const sc = Math.max(0.2, Number(props.scale) || 1);
    const target = e.currentTarget as HTMLElement | null;
    try {
      target?.setPointerCapture?.(e.pointerId);
    } catch (_err) {
      /* 忽略 */
    }
    const onMove = (ev: PointerEvent) => {
      const deltaMm = (ev.clientX - startX) / sc / PX_PER_MM;
      const next = redistributeColEdge(base, edge, deltaMm, totalW);
      if (next) {
        emit('update-tracks', { colWidths: next });
      }
    };
    const onUp = (ev: PointerEvent) => {
      try {
        target?.releasePointerCapture?.(ev.pointerId);
      } catch (_err) {
        /* 忽略 */
      }
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      window.removeEventListener('pointercancel', onUp);
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    window.addEventListener('pointercancel', onUp);
  }

  function onRowGripPointerDown(edge: number, e: PointerEvent) {
    if (e.button !== 0) {
      return;
    }
    const el = props.element;
    const base = [...resolveFreeTableRowHeightsMm(el)];
    const totalH = Math.max(0.01, Number(el.h) || 0.01);
    const startY = e.clientY;
    const sc = Math.max(0.2, Number(props.scale) || 1);
    const target = e.currentTarget as HTMLElement | null;
    try {
      target?.setPointerCapture?.(e.pointerId);
    } catch (_err) {
      /* 忽略 */
    }
    const onMove = (ev: PointerEvent) => {
      const deltaMm = (ev.clientY - startY) / sc / PX_PER_MM;
      const next = redistributeRowEdge(base, edge, deltaMm, totalH);
      if (next) {
        emit('update-tracks', { rowHeights: next });
      }
    };
    const onUp = (ev: PointerEvent) => {
      try {
        target?.releasePointerCapture?.(ev.pointerId);
      } catch (_err) {
        /* 忽略 */
      }
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      window.removeEventListener('pointercancel', onUp);
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    window.addEventListener('pointercancel', onUp);
  }

  function startCellSwapDrag(event: PointerEvent, fromRow: number, fromCol: number) {
    if (event.button !== 0) return;
    const startId = props.element.id;
    const onMove = (_e: PointerEvent) => {};
    const onUp = (up: PointerEvent) => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      const top = document.elementFromPoint(up.clientX, up.clientY) as HTMLElement | null;
      const host = top?.closest?.('[data-free-table-id]') as HTMLElement | null;
      if (!host || host.getAttribute('data-free-table-id') !== startId) {
        return;
      }
      const td = top?.closest?.('td[data-ft-row]') as HTMLElement | null;
      if (!td) return;
      const toRow = Number(td.getAttribute('data-ft-row'));
      const toCol = Number(td.getAttribute('data-ft-col'));
      if (!Number.isFinite(toRow) || !Number.isFinite(toCol)) return;
      if (fromRow === toRow && fromCol === toCol) return;
      emit('swap-cells', { fromRow, fromCol, toRow, toCol });
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
  }
</script>

<style scoped lang="less">
  .free-table-element {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    overflow: hidden;
    border: 1px dashed #999;
    background: #fff;
    box-sizing: border-box;
  }

  .free-table-move-handle {
    position: absolute;
    left: -1px;
    top: -1px;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    border: 1px solid #bfbfbf;
    border-radius: 2px;
    background: linear-gradient(180deg, #ffffff 0%, #e8eef8 55%, #dce6f5 100%);
    color: #1f1f1f;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
    cursor: grab;
    user-select: none;
    line-height: 0;

    &:active {
      cursor: grabbing;
    }

    &:focus-visible {
      outline: 2px solid #1677ff;
      outline-offset: 1px;
    }
  }

  .free-table-move-icon {
    display: block;
    flex-shrink: 0;
    opacity: 0.92;
  }

  .free-table-surface {
    position: relative;
    width: 100%;
    height: 100%;
    min-height: 0;
    /* 设计器内不显示滚动条，与打印区域一致；略溢出时裁切即可 */
    overflow: hidden;
  }

  .free-table-track-layer {
    position: absolute;
    inset: 0;
    z-index: 4;
    pointer-events: none;
  }

  .track-grip {
    position: absolute;
    pointer-events: auto;
    z-index: 6;
    box-sizing: border-box;
  }

  .track-grip--col {
    top: 0;
    bottom: 0;
    width: 10px;
    margin-left: -5px;
    cursor: col-resize;
    background: rgba(22, 119, 255, 0.14);
    border-radius: 2px;
  }

  .track-grip--row {
    left: 0;
    right: 0;
    height: 10px;
    margin-top: -5px;
    cursor: row-resize;
    background: rgba(22, 119, 255, 0.14);
    border-radius: 2px;
  }

  .track-grip:hover {
    background: rgba(22, 119, 255, 0.28);
  }

  .free-table-surface table {
    width: 100%;
    height: 100%;
    border-collapse: collapse;
    border-spacing: 0;
    table-layout: fixed;
    overflow: hidden;
    box-sizing: border-box;
  }

  .free-table-surface tbody tr {
    box-sizing: border-box;
  }

  .free-table-cell {
    position: relative;
    box-sizing: border-box;
    min-width: 20px;
    min-height: 20px;
    padding: 10px 4px 4px;
    white-space: pre-wrap;
    word-break: break-all;
    user-select: none;
    cursor: pointer;
    vertical-align: top;
  }

  .free-table-cell.is-merge-range:not(.is-selected) {
    box-shadow: inset 0 0 0 1px rgba(250, 140, 22, 0.65);
    background-color: rgba(255, 247, 230, 0.55);
  }

  .cell-body {
    display: block;
  }

  .table-media-free {
    box-sizing: border-box;
  }

  .cell-move-handle {
    position: absolute;
    left: 50%;
    top: 1px;
    transform: translateX(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 12px;
    padding: 0 4px;
    border-radius: 3px;
    background: #1677ff;
    color: #fff;
    font-size: 10px;
    line-height: 1;
    opacity: 0;
    pointer-events: none;
    cursor: grab;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
    transition: opacity 0.12s ease;

    &:active {
      cursor: grabbing;
    }
  }

  .cell-move-handle-icon {
    display: block;
    transform: scaleY(0.85);
    font-weight: 700;
  }

  .free-table-cell:hover .cell-move-handle,
  .free-table-cell.is-selected .cell-move-handle {
    opacity: 1;
    pointer-events: auto;
  }

  .free-table-cell.is-selected {
    box-shadow: inset 0 0 0 2px #1677ff;
    z-index: 1;
  }
</style>
