import { styledLineToString } from './tokenize.js';
import { toStyledCharacters, inkCharacterWidth, styledCharsWidth, } from './measure-text.js';
import { calculateScrollbarLayout } from './measure-element.js';
import { renderScrollbar } from './render-scrollbar.js';
/**
"Virtual" output class

Handles the positioning and saving of the output of each node in the tree. Also responsible for applying transformations to each character of the output.

Used to generate the final output of all nodes before writing it to actual output stream (e.g. stdout)
*/
import { StyledLine } from './styled-line.js';
export function clampCursorColumn(line, col) {
    let currentLineCol = 0;
    let lastContentCol = 0;
    for (let i = 0; i < line.length; i++) {
        const val = line.getValue(i);
        const charWidth = inkCharacterWidth(val);
        if (val !== ' ' || line.hasStyles(i)) {
            lastContentCol = currentLineCol + charWidth;
        }
        currentLineCol += charWidth;
    }
    return col > lastContentCol ? lastContentCol : col;
}
export function intersectRect(a, b) {
    const x1 = Math.max(a.x, b.x);
    const y1 = Math.max(a.y, b.y);
    const x2 = Math.min(a.x + a.w, b.x + b.w);
    const y2 = Math.min(a.y + a.h, b.y + b.h);
    if (x2 <= x1 || y2 <= y1) {
        return undefined;
    }
    return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
}
export function isRectIntersectingClip(rect, clip) {
    const clipLeft = clip.x1 ?? -Infinity;
    const clipRight = clip.x2 ?? Infinity;
    const clipTop = clip.y1 ?? -Infinity;
    const clipBottom = clip.y2 ?? Infinity;
    return (rect.x2 > clipLeft &&
        rect.x1 < clipRight &&
        rect.y2 > clipTop &&
        rect.y1 < clipBottom);
}
export function treesEqual(a, b) {
    if (a === b)
        return true;
    if (a.id !== b.id)
        return false;
    if (a.children.length !== b.children.length)
        return false;
    for (let i = 0; i < a.children.length; i++) {
        if (!treesEqual(a.children[i], b.children[i]))
            return false;
    }
    return true;
}
export const regionLayoutProperties = [
    'x',
    'y',
    'width',
    'height',
    'scrollTop',
    'scrollLeft',
    'scrollHeight',
    'scrollWidth',
    'isScrollable',
    'isVerticallyScrollable',
    'isHorizontallyScrollable',
    'scrollbarVisible',
    'overflowToBackbuffer',
    'marginRight',
    'marginBottom',
    'scrollbarThumbColor',
    'backgroundColor',
    'opaque',
    'borderTop',
    'borderBottom',
];
export function copyRegionProperty(target, source, key) {
    const value = source[key];
    if (value !== undefined) {
        target[key] = value;
    }
}
export default class Output {
    width;
    height;
    trackSelection;
    // The root region represents the main screen area (non-scrollable background)
    root;
    activeRegionStack = [];
    clips = [];
    constructor(options) {
        const { width, height, node, id = 'root', trackSelection = false } = options;
        this.width = width;
        this.height = height;
        this.trackSelection = trackSelection;
        this.root = {
            id,
            x: 0,
            y: 0,
            width,
            height,
            lines: [],
            styledOutput: [],
            isScrollable: false,
            stickyHeaders: [],
            children: [],
            node,
            selectableSpans: [],
        };
        this.initLines(this.root, width, height);
        this.activeRegionStack.push(this.root);
    }
    getCurrentClip() {
        return this.clips.at(-1);
    }
    getActiveRegion() {
        return this.activeRegionStack.at(-1);
    }
    getRegionAbsoluteOffset() {
        let x = 0;
        let y = 0;
        for (const region of this.activeRegionStack) {
            x += region.x - (region.scrollLeft ?? 0);
            y += region.y - (region.scrollTop ?? 0);
        }
        return { x, y };
    }
    startChildRegion(options) {
        const { id, x, y, width, height, isScrollable, isVerticallyScrollable, isHorizontallyScrollable, scrollState, scrollbarVisible, overflowToBackbuffer, marginRight, marginBottom, scrollbarThumbColor, backgroundColor, opaque, nodeId, stableScrollback, borderTop, borderBottom, } = options;
        // Create new region
        // The buffer size should match scrollDimensions if scrollable, or bounds if not.
        // If scrollable, we want to capture the FULL content.
        const bufferWidth = scrollState?.scrollWidth ?? width;
        const bufferHeight = scrollState?.scrollHeight ?? height;
        const activeRegion = this.getActiveRegion();
        const inheritedOverflowToBackbuffer = isScrollable
            ? overflowToBackbuffer
            : (overflowToBackbuffer ?? activeRegion.overflowToBackbuffer);
        const region = {
            id,
            x,
            y,
            width,
            height,
            lines: [],
            styledOutput: [],
            isScrollable,
            isVerticallyScrollable,
            isHorizontallyScrollable,
            scrollTop: scrollState?.scrollTop,
            scrollLeft: scrollState?.scrollLeft,
            scrollHeight: scrollState?.scrollHeight,
            scrollWidth: scrollState?.scrollWidth,
            scrollbarVisible,
            overflowToBackbuffer: inheritedOverflowToBackbuffer,
            marginRight,
            marginBottom,
            scrollbarThumbColor,
            backgroundColor,
            opaque,
            borderTop,
            borderBottom,
            stickyHeaders: [],
            children: [],
            nodeId,
            stableScrollback,
            selectableSpans: [],
        };
        this.initLines(region, bufferWidth, bufferHeight);
        // Add to current active region's children
        this.getActiveRegion().children.push(region);
        // Push to stack
        this.activeRegionStack.push(region);
    }
    endChildRegion() {
        if (this.activeRegionStack.length > 1) {
            this.activeRegionStack.pop();
        }
    }
    addStickyHeader(header) {
        this.getActiveRegion().stickyHeaders.push(header);
    }
    write(x, y, items, options) {
        const { transformers = [], lineIndex = 0, preserveBackgroundColor = false, isTerminalCursorFocused = false, terminalCursorPosition, isSelectable = false, } = options;
        if (items.length === 0 && !isTerminalCursorFocused) {
            return;
        }
        if (isTerminalCursorFocused) {
            const region = this.getActiveRegion();
            let col = 0;
            let row = 0;
            const chars = typeof items === 'string' ? toStyledCharacters(items) : items;
            let charOffset = 0;
            const targetOffset = terminalCursorPosition ?? Number.POSITIVE_INFINITY;
            for (let i = 0; i < chars.length; i++) {
                if (charOffset >= targetOffset) {
                    break;
                }
                const val = chars.getValue(i);
                if (val === '\n') {
                    row++;
                    col = 0;
                }
                else {
                    col += inkCharacterWidth(val);
                }
                charOffset += val.length;
            }
            region.cursorPosition = {
                row: y + row,
                col: x + col,
            };
        }
        if (items.length > 0) {
            this.applyWrite(x, y, items, transformers, lineIndex, preserveBackgroundColor, isSelectable);
        }
    }
    clip(clip) {
        const previousClip = this.clips.at(-1);
        const nextClip = { ...clip };
        if (previousClip) {
            nextClip.x1 =
                previousClip.x1 === undefined
                    ? nextClip.x1
                    : nextClip.x1 === undefined
                        ? previousClip.x1
                        : Math.max(previousClip.x1, nextClip.x1);
            nextClip.x2 =
                previousClip.x2 === undefined
                    ? nextClip.x2
                    : nextClip.x2 === undefined
                        ? previousClip.x2
                        : Math.min(previousClip.x2, nextClip.x2);
            nextClip.y1 =
                previousClip.y1 === undefined
                    ? nextClip.y1
                    : nextClip.y1 === undefined
                        ? previousClip.y1
                        : Math.max(previousClip.y1, nextClip.y1);
            nextClip.y2 =
                previousClip.y2 === undefined
                    ? nextClip.y2
                    : nextClip.y2 === undefined
                        ? previousClip.y2
                        : Math.min(previousClip.y2, nextClip.y2);
        }
        this.clips.push(nextClip);
    }
    unclip() {
        this.clips.pop();
    }
    get() {
        this.clampCursorPosition(this.root);
        this.trimRegionLines(this.root);
        return this.root;
    }
    addRegionTree(region, x, y) {
        const activeRegion = this.getActiveRegion();
        const overflowToBackbuffer = region.isScrollable
            ? region.overflowToBackbuffer
            : (region.overflowToBackbuffer ?? activeRegion.overflowToBackbuffer);
        const regionRef = Object.create(region);
        regionRef.x = region.x + x;
        regionRef.y = region.y + y;
        regionRef.overflowToBackbuffer = overflowToBackbuffer;
        activeRegion.children.push(regionRef);
    }
    trimRegionLines(region) {
        if (region.isTrimmed)
            return;
        const limit = Math.min(region.lines.length, (region.maxWrittenY ?? -1) + 1);
        for (let y = 0; y < limit; y++) {
            const line = region.lines[y];
            const trimmedLength = line.getTrimmedLength();
            if (region.styledOutput[y]?.length !== trimmedLength) {
                region.styledOutput[y] =
                    trimmedLength === line.length ? line : line.slice(0, trimmedLength);
            }
        }
        for (let y = limit; y < region.lines.length; y++) {
            if (region.styledOutput[y]?.length !== 0) {
                region.styledOutput[y] = StyledLine.empty(0);
            }
        }
        for (const child of region.children) {
            this.trimRegionLines(child);
        }
        region.isTrimmed = true;
    }
    clampCursorPosition(region) {
        if (region.cursorPosition) {
            const { row, col } = region.cursorPosition;
            const line = region.lines[row];
            if (line) {
                region.cursorPosition.col = clampCursorColumn(line, col);
            }
        }
        for (const child of region.children) {
            this.clampCursorPosition(child);
        }
    }
    initLines(region, width, height) {
        if (height <= 0)
            return;
        const baseRow = StyledLine.empty(width);
        for (let y = 0; y < height; y++) {
            const row = y === 0 ? baseRow : baseRow.clone();
            region.lines.push(row);
            region.styledOutput.push(row);
        }
    }
    applyWrite(x, y, items, transformers, lineIndex, _preserveBackgroundColor, isSelectable) {
        const region = this.getActiveRegion();
        const { lines } = region;
        const bufferWidth = lines[0]?.length ?? 0;
        let chars = typeof items === 'string' ? toStyledCharacters(items) : items;
        const clip = this.getCurrentClip();
        let fromX;
        let toX;
        if (clip) {
            const regionOffset = this.getRegionAbsoluteOffset();
            const clipResult = this.clipChars(chars, x + regionOffset.x, y + regionOffset.y, clip);
            if (!clipResult) {
                return;
            }
            let absoluteX;
            let absoluteY;
            ({ chars, x: absoluteX, y: absoluteY, fromX, toX } = clipResult);
            x = absoluteX - regionOffset.x;
            y = absoluteY - regionOffset.y;
        }
        const currentLine = lines[y];
        if (!currentLine) {
            return;
        }
        region.maxWrittenY = Math.max(region.maxWrittenY ?? -1, y);
        if (transformers.length > 0) {
            let line = styledLineToString(chars);
            for (const transformer of transformers) {
                line = transformer(line, lineIndex);
            }
            chars = toStyledCharacters(line);
        }
        let offsetX = x;
        let relativeX = 0;
        let spanStartX = -1;
        let spanText = '';
        for (let i = 0; i < chars.length; i++) {
            const val = chars.getValue(i);
            const characterWidth = inkCharacterWidth(val);
            if (toX !== undefined && relativeX >= toX) {
                break;
            }
            if (fromX === undefined || relativeX >= fromX) {
                if (offsetX >= bufferWidth) {
                    break;
                }
                currentLine.setChar(offsetX, val, chars.getFormatFlags(i), chars.getFgColor(i), chars.getBgColor(i), chars.getLink(i));
                if (isSelectable) {
                    if (spanStartX === -1)
                        spanStartX = offsetX;
                    spanText += val;
                }
                if (characterWidth > 1) {
                    this.clearRange(currentLine, { start: offsetX + 1, end: offsetX + characterWidth }, '', bufferWidth, chars.getBgColor(i));
                }
                offsetX += characterWidth;
            }
            else if (characterWidth > 1 &&
                fromX !== undefined &&
                relativeX < fromX &&
                relativeX + characterWidth > fromX) {
                const clearLength = relativeX + characterWidth - fromX;
                this.clearRange(currentLine, { start: offsetX, end: offsetX + clearLength }, ' ', bufferWidth);
                offsetX += clearLength;
            }
            relativeX += characterWidth;
        }
        if (this.trackSelection && isSelectable && spanStartX !== -1) {
            region.selectableSpans.push({
                y,
                startX: spanStartX,
                endX: offsetX,
                text: spanText,
            });
        }
        if (toX !== undefined) {
            const absoluteToX = x - (fromX ?? 0) + toX;
            this.clearRange(currentLine, { start: offsetX, end: absoluteToX }, ' ', bufferWidth);
        }
    }
    clearRange(currentLine, range, value, maxWidth, bgColor) {
        for (let offset = range.start; offset < range.end; offset++) {
            if (offset >= 0 && offset < maxWidth) {
                currentLine.setChar(offset, value, 0, undefined, bgColor);
            }
        }
    }
    clipChars(chars, x, y, clip) {
        const { x1, x2 } = clip;
        const clipHorizontally = typeof x1 === 'number' && typeof x2 === 'number';
        const width = styledCharsWidth(chars);
        const effectiveY1 = this.getActiveRegion().overflowToBackbuffer
            ? -Infinity
            : (clip.y1 ?? -Infinity);
        if (!isRectIntersectingClip({ x1: x, y1: y, x2: x + width, y2: y + 1 }, { ...clip, y1: effectiveY1 })) {
            return undefined;
        }
        let fromX;
        let toX;
        if (clipHorizontally) {
            fromX = x < clip.x1 ? clip.x1 - x : 0;
            const width = styledCharsWidth(chars);
            toX = x + width > clip.x2 ? clip.x2 - x : width;
            if (x < clip.x1) {
                x = clip.x1;
            }
        }
        return { chars, x, y, fromX, toX };
    }
}
/**
 * Flattens a hierarchy of nested regions into a single 2D array of styled characters
 * that represents the final visual output to be written to the terminal.
 * This effectively renders the nested region tree (much like compositing layers
 * in a web browser) into a single screen buffer.
 */
export function flattenRegion(root, options) {
    const { width, height } = root;
    const lines = [];
    for (let i = 0; i < height; i++) {
        lines.push(StyledLine.empty(width));
    }
    composeRegion(root, lines, {
        clip: { x: 0, y: 0, w: width, h: height },
    }, options);
    return lines;
}
/**
 * Recursively traverses a Region and its children, drawing its content
 * into the given `targetLines` buffer while applying coordinate offsets
 * and clipping boundaries. Handles scroll offsets, scrollbars, and floating
 * elements like sticky headers.
 */
function composeRegion(region, targetLines, { clip, offsetX = 0, offsetY = 0, }, options) {
    const { x, y, width, height, lines, children, stickyHeaders, scrollTop: regionScrollTop, scrollLeft: regionScrollLeft, cursorPosition: regionCursorPosition, } = region;
    const absX = x + offsetX;
    const absY = y + offsetY;
    const myClip = intersectRect(clip, { x: absX, y: absY, w: width, h: height });
    if (!myClip) {
        return;
    }
    const scrollTop = regionScrollTop ?? 0;
    const scrollLeft = regionScrollLeft ?? 0;
    if (regionCursorPosition && options?.context) {
        const cursorX = absX + regionCursorPosition.col - scrollLeft;
        const cursorY = absY + regionCursorPosition.row - scrollTop;
        if (cursorX >= myClip.x &&
            cursorX <= myClip.x + myClip.w &&
            cursorY >= myClip.y &&
            cursorY <= myClip.y + myClip.h) {
            options.context.cursorPosition = { row: cursorY, col: cursorX };
        }
    }
    const { x: myClipX, y: myClipY, w: myClipW, h: myClipH } = myClip;
    for (let sy = myClipY; sy < myClipY + myClipH; sy++) {
        const row = targetLines[sy];
        if (!row) {
            continue;
        }
        const localY = sy - absY + scrollTop;
        const sourceLine = lines[localY];
        if (!sourceLine) {
            continue;
        }
        for (let sx = myClipX; sx < myClipX + myClipW; sx++) {
            const localX = sx - absX + scrollLeft;
            if (localX < sourceLine.length) {
                row.setChar(sx, sourceLine.getValue(localX), sourceLine.getFormatFlags(localX), sourceLine.getFgColor(localX), sourceLine.getBgColor(localX), sourceLine.getLink(localX));
            }
        }
    }
    for (const child of children) {
        composeRegion(child, targetLines, {
            clip: myClip,
            offsetX: absX - scrollLeft,
            offsetY: absY - scrollTop,
        }, options);
    }
    if (!options?.skipStickyHeaders) {
        for (const header of stickyHeaders) {
            const headerY = header.y + absY;
            const headerH = header.styledOutput.length;
            for (let i = 0; i < headerH; i++) {
                const sy = headerY + i;
                if (sy < myClipY || sy >= myClipY + myClipH) {
                    continue;
                }
                const row = targetLines[sy];
                if (!row) {
                    continue;
                }
                const line = header.styledOutput[i];
                if (!line) {
                    continue;
                }
                const headerX = header.x + absX;
                const headerW = line.length;
                const hx1 = Math.max(headerX, myClipX);
                const hx2 = Math.min(headerX + headerW, myClipX + myClipW);
                for (let sx = hx1; sx < hx2; sx++) {
                    const cx = sx - headerX;
                    if (cx < line.length) {
                        row.setChar(sx, line.getValue(cx), line.getFormatFlags(cx), line.getFgColor(cx), line.getBgColor(cx), line.getLink(cx));
                    }
                }
            }
        }
    }
    if (!options?.skipScrollbars &&
        region.isScrollable &&
        (region.scrollbarVisible ?? true)) {
        const scrollHeight = region.scrollHeight ?? 0;
        const scrollWidth = region.scrollWidth ?? 0;
        const isVerticalScrollbarVisible = (region.isVerticallyScrollable ?? false) && scrollHeight > region.height;
        const isHorizontalScrollbarVisible = (region.isHorizontallyScrollable ?? false) && scrollWidth > region.width;
        if (isVerticalScrollbarVisible) {
            const verticalLayout = calculateScrollbarLayout({
                x: absX,
                y: absY,
                width: region.width,
                height: region.height,
                marginRight: region.marginRight ?? 0,
                marginBottom: region.marginBottom ?? 0,
                clientDimension: region.height,
                scrollDimension: scrollHeight,
                scrollPosition: scrollTop,
                hasOppositeScrollbar: false,
                axis: 'vertical',
            });
            if (verticalLayout) {
                renderScrollbar({
                    layout: verticalLayout,
                    clip: myClip,
                    axis: 'vertical',
                    color: region.scrollbarThumbColor,
                    setChar(x, y, value, formatFlags, fgColor, bgColor, link) {
                        if (y >= 0 &&
                            y < targetLines.length &&
                            x >= 0 &&
                            x < (targetLines[0]?.length ?? 0)) {
                            targetLines[y].setChar(x, value, formatFlags, fgColor, bgColor, link);
                        }
                    },
                });
            }
        }
        if (isHorizontalScrollbarVisible) {
            const horizontalLayout = calculateScrollbarLayout({
                x: absX,
                y: absY,
                width: region.width,
                height: region.height,
                marginRight: region.marginRight ?? 0,
                marginBottom: region.marginBottom ?? 0,
                clientDimension: region.width,
                scrollDimension: scrollWidth,
                scrollPosition: scrollLeft,
                hasOppositeScrollbar: isVerticalScrollbarVisible,
                axis: 'horizontal',
            });
            if (horizontalLayout) {
                renderScrollbar({
                    layout: horizontalLayout,
                    clip: myClip,
                    axis: 'horizontal',
                    color: region.scrollbarThumbColor,
                    setChar(x, y, value, formatFlags, fgColor, bgColor, link) {
                        if (y >= 0 &&
                            y < targetLines.length &&
                            x >= 0 &&
                            x < (targetLines[0]?.length ?? 0)) {
                            targetLines[y].setChar(x, value, formatFlags, fgColor, bgColor, link);
                        }
                    },
                });
            }
        }
    }
}
export const extractSelectableText = (spans) => {
    if (spans.length === 0) {
        return '';
    }
    const sortedSpans = [...spans].sort((a, b) => a.y === b.y ? a.startX - b.startX : a.y - b.y);
    let selectableText = '';
    let currentY = sortedSpans[0]?.y ?? 0;
    let currentX = sortedSpans[0]?.startX ?? 0;
    for (const span of sortedSpans) {
        if (span.y > currentY) {
            selectableText += '\n'.repeat(span.y - currentY);
            currentX = 0;
            currentY = span.y;
        }
        if (span.startX > currentX) {
            selectableText += ' '.repeat(span.startX - currentX);
        }
        selectableText += span.text;
        currentX = span.endX;
    }
    return selectableText;
};
//# sourceMappingURL=output.js.map