import { formatAddress, parseCellAddress } from '@bilig/formula';
import { WORKBOOK_DEFAULT_FORMAT_ID, WORKBOOK_DEFAULT_STYLE_ID } from '../workbook-default-style-format.js';
import { translateMutationFormulaForTarget } from './services/mutation-cell-content-helpers.js';
export function buildSortRangeOps(snapshot, sheetName, range, keys, options = {}) {
    if (keys.length === 0) {
        throw new Error('sortRange requires at least one sort key');
    }
    const sheet = snapshot.sheets.find((candidate) => candidate.name === sheetName);
    if (!sheet) {
        throw new Error(`Unknown sheet: ${sheetName}`);
    }
    const bounds = sortBounds(sheetName, range);
    const bodyStartRow = options.header === true ? bounds.startRow + 1 : bounds.startRow;
    if (bodyStartRow > bounds.endRow) {
        return [];
    }
    const cellsByAddress = new Map();
    for (const cell of sheet.cells) {
        cellsByAddress.set(cell.address, {
            ...(cell.formula !== undefined ? { formula: cell.formula } : {}),
            ...(cell.value !== undefined ? { value: cell.value } : {}),
            ...(cell.format !== undefined ? { format: cell.format } : {}),
        });
    }
    const keyColumns = keys.map((key) => parseCellAddress(key.keyAddress, sheetName).col);
    const rows = Array.from({ length: bounds.endRow - bodyStartRow + 1 }, (_, index) => {
        return { row: bodyStartRow + index, originalIndex: index };
    });
    const sortedRows = rows.toSorted((left, right) => compareSortRows(cellsByAddress, keyColumns, keys, left, right));
    const ops = [];
    const rewrites = buildSortCellRewrites(sortedRows, bounds, bodyStartRow);
    for (const rewrite of rewrites) {
        const source = cellsByAddress.get(rewrite.sourceAddress);
        const existing = cellsByAddress.get(rewrite.targetAddress);
        appendCellReplacementOps(ops, sheetName, rewrite, existing, source);
    }
    appendRowBundleMetadataSortOps(ops, sheet.metadata, rewrites, sheetName);
    if (options.recordSortMetadata !== false) {
        const metadataRange = {
            sheetName,
            startAddress: formatAddress(bodyStartRow, bounds.startCol),
            endAddress: formatAddress(bounds.endRow, bounds.endCol),
        };
        ops.push({
            kind: 'setSort',
            sheetName,
            range: metadataRange,
            keys: keys.map((key) => ({ keyAddress: key.keyAddress, direction: key.direction })),
        });
    }
    return ops;
}
export function buildSortTableOps(snapshot, sheetName, tableName, keys) {
    if (keys.length === 0) {
        throw new Error('sortTable requires at least one sort key');
    }
    const table = snapshot.workbook.metadata?.tables?.find((candidate) => candidate.name === tableName && candidate.sheetName === sheetName);
    if (!table) {
        throw new Error(`Unknown table: ${tableName}`);
    }
    const tableBounds = sortBounds(sheetName, { sheetName, startAddress: table.startAddress, endAddress: table.endAddress });
    const bodyStartRow = table.headerRow ? tableBounds.startRow + 1 : tableBounds.startRow;
    const bodyEndRow = table.totalsRow ? tableBounds.endRow - 1 : tableBounds.endRow;
    if (bodyStartRow > bodyEndRow) {
        return [];
    }
    const bodyRange = {
        sheetName,
        startAddress: formatAddress(bodyStartRow, tableBounds.startCol),
        endAddress: formatAddress(bodyEndRow, tableBounds.endCol),
    };
    const ops = buildSortRangeOps(snapshot, sheetName, bodyRange, keys, { recordSortMetadata: false });
    ops.push({
        kind: 'upsertTable',
        table: {
            ...structuredClone(table),
            sortState: buildTableSortStateXml(sheetName, bodyRange, keys),
        },
    });
    return ops;
}
function buildTableSortStateXml(sheetName, bodyRange, keys) {
    const bodyBounds = sortBounds(sheetName, bodyRange);
    const ref = `${bodyRange.startAddress}:${bodyRange.endAddress}`;
    const conditions = keys
        .map((key) => {
        const keyCol = parseCellAddress(key.keyAddress, sheetName).col;
        const conditionRef = `${formatAddress(bodyBounds.startRow, keyCol)}:${formatAddress(bodyBounds.endRow, keyCol)}`;
        const descending = key.direction === 'desc' ? ' descending="1"' : '';
        return `<sortCondition${descending} ref="${escapeXml(conditionRef)}"/>`;
    })
        .join('');
    return `<sortState ref="${escapeXml(ref)}">${conditions}</sortState>`;
}
function escapeXml(value) {
    return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
function buildSortCellRewrites(sortedRows, bounds, bodyStartRow) {
    return sortedRows.flatMap((sortRow, targetOffset) => {
        const targetRow = bodyStartRow + targetOffset;
        return Array.from({ length: bounds.endCol - bounds.startCol + 1 }, (_, index) => {
            const col = bounds.startCol + index;
            return {
                col,
                sourceRow: sortRow.row,
                targetRow,
                sourceAddress: formatAddress(sortRow.row, col),
                targetAddress: formatAddress(targetRow, col),
            };
        });
    });
}
function appendRowBundleMetadataSortOps(ops, metadata, rewrites, sheetName) {
    if (!metadata) {
        return;
    }
    appendStyleRangeSortOps(ops, metadata.styleRanges, rewrites, sheetName);
    appendFormatRangeSortOps(ops, metadata.formatRanges, rewrites, sheetName);
    appendCommentThreadSortOps(ops, metadata.commentThreads, rewrites, sheetName);
    appendNoteSortOps(ops, metadata.notes, rewrites, sheetName);
    appendHyperlinkSortOps(ops, metadata.hyperlinks, rewrites, sheetName);
}
function appendCommentThreadSortOps(ops, commentThreads, rewrites, sheetName) {
    const threadsByAddress = new Map((commentThreads ?? []).map((thread) => [thread.address, thread]));
    for (const rewrite of rewrites) {
        const source = threadsByAddress.get(rewrite.sourceAddress);
        const existing = threadsByAddress.get(rewrite.targetAddress);
        if (source) {
            ops.push({
                kind: 'upsertCommentThread',
                thread: {
                    ...structuredClone(source),
                    sheetName,
                    address: rewrite.targetAddress,
                },
            });
        }
        else if (existing) {
            ops.push({ kind: 'deleteCommentThread', sheetName, address: rewrite.targetAddress });
        }
    }
}
function appendNoteSortOps(ops, notes, rewrites, sheetName) {
    const notesByAddress = new Map((notes ?? []).map((note) => [note.address, note]));
    for (const rewrite of rewrites) {
        const source = notesByAddress.get(rewrite.sourceAddress);
        const existing = notesByAddress.get(rewrite.targetAddress);
        if (source) {
            ops.push({
                kind: 'upsertNote',
                note: {
                    ...structuredClone(source),
                    sheetName,
                    address: rewrite.targetAddress,
                },
            });
        }
        else if (existing) {
            ops.push({ kind: 'deleteNote', sheetName, address: rewrite.targetAddress });
        }
    }
}
function appendHyperlinkSortOps(ops, hyperlinks, rewrites, sheetName) {
    const hyperlinksByAddress = new Map((hyperlinks ?? []).map((hyperlink) => [hyperlink.address, hyperlink]));
    for (const rewrite of rewrites) {
        const source = hyperlinksByAddress.get(rewrite.sourceAddress);
        const existing = hyperlinksByAddress.get(rewrite.targetAddress);
        if (source) {
            ops.push({
                kind: 'upsertHyperlink',
                hyperlink: {
                    ...structuredClone(source),
                    sheetName,
                    address: rewrite.targetAddress,
                },
            });
        }
        else if (existing) {
            ops.push({ kind: 'deleteHyperlink', sheetName, address: rewrite.targetAddress });
        }
    }
}
function appendStyleRangeSortOps(ops, styleRanges, rewrites, sheetName) {
    if ((styleRanges?.length ?? 0) === 0) {
        return;
    }
    appendRangeIdSortOps(rewrites, WORKBOOK_DEFAULT_STYLE_ID, (row, col) => rangeStyleIdAt(styleRanges, row, col), (range, styleId) => ops.push({ kind: 'setStyleRange', range: { ...range, sheetName }, styleId }));
}
function appendFormatRangeSortOps(ops, formatRanges, rewrites, sheetName) {
    if ((formatRanges?.length ?? 0) === 0) {
        return;
    }
    appendRangeIdSortOps(rewrites, WORKBOOK_DEFAULT_FORMAT_ID, (row, col) => rangeFormatIdAt(formatRanges, row, col), (range, formatId) => ops.push({ kind: 'setFormatRange', range: { ...range, sheetName }, formatId }));
}
function appendRangeIdSortOps(rewrites, defaultId, idAt, appendOp) {
    let pending;
    const flush = () => {
        if (!pending) {
            return;
        }
        appendOp({
            startAddress: formatAddress(pending.targetRow, pending.startCol),
            endAddress: formatAddress(pending.targetRow, pending.endCol),
        }, pending.id);
        pending = undefined;
    };
    for (const rewrite of rewrites) {
        const sourceId = idAt(rewrite.sourceRow, rewrite.col) ?? defaultId;
        const targetId = idAt(rewrite.targetRow, rewrite.col) ?? defaultId;
        if (sourceId === targetId) {
            flush();
            continue;
        }
        if (pending && pending.targetRow === rewrite.targetRow && pending.endCol + 1 === rewrite.col && pending.id === sourceId) {
            pending = { ...pending, endCol: rewrite.col };
            continue;
        }
        flush();
        pending = {
            targetRow: rewrite.targetRow,
            startCol: rewrite.col,
            endCol: rewrite.col,
            id: sourceId,
        };
    }
    flush();
}
function rangeStyleIdAt(styleRanges, row, col) {
    return rangeRecordAt(styleRanges, row, col, (record) => record.styleId);
}
function rangeFormatIdAt(formatRanges, row, col) {
    return rangeRecordAt(formatRanges, row, col, (record) => record.formatId);
}
function rangeRecordAt(records, row, col, readId) {
    if (!records) {
        return undefined;
    }
    for (let index = records.length - 1; index >= 0; index -= 1) {
        const record = records[index];
        if (rangeContainsCell(record.range, row, col)) {
            return readId(record);
        }
    }
    return undefined;
}
function rangeContainsCell(range, row, col) {
    const start = parseCellAddress(range.startAddress, range.sheetName);
    const end = parseCellAddress(range.endAddress, range.sheetName);
    return (row >= Math.min(start.row, end.row) &&
        row <= Math.max(start.row, end.row) &&
        col >= Math.min(start.col, end.col) &&
        col <= Math.max(start.col, end.col));
}
function sortBounds(sheetName, range) {
    const start = parseCellAddress(range.startAddress, sheetName);
    const end = parseCellAddress(range.endAddress, sheetName);
    return {
        startRow: Math.min(start.row, end.row),
        endRow: Math.max(start.row, end.row),
        startCol: Math.min(start.col, end.col),
        endCol: Math.max(start.col, end.col),
    };
}
function compareSortRows(cellsByAddress, keyColumns, keys, left, right) {
    for (let index = 0; index < keys.length; index += 1) {
        const key = keys[index];
        const col = keyColumns[index];
        const leftValue = cellsByAddress.get(formatAddress(left.row, col))?.value;
        const rightValue = cellsByAddress.get(formatAddress(right.row, col))?.value;
        const compared = compareSortValues(leftValue, rightValue, key.direction);
        if (compared !== 0) {
            return compared;
        }
    }
    return left.originalIndex - right.originalIndex;
}
function compareSortValues(left, right, direction) {
    const leftBlank = left === undefined || left === null;
    const rightBlank = right === undefined || right === null;
    if (leftBlank || rightBlank) {
        if (leftBlank === rightBlank) {
            return 0;
        }
        return leftBlank ? 1 : -1;
    }
    const leftRank = sortValueRank(left);
    const rightRank = sortValueRank(right);
    const compared = leftRank !== rightRank
        ? leftRank - rightRank
        : typeof left === 'number' && typeof right === 'number'
            ? left - right
            : typeof left === 'boolean' && typeof right === 'boolean'
                ? Number(left) - Number(right)
                : String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
    return direction === 'desc' ? -compared : compared;
}
function sortValueRank(value) {
    if (value === undefined || value === null) {
        return 3;
    }
    if (typeof value === 'number') {
        return 0;
    }
    if (typeof value === 'string') {
        return 1;
    }
    if (typeof value === 'boolean') {
        return 2;
    }
    return 3;
}
function appendCellReplacementOps(ops, sheetName, rewrite, existing, source) {
    const address = rewrite.targetAddress;
    if (!source) {
        if (existing) {
            ops.push({ kind: 'clearCell', sheetName, address });
            if (existing.format !== undefined) {
                ops.push({ kind: 'setCellFormat', sheetName, address, format: null });
            }
        }
        return;
    }
    if (source.formula !== undefined) {
        const formula = translateSortedFormula(source.formula, sheetName, rewrite);
        if (existing?.formula !== formula) {
            ops.push({ kind: 'setCellFormula', sheetName, address, formula });
        }
    }
    else if (source.value !== undefined) {
        if (existing?.formula !== undefined || existing?.value !== source.value) {
            ops.push({ kind: 'setCellValue', sheetName, address, value: source.value });
        }
    }
    else if (existing?.formula !== undefined || existing?.value !== undefined) {
        ops.push({ kind: 'clearCell', sheetName, address });
    }
    if (existing?.format !== source.format) {
        ops.push({ kind: 'setCellFormat', sheetName, address, format: source.format ?? null });
    }
}
function translateSortedFormula(formula, sheetName, rewrite) {
    if (rewrite.sourceAddress === rewrite.targetAddress) {
        return formula;
    }
    return translateMutationFormulaForTarget(formula, sheetName, rewrite.sourceAddress, sheetName, rewrite.targetAddress);
}
//# sourceMappingURL=engine-sort-range.js.map