<template>
  <div class="list-table-v2" :style="containerStyle">
    <!-- Mini hidden table for measuring row height and header height (only when using dynamic height mode) -->
    <!-- This mirrors the real table's cell rendering for accurate height estimation -->
    <div v-if="shouldUseProbeRow" ref="miniTableRef" class="mini-table" aria-hidden="true">
      <div class="mini-table-inner">
        <!-- Mini header row -->
        <div ref="miniHeaderRef" class="mini-row mini-header">
          <div
            v-for="(column, columnIndex) in tableColumns || []"
            :key="column.key"
            class="mini-cell mini-header-cell"
            :style="getMiniCellStyle(column)"
          >
            <MiniTableHeader :cellData="undefined as T" :columnIndex :columns="tableColumns" :column />
          </div>
        </div>
        <!-- Mini data rows -->
        <div v-if="miniTableData.length > 0" ref="miniTableRowsRef" class="mini-table-rows">
          <div v-for="(rowData, rowIndex) in miniTableData" :key="rowIndex" class="mini-row">
            <div
              v-for="(column, columnIndex) in tableColumns || []"
              :key="column.key"
              class="mini-cell"
              :style="getMiniCellStyle(column)"
            >
              <MiniTableCell
                :cellData="lodash.get(rowData, String(column.dataKey || column.key))"
                :columnIndex
                :columns="tableColumns"
                :column
                :rowData
                :rowIndex
              />
            </div>
          </div>
        </div>
      </div>
    </div>

    <div v-if="debug" class="debug-info">
      <div>Estimated Row Height: {{ resolvedRowHeight }}</div>
      <div>Estimated Header Height: {{ resolvedHeaderHeight }}</div>
    </div>
    <div ref="myTableRef" class="my-table">
      <el-auto-resizer>
        <template #default="{ height, width }">
          <el-table-v2
            ref="table"
            :columns="tableColumns"
            :data="pageData"
            :width="width"
            :height="resolvedHeight(height)"
            :row-height="prop.rowHeight"
            :header-height="resolvedHeaderHeight"
            :estimated-row-height="resolvedRowHeight"
            :border="border"
            :row-key="rowKey"
            :class="border ? 'has-border' : ''"
            @scroll="onScroll"
            @row-click="onRowClick"
            @cell-click="onCellClick"
          />
        </template>
      </el-auto-resizer>
    </div>
    <div v-if="page" class="my-pagination">
      <el-pagination
        :small="state.size.size == 'small'"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="example.page"
        :page-sizes="[10, 20, 50, 100, 200]"
        :page-size="example.size"
        layout="total, sizes, prev, pager, next, jumper"
        :total="data.total"
      />
    </div>
  </div>
</template>

<script lang="tsx" setup generic="T">
import { useStore } from "vuex";
import {
  ref,
  computed,
  watch,
  h,
  onMounted,
  onUpdated,
  onUnmounted,
  useSlots,
  renderSlot,
  nextTick,
  VNode,
  StyleValue,
  useTemplateRef,
} from "vue";
import { useI18n } from "vue3-i18n";
import { Column, ElAutoResizer, ElTableV2 } from "element-plus";
import * as lodash from "lodash-es";
import TzDateTime from "../item/tzDateTime.vue";
import {
  CellRenderer,
  CellRendererParams,
  HeaderCellRenderer,
  HeaderCellRendererParams,
} from "element-plus/es/components/table-v2/src/types.mjs";
import { match, P } from "ts-pattern";
import { FixedDir } from "element-plus/es/components/table-v2/src/constants.mjs";

const slots = useSlots();

const { t } = useI18n();
const { state } = useStore();
const miniTableRef = useTemplateRef("miniTableRef");
const miniHeaderRef = useTemplateRef("miniHeaderRef");
const miniTableRowsRef = useTemplateRef("miniTableRowsRef");
const myTableRef = useTemplateRef("myTableRef"); // Reference to .my-table for ResizeObserver
const miniTableData = ref<any[]>([]);
const estimatedRowHeight = ref<number | undefined>(undefined);
const estimatedHeaderHeight = ref<number | undefined>(undefined);
let miniTableResizeObserver: ResizeObserver | null = null;
let lastMiniTableHeight = 0;

// Track if table is visible in viewport - only probe when visible
const isInViewport = ref(false);
let viewportObserver: IntersectionObserver | null = null;

// Header height constant

type TimestampValue =
  | undefined
  | boolean
  | string
  | {
      valueFormat?: string;
      valueTz?: string;
      displayFormat?: string;
      locale?: string;
      type?: "iso8601" | "unix" | "unixMillis";
    };

interface TzDateTimeConfig {
  valueFormat?: string;
  valueTz?: string;
  displayFormat?: string;
  locale?: string;
  type?: "iso8601" | "unix" | "unixMillis";
}

interface ListTableColumn<T> {
  key: string;
  dataKey?: string;
  name?: string;
  i18n?: string;
  type?: string;
  width?: string | number;
  minWidth?: string | number;
  fixed?: boolean | "left" | "right";
  align?: "left" | "center" | "right";
  slot?: boolean;
  dict?: string;
  timestamp?: TimestampValue;
  filesize?: boolean;
  // Custom renderers - JSX-returning functions that override slot/default rendering
  cellRenderer?: CellRenderer<T>;
  headerCellRenderer?: HeaderCellRenderer<T>;

  [others: string]: any;
}

interface Props {
  data?: any;
  columns?: ListTableColumn<T>[];
  page?: boolean;
  height?: number | string;
  minHeight?: number | string;
  maxHeight?: number | string;
  example?: any;
  rowKey?: string;
  selectKey?: string;
  treeProps?: any;
  lazy?: boolean;
  border?: boolean;
  timestampFormat?: string;
  rowHeight?: number;
  estimatedRowHeight?: number;
  headerHeight?: number;
  debug?: boolean;
}

const prop = withDefaults(defineProps<Props>(), {
  data: () => [],
  columns: () => [],
  page: false,
  height: undefined,
  minHeight: 300,
  maxHeight: undefined,
  example: () => ({}),
  rowKey: "id",
  selectKey: undefined,
  treeProps: undefined,
  lazy: false,
  border: false,
  timestampFormat: "YYYY-MM-DD HH:mm:ss",
  rowHeight: undefined,
  estimatedRowHeight: undefined,
  headerHeight: undefined,
});

const emit = defineEmits(["query", "selection-change", "row-click", "cell-click"]);

// Pagination data - use data.data if page response, otherwise data array
const pageData = computed(() => {
  if (!prop.data) return [];
  if (prop.page && prop.data.data) {
    return prop.data.data;
  }
  if (Array.isArray(prop.data)) {
    return prop.data;
  }
  return [];
});

// Whether to use probe row for dynamic height measurement
// Only use probe row when rowHeight is NOT explicitly specified
const shouldUseProbeRow = computed(() => {
  return prop.rowHeight === undefined && prop.estimatedRowHeight === undefined;
});

// The estimated row height to pass to el-table-v2
// Only used when rowHeight is NOT set (dynamic height mode)
const resolvedRowHeight = computed(() => {
  if (prop.rowHeight !== undefined) return undefined; // fixed height mode, don't use estimated
  if (prop.estimatedRowHeight !== undefined) return prop.estimatedRowHeight; // user provided estimate
  return estimatedRowHeight.value; // use probe-measured value
});

// The header height to pass to el-table-v2
// Only use measured header height when prop.headerHeight is NOT explicitly set
const resolvedHeaderHeight = computed(() => {
  if (prop.headerHeight !== undefined) return prop.headerHeight; // user provided, use it
  return estimatedHeaderHeight.value; // use mini-table measured header height
});

// Measure mini table row and header heights when data changes (only when using probe row)
// We use TWO frames delay to let el-table-v2 fully settle before measuring
watch(
  () => pageData.value,
  async (data) => {
    // Skip if rowHeight is set (fixed height mode) or no data
    if (!shouldUseProbeRow.value || !data || data.length === 0) {
      return;
    }
    // Use up to 3 rows for mini table
    miniTableData.value = lodash.sampleSize(data, 3);
    // Wait for mini table to render
    await nextTick();
    // Wait TWO frames for layout to settle
    await new Promise((resolve) => requestAnimationFrame(resolve));
    await new Promise((resolve) => requestAnimationFrame(resolve));

    const { headerHeight, rowHeight } = estimateTableRowHeight();
    if (headerHeight && headerHeight > 0) {
      estimatedHeaderHeight.value = headerHeight;
    }
    if (rowHeight && rowHeight > 0) {
      estimatedRowHeight.value = rowHeight;
    }
    // Update last known mini-table height for ResizeObserver comparison
    lastMiniTableHeight = miniTableRef.value?.offsetHeight || 0;
  },
  { immediate: true }
);

function estimateTableRowHeight() {
  const headerHeight = miniHeaderRef.value?.offsetHeight;
  const tableRowsHeight = miniTableRowsRef.value?.offsetHeight;
  const rowHeight =
    tableRowsHeight && tableRowsHeight > 0 && miniTableData.value.length > 0
      ? lodash.round(tableRowsHeight / miniTableData.value.length)
      : undefined;

  return { headerHeight, rowHeight };
}

// Setup ResizeObserver on mount
// Observe miniTableRef instead of myTableRef - only re-probe if mini-table's rendered height changes
// This avoids unnecessary re-probing during window resize when container width changes but row heights stay same
onMounted(() => {
  // Resize handler - only re-probe if the mini-table's actual height changed AND table is visible
  const handleResize = lodash.debounce(async () => {
    // Skip if not using probe, no data, or not visible in viewport
    if (!shouldUseProbeRow.value || miniTableData.value.length === 0 || !isInViewport.value) {
      return;
    }

    // Check if mini-table height actually changed (e.g., due to text wrapping)
    const currentHeight = miniTableRef.value?.offsetHeight || 0;
    if (currentHeight === lastMiniTableHeight) {
      return; // Height unchanged, no need to re-probe
    }
    lastMiniTableHeight = currentHeight;

    await nextTick();
    await new Promise((resolve) => requestAnimationFrame(resolve));
    await new Promise((resolve) => requestAnimationFrame(resolve));

    // Measure first while old values still set
    const { headerHeight, rowHeight } = estimateTableRowHeight();

    const newHeader = headerHeight && headerHeight > 0 ? headerHeight : estimatedHeaderHeight.value;
    const newRow = rowHeight && rowHeight > 0 ? rowHeight : estimatedRowHeight.value;

    // Clear then set in same microtask to minimize gap
    estimatedRowHeight.value = undefined;
    estimatedHeaderHeight.value = undefined;
    queueMicrotask(() => {
      estimatedRowHeight.value = newRow;
      estimatedHeaderHeight.value = newHeader;
    });
  }, 50);

  // ResizeObserver for mini-table height changes
  miniTableResizeObserver = new ResizeObserver(() => {
    handleResize();
  });

  // IntersectionObserver to track viewport visibility
  // Only attach ResizeObserver when table is visible, detach when off-screen
  // This saves CPU for tables scrolled out of view
  viewportObserver = new IntersectionObserver(
    (entries) => {
      const entry = entries[0];
      isInViewport.value = entry.isIntersecting;

      if (entry.isIntersecting) {
        // Table became visible - reconnect ResizeObserver
        if (miniTableRef.value && miniTableResizeObserver) {
          miniTableResizeObserver.observe(miniTableRef.value);
        }
      } else {
        // Table went off-screen - disconnect ResizeObserver to save CPU
        // Heights are cached, so no need to keep observing
        if (miniTableResizeObserver) {
          miniTableResizeObserver.disconnect();
        }
      }
    },
    { threshold: 0 } // Trigger as soon as any part is visible
  );

  if (myTableRef.value) {
    viewportObserver.observe(myTableRef.value);
  }

  // Initial observation of mini-table if already in viewport
  if (miniTableRef.value && isInViewport.value && miniTableResizeObserver) {
    miniTableResizeObserver.observe(miniTableRef.value);
  }
});

// Cleanup on unmount
onUnmounted(() => {
  if (miniTableResizeObserver) {
    miniTableResizeObserver.disconnect();
    miniTableResizeObserver = null;
  }
  if (viewportObserver) {
    viewportObserver.disconnect();
    viewportObserver = null;
  }
});

const containerStyle = computed(() => {
  const style: Record<string, string> = {};
  if (prop.height !== undefined) {
    style.height = typeof prop.height === "number" ? `${prop.height}px` : String(prop.height);
  } else {
    style.height = "100%";
  }
  if (prop.maxHeight !== undefined) {
    style.maxHeight = typeof prop.maxHeight === "number" ? `${prop.maxHeight}px` : String(prop.maxHeight);
  }
  return style;
});

// Use explicit height if provided, otherwise use auto from auto-resizer
const resolvedHeight = (autoHeight: number) => {
  if (prop.height !== undefined) {
    return typeof prop.height === "number" ? prop.height : parseInt(String(prop.height));
  }
  return autoHeight;
};

const getValue = (value: any) => {
  if ((typeof value === "undefined" || value === null || value === "") && value !== 0) {
    return "--";
  }
  return value;
};

const formatStamp = (value: any) => {
  const date = new Date(value * 1000);
  const month = date.getMonth() < 9 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1;
  const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
  const hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
  const minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
  const second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
  return date.getFullYear() + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;
};

const formatFileSize = (value: any) => {
  const k = value / 1024;
  if (k < 1) {
    return value + "B";
  }
  const m = k / 1024;
  if (m < 1) {
    return k.toFixed(2) + "K";
  }
  const g = m / 1024;
  if (g < 1) {
    return m.toFixed(2) + "M";
  }
  return g.toFixed(2) + "G";
};

// Helper to resolve timestamp column config to TzDateTime props
const resolveTimestampProps = (ts: TimestampValue): TzDateTimeConfig | null => {
  if (!ts) return null;
  if (ts === true) return { type: "unixMillis", displayFormat: prop.timestampFormat };
  if (typeof ts === "string") {
    // If it's 'unix', 'iso8601', or 'unixMillis' treat as type
    if (["unix", "iso8601", "unixMillis"].includes(ts)) {
      return { type: ts as TzDateTimeConfig["type"], displayFormat: prop.timestampFormat };
    }
    // Otherwise treat as valueFormat
    return { valueFormat: ts, displayFormat: prop.timestampFormat };
  }
  // For object config, apply default displayFormat if not specified
  const config = ts as TzDateTimeConfig;
  if (!config.displayFormat) {
    config.displayFormat = prop.timestampFormat;
  }
  return config;
};

const formatterByDist = (dictKey: string, cellData: any) => {
  if (!dictKey) {
    return getValue(cellData);
  }
  const mapping = state.dict[dictKey];
  if (mapping == null) {
    return getValue(cellData);
  }
  return mapping[cellData] == null ? cellData : mapping[cellData];
};

const formatCellValue = (cellData: T, column: ListTableColumn<T>, rowData: any) => {
  if (column.dict) return formatterByDist(column.dict, cellData);
  if (column.timestamp) return formatStamp(cellData);
  if (column.filesize) return formatFileSize(cellData);
  if (rowData.scheme) return formatterByDist(rowData.scheme + "_" + (column.dataKey || column.key), cellData);
  return getValue(cellData);
};

const handleSizeChange = (val: number) => {
  prop.example.size = val;
  emit("query");
};

const handleCurrentChange = (val: number) => {
  prop.example.page = val;
  emit("query");
};

const onScroll = ({
  scrollTop,
}: {
  scrollTop: number;
  scrollLeft?: number;
  horizontal?: boolean;
  vertical?: boolean;
}) => {
  // Can be used for lazy loading or other scroll-based features
};

const onRowClick = (row: any) => {
  emit("row-click", row);
};

const onCellClick = ({ row, column }: { row: any; column: any }) => {
  emit("cell-click", row, column, null, null);
};

// Build columns for el-table-v2
const tableColumns = computed(() => {
  return prop.columns.map((column): Column<T> => {
    const col: Column<T> = {
      key: column.key,
      title: column.name || (column.i18n ? t(column.i18n) : column.key),
      dataKey: column.dataKey || column.key,
      align: column.align || "center",
      fixed: match(column.fixed)
        .with("left", () => FixedDir.LEFT)
        .with("right", () => FixedDir.RIGHT)
        .with(false, () => undefined)
        .otherwise((value) => value),
      minWidth: match(column.minWidth)
        .with(P.number, (n) => n)
        .with(
          P.string,
          (str) => !isNaN(parseInt(str)),
          (str) => parseInt(str)
        )
        .otherwise(() => 120),

      // If width is explicitly provided, use it; otherwise use flexGrow to auto-distribute
      ...match(column.width)
        .with(P.number, (width) => ({ width }))
        .with(
          P.string,
          (str) => !isNaN(parseInt(str)),
          (str) => ({ width: parseInt(str) })
        )
        .otherwise(() => ({ width: 120, flexGrow: 1 })),
    };

    // Cell renderer - uses renderCellContent which handles slot, cellRenderer, and built-in types
    col.cellRenderer = renderCellContent;

    // Header cell renderer - use custom headerCellRenderer if provided
    col.headerCellRenderer = renderHeaderCellContent;

    col._listTableColumn = column;
    return col;
  });
});

const MiniTableCell = (params: CellRendererParams<T>) => renderCellContent(params);
const MiniTableHeader = (params: HeaderCellRendererParams<T>) => renderHeaderCellContent(params);

// Shared cell renderer - used by both el-table-v2 and mini table for consistent rendering
const renderCellContent = (params: CellRendererParams<T>) => {
  const { cellData, rowData, column: elColumn } = params;
  const column: ListTableColumn<T> = elColumn._listTableColumn;
  const slotName = column.key;

  // If custom cellRenderer is provided, use it (highest priority)
  if (column.cellRenderer) {
    return column.cellRenderer(params);
  }

  // If column has slot=true, render the parent's slot content
  if (column.slot && slots[slotName]) {
    return renderSlot(slots, slotName, { row: rowData });
  }

  // Handle timestamp display using TzDateTime component
  if (column.timestamp && (typeof cellData === "string" || typeof cellData === "number")) {
    const tzProps = resolveTimestampProps(column.timestamp);
    if (tzProps) {
      const { valueFormat, valueTz, displayFormat, locale, type } = tzProps;
      return (
        <TzDateTime
          value={cellData}
          valueFormat={valueFormat}
          valueTz={valueTz}
          displayFormat={displayFormat}
          locale={locale}
          type={type}
        />
      );
    }
  }

  // Handle dict display
  if (column.dict) {
    return <span class="mini-cell-text">{formatterByDist(column.dict, cellData)}</span>;
  }

  // Handle formatting
  const formatted = formatCellValue(cellData, column, rowData);
  return <span class="mini-cell-text">{formatted}</span>;
};

const renderHeaderCellContent = (params: HeaderCellRendererParams<T>) => {
  const { column: elColumn } = params;
  const column: ListTableColumn<T> = elColumn._listTableColumn;

  if (column.headerCellRenderer) {
    return column.headerCellRenderer(params);
  }
  const header = match(column)
    .with({ name: P.select(P.string) }, (name) => name)
    .with({ i18n: P.select(P.string) }, (i18n) => t(i18n))
    .otherwise((c) => c.key);

  return <span class="mini-header-cell-text">{header}</span>;
};

// Get mini cell style - mirrors the real table's column width/flex distribution
const getMiniCellStyle = ({ width, minWidth, maxWidth, flexGrow, flexShrink }: Column<T>): StyleValue => {
  return { width: `${width}px`, minWidth: `${minWidth}px`, maxWidth: `${maxWidth}px`, flexGrow, flexShrink };
};
</script>

<style lang="scss" scoped>
.debug-info {
  font-size: 9px;
}

.list-table-v2 {
  display: flex;
  flex-direction: column;
  flex: 1;
  overflow: hidden;
  position: relative;
}

// Mini hidden table for measuring row height (flex boxes, not el-table-v2)
.mini-table {
  position: absolute;
  visibility: hidden;
  pointer-events: none;
  left: -9999px;
  top: 0;
  width: 100%;
  font-size: var(--el-font-size-base);
}

.mini-table-inner {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.mini-table-rows {
  display: flex;
  flex-direction: column;
  flex: 1;
}

.mini-row {
  display: flex;
  align-items: stretch;
  padding: v-bind("state.size.tablePad");
  border-right: var(--el-table-border);
  border-bottom: var(--el-table-border);
  background: v-bind("state.style.tableBg");
  color: v-bind("state.style.tableColor");
  font-size: var(--el-font-size-base);
  box-sizing: border-box;
  overflow: hidden;
  width: 100%;
  min-height: min-content;
}

.mini-header {
  background: v-bind("state.style.tableHeadBg");
  font-weight: bold;
}

.mini-header-cell {
  justify-content: center;
}

.mini-header-cell-text {
  display: block;
}

.mini-cell {
  display: flex;
  flex-direction: row;
  align-items: center;
  padding: 4px;
  box-sizing: border-box;
  white-space: normal;
  word-break: normal;
  overflow: hidden;
}

.mini-cell-text {
  display: block;
}

.my-table {
  flex: 1;
  width: 100%;
  min-height: 0; // Important for flex child to shrink properly
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative; // For mini-table's containing block
}

.my-table :deep(.el-table-v2) {
  --el-table-bg-color: v-bind("state.style.tableBg") !important;
  --el-table-tr-bg-color: v-bind("state.style.tableBg") !important;
  --el-table-expanded-cell-bg-color: v-bind("state.style.tableBg") !important;
  --el-table-border-color: v-bind("state.style.tableBorderColor");
  --el-table-text-color: v-bind("state.style.tableColor");
  --el-table-header-text-color: v-bind("state.style.tableColor");
  --el-table-row-hover-bg-color: v-bind("state.style.tableCurBg");
  --el-table-current-row-bg-color: v-bind("state.style.tableCurBg");
  --el-table-header-bg-color: v-bind("state.style.tableHeadBg");
  --el-bg-color: v-bind("state.style.tableBg") !important;

  // Border - el-table-v2 uses --el-table-border
  --el-table-border: 1px solid var(--el-table-border-color);
}

.my-table :deep(.el-table-v2__row) {
  background: v-bind("state.style.tableBg") !important;
}

.my-table :deep(.el-table-v2__row:hover) {
  background: v-bind("state.style.tableCurBg") !important;
}

.my-table :deep(.el-table-v2__header-cell) {
  padding: v-bind("state.size.tablePad");
  border-right: var(--el-table-border);
  color: v-bind("state.style.tableColor");
}

.my-table :deep(.el-table-v2__row-cell) {
  padding: v-bind("state.size.tablePad");
  border-right: var(--el-table-border);
  color: v-bind("state.style.tableColor");
}

// Child row background for tree data
.my-table :deep(.el-table-v2__row[data-level="1"]),
.my-table :deep(.el-table-v2__row[data-level="3"]) {
  background: v-bind("state.style.tableChildBg") !important;
}

.my-pagination {
  display: flex;
  justify-content: center;
  padding-top: 5px;
  flex-shrink: 0;
}

.my-pagination * {
  --el-pagination-bg-color: v-bind("state.style.bodyBg") !important;
  --el-pagination-disabled-bg-color: v-bind("state.style.bodyBg") !important;
  --el-pagination-text-color: v-bind("state.style.color") !important;
  --el-pagination-button-color: v-bind("state.style.color") !important;
  --el-pagination-button-disabled-bg-color: v-bind("state.style.bodyBg") !important;
}
</style>
