"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importStar(require("react"));
const ReactKonvaCore_1 = require("react-konva/lib/ReactKonvaCore");
const helpers_1 = require("./helpers");
const defaultRowHeight = () => 20;
const defaultColumnWidth = () => 100;
const defaultProps = {
    width: 800,
    height: 800,
    rowCount: 200,
    columnCount: 200,
    rowHeight: defaultRowHeight,
    columnWidth: defaultColumnWidth,
    scrollbarSize: 17,
    showScrollbar: true,
    selectionBackgroundColor: "rgb(14, 101, 235, 0.1)",
    selectionBorderColor: "#1a73e8",
    selections: [],
    mergedCells: [],
    frozenRows: 0,
    frozenColumns: 0,
};
const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
/**
 * Grid component using React Konva
 * @param props
 */
const Grid = react_1.memo(react_1.forwardRef((props, forwardedRef) => {
    const { width: containerWidth, height: containerHeight, estimatedColumnWidth, estimatedRowHeight, rowHeight, columnWidth, rowCount, columnCount, scrollbarSize, onScroll, showScrollbar, selectionBackgroundColor, selectionBorderColor, selections, frozenRows, frozenColumns, itemRenderer, mergedCells, onViewChange, ...rest } = props;
    /* Expose some methods in ref */
    react_1.useImperativeHandle(forwardedRef, () => {
        return {
            scrollTo,
            stage: stageRef.current,
            resetAfterIndices,
            getScrollPosition,
            isMergedCell,
            getCellBounds,
            getCellCoordsFromOffsets,
            getCellOffsetFromCoords,
        };
    });
    const instanceProps = react_1.useRef({
        columnMetadataMap: {},
        rowMetadataMap: {},
        lastMeasuredColumnIndex: -1,
        lastMeasuredRowIndex: -1,
        estimatedColumnWidth: estimatedColumnWidth || DEFAULT_ESTIMATED_ITEM_SIZE,
        estimatedRowHeight: estimatedRowHeight || DEFAULT_ESTIMATED_ITEM_SIZE,
    });
    const stageRef = react_1.useRef(null);
    const verticalScrollRef = react_1.useRef(null);
    const wheelingRef = react_1.useRef(null);
    const horizontalScrollRef = react_1.useRef(null);
    const [_, forceRender] = react_1.useReducer((s) => s + 1, 0);
    const [scrollTop, setScrollTop] = react_1.useState(0);
    const [scrollLeft, setScrollLeft] = react_1.useState(0);
    const getScrollPosition = react_1.useCallback(() => {
        return {
            scrollTop,
            scrollLeft,
        };
    }, [scrollTop, scrollLeft]);
    /* Redraw grid imperatively */
    const resetAfterIndices = react_1.useCallback(({ columnIndex, rowIndex, shouldForceUpdate = true, }) => {
        if (typeof columnIndex === "number") {
            instanceProps.current.lastMeasuredColumnIndex = Math.min(instanceProps.current.lastMeasuredColumnIndex, columnIndex - 1);
        }
        if (typeof rowIndex === "number") {
            instanceProps.current.lastMeasuredRowIndex = Math.min(instanceProps.current.lastMeasuredRowIndex, rowIndex - 1);
        }
        if (shouldForceUpdate)
            forceRender();
    }, []);
    /**
     * Create a map of merged cells
     * [rowIndex, columnindex] => [parentRowIndex, parentColumnIndex]
     */
    const mergedCellMap = react_1.useMemo(() => {
        const mergedCellMap = new Map();
        for (let i = 0; i < mergedCells.length; i++) {
            const bounds = mergedCells[i];
            const { top, left } = bounds;
            for (const cell of helpers_1.getBoundedCells(bounds)) {
                mergedCellMap.set(cell, bounds);
            }
        }
        return mergedCellMap;
    }, [mergedCells]);
    /* Check if a cell is part of a merged cell */
    const isMergedCell = react_1.useCallback((rowIndex, columnIndex) => {
        return mergedCellMap.has(helpers_1.cellIndentifier(rowIndex, columnIndex));
    }, [mergedCellMap]);
    /* Get top, left bounds of a cell */
    const getCellBounds = react_1.useCallback(({ rowIndex, columnIndex }) => {
        const isMerged = isMergedCell(rowIndex, columnIndex);
        if (isMerged)
            return mergedCellMap.get(helpers_1.cellIndentifier(rowIndex, columnIndex));
        return {
            top: rowIndex,
            left: columnIndex,
            right: columnIndex,
            bottom: rowIndex,
        };
    }, [mergedCellMap]);
    /* Handle vertical scroll */
    const handleScroll = react_1.useCallback((e) => {
        setScrollTop(e.target.scrollTop);
        /* Scroll callbacks */
        onScroll && onScroll({ scrollTop: e.target.scrollTop, scrollLeft });
    }, [scrollLeft]);
    /* Handle horizontal scroll */
    const handleScrollLeft = react_1.useCallback((e) => {
        setScrollLeft(e.target.scrollLeft);
        /* Scroll callbacks */
        onScroll && onScroll({ scrollLeft: e.target.scrollLeft, scrollTop });
    }, [scrollTop]);
    /* Scroll based on left, top position */
    const scrollTo = react_1.useCallback(({ scrollTop, scrollLeft }) => {
        /* If scrollbar is visible, lets update it which triggers a state change */
        if (showScrollbar) {
            if (horizontalScrollRef.current)
                horizontalScrollRef.current.scrollLeft = scrollLeft;
            if (verticalScrollRef.current)
                verticalScrollRef.current.scrollTop = scrollTop;
        }
        else {
            scrollLeft !== void 0 && setScrollLeft(scrollLeft);
            scrollTop !== void 0 && setScrollTop(scrollTop);
        }
    }, [showScrollbar]);
    const handleWheel = react_1.useCallback((event) => {
        if (wheelingRef.current)
            return;
        const { deltaX, deltaY, deltaMode } = event.nativeEvent;
        let dx = deltaX;
        let dy = deltaY;
        if (deltaMode === 1) {
            dy = dy * 17;
        }
        if (!horizontalScrollRef.current || !verticalScrollRef.current)
            return;
        const x = horizontalScrollRef.current?.scrollLeft;
        const y = verticalScrollRef.current?.scrollTop;
        wheelingRef.current = window.requestAnimationFrame(() => {
            wheelingRef.current = null;
            if (horizontalScrollRef.current)
                horizontalScrollRef.current.scrollLeft = x + dx;
            if (verticalScrollRef.current)
                verticalScrollRef.current.scrollTop = y + dy;
        });
    }, []);
    const rowStartIndex = helpers_1.getRowStartIndexForOffset({
        rowHeight,
        columnWidth,
        rowCount,
        columnCount,
        instanceProps: instanceProps.current,
        offset: scrollTop,
    });
    const rowStopIndex = helpers_1.getRowStopIndexForStartIndex({
        startIndex: rowStartIndex,
        rowCount,
        rowHeight,
        columnWidth,
        scrollTop,
        containerHeight,
        instanceProps: instanceProps.current,
    });
    const columnStartIndex = helpers_1.getColumnStartIndexForOffset({
        rowHeight,
        columnWidth,
        rowCount,
        columnCount,
        instanceProps: instanceProps.current,
        offset: scrollLeft,
    });
    const columnStopIndex = helpers_1.getColumnStopIndexForStartIndex({
        startIndex: columnStartIndex,
        columnCount,
        rowHeight,
        columnWidth,
        scrollLeft,
        containerWidth,
        instanceProps: instanceProps.current,
    });
    const estimatedTotalHeight = helpers_1.getEstimatedTotalHeight(rowCount, instanceProps.current.estimatedRowHeight, instanceProps.current);
    const estimatedTotalWidth = helpers_1.getEstimatedTotalWidth(columnCount, instanceProps.current.estimatedColumnWidth, instanceProps.current);
    /* Callback when visible rows or columns have changed */
    react_1.useEffect(() => {
        onViewChange &&
            onViewChange({
                rowStartIndex,
                rowStopIndex,
                columnStartIndex,
                columnStopIndex,
            });
    }, [rowStartIndex, rowStopIndex, columnStartIndex, columnStopIndex]);
    /* Draw all cells */
    const cells = [];
    if (columnCount > 0 && rowCount) {
        for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
            /* Skip frozen rows */
            if (rowIndex < frozenRows) {
                continue;
            }
            for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
                /* Skip frozen columns */
                if (columnIndex < frozenColumns ||
                    isMergedCell(rowIndex, columnIndex)) {
                    continue;
                }
                const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
                const x = helpers_1.getColumnOffset({
                    index: columnIndex,
                    rowHeight,
                    columnWidth,
                    instanceProps: instanceProps.current,
                });
                const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
                const y = helpers_1.getRowOffset({
                    index: rowIndex,
                    rowHeight,
                    columnWidth,
                    instanceProps: instanceProps.current,
                });
                cells.push(itemRenderer({
                    x,
                    y,
                    width,
                    height,
                    rowIndex,
                    columnIndex,
                    key: helpers_1.itemKey({ rowIndex, columnIndex }),
                }));
            }
        }
    }
    /* Draw merged cells */
    const mergedCellAreas = react_1.useMemo(() => {
        const areas = [];
        for (let i = 0; i < mergedCells.length; i++) {
            const { top: rowIndex, left: columnIndex, right, bottom } = mergedCells[i];
            const x = helpers_1.getColumnOffset({
                index: columnIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            const y = helpers_1.getRowOffset({
                index: rowIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            const width = helpers_1.getColumnOffset({
                index: right + 1,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            }) - x;
            const height = helpers_1.getRowOffset({
                index: bottom + 1,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            }) - y;
            areas.push(itemRenderer({
                x,
                y,
                width,
                height,
                rowIndex,
                columnIndex,
                key: helpers_1.itemKey({ rowIndex, columnIndex }),
            }));
        }
        return areas;
    }, [
        mergedCells,
        rowStartIndex,
        rowStopIndex,
        columnStartIndex,
        columnStopIndex,
    ]);
    /* Draw frozen rows */
    const frozenRowCells = [];
    for (let rowIndex = 0; rowIndex < Math.min(columnStopIndex, frozenRows); rowIndex++) {
        for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
            const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
            const x = helpers_1.getColumnOffset({
                index: columnIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
            const y = helpers_1.getRowOffset({
                index: rowIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            frozenRowCells.push(itemRenderer({
                x,
                y,
                width,
                height,
                rowIndex,
                columnIndex,
                key: helpers_1.itemKey({ rowIndex, columnIndex }),
            }));
        }
    }
    /* Draw frozen columns */
    const frozenColumnCells = [];
    for (let columnIndex = 0; columnIndex < Math.min(columnStopIndex, frozenColumns); columnIndex++) {
        for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
            const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
            const x = helpers_1.getColumnOffset({
                index: columnIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
            const y = helpers_1.getRowOffset({
                index: rowIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            frozenColumnCells.push(itemRenderer({
                x,
                y,
                width,
                height,
                rowIndex,
                columnIndex,
                key: helpers_1.itemKey({ rowIndex, columnIndex }),
            }));
        }
    }
    /* Draw frozen intersection cells */
    const frozenIntersectionCells = [];
    for (let rowIndex = 0; rowIndex < Math.min(rowStopIndex, frozenRows); rowIndex++) {
        for (let columnIndex = 0; columnIndex < Math.min(columnStopIndex, frozenColumns); columnIndex++) {
            const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
            const x = helpers_1.getColumnOffset({
                index: columnIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
            const y = helpers_1.getRowOffset({
                index: rowIndex,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            });
            frozenIntersectionCells.push(itemRenderer({
                x,
                y,
                width,
                height,
                rowIndex,
                columnIndex,
                key: helpers_1.itemKey({ rowIndex, columnIndex }),
            }));
        }
    }
    /**
     * Convert selections to area
     * Removed useMemo as changes to lastMeasureRowIndex, lastMeasuredColumnIndex,
     * does not trigger useMemo
     * Dependencies : [selections, rowStopIndex, columnStopIndex, instanceProps]
     */
    const selectionAreas = [];
    for (let i = 0; i < selections.length; i++) {
        const { top, left, right, bottom } = selections[i];
        const selectionBounds = { x: 0, y: 0, width: 0, height: 0 };
        const actualBottom = Math.min(rowStopIndex, bottom);
        const actualRight = Math.min(columnStopIndex, right);
        selectionBounds.y = helpers_1.getRowOffset({
            index: top,
            rowHeight,
            columnWidth,
            instanceProps: instanceProps.current,
        });
        selectionBounds.height =
            helpers_1.getRowOffset({
                index: actualBottom,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            }) -
                selectionBounds.y +
                helpers_1.getRowHeight(actualBottom, instanceProps.current);
        selectionBounds.x = helpers_1.getColumnOffset({
            index: left,
            rowHeight,
            columnWidth,
            instanceProps: instanceProps.current,
        });
        selectionBounds.width =
            helpers_1.getColumnOffset({
                index: actualRight,
                rowHeight,
                columnWidth,
                instanceProps: instanceProps.current,
            }) -
                selectionBounds.x +
                helpers_1.getColumnWidth(actualRight, instanceProps.current);
        selectionAreas.push(react_1.default.createElement(ReactKonvaCore_1.Rect, { key: i, stroke: selectionBorderColor, x: selectionBounds.x, y: selectionBounds.y, width: selectionBounds.width, height: selectionBounds.height, fill: selectionBackgroundColor, shadowForStrokeEnabled: false, listening: false, hitStrokeWidth: 0 }));
    }
    /**
     * Get cell offset position from rowIndex, columnIndex
     */
    const getCellOffsetFromCoords = react_1.useCallback(({ rowIndex, columnIndex }) => {
        const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
        const x = helpers_1.getColumnOffset({
            index: columnIndex,
            rowHeight,
            columnWidth,
            instanceProps: instanceProps.current,
        });
        const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
        const y = helpers_1.getRowOffset({
            index: rowIndex,
            rowHeight,
            columnWidth,
            instanceProps: instanceProps.current,
        });
        return {
            x,
            y,
            width,
            height,
        };
    }, []);
    /**
     * Get cell cordinates from current mouse x/y positions
     */
    const getCellCoordsFromOffsets = react_1.useCallback((x, y) => {
        const rowIndex = helpers_1.getRowStartIndexForOffset({
            rowHeight,
            columnWidth,
            rowCount,
            columnCount,
            instanceProps: instanceProps.current,
            offset: y + scrollTop,
        });
        const columnIndex = helpers_1.getColumnStartIndexForOffset({
            rowHeight,
            columnWidth,
            rowCount,
            columnCount,
            instanceProps: instanceProps.current,
            offset: x + scrollLeft,
        });
        return { rowIndex, columnIndex };
    }, [scrollLeft, scrollTop, rowCount, columnCount]);
    return (react_1.default.createElement("div", { style: { position: "relative", width: containerWidth } },
        react_1.default.createElement("div", Object.assign({ onWheel: handleWheel, tabIndex: -1 }, rest),
            react_1.default.createElement(ReactKonvaCore_1.Stage, { width: containerWidth, height: containerHeight, ref: stageRef },
                react_1.default.createElement(ReactKonvaCore_1.Layer, null,
                    react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: scrollTop, offsetX: scrollLeft },
                        cells,
                        mergedCellAreas),
                    react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: scrollTop, offsetX: 0 }, frozenColumnCells),
                    react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: 0, offsetX: scrollLeft }, frozenRowCells),
                    react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: 0, offsetX: 0 }, frozenIntersectionCells)),
                react_1.default.createElement(ReactKonvaCore_1.Layer, { listening: false, offsetY: scrollTop, offsetX: scrollLeft }, selectionAreas))),
        showScrollbar ? (react_1.default.createElement(react_1.default.Fragment, null,
            react_1.default.createElement("div", { style: {
                    height: containerHeight,
                    overflow: "scroll",
                    position: "absolute",
                    right: 0,
                    top: 0,
                    width: scrollbarSize,
                }, onScroll: handleScroll, ref: verticalScrollRef },
                react_1.default.createElement("div", { style: {
                        position: "absolute",
                        height: estimatedTotalHeight,
                        width: 1,
                    } })),
            react_1.default.createElement("div", { style: {
                    overflow: "scroll",
                    position: "absolute",
                    bottom: 0,
                    left: 0,
                    width: containerWidth,
                    height: scrollbarSize,
                }, onScroll: handleScrollLeft, ref: horizontalScrollRef },
                react_1.default.createElement("div", { style: {
                        position: "absolute",
                        width: estimatedTotalWidth,
                        height: 1,
                    } })))) : null));
}));
Grid.defaultProps = defaultProps;
exports.default = Grid;
