/*
 * Copyright 2021 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 type { Rect } from "./common";
import type { CellMapper, Grid } from "./common/grid";
import { Utils } from "./common/utils";
import { type Locator, LocatorImpl } from "./locator";

export interface ResizeRowsByApproximateHeightOptions {
    /**
     * Approximate width (in pixels) of an average character of text.
     */
    getApproximateCharWidth?: number | CellMapper<number>;

    /**
     * Approximate height (in pixels) of an average line of text.
     */
    getApproximateLineHeight?: number | CellMapper<number>;

    /**
     * Sum of horizontal paddings (in pixels) from the left __and__ right sides
     * of the cell.
     */
    getCellHorizontalPadding?: number | CellMapper<number>;

    /**
     * Number of extra lines to add in case the calculation is imperfect.
     */
    getNumBufferLines?: number | CellMapper<number>;
}

export interface IResizeRowsByApproximateHeightResolvedOptions {
    getApproximateCharWidth: number;
    getApproximateLineHeight: number;
    getCellHorizontalPadding: number;
    getNumBufferLines: number;
}

// these default values for `resizeRowsByApproximateHeight` have been
// fine-tuned to work well with default Table font styles.
const resizeRowsByApproximateHeightDefaults: Record<keyof ResizeRowsByApproximateHeightOptions, number> = {
    getApproximateCharWidth: 8,
    getApproximateLineHeight: 18,
    getCellHorizontalPadding: 2 * LocatorImpl.CELL_HORIZONTAL_PADDING,
    getNumBufferLines: 1,
};

/**
 * Returns an object with option keys mapped to their resolved values
 * (falling back to default values as necessary).
 */
function resolveResizeRowsByApproximateHeightOptions(
    options: ResizeRowsByApproximateHeightOptions | null | undefined,
    rowIndex: number,
    columnIndex: number,
) {
    const optionKeys = Object.keys(resizeRowsByApproximateHeightDefaults) as Array<
        keyof ResizeRowsByApproximateHeightOptions
    >;
    const optionReducer = (
        agg: Partial<IResizeRowsByApproximateHeightResolvedOptions>,
        key: keyof ResizeRowsByApproximateHeightOptions,
    ) => {
        const valueOrMapper = options?.[key];
        if (typeof valueOrMapper === "function") {
            agg[key] = valueOrMapper(rowIndex, columnIndex);
        } else if (valueOrMapper != null) {
            agg[key] = valueOrMapper;
        } else {
            agg[key] = resizeRowsByApproximateHeightDefaults[key];
        }

        return agg;
    };
    return optionKeys.reduce(optionReducer, {}) as IResizeRowsByApproximateHeightResolvedOptions;
}

/**
 * Resizes all rows in the table to the approximate
 * maximum height of wrapped cell content in each row. Works best when each
 * cell contains plain text of a consistent font style (though font style
 * may vary between cells). Since this function uses approximate
 * measurements, results may not be perfect.
 */
export function resizeRowsByApproximateHeight(
    numRows: number,
    columnWidths: number[],
    getCellText: CellMapper<string>,
    options?: ResizeRowsByApproximateHeightOptions,
) {
    const numColumns = columnWidths.length;

    const rowHeights: number[] = [];

    for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
        let maxCellHeightInRow = 0;

        // iterate through each cell in the row
        for (let columnIndex = 0; columnIndex < numColumns; columnIndex++) {
            // resolve all parameters to raw values
            const {
                getApproximateCharWidth: approxCharWidth,
                getApproximateLineHeight: approxLineHeight,
                getCellHorizontalPadding: horizontalPadding,
                getNumBufferLines: numBufferLines,
            } = resolveResizeRowsByApproximateHeightOptions(options, rowIndex, columnIndex);

            const cellText = getCellText(rowIndex, columnIndex);
            const approxCellHeight = Utils.getApproxCellHeight(
                cellText,
                columnWidths[columnIndex],
                approxCharWidth,
                approxLineHeight,
                horizontalPadding,
                numBufferLines,
            );

            if (approxCellHeight > maxCellHeightInRow) {
                maxCellHeightInRow = approxCellHeight;
            }
        }

        rowHeights.push(maxCellHeightInRow);
    }

    return rowHeights;
}

/**
 * Resize all rows in the table to the height of the tallest visible cell in the specified columns.
 * If no indices are provided, default to using the tallest visible cell from all columns in view.
 */
export function resizeRowsByTallestCell(
    grid: Grid,
    viewportRect: Rect,
    locator: Locator,
    numRows: number,
    columnIndices?: number | number[],
) {
    let tallest = 0;
    if (columnIndices === undefined) {
        // Consider all columns currently in viewport
        const viewportColumnIndices = grid.getColumnIndicesInRect(viewportRect);
        for (let col = viewportColumnIndices.columnIndexStart; col <= viewportColumnIndices.columnIndexEnd; col++) {
            tallest = Math.max(tallest, locator.getTallestVisibleCellInColumn(col));
        }
    } else {
        const columnIndicesArray = Array.isArray(columnIndices) ? columnIndices : [columnIndices];
        const tallestByColumns = columnIndicesArray.map(col => locator.getTallestVisibleCellInColumn(col));
        tallest = Math.max(...tallestByColumns);
    }
    return Array(numRows).fill(tallest);
}
