"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makePivotTable = makePivotTable;
const utils_1 = require("../../utils/utils.js");
const col_cache_1 = require("./utils/col-cache.js");
// TK(2023-10-10): turn this into a class constructor.
/**
 * Creates a PivotTableSource adapter from a Table object.
 * This allows Tables to be used as pivot table data sources with the same interface as Worksheets.
 */
function createTableSourceAdapter(table) {
    const tableModel = table.model;
    // Validate that table has headerRow enabled (required for pivot table column names)
    if (tableModel.headerRow === false) {
        throw new Error("Cannot create pivot table from a table without headers. Set headerRow: true on the table.");
    }
    // Validate table has data rows
    if (!tableModel.rows || tableModel.rows.length === 0) {
        throw new Error("Cannot create pivot table from an empty table. Add data rows to the table.");
    }
    const columnNames = tableModel.columns.map(col => col.name);
    // Check for duplicate column names
    const nameSet = new Set();
    for (const name of columnNames) {
        if (nameSet.has(name)) {
            throw new Error(`Duplicate column name "${name}" found in table. Pivot tables require unique column names.`);
        }
        nameSet.add(name);
    }
    // Build the full data array: headers + rows
    const headerRow = [undefined, ...columnNames]; // sparse array starting at index 1
    const dataRows = tableModel.rows.map(row => [undefined, ...row]); // sparse array starting at index 1
    // Calculate the range reference for the table
    const tl = tableModel.tl;
    const startRow = tl.row;
    const startCol = tl.col;
    const endRow = startRow + tableModel.rows.length; // header row + data rows
    const endCol = startCol + columnNames.length - 1;
    const shortRange = col_cache_1.colCache.encode(startRow, startCol, endRow, endCol);
    // Use the worksheet name (not table name) for pivotCacheDefinition's worksheetSource
    // The sheet attribute in worksheetSource must reference the actual worksheet name
    const worksheetName = table.worksheet.name;
    return {
        name: worksheetName,
        getRow(rowNumber) {
            if (rowNumber === 1) {
                return { values: headerRow };
            }
            const dataIndex = rowNumber - 2; // rowNumber 2 maps to index 0
            if (dataIndex >= 0 && dataIndex < dataRows.length) {
                return { values: dataRows[dataIndex] };
            }
            return { values: [] };
        },
        getColumn(columnNumber) {
            // Validate column number is within bounds
            if (columnNumber < 1 || columnNumber > columnNames.length) {
                return { values: [] };
            }
            // Values should be sparse array with header at index 1, data starting at index 2
            const values = [];
            values[1] = columnNames[columnNumber - 1];
            for (let i = 0; i < tableModel.rows.length; i++) {
                values[i + 2] = tableModel.rows[i][columnNumber - 1];
            }
            return { values };
        },
        getSheetValues() {
            // Return sparse array where index 1 is header row, and subsequent indices are data rows
            const result = [];
            result[1] = headerRow;
            for (let i = 0; i < dataRows.length; i++) {
                result[i + 2] = dataRows[i];
            }
            return result;
        },
        dimensions: { shortRange }
    };
}
/**
 * Resolves the data source from the model, supporting both sourceSheet and sourceTable.
 */
function resolveSource(model) {
    if (model.sourceTable) {
        return createTableSourceAdapter(model.sourceTable);
    }
    // For sourceSheet, it already implements the required interface
    return model.sourceSheet;
}
function makePivotTable(worksheet, model) {
    // Example `model`:
    // {
    //   // Source of data (either sourceSheet OR sourceTable):
    //   sourceSheet: worksheet1,  // Use entire sheet range
    //   // OR
    //   sourceTable: table,       // Use table data
    //
    //   // Pivot table fields: values indicate field names;
    //   // they come from the first row in `worksheet1` or table column names.
    //   rows: ['A', 'B'],
    //   columns: ['C'],
    //   values: ['E'], // only 1 item possible for now
    //   metric: 'sum', // only 'sum' possible for now
    // }
    // Validate source exists before trying to resolve it
    if (!model.sourceSheet && !model.sourceTable) {
        throw new Error("Either sourceSheet or sourceTable must be provided.");
    }
    if (model.sourceSheet && model.sourceTable) {
        throw new Error("Cannot specify both sourceSheet and sourceTable. Choose one.");
    }
    // Resolve source first to avoid creating adapter multiple times
    const source = resolveSource(model);
    validate(worksheet, model, source);
    const { rows, values } = model;
    const columns = model.columns ?? [];
    const cacheFields = makeCacheFields(source, [...rows, ...columns], values);
    const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
        result[cacheField.name] = index;
        return result;
    }, {});
    const rowIndices = rows.map(row => nameToIndex[row]);
    const columnIndices = columns.map(column => nameToIndex[column]);
    const valueIndices = values.map(value => nameToIndex[value]);
    // Calculate tableNumber based on existing pivot tables (1-indexed)
    const tableNumber = worksheet.workbook.pivotTables.length + 1;
    // Base cache ID starts at 10 (Excel convention), each subsequent table increments
    const BASE_CACHE_ID = 10;
    // form pivot table object
    return {
        source,
        rows: rowIndices,
        columns: columnIndices,
        values: valueIndices,
        metric: model.metric ?? "sum",
        cacheFields,
        // Dynamic cacheId: 10 for first table, 11 for second, etc.
        // Used in <pivotTableDefinition> and xl/workbook.xml
        cacheId: String(BASE_CACHE_ID + tableNumber - 1),
        // Control whether pivot table style overrides worksheet column widths
        // '0' = preserve worksheet column widths (useful for custom sizing)
        // '1' = apply pivot table style width/height (default Excel behavior)
        applyWidthHeightFormats: model.applyWidthHeightFormats ?? "1",
        // Table number for file naming (pivotTable1.xml, pivotTable2.xml, etc.)
        tableNumber
    };
}
function validate(_worksheet, model, source) {
    if (model.metric && model.metric !== "sum" && model.metric !== "count") {
        throw new Error('Only the "sum" and "count" metrics are supported at this time.');
    }
    const columns = model.columns ?? [];
    // Get header names from source (already resolved)
    const headerNames = source.getRow(1).values.slice(1);
    // Use Set for O(1) lookup
    const headerNameSet = new Set(headerNames);
    for (const name of [...model.rows, ...columns, ...model.values]) {
        if (!headerNameSet.has(name)) {
            throw new Error(`The header name "${name}" was not found in ${source.name}.`);
        }
    }
    if (!model.rows.length) {
        throw new Error("No pivot table rows specified.");
    }
    // Allow empty columns - Excel will use "Values" as column field
    // But can't have multiple values with columns specified
    if (model.values.length < 1) {
        throw new Error("Must have at least one value.");
    }
    if (model.values.length > 1 && columns.length > 0) {
        throw new Error("It is currently not possible to have multiple values when columns are specified. Please either supply an empty array for columns or a single value.");
    }
}
function makeCacheFields(source, fieldNamesWithSharedItems, valueFieldNames) {
    // Cache fields are used in pivot tables to reference source data.
    // Fields in fieldNamesWithSharedItems get their unique values extracted as sharedItems.
    // Fields in valueFieldNames (but not in fieldNamesWithSharedItems) get min/max calculated.
    // Other fields are unused and get empty sharedItems.
    const names = source.getRow(1).values;
    // Use Set for O(1) lookup instead of object
    const sharedItemsFields = new Set(fieldNamesWithSharedItems);
    const valueFields = new Set(valueFieldNames);
    const aggregate = (columnIndex) => {
        const columnValues = source.getColumn(columnIndex).values;
        // Build unique values set directly, skipping header (index 0,1) and null/undefined
        const uniqueValues = new Set();
        for (let i = 2; i < columnValues.length; i++) {
            const v = columnValues[i];
            if (v !== null && v !== undefined) {
                uniqueValues.add(v);
            }
        }
        return (0, utils_1.toSortedArray)(uniqueValues);
    };
    // Calculate min/max for numeric fields
    const getMinMax = (columnIndex) => {
        const columnValues = source.getColumn(columnIndex).values;
        let min = Infinity;
        let max = -Infinity;
        let hasNumeric = false;
        for (let i = 2; i < columnValues.length; i++) {
            const v = columnValues[i];
            if (typeof v === "number" && !isNaN(v)) {
                hasNumeric = true;
                if (v < min) {
                    min = v;
                }
                if (v > max) {
                    max = v;
                }
            }
        }
        return hasNumeric ? { minValue: min, maxValue: max } : null;
    };
    // Build result array
    const result = [];
    for (const columnIndex of (0, utils_1.range)(1, names.length)) {
        const name = names[columnIndex];
        if (sharedItemsFields.has(name)) {
            // Field used for rows/columns - extract unique values as sharedItems
            result.push({ name, sharedItems: aggregate(columnIndex) });
        }
        else if (valueFields.has(name)) {
            // Field used only for values (aggregation) - calculate min/max
            const minMax = getMinMax(columnIndex);
            result.push({
                name,
                sharedItems: null,
                minValue: minMax?.minValue,
                maxValue: minMax?.maxValue
            });
        }
        else {
            // Unused field - just empty sharedItems (like Excel does)
            result.push({ name, sharedItems: null });
        }
    }
    return result;
}
