import { getPointsToPixels } from "../state";
import { getDevicePixelRatio, toCssPixels, toDevicePixels } from "./pixelAlign";
/**
 * Calculate the spread composition for a document.
 *
 * @param pageCount - Total number of pages in the document
 * @param layoutMode - The layout mode determining pages per spread
 * @returns Array of spreads with their page assignments
 */
export function calculateSpreads(pageCount, layoutMode) {
    if (pageCount === 0)
        return [];
    switch (layoutMode) {
        case "single-page":
            return calculateSinglePageSpreads(pageCount);
        case "double-page":
            return calculateDoublePageSpreads(pageCount);
        case "double-page-odd-right":
            return calculateDoublePageOddRightSpreads(pageCount);
        case "double-page-odd-left":
            return calculateDoublePageOddLeftSpreads(pageCount);
    }
}
/**
 * Single page layout: one page per spread.
 * Spread 0: [1], Spread 1: [2], etc.
 */
function calculateSinglePageSpreads(pageCount) {
    const spreads = [];
    for (let i = 0; i < pageCount; i++) {
        spreads.push({
            index: i,
            slots: [i + 1],
        });
    }
    return spreads;
}
/**
 * Double page layout: two pages per spread, starting from page 1.
 * Spread 0: [1, 2], Spread 1: [3, 4], etc.
 * Last spread may have empty right slot if odd page count.
 */
function calculateDoublePageSpreads(pageCount) {
    const spreads = [];
    let spreadIndex = 0;
    for (let page = 1; page <= pageCount; page += 2) {
        const leftPage = page;
        const rightPage = page + 1 <= pageCount ? page + 1 : null;
        spreads.push({
            index: spreadIndex++,
            slots: [leftPage, rightPage],
        });
    }
    return spreads;
}
/**
 * Double page with odd pages on right (cover mode).
 * Spread 0: [null, 1] (cover on right)
 * Spread 1: [2, 3], Spread 2: [4, 5], etc.
 * Last spread may have empty right slot.
 */
function calculateDoublePageOddRightSpreads(pageCount) {
    const spreads = [];
    // First spread: cover page on right
    spreads.push({
        index: 0,
        slots: [null, 1],
    });
    // Remaining spreads: pairs starting from page 2
    let spreadIndex = 1;
    for (let page = 2; page <= pageCount; page += 2) {
        const leftPage = page;
        const rightPage = page + 1 <= pageCount ? page + 1 : null;
        spreads.push({
            index: spreadIndex++,
            slots: [leftPage, rightPage],
        });
    }
    return spreads;
}
/**
 * Double page with odd pages on left (right-to-left reading order).
 * Spread 0: [1, null] (cover on left)
 * Spread 1: [3, 2], Spread 2: [5, 4], etc.
 * Last spread may have empty left slot if even page count.
 */
function calculateDoublePageOddLeftSpreads(pageCount) {
    const spreads = [];
    if (pageCount === 0)
        return spreads;
    // First spread: cover page on left
    spreads.push({
        index: 0,
        slots: [1, null],
    });
    // Remaining spreads: [odd, previous even] pairs
    let spreadIndex = 1;
    for (let oddPage = 3; oddPage <= pageCount; oddPage += 2) {
        const evenPage = oddPage - 1;
        spreads.push({
            index: spreadIndex++,
            slots: [oddPage, evenPage],
        });
    }
    // If last page is even, it needs its own spread
    if (pageCount % 2 === 0) {
        spreads.push({
            index: spreadIndex,
            slots: [null, pageCount],
        });
    }
    return spreads;
}
/**
 * Find the spread index containing a specific page using O(1) arithmetic.
 * Since spread structure is fully deterministic from layout mode, we can compute
 * the index directly without any search.
 */
export function findSpreadForPage(spreads, page) {
    if (spreads.length === 0)
        return 0;
    const firstSpread = spreads[0];
    const lastSpread = spreads[spreads.length - 1];
    // Check if page is outside document range
    const maxPageInDoc = Math.max(...lastSpread.slots.filter((slot) => slot !== null));
    if (page < 1 || page > maxPageInDoc) {
        return 0;
    }
    // Infer layout mode from the first spread's structure
    let spreadIndex;
    if (firstSpread.slots.length === 1) {
        // single-page: [1], [2], [3], ...
        spreadIndex = page - 1;
    }
    else {
        // Double-page modes: check first spread's pattern
        const [slot0, slot1] = firstSpread.slots;
        if (slot0 === null && slot1 === 1) {
            // double-page-odd-right: [null, 1], [2, 3], [4, 5], ...
            spreadIndex = Math.floor(page / 2);
        }
        else if (slot0 === 1 && slot1 === null) {
            // double-page-odd-left: [1, null], [3, 2], [5, 4], ...
            spreadIndex = Math.floor(page / 2);
        }
        else {
            // double-page: [1, 2], [3, 4], [5, 6], ...
            spreadIndex = Math.floor((page - 1) / 2);
        }
    }
    return spreadIndex;
}
/**
 * Get the primary page of a spread (first non-null slot).
 */
export function getSpreadPrimaryPage(spread) {
    for (const slot of spread.slots) {
        if (slot !== null)
            return slot;
    }
    return 1;
}
function normalizeRotation(rotation) {
    switch (rotation) {
        case 90:
        case 180:
        case 270:
            return rotation;
        default:
            return 0;
    }
}
/**
 * Combine document rotation with user rotation to get effective rotation.
 */
function combineRotation(documentRotation, userRotation) {
    const combined = (documentRotation + userRotation) % 360;
    return normalizeRotation(combined);
}
/**
 * Apply rotation to get the effective display size.
 * Combines document rotation (from PDF) with user rotation (viewer setting).
 */
function applyRotation(pageInfo, userRotation) {
    const documentRotation = normalizeRotation((pageInfo.rotation ?? 0));
    const effectiveRotation = combineRotation(documentRotation, userRotation);
    if (effectiveRotation === 90 || effectiveRotation === 270) {
        return { width: pageInfo.height, height: pageInfo.width };
    }
    return { width: pageInfo.width, height: pageInfo.height };
}
/**
 * Calculate spread layouts with positions for virtual scrolling.
 *
 * @param spreads - Array of spreads from calculateSpreads()
 * @param pageInfos - Array of page sizes (0-indexed, in points)
 * @param scale - Zoom scale factor
 * @param pageSpacing - Horizontal spacing between pages in a spread
 * @param spreadSpacing - Vertical spacing between spreads
 * @param dpi - Display DPI for point-to-pixel conversion
 * @param rotation - Page rotation in degrees
 * @returns Layouts plus aggregated content size
 */
export function calculateSpreadLayouts(spreads, pageInfos, scale, pageSpacing, spreadSpacing, dpi, rotation) {
    // Snap all layout values to device pixels to ensure adjacent spreads align.
    // We use CUMULATIVE snapping: each spread's top is calculated from the
    // previous spread's snapped bottom, guaranteeing no gaps or overlaps.
    const dpr = getDevicePixelRatio();
    const layouts = [];
    const spacingDevice = toDevicePixels(spreadSpacing, dpr);
    let currentTopDevice = spreads.length > 0 ? spacingDevice : 0;
    let maxWidthDevice = 0;
    for (const spread of spreads) {
        const { width, height } = getSpreadDimensionsDevice(spread, pageInfos, scale, pageSpacing, dpi, rotation, dpr);
        layouts.push({
            index: spread.index,
            slots: spread.slots,
            top: toCssPixels(currentTopDevice, dpr),
            width: toCssPixels(width, dpr),
            height: toCssPixels(height, dpr),
        });
        // Next spread's top = this spread's bottom + spacing
        // Using snapped values ensures cumulative consistency
        currentTopDevice += height + spacingDevice;
        maxWidthDevice = Math.max(maxWidthDevice, width);
    }
    const contentHeight = spreads.length > 0 ? toCssPixels(currentTopDevice, dpr) : 0;
    return {
        layouts,
        contentWidth: toCssPixels(maxWidthDevice, dpr),
        contentHeight,
    };
}
/**
 * Calculate the dimensions of a spread based on its pages.
 * Returns dimensions in CSS pixels (accounting for DPI conversion).
 */
export function getSpreadDimensions(spread, pageInfos, scale, pageSpacing, dpi, rotation) {
    const dpr = getDevicePixelRatio();
    const deviceDims = getSpreadDimensionsDevice(spread, pageInfos, scale, pageSpacing, dpi, rotation, dpr);
    return {
        width: toCssPixels(deviceDims.width, dpr),
        height: toCssPixels(deviceDims.height, dpr),
    };
}
function getSpreadDimensionsDevice(spread, pageInfos, scale, pageSpacing, dpi, rotation, dpr) {
    const pointsToPixels = getPointsToPixels(dpi);
    let totalWidth = 0;
    let maxHeight = 0;
    const hasMultipleSlots = spread.slots.length > 1;
    let referenceSize = null;
    for (const slot of spread.slots) {
        if (slot !== null) {
            const pageInfo = pageInfos[slot - 1];
            if (pageInfo) {
                referenceSize = applyRotation(pageInfo, rotation);
                break;
            }
        }
    }
    for (const slot of spread.slots) {
        if (slot !== null) {
            const pageIndex = slot - 1; // Convert 1-indexed page to 0-indexed
            const size = pageInfos[pageIndex];
            if (size) {
                const rotated = applyRotation(size, rotation);
                // Convert points to pixels: points * (dpi/72) * zoom
                const widthDevice = toDevicePixels(rotated.width * pointsToPixels * scale, dpr);
                const heightDevice = toDevicePixels(rotated.height * pointsToPixels * scale, dpr);
                totalWidth += widthDevice;
                maxHeight = Math.max(maxHeight, heightDevice);
            }
        }
        else if (referenceSize) {
            const widthDevice = toDevicePixels(referenceSize.width * pointsToPixels * scale, dpr);
            const heightDevice = toDevicePixels(referenceSize.height * pointsToPixels * scale, dpr);
            totalWidth += widthDevice;
            maxHeight = Math.max(maxHeight, heightDevice);
        }
    }
    // Add spacing between slots in multi-slot spreads (even if a slot is empty).
    if (hasMultipleSlots) {
        totalWidth += toDevicePixels(pageSpacing, dpr);
    }
    return { width: totalWidth, height: maxHeight };
}
/**
 * Calculate total content height including all spreads and spacing.
 */
export function calculateTotalHeight(layouts, spreadSpacing) {
    if (layouts.length === 0)
        return 0;
    const lastLayout = layouts[layouts.length - 1];
    return lastLayout.top + lastLayout.height + snapSpacing(spreadSpacing);
}
function snapSpacing(value) {
    const dpr = getDevicePixelRatio();
    return toCssPixels(toDevicePixels(value, dpr), dpr);
}
/**
 * Find visible spread indices based on scroll position and viewport height.
 * Uses binary search for O(log n) complexity.
 *
 * @param layouts - Array of spread layouts
 * @param scrollTop - Current scroll position
 * @param viewportHeight - Height of the visible viewport
 * @param buffer - Number of extra spreads to include above/below (for smooth scrolling)
 * @returns Object with start and end indices (inclusive)
 */
export function findVisibleSpreadRange(layouts, scrollTop, viewportHeight, buffer = 1) {
    if (layouts.length === 0) {
        return { start: 0, end: -1 };
    }
    const viewportBottom = scrollTop + viewportHeight;
    const start = findFirstVisibleSpread(layouts, scrollTop);
    const end = findLastVisibleSpread(layouts, viewportBottom, start);
    return {
        start: Math.max(0, start - buffer),
        end: Math.min(layouts.length - 1, end + buffer),
    };
}
function findFirstVisibleSpread(layouts, scrollTop) {
    let left = 0;
    let right = layouts.length - 1;
    let result = 0;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const layout = layouts[mid];
        if (layout.top + layout.height >= scrollTop) {
            result = mid;
            right = mid - 1;
        }
        else {
            left = mid + 1;
        }
    }
    return result;
}
function findLastVisibleSpread(layouts, viewportBottom, startFrom) {
    let left = startFrom;
    let right = layouts.length - 1;
    let result = startFrom;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const layout = layouts[mid];
        if (layout.top <= viewportBottom) {
            result = mid;
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    return result;
}
// -----------------------------------------------------------------------------
// Continuous Mode Layout
// -----------------------------------------------------------------------------
/**
 * Get the cropped content size for a page in continuous mode.
 * Uses contentRect if available, otherwise falls back to full page size.
 */
export function getCroppedPageSize(pageInfo, rotation) {
    if (pageInfo.contentRect) {
        const cr = pageInfo.contentRect;
        const size = { width: cr.width, height: cr.height };
        return applyRotationToSize(size, pageInfo, rotation);
    }
    return applyRotation(pageInfo, rotation);
}
function applyRotationToSize(size, pageInfo, userRotation) {
    const documentRotation = normalizeRotation((pageInfo.rotation ?? 0));
    const effectiveRotation = combineRotation(documentRotation, userRotation);
    if (effectiveRotation === 90 || effectiveRotation === 270) {
        return { width: size.height, height: size.width };
    }
    return size;
}
/**
 * Derives the grid structure from page infos.
 * For XLSX: uses tilePos from each page.
 * For DOCX/PDF: single column, pages stack vertically.
 */
export function deriveGrid(pageInfos) {
    const pageGrid = [];
    let maxCol = 0;
    let maxRow = 0;
    let hasTilePos = false;
    for (let i = 0; i < pageInfos.length; i++) {
        const pi = pageInfos[i];
        if (pi.tilePos) {
            hasTilePos = true;
            pageGrid.push({ pageIndex: i, row: pi.tilePos.row, col: pi.tilePos.col });
            maxCol = Math.max(maxCol, pi.tilePos.col);
            maxRow = Math.max(maxRow, pi.tilePos.row);
        }
    }
    if (!hasTilePos) {
        // Linear layout: single column
        for (let i = 0; i < pageInfos.length; i++) {
            pageGrid.push({ pageIndex: i, row: i, col: 0 });
        }
        return { columns: 1, rows: pageInfos.length, pageGrid };
    }
    return { columns: maxCol + 1, rows: maxRow + 1, pageGrid };
}
/**
 * Calculate continuous mode layout.
 *
 * Two sub-modes determined by the presence of `tilePos` on pages:
 *
 * 1. **Linear stitch** (Word/PDF/PPTX — no tilePos): Full-width pages stacked
 *    vertically with zero spacing, centered. No content-rect cropping.
 *    This is visually the same as paged mode but without shadows/gaps.
 *
 * 2. **Grid stitch** (XLSX — has tilePos): Pages cropped to contentRect and
 *    arranged in a 2D grid with column alignment.
 */
export function calculateContinuousLayout(spreads, pageInfos, scale, dpi, rotation) {
    const grid = deriveGrid(pageInfos);
    if (grid.columns === 1 && !pageInfos.some((p) => p.tilePos)) {
        // Linear stitch: full pages, no cropping, zero spacing, centered
        return calculateLinearContinuousLayout(spreads, pageInfos, scale, dpi, rotation);
    }
    // Grid stitch: crop to contentRect, tile-based positioning
    return calculateGridContinuousLayout(spreads, pageInfos, scale, dpi, rotation, grid);
}
/**
 * Get the vertically-cropped page size (crop top/bottom via contentRect, keep full width).
 * Used for Word/PDF/PPTX continuous mode to remove headers/footers.
 */
function getVerticallyCroppedPageSize(pageInfo, rotation) {
    const fullSize = applyRotation(pageInfo, rotation);
    if (!pageInfo.contentRect)
        return fullSize;
    const cr = pageInfo.contentRect;
    // Keep full page width, use contentRect height
    const size = { width: pageInfo.width, height: cr.height };
    return applyRotationToSize(size, pageInfo, rotation);
}
/**
 * Linear continuous layout for Word/PDF/PPTX.
 * Full page width, centered, vertically cropped to contentRect (headers/footers removed).
 */
function calculateLinearContinuousLayout(spreads, pageInfos, scale, dpi, rotation) {
    const dpr = getDevicePixelRatio();
    const pointsToPixels = getPointsToPixels(dpi);
    const layouts = [];
    let currentTopDevice = 0;
    let maxWidthDevice = 0;
    for (const spread of spreads) {
        // Use full page width but cropped height
        let spreadWidthDevice = 0;
        let spreadHeightDevice = 0;
        for (const slot of spread.slots) {
            if (slot !== null) {
                const pageInfo = pageInfos[slot - 1];
                if (pageInfo) {
                    const cropped = getVerticallyCroppedPageSize(pageInfo, rotation);
                    const wDevice = toDevicePixels(cropped.width * pointsToPixels * scale, dpr);
                    const hDevice = toDevicePixels(cropped.height * pointsToPixels * scale, dpr);
                    spreadWidthDevice = Math.max(spreadWidthDevice, wDevice);
                    spreadHeightDevice = Math.max(spreadHeightDevice, hDevice);
                }
            }
        }
        layouts.push({
            index: spread.index,
            slots: spread.slots,
            top: toCssPixels(currentTopDevice, dpr),
            width: toCssPixels(spreadWidthDevice, dpr),
            height: toCssPixels(spreadHeightDevice, dpr),
        });
        currentTopDevice += spreadHeightDevice;
        maxWidthDevice = Math.max(maxWidthDevice, spreadWidthDevice);
    }
    return {
        layouts,
        rowLayouts: [],
        contentWidth: toCssPixels(maxWidthDevice, dpr),
        contentHeight: toCssPixels(currentTopDevice, dpr),
        columnWidths: [],
        isGridLayout: false,
    };
}
/**
 * Grid continuous layout for XLSX.
 * Pages cropped to contentRect and arranged in a 2D grid.
 */
function calculateGridContinuousLayout(spreads, pageInfos, scale, dpi, rotation, grid) {
    const dpr = getDevicePixelRatio();
    const pointsToPixels = getPointsToPixels(dpi);
    // Calculate cropped dimensions for each page
    const pageDims = [];
    for (let i = 0; i < pageInfos.length; i++) {
        const cropped = getCroppedPageSize(pageInfos[i], rotation);
        pageDims.push({
            width: toCssPixels(toDevicePixels(cropped.width * pointsToPixels * scale, dpr), dpr),
            height: toCssPixels(toDevicePixels(cropped.height * pointsToPixels * scale, dpr), dpr),
        });
    }
    // Calculate column widths (max width in each column across all rows)
    const columnWidths = new Array(grid.columns).fill(0);
    for (const pg of grid.pageGrid) {
        const dim = pageDims[pg.pageIndex];
        if (dim) {
            columnWidths[pg.col] = Math.max(columnWidths[pg.col], dim.width);
        }
    }
    // Group pages by row
    const rowPages = new Map();
    for (const pg of grid.pageGrid) {
        let pages = rowPages.get(pg.row);
        if (!pages) {
            pages = [];
            rowPages.set(pg.row, pages);
        }
        pages.push({ pageIndex: pg.pageIndex, col: pg.col });
    }
    // Sort rows and calculate layouts
    const rowIndices = Array.from(rowPages.keys()).sort((a, b) => a - b);
    const rowLayouts = [];
    const layouts = [];
    let currentTop = 0;
    const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0);
    for (const rowIdx of rowIndices) {
        const pages = rowPages.get(rowIdx);
        pages.sort((a, b) => a.col - b.col);
        // Row height = max cropped page height in this row
        let rowHeight = 0;
        for (const p of pages) {
            const dim = pageDims[p.pageIndex];
            if (dim) {
                rowHeight = Math.max(rowHeight, dim.height);
            }
        }
        // Calculate left offsets for each page based on column widths
        const rowPageLayouts = [];
        for (const p of pages) {
            let left = 0;
            for (let c = 0; c < p.col; c++) {
                left += columnWidths[c];
            }
            const dim = pageDims[p.pageIndex];
            rowPageLayouts.push({
                pageIndex: p.pageIndex,
                col: p.col,
                left,
                width: dim?.width ?? 0,
                height: dim?.height ?? 0,
            });
        }
        rowLayouts.push({
            row: rowIdx,
            top: currentTop,
            width: totalWidth,
            height: rowHeight,
            pages: rowPageLayouts,
        });
        // Create SpreadLayout for each page in this row (for Viewport compatibility)
        // All pages in a row share the row height so they align vertically.
        for (const p of pages) {
            const spreadIndex = p.pageIndex;
            if (spreadIndex < spreads.length) {
                const dim = pageDims[p.pageIndex];
                layouts.push({
                    index: spreadIndex,
                    slots: spreads[spreadIndex].slots,
                    top: currentTop,
                    width: dim?.width ?? 0,
                    height: rowHeight,
                });
            }
        }
        currentTop += rowHeight;
    }
    return {
        layouts,
        rowLayouts,
        contentWidth: totalWidth,
        contentHeight: currentTop,
        columnWidths,
        isGridLayout: true,
    };
}
//# sourceMappingURL=spreadLayout.js.map