import React from "react";
import { FlatList, PanResponder, Pressable, View, } from "react-native";
import { cn } from "../utils/cn";
import { MutedText, Text } from "./text";
import { Skeleton } from "./skeleton";
const alignClass = {
    left: "text-left",
    center: "text-center",
    right: "text-right",
};
const alignItems = {
    left: "flex-start",
    center: "center",
    right: "flex-end",
};
/**
 * Resolve a column's layout style. `width` (fixed px) takes precedence over
 * `flex` (grow factor, default 1). Kept pure so layout rules are unit-testable
 * without a render host.
 */
export function resolveColumnStyle(column) {
    const align = column.align ?? "left";
    const base = {
        paddingHorizontal: 12,
        paddingVertical: 12,
        justifyContent: "center",
        alignItems: alignItems[align],
    };
    if (typeof column.width === "number") {
        return { ...base, width: column.width };
    }
    return { ...base, flex: column.flex ?? 1 };
}
/** Below this a cell shows nothing but its own padding. */
export const MIN_COLUMN_WIDTH = 64;
/**
 * The web pointer for a draggable boundary.
 *
 * `col-resize` is a CSS cursor react-native-web passes through, and RN's own
 * `ViewStyle` types only the few cursors native supports — so this is typed as
 * a plain style object rather than cast, which keeps the assertion off the
 * component and out of every future reader's way. Native ignores the property.
 */
const WEB_COL_RESIZE_CURSOR = { cursor: "col-resize" };
/**
 * Starting widths for a resizable table: the container divided by flex, with
 * fixed-width columns taken out first.
 *
 * Pure so the layout rule is testable without a render host, and so the answer
 * is the same on the first frame as after a resize — a table that measures
 * itself and then computes differently is a table that jumps.
 */
export function distributeColumnWidths(columns, totalWidth, minWidth = MIN_COLUMN_WIDTH) {
    const fixed = columns.reduce((sum, c) => sum + (typeof c.width === "number" ? c.width : 0), 0);
    const flexColumns = columns.filter((c) => typeof c.width !== "number");
    const flexTotal = flexColumns.reduce((sum, c) => sum + (c.flex ?? 1), 0) || 1;
    // Never negative: a container narrower than its fixed columns would
    // otherwise hand out negative widths and collapse every flexible cell.
    const spare = Math.max(0, totalWidth - fixed);
    return columns.map((c) => typeof c.width === "number"
        ? c.width
        : Math.max(minWidth, (spare * (c.flex ?? 1)) / flexTotal));
}
/**
 * Apply one drag to a pair of adjacent columns.
 *
 * Resizing takes width from the NEIGHBOUR rather than from the table, so the
 * total stays put and the columns to the right do not crawl sideways as the
 * handle moves. Both sides clamp at `minWidth`, so a drag past the limit stops
 * instead of inverting the pair.
 */
export function resizeColumnPair(widths, index, delta, minWidth = MIN_COLUMN_WIDTH) {
    const next = [...widths];
    const left = next[index];
    const right = next[index + 1];
    if (left === undefined || right === undefined) {
        return next;
    }
    const clamped = Math.max(minWidth - left, Math.min(delta, right - minWidth));
    next[index] = left + clamped;
    next[index + 1] = right - clamped;
    return next;
}
/**
 * A cell's style, with an explicit width when the table is resizable.
 *
 * Wraps {@link resolveColumnStyle} rather than replacing it: the flex path is
 * still what a non-resizable table uses, and one function deciding padding and
 * alignment keeps the two from drifting apart by a pixel.
 */
function cellStyle(column, width) {
    const base = resolveColumnStyle(column);
    if (width === undefined) {
        return base;
    }
    // `flex` has to go, not just be overridden: a box with both grows past the
    // width it was told to take.
    const { flex: _flex, ...rest } = base;
    return { ...rest, width };
}
/**
 * Decide the body mode from the loading flag + row count. Loading wins over
 * everything; an empty data set (when not loading) shows the empty state.
 * Pure so the branch logic is unit-testable without a render host.
 */
export function dataTableBodyMode(loading, rowCount) {
    if (loading) {
        return "loading";
    }
    return rowCount === 0 ? "empty" : "data";
}
function HeaderRowInner({ columns, widths, onResize, }) {
    return (<View className={cn("flex-row border-b border-border bg-card dark:border-neutral-800 dark:bg-neutral-900/50")} accessibilityRole="header">
      {columns.map((column, i) => (<View key={column.key} style={cellStyle(column, widths?.[i])} 
        // `relative` so the handle can sit on this cell's trailing edge; it
        // is absolutely placed and half of it overhangs the boundary, which
        // is what makes the target land where the eye expects.
        className="relative">
          <Text className={cn("text-xs font-semibold uppercase tracking-wider text-muted-foreground dark:text-neutral-400", alignClass[column.align ?? "left"])} numberOfLines={1}>
            {column.header}
          </Text>
          {onResize && i < columns.length - 1 ? (<ColumnResizeHandle onResize={(delta) => onResize(i, delta)}/>) : null}
        </View>))}
    </View>);
}
/**
 * The draggable boundary between two header cells.
 *
 * A `PanResponder` rather than a gesture-handler recogniser: this package does
 * not depend on `react-native-gesture-handler`, and a header handle needs no
 * more than "how far has the pointer moved since it went down".
 *
 * `dx` is reported cumulatively from the gesture's start, so each move is fed
 * the DIFFERENCE from the last one — passing `dx` straight through would apply
 * the whole travel again on every frame and the column would run away.
 */
function ColumnResizeHandle({ onResize }) {
    const last = React.useRef(0);
    const responder = React.useMemo(() => PanResponder.create({
        onStartShouldSetPanResponder: () => true,
        onMoveShouldSetPanResponder: () => true,
        onPanResponderGrant: () => {
            last.current = 0;
        },
        onPanResponderMove: (_event, gesture) => {
            onResize(gesture.dx - last.current);
            last.current = gesture.dx;
        },
        onPanResponderRelease: () => {
            last.current = 0;
        },
    }), [onResize]);
    return (<View {...responder.panHandlers} 
    // Wider than it looks: the visible rule is 1px and the target is 9,
    // because a 1px drag target is a 1px drag target.
    style={[
            {
                position: "absolute",
                top: 0,
                bottom: 0,
                right: -4,
                width: 9,
                alignItems: "center",
            },
            // `col-resize` is a WEB cursor and RN's `ViewStyle` only types the
            // handful native supports, so it goes in as its own object rather than
            // widening the whole style to `unknown`. Without it the pointer gives
            // no sign the boundary can be dragged; react-native-web passes it
            // through and native ignores it.
            WEB_COL_RESIZE_CURSOR,
        ]} accessibilityRole="adjustable" accessibilityLabel="Resize column">
      <View className="h-full w-px bg-border dark:bg-neutral-800"/>
    </View>);
}
// Memoized so an unchanged row (stable `columns` + `onPress` identity) skips
// re-rendering when the FlatList recycles. `React.memo` on a generic function
// erases the type parameter, so we cast back to a generic-preserving signature.
const HeaderRow = React.memo(HeaderRowInner);
function DataRowInner({ row, columns, widths, onPress, }) {
    const cells = (<>
      {columns.map((column, i) => (<View key={column.key} style={cellStyle(column, widths?.[i])}>
          {column.render ? (column.render(row)) : (<Text className={cn("text-sm text-foreground", alignClass[column.align ?? "left"])}>
              {String(row[column.key] ?? "")}
            </Text>)}
        </View>))}
    </>);
    if (onPress) {
        return (<Pressable className="flex-row border-b border-border active:bg-accent dark:active:bg-neutral-800 dark:border-neutral-800" onPress={() => onPress(row)} accessibilityRole="button">
        {cells}
      </Pressable>);
    }
    return <View className="flex-row border-b border-border dark:border-neutral-800">{cells}</View>;
}
const DataRow = React.memo(DataRowInner);
/**
 * Generic, typed data table for admin dashboards. Renders a sticky-ish header
 * row above a virtualised `FlatList` body (react-native's built-in list — the
 * package does not depend on `@shopify/flash-list`, and adding a runtime dep is
 * out of scope). Works on RN web (the primary admin target) and native.
 *
 * Loading shows skeleton rows; an empty data set shows `emptyState` (or
 * `emptyMessage`). Columns size by fixed `width` or `flex`, support custom
 * `render`, and alignment.
 */
export function DataTable({ columns, data, keyExtractor, onRowPress, loading = false, loadingRowCount = 5, emptyState, emptyMessage = "No data", resizable = false, className, }) {
    const mode = dataTableBodyMode(loading, data.length);
    // ── Resizable columns ──────────────────────────────────────────────────
    //
    // Widths are held here rather than on each column so a drag moves ONE pair
    // and the table's total width is unchanged. They are seeded from the
    // measured container, so a resizable table still fills its space instead of
    // needing pixel widths written by hand at every call site.
    //
    // A manual drag wins over a re-seed: `touched` stops a window resize from
    // discarding the operator's own column widths, which is what made the first
    // cut feel broken — every layout pass reset them.
    const [tableWidth, setTableWidth] = React.useState(0);
    const [widths, setWidths] = React.useState(null);
    const touched = React.useRef(false);
    const onLayout = React.useCallback((e) => {
        const next = e.nativeEvent.layout.width;
        setTableWidth((prev) => (Math.abs(prev - next) < 1 ? prev : next));
    }, []);
    React.useEffect(() => {
        if (!resizable || tableWidth <= 0 || touched.current) {
            return;
        }
        setWidths(distributeColumnWidths(columns, tableWidth));
        // `columns` is in the deps because adding or removing one has to
        // redistribute; a caller that rebuilds the array every render should
        // memoize it, which every manager in this estate already does for the
        // row-recycling reason below.
    }, [resizable, tableWidth, columns]);
    const handleResize = React.useCallback((index, delta) => {
        touched.current = true;
        setWidths((prev) => (prev ? resizeColumnPair(prev, index, delta) : prev));
    }, []);
    const resolved = resizable ? (widths ?? undefined) : undefined;
    // Stable `renderItem` identity (only re-created when `columns`/`onRowPress`
    // change) so the memoized `DataRow` can recycle. `extraData` tells FlatList
    // to re-render rows when either of those closed-over deps change.
    const renderItem = React.useCallback(({ item }) => (<DataRow row={item} columns={columns} widths={resolved} onPress={onRowPress}/>), [columns, onRowPress, resolved]);
    return (<View onLayout={resizable ? onLayout : undefined} className={cn("overflow-hidden rounded-lg border border-border dark:border-neutral-800", className)}>
      <HeaderRow columns={columns} widths={resolved} onResize={resizable && resolved ? handleResize : undefined}/>
      {mode === "loading" ? (<View>
          {Array.from({ length: loadingRowCount }).map((_, i) => (<View key={i} className="flex-row items-center border-b border-border px-3 py-3 dark:border-neutral-800">
              <Skeleton className="h-4 flex-1"/>
            </View>))}
        </View>) : mode === "empty" ? (<View className="items-center justify-center px-4 py-12">
          {emptyState ?? <MutedText className="text-sm">{emptyMessage}</MutedText>}
        </View>) : (<FlatList data={data} keyExtractor={keyExtractor} renderItem={renderItem} extraData={[columns, onRowPress]}/>)}
    </View>);
}
//# sourceMappingURL=data-table.jsx.map