/*
 * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { RegionCardinality, Regions } from "../regions";
import * as Classes from "./classes";
import { Rect } from "./rect";
import { Utils } from "./utils";
/**
 * This class manages the sizes of grid cells using arrays of individual row/column sizes.
 */
export class Grid {
    static DEFAULT_BLEED = 3;
    static DEFAULT_MAX_COLUMNS = 50;
    static DEFAULT_MAX_ROWS = 200;
    static DEFAULT_GHOST_HEIGHT = 20;
    static DEFAULT_GHOST_WIDTH = 150;
    // Used on first render of the top-left and top quadrants to avoid collapsing
    // their heights to 0. originally defined in headers/_common.scss
    static MIN_COLUMN_HEADER_HEIGHT = 30;
    // Used on first render of the top-left and left quadrants to avoid collapsing
    // their widths to 0. originally defined in headers/_common.scss
    static MIN_ROW_HEADER_WIDTH = 30;
    numCols;
    numRows;
    bleed;
    columnWidths;
    rowHeights;
    cumulativeColumnWidths;
    cumulativeRowHeights;
    ghostHeight;
    ghostWidth;
    /**
     * This constructor accumulates the heights and widths in `O(n)`, saving
     * time in later calculations.
     *
     * @param bleed - The number of rows/cols that we expand beyond the
     *     viewport (on all sides). This helps avoid displaying an empty
     *     viewport when the user scrolls quickly.
     */
    constructor(rowHeights, columnWidths, bleed = Grid.DEFAULT_BLEED, ghostHeight = Grid.DEFAULT_GHOST_HEIGHT, ghostWidth = Grid.DEFAULT_GHOST_WIDTH) {
        this.columnWidths = columnWidths;
        this.rowHeights = rowHeights;
        this.cumulativeColumnWidths = Utils.accumulate(columnWidths);
        this.cumulativeRowHeights = Utils.accumulate(rowHeights);
        this.numCols = columnWidths.length;
        this.numRows = rowHeights.length;
        this.bleed = bleed;
        this.ghostHeight = ghostHeight;
        this.ghostWidth = ghostWidth;
    }
    /**
     * Returns the `Rect` bounds of a cell in scrollpane client space.
     *
     * Scrollpane client coordinate space uses the origin of the scrollpane
     * client (the inside part that you're moving around).
     *
     * For example, let's say you're scrolling around a block of 1000 x 1000
     * cells. Regardless where you've scrolled, the first cell is always at
     * 0,0 in scrollpane client space. the cell to the right of it is always
     * at, e.g., 100,0.
     */
    getCellRect(rowIndex, columnIndex) {
        const height = this.rowHeights[rowIndex];
        const top = this.cumulativeRowHeights[rowIndex] - height;
        const width = this.columnWidths[columnIndex];
        const left = this.cumulativeColumnWidths[columnIndex] - width;
        return new Rect(left, top, width, height);
    }
    /**
     * Returns the `Rect` bounds of a cell in scrollpane client space.
     *
     * If the cell is beyond the bounds of the user-defined table cells, it is
     * considered a "ghost" cell. If a width/height is not defined for that
     * row/column, we use the default width/height.
     */
    getGhostCellRect(rowIndex, columnIndex) {
        let left = 0;
        let top = 0;
        let width = 0;
        let height = 0;
        if (rowIndex >= this.rowHeights.length) {
            height = this.ghostHeight;
            top = this.getHeight() + this.ghostHeight * (rowIndex - this.numRows);
        }
        else {
            height = this.rowHeights[rowIndex];
            top = this.cumulativeRowHeights[rowIndex] - height;
        }
        if (columnIndex >= this.columnWidths.length) {
            width = this.ghostWidth;
            left = this.getWidth() + this.ghostWidth * (columnIndex - this.numCols);
        }
        else {
            width = this.columnWidths[columnIndex];
            left = this.cumulativeColumnWidths[columnIndex] - width;
        }
        return new Rect(left, top, width, height);
    }
    /**
     * Returns the `Rect` with the base coordinate and height of the specified row.
     */
    getRowRect(rowIndex) {
        const height = this.rowHeights[rowIndex];
        const top = this.cumulativeRowHeights[rowIndex] - height;
        return new Rect(0, top, this.getWidth(), height);
    }
    /**
     * Returns the `Rect` with the base coordinate and width of the specified column.
     */
    getColumnRect(columnIndex) {
        const width = this.columnWidths[columnIndex];
        const left = this.cumulativeColumnWidths[columnIndex] - width;
        return new Rect(left, 0, width, this.getHeight());
    }
    /**
     * Returns the total width of the entire grid
     */
    getWidth() {
        return this.numCols === 0 ? 0 : this.cumulativeColumnWidths[this.numCols - 1];
    }
    /**
     * Returns the total width of the entire grid
     */
    getHeight() {
        return this.numRows === 0 ? 0 : this.cumulativeRowHeights[this.numRows - 1];
    }
    /**
     * Returns the `Rect` bounds of entire grid
     */
    getRect() {
        return new Rect(0, 0, this.getWidth(), this.getHeight());
    }
    /**
     * Maps each cell that intersects with the given `Rect` argument. The
     * indices of iteration are extended in both directions by the integer
     * `bleed` class property, then are clamped between 0 and the number of
     * rows/columns.
     *
     * Uses a binary search for each of the 4 edges of the bounds, resulting
     * in a runtime of `O(log(rows) + log(cols))` plus the `O(irows * icols)`
     * iteration of intersecting cells.
     */
    mapCellsInRect(rect, callback) {
        const results = [];
        if (rect == null) {
            return results;
        }
        const { rowIndexStart, rowIndexEnd } = this.getRowIndicesInRect({ rect });
        const { columnIndexStart, columnIndexEnd } = this.getColumnIndicesInRect(rect);
        for (let rowIndex = rowIndexStart; rowIndex <= rowIndexEnd; rowIndex++) {
            for (let columnIndex = columnIndexStart; columnIndex <= columnIndexEnd; columnIndex++) {
                results.push(callback(rowIndex, columnIndex));
            }
        }
        return results;
    }
    /**
     * Maps each row that intersects with the given `Rect` argument.
     *
     * See Grid.mapCellsInRect for more details.
     */
    mapRowsInRect(rect, callback) {
        const results = [];
        if (rect == null) {
            return results;
        }
        const { rowIndexStart, rowIndexEnd } = this.getRowIndicesInRect({ rect });
        for (let rowIndex = rowIndexStart; rowIndex <= rowIndexEnd; rowIndex++) {
            results.push(callback(rowIndex));
        }
        return results;
    }
    /**
     * Maps each column that intersects with the given `Rect` argument.
     *
     * See Grid.mapCellsInRect for more details.
     */
    mapColumnsInRect(rect, callback) {
        const results = [];
        if (rect == null) {
            return results;
        }
        const { columnIndexStart, columnIndexEnd } = this.getColumnIndicesInRect(rect);
        for (let columnIndex = columnIndexStart; columnIndex <= columnIndexEnd; columnIndex++) {
            results.push(callback(columnIndex));
        }
        return results;
    }
    /**
     * Returns the start and end indices of rows that intersect with the given
     * `Rect` argument.
     */
    getRowIndicesInRect(options) {
        const { rect, includeGhostCells = false, columnHeaderHeight = 0, limit = Grid.DEFAULT_MAX_ROWS } = options;
        if (rect == null) {
            return { rowIndexEnd: 0, rowIndexStart: 0 };
        }
        const searchEnd = includeGhostCells ? Math.max(this.numRows, Grid.DEFAULT_MAX_ROWS) : this.numRows;
        const { top = 0, height } = rect;
        const { start, end } = this.getIndicesInInterval(top, top + (height - columnHeaderHeight), searchEnd, !includeGhostCells, this.getCumulativeHeightAt);
        const rowIndexEnd = limit > 0 && end - start > limit ? start + limit : end;
        return {
            rowIndexEnd,
            rowIndexStart: start,
        };
    }
    /**
     * Returns the start and end indices of columns that intersect with the
     * given `Rect` argument.
     */
    getColumnIndicesInRect(rect, includeGhostCells = false, limit = Grid.DEFAULT_MAX_COLUMNS) {
        if (rect == null) {
            return { columnIndexEnd: 0, columnIndexStart: 0 };
        }
        const searchEnd = includeGhostCells ? Math.max(this.numCols, Grid.DEFAULT_MAX_COLUMNS) : this.numCols;
        const { left = 0, width } = rect;
        const { start, end } = this.getIndicesInInterval(left, left + width, searchEnd, !includeGhostCells, this.getCumulativeWidthAt);
        const columnIndexEnd = limit > 0 && end - start > limit ? start + limit : end;
        return {
            columnIndexEnd,
            columnIndexStart: start,
        };
    }
    isGhostIndex(rowIndex, columnIndex) {
        return rowIndex >= this.numRows || columnIndex >= this.numCols;
    }
    isGhostColumn(columnIndex) {
        return columnIndex >= this.numCols;
    }
    getExtremaClasses(rowIndex, columnIndex, rowEnd, columnEnd) {
        if (rowIndex === rowEnd && columnIndex === columnEnd) {
            return [Classes.TABLE_LAST_IN_COLUMN, Classes.TABLE_LAST_IN_ROW];
        }
        if (rowIndex === rowEnd) {
            return [Classes.TABLE_LAST_IN_COLUMN];
        }
        if (columnIndex === columnEnd) {
            return [Classes.TABLE_LAST_IN_ROW];
        }
        return [];
    }
    getRegionStyle(region) {
        const cardinality = Regions.getRegionCardinality(region);
        switch (cardinality) {
            case RegionCardinality.CELLS: {
                const [rowStart, rowEnd] = region.rows;
                const [colStart, colEnd] = region.cols;
                // if the region is outside the bounds of the table, don't display it
                if (this.isGhostIndex(rowStart, colStart) || this.isGhostIndex(rowEnd, colEnd)) {
                    return { display: "none" };
                }
                const cellRect0 = this.getCellRect(rowStart, colStart);
                const cellRect1 = this.getCellRect(rowEnd, colEnd);
                const offsetLeft = colStart === 0 ? 0 : 1;
                const offsetTop = rowStart === 0 ? 0 : 1;
                const rect = cellRect0.union(cellRect1);
                rect.height += offsetTop;
                rect.left -= offsetLeft;
                rect.width += offsetLeft;
                rect.top -= offsetTop;
                return { ...rect.style(), display: "block" };
            }
            case RegionCardinality.FULL_COLUMNS: {
                const [colStart, colEnd] = region.cols;
                // if the region is outside the bounds of the table, don't display it
                if (this.isGhostIndex(0, colStart) || this.isGhostIndex(0, colEnd)) {
                    return { display: "none" };
                }
                const cellRect0 = this.getCellRect(0, colStart);
                const cellRect1 = this.getCellRect(0, colEnd);
                const rect = cellRect0.union(cellRect1);
                const offsetLeft = colStart === 0 ? 0 : 1;
                return {
                    bottom: 0,
                    display: "block",
                    left: rect.left - offsetLeft,
                    top: 0,
                    width: rect.width + offsetLeft,
                };
            }
            case RegionCardinality.FULL_ROWS: {
                const [rowStart, rowEnd] = region.rows;
                // if the region is outside the bounds of the table, don't display it
                if (this.isGhostIndex(rowStart, 0) || this.isGhostIndex(rowEnd, 0)) {
                    return { display: "none" };
                }
                const cellRect0 = this.getCellRect(rowStart, 0);
                const cellRect1 = this.getCellRect(rowEnd, 0);
                const rect = cellRect0.union(cellRect1);
                const offsetTop = rowStart === 0 ? 0 : 1;
                return {
                    display: "block",
                    height: rect.height + offsetTop,
                    left: 0,
                    right: 0,
                    top: rect.top - offsetTop,
                };
            }
            case RegionCardinality.FULL_TABLE:
                return {
                    bottom: 0,
                    display: "block",
                    left: 0,
                    right: 0,
                    top: 0,
                };
            default:
                return { display: "none" };
        }
    }
    getCumulativeWidthBefore = (index) => {
        return index === 0 ? 0 : this.getCumulativeWidthAt(index - 1);
    };
    getCumulativeWidthAt = (index) => {
        if (this.numCols === 0) {
            return this.ghostWidth * index;
        }
        else if (index >= this.numCols) {
            return this.cumulativeColumnWidths[this.numCols - 1] + this.ghostWidth * (index - this.numCols + 1);
        }
        else {
            return this.cumulativeColumnWidths[index];
        }
    };
    getCumulativeHeightBefore = (index) => {
        return index === 0 ? 0 : this.getCumulativeHeightAt(index - 1);
    };
    getCumulativeHeightAt = (index) => {
        if (this.numRows === 0) {
            return this.ghostHeight * index;
        }
        else if (index >= this.numRows) {
            return this.cumulativeRowHeights[this.numRows - 1] + this.ghostHeight * (index - this.numRows + 1);
        }
        else {
            return this.cumulativeRowHeights[index];
        }
    };
    getIndicesInInterval(min, max, count, useEndBleed, lookup) {
        let start = Utils.binarySearch(min, count - 1, lookup);
        let end = Utils.binarySearch(max, count - 1, lookup);
        // correct exact pixel alignment
        if (start >= 0 && min === lookup(start)) {
            start += 1;
        }
        // apply bounded bleeds
        start = Math.max(0, start - this.bleed);
        if (useEndBleed) {
            end = Math.min(count - 1, end + this.bleed);
        }
        else {
            end = Math.min(count - 1, end);
        }
        return { start, end };
    }
}
//# sourceMappingURL=grid.js.map