import { styledCharsToString } from '@alcalzone/ansi-tokenize';
import { toStyledCharacters, inkCharacterWidth, styledCharsWidth, splitStyledCharsByNewline, getPositionAtOffset, } from './measure-text.js';
export default class Output {
    width;
    height;
    operations = [];
    cursorFocusInfo = undefined;
    clips = [];
    constructor(options) {
        const { width, height } = options;
        this.width = width;
        this.height = height;
    }
    getCurrentClip() {
        return this.clips.at(-1);
    }
    write(x, y, items, options) {
        const { transformers, lineIndex, preserveBackgroundColor, isTerminalCursorFocused, terminalCursorPosition, } = options;
        // Track cursor target position for terminal cursor synchronization
        if (isTerminalCursorFocused) {
            const styledChars = typeof items === 'string' ? toStyledCharacters(items) : items;
            this.cursorFocusInfo = {
                x,
                y,
                styledChars,
                terminalCursorPosition,
            };
        }
        if (items.length === 0) {
            return;
        }
        this.operations.push({
            type: 'write',
            x,
            y,
            items,
            transformers,
            lineIndex,
            preserveBackgroundColor,
        });
    }
    clip(clip) {
        this.operations.push({
            type: '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.operations.push({
            type: 'unclip',
        });
        this.clips.pop();
    }
    get() {
        // Initialize output array with a specific set of rows, so that margin/padding at the bottom is preserved
        const output = [];
        for (let y = 0; y < this.height; y++) {
            const row = [];
            for (let x = 0; x < this.width; x++) {
                row.push({
                    type: 'char',
                    value: ' ',
                    fullWidth: false,
                    styles: [],
                });
            }
            output.push(row);
        }
        const clips = [];
        for (const operation of this.operations) {
            if (operation.type === 'clip') {
                const previousClip = clips.at(-1);
                const nextClip = { ...operation.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);
                }
                clips.push(nextClip);
                continue;
            }
            if (operation.type === 'unclip') {
                clips.pop();
                continue;
            }
            if (operation.type === 'write') {
                this.applyWriteOperation(output, clips, operation);
            }
        }
        // Calculate cursor position from cursor target (if exists)
        let cursorPosition;
        if (this.cursorFocusInfo) {
            const { x, y, styledChars, terminalCursorPosition: charIndex, } = this.cursorFocusInfo;
            if (charIndex === undefined) {
                // Use text end (backward compatible)
                const lines = splitStyledCharsByNewline(styledChars);
                const lastLineIndex = lines.length - 1;
                const lastLine = lines[lastLineIndex] ?? [];
                cursorPosition = {
                    row: y + lastLineIndex,
                    col: lastLineIndex === 0
                        ? x + styledCharsWidth(lastLine)
                        : styledCharsWidth(lastLine),
                };
            }
            else {
                // Use character index to calculate cursor position using StyledChar[]
                const { row, col } = getPositionAtOffset(styledChars, charIndex);
                cursorPosition = {
                    row: y + row,
                    col: x + col,
                };
            }
        }
        const generatedOutput = output
            .map(line => {
            // See https://github.com/vadimdemedes/ink/pull/564#issuecomment-1637022742
            const lineWithoutEmptyItems = line.filter(item => item !== undefined);
            return styledCharsToString(lineWithoutEmptyItems).trimEnd();
        })
            .join('\n');
        // Adjust cursor position based on actual output (after trimEnd)
        if (cursorPosition) {
            const lines = generatedOutput.split('\n');
            const cursorLine = lines[cursorPosition.row];
            if (cursorLine !== undefined) {
                const actualLineWidth = styledCharsWidth(toStyledCharacters(cursorLine));
                // Cursor should not go beyond the actual trimmed line width
                cursorPosition.col = Math.min(cursorPosition.col, actualLineWidth);
            }
        }
        return {
            output: generatedOutput,
            height: output.length,
            styledOutput: output,
            cursorPosition,
        };
    }
    clearRange(currentLine, range, styles, value = ' ') {
        for (let offset = range.start; offset < range.end; offset++) {
            if (offset >= 0 && offset < this.width) {
                currentLine[offset] = {
                    type: 'char',
                    value,
                    fullWidth: false,
                    styles,
                };
            }
        }
    }
    applyWriteOperation(output, clips, operation) {
        const { transformers, lineIndex = 0 } = operation;
        let { x, y, items } = operation;
        let chars = typeof items === 'string' ? toStyledCharacters(items) : items;
        const clip = clips.at(-1);
        let fromX;
        let toX;
        if (clip) {
            const clipResult = this.clipChars(chars, x, y, clip);
            if (!clipResult) {
                return;
            }
            chars = clipResult.chars;
            x = clipResult.x;
            y = clipResult.y;
            fromX = clipResult.fromX;
            toX = clipResult.toX;
        }
        const currentLine = output[y];
        // Line can be missing if `text` is taller than height of pre-initialized `this.output`
        if (!currentLine) {
            return;
        }
        if (transformers.length > 0) {
            let line = styledCharsToString(chars);
            for (const transformer of transformers) {
                line = transformer(line, lineIndex);
            }
            chars = toStyledCharacters(line);
        }
        let offsetX = x;
        let relativeX = 0;
        for (const character of chars) {
            const characterWidth = inkCharacterWidth(character.value);
            if (toX !== undefined && relativeX >= toX) {
                break;
            }
            if (fromX === undefined || relativeX >= fromX) {
                if (offsetX >= this.width) {
                    break;
                }
                currentLine[offsetX] = character;
                if (characterWidth > 1) {
                    this.clearRange(currentLine, { start: offsetX + 1, end: offsetX + characterWidth }, character.styles, '');
                }
                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 }, character.styles, ' ');
                offsetX += clearLength;
            }
            relativeX += characterWidth;
        }
        if (toX !== undefined) {
            const absoluteToX = x - (fromX ?? 0) + toX;
            this.clearRange(currentLine, { start: offsetX, end: absoluteToX }, [], ' ');
        }
    }
    clipChars(chars, x, y, clip) {
        const { x1, x2, y1, y2 } = clip;
        const clipHorizontally = typeof x1 === 'number' && typeof x2 === 'number';
        const clipVertically = typeof y1 === 'number' && typeof y2 === 'number';
        if (clipHorizontally) {
            const width = styledCharsWidth(chars);
            if (x + width < clip.x1 || x > clip.x2) {
                return undefined;
            }
        }
        if (clipVertically && (y < clip.y1 || y >= clip.y2)) {
            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 };
    }
}
//# sourceMappingURL=output.js.map