import { formatAddress, parseCellAddress } from '@bilig/formula';
const geometryKeys = [
    'styleIndex',
    'xlsxWidth',
    'xlsxHeight',
    'customFormat',
    'customWidth',
    'bestFit',
    'outlineLevel',
    'collapsed',
    'customHeight',
    'thickTop',
    'thickBottom',
];
export function buildSetWorksheetAutoFilterOps(snapshot, filter) {
    const sheet = snapshot.sheets.find((candidate) => candidate.name === filter.sheetName);
    if (!sheet) {
        throw new Error(`Unknown sheet: ${filter.sheetName}`);
    }
    const bounds = worksheetFilterBounds(filter);
    const visibilityOps = filter.criteria && filter.criteria.length > 0
        ? buildAutoFilterRowVisibilityOps(sheet, filter, bounds)
        : buildClearFilterRowVisibilityOps(sheet, bounds.bodyStartRow, bounds.bodyEndRow);
    const existingFilter = sheet.metadata?.filters?.find((candidate) => candidate.sheetName === filter.sheetName &&
        candidate.startAddress === filter.startAddress &&
        candidate.endAddress === filter.endAddress);
    if (visibilityOps.length === 0 && existingFilter && JSON.stringify(existingFilter) === JSON.stringify(filter)) {
        return [];
    }
    return [...visibilityOps, { kind: 'setFilter', sheetName: filter.sheetName, range: structuredClone(filter) }];
}
export function buildClearWorksheetAutoFilterOps(snapshot, range) {
    const sheet = snapshot.sheets.find((candidate) => candidate.name === range.sheetName);
    if (!sheet) {
        return [{ kind: 'clearFilter', sheetName: range.sheetName, range: { ...range } }];
    }
    const bounds = worksheetFilterBounds(range);
    return [
        ...buildClearFilterRowVisibilityOps(sheet, bounds.bodyStartRow, bounds.bodyEndRow),
        { kind: 'clearFilter', sheetName: range.sheetName, range: { ...range } },
    ];
}
export function buildApplyTableAutoFilterOps(snapshot, sheetName, tableName, criteria) {
    const sheet = snapshot.sheets.find((candidate) => candidate.name === sheetName);
    if (!sheet) {
        throw new Error(`Unknown sheet: ${sheetName}`);
    }
    const table = snapshot.workbook.metadata?.tables?.find((candidate) => candidate.name === tableName && candidate.sheetName === sheetName);
    if (!table) {
        throw new Error(`Unknown table: ${tableName}`);
    }
    const filter = {
        sheetName,
        startAddress: table.startAddress,
        endAddress: table.endAddress,
        ...(criteria.length > 0 ? { criteria: criteria.map((entry) => structuredClone(entry)) } : {}),
    };
    const bounds = tableFilterBounds(table);
    return [
        ...(criteria.length > 0
            ? buildAutoFilterRowVisibilityOps(sheet, filter, bounds)
            : buildClearFilterRowVisibilityOps(sheet, bounds.bodyStartRow, bounds.bodyEndRow)),
        {
            kind: 'upsertTable',
            table: {
                ...structuredClone(table),
                autoFilter: filter,
            },
        },
    ];
}
function worksheetFilterBounds(range) {
    const start = parseCellAddress(range.startAddress, range.sheetName);
    const end = parseCellAddress(range.endAddress, range.sheetName);
    return {
        headerRow: start.row,
        bodyStartRow: Math.min(start.row + 1, end.row + 1),
        bodyEndRow: end.row,
        startCol: start.col,
    };
}
function tableFilterBounds(table) {
    const start = parseCellAddress(table.startAddress, table.sheetName);
    const end = parseCellAddress(table.endAddress, table.sheetName);
    const bodyStartRow = table.headerRow ? start.row + 1 : start.row;
    const bodyEndRow = table.totalsRow ? end.row - 1 : end.row;
    return {
        headerRow: start.row,
        bodyStartRow,
        bodyEndRow,
        startCol: start.col,
    };
}
function buildAutoFilterRowVisibilityOps(sheet, filter, bounds) {
    if (bounds.bodyStartRow > bounds.bodyEndRow) {
        return [];
    }
    const cellsByAddress = new Map(sheet.cells.map((cell) => [cell.address, cell.value]));
    const updates = [];
    for (let row = bounds.bodyStartRow; row <= bounds.bodyEndRow; row += 1) {
        const rowMatches = autoFilterRowMatches(cellsByAddress, filter.criteria ?? [], row, bounds.startCol);
        const state = rowStateAt(sheet.metadata, row);
        const nextFilterHidden = rowMatches ? null : true;
        if (state.filterHidden === nextFilterHidden) {
            continue;
        }
        updates.push({ ...state, row, nextFilterHidden });
    }
    return rowVisibilityUpdatesToOps(sheet.name, updates);
}
function buildClearFilterRowVisibilityOps(sheet, bodyStartRow, bodyEndRow) {
    const updates = [];
    for (let row = bodyStartRow; row <= bodyEndRow; row += 1) {
        const state = rowStateAt(sheet.metadata, row);
        if (state.filterHidden !== true) {
            continue;
        }
        updates.push({ ...state, row, nextFilterHidden: null });
    }
    return rowVisibilityUpdatesToOps(sheet.name, updates);
}
function autoFilterRowMatches(cellsByAddress, criteria, row, startCol) {
    return criteria.every((criterion) => {
        const value = cellsByAddress.get(formatAddress(row, startCol + criterion.colId));
        if (criterion.filters && !valueFilterMatches(value, criterion.filters)) {
            return false;
        }
        if (criterion.customFilters && !customFiltersMatch(value, criterion.customFilters)) {
            return false;
        }
        return true;
    });
}
function valueFilterMatches(value, criteria) {
    if (valueIsBlank(value)) {
        return criteria.blank === true;
    }
    const text = filterText(value);
    return criteria.values.some((candidate) => textEqualsFilterValue(text, candidate));
}
function customFiltersMatch(value, criteria) {
    const matches = criteria.filters.map((filter) => customFilterMatches(value, filter));
    return criteria.and === true ? matches.every(Boolean) : matches.some(Boolean);
}
function customFilterMatches(value, criterion) {
    const actual = filterText(value);
    const expected = criterion.value;
    switch (criterion.operator ?? 'equal') {
        case 'equal':
            return textEqualsFilterValue(actual, expected);
        case 'notEqual':
            return !textEqualsFilterValue(actual, expected);
        case 'lessThan':
            return compareFilterValues(actual, expected) < 0;
        case 'lessThanOrEqual':
            return compareFilterValues(actual, expected) <= 0;
        case 'greaterThan':
            return compareFilterValues(actual, expected) > 0;
        case 'greaterThanOrEqual':
            return compareFilterValues(actual, expected) >= 0;
    }
}
function filterText(value) {
    if (value === undefined || value === null) {
        return '';
    }
    if (typeof value === 'boolean') {
        return value ? 'TRUE' : 'FALSE';
    }
    return String(value);
}
function valueIsBlank(value) {
    return value === undefined || value === null || value === '';
}
function textEqualsFilterValue(actual, expected) {
    return actual.localeCompare(expected, undefined, { sensitivity: 'accent' }) === 0;
}
function compareFilterValues(actual, expected) {
    const left = Number(actual);
    const right = Number(expected);
    if (Number.isFinite(left) && Number.isFinite(right)) {
        return left - right;
    }
    return actual.localeCompare(expected, undefined, { sensitivity: 'accent' });
}
function rowVisibilityUpdatesToOps(sheetName, updates) {
    const ops = [];
    let run = [];
    const flush = () => {
        if (run.length === 0) {
            return;
        }
        const first = run[0];
        ops.push({
            kind: 'updateRowMetadata',
            sheetName,
            start: first.row,
            count: run.length,
            size: first.size,
            hidden: first.hidden,
            filterHidden: first.nextFilterHidden,
            ...(first.geometry ? { geometry: first.geometry } : {}),
        });
        run = [];
    };
    for (const update of updates) {
        const previous = run.at(-1);
        if (previous &&
            update.row === previous.row + 1 &&
            update.size === previous.size &&
            update.hidden === previous.hidden &&
            update.nextFilterHidden === previous.nextFilterHidden &&
            geometryMatches(update.geometry, previous.geometry)) {
            run.push(update);
            continue;
        }
        flush();
        run.push(update);
    }
    flush();
    return ops;
}
function rowStateAt(metadata, row) {
    const fromMetadata = metadata?.rowMetadata?.find((entry) => row >= entry.start && row < entry.start + entry.count);
    const fromEntry = metadata?.rows?.find((entry) => entry.index === row);
    const merged = {
        id: fromEntry?.id ?? `row:${row}`,
        index: row,
        start: row,
        count: 1,
        ...fromMetadata,
        ...fromEntry,
    };
    const geometry = geometryPatch(merged);
    return {
        size: merged.size ?? null,
        hidden: merged.hidden ?? null,
        filterHidden: merged.filterHidden ?? null,
        ...(geometry ? { geometry } : {}),
    };
}
function geometryPatch(record) {
    const patch = {};
    for (const key of geometryKeys) {
        if (record[key] !== undefined) {
            Object.assign(patch, { [key]: record[key] });
        }
    }
    return Object.keys(patch).length > 0 ? patch : undefined;
}
function geometryMatches(left, right) {
    return geometryKeys.every((key) => (left?.[key] ?? null) === (right?.[key] ?? null));
}
//# sourceMappingURL=engine-autofilter.js.map