export class ScreenRecorder {
    columns;
    rows;
    lines = [[]];
    cursorRow = 0;
    cursorCol = 0;
    constructor({ columns, rows }) {
        this.columns = columns;
        this.rows = rows;
    }
    write(data) {
        let index = 0;
        while (index < data.length) {
            const char = data[index];
            if (char === "\u001B") {
                const parsed = this.parseEscape(data, index);
                if (parsed === null) {
                    index += 1;
                    continue;
                }
                this.applyCsi(parsed.command);
                index = parsed.nextIndex;
                continue;
            }
            if (char === "\r") {
                this.cursorCol = 0;
                index += 1;
                continue;
            }
            if (char === "\n") {
                this.newLine();
                index += 1;
                continue;
            }
            this.putChar(char ?? " ");
            index += 1;
        }
    }
    /** The bottom `rows` lines of the buffer, trailing-trimmed. */
    viewportLines() {
        const rendered = this.lines.map((line) => line.join("").trimEnd());
        const start = Math.max(0, rendered.length - this.rows);
        return rendered.slice(start);
    }
    /** Whole buffer joined as text (trailing-trimmed per line). */
    fullText() {
        return this.lines.map((line) => line.join("").trimEnd()).join("\n");
    }
    /** 0-based row of the first viewport line containing `needle`, or -1. */
    footerRow(needle) {
        return this.viewportLines().findIndex((line) => line.includes(needle));
    }
    parseEscape(data, start) {
        if (data[start + 1] !== "[")
            return null;
        let index = start + 2;
        let params = "";
        while (index < data.length) {
            const char = data[index];
            if (char && /[A-Za-z~]/u.test(char)) {
                return { command: { params, final: char }, nextIndex: index + 1 };
            }
            params += char;
            index += 1;
        }
        return null;
    }
    applyCsi({ params, final }) {
        if (params.startsWith("?") || params.startsWith(">"))
            return;
        if (final === "m")
            return;
        const values = params
            .split(";")
            .filter((part) => part.length > 0)
            .map((part) => Number(part));
        const first = Number.isFinite(values[0]) ? values[0] : 0;
        if (final === "A") {
            this.cursorRow = Math.max(0, this.cursorRow - (first || 1));
            return;
        }
        if (final === "B") {
            this.cursorRow += first || 1;
            this.ensureLine(this.cursorRow);
            return;
        }
        if (final === "C") {
            this.cursorCol = Math.min(this.columns - 1, this.cursorCol + (first || 1));
            return;
        }
        if (final === "D") {
            this.cursorCol = Math.max(0, this.cursorCol - (first || 1));
            return;
        }
        if (final === "E") {
            this.cursorRow += first || 1;
            this.cursorCol = 0;
            this.ensureLine(this.cursorRow);
            return;
        }
        if (final === "F") {
            this.cursorRow = Math.max(0, this.cursorRow - (first || 1));
            this.cursorCol = 0;
            return;
        }
        if (final === "G") {
            this.cursorCol = Math.max(0, Math.min(this.columns - 1, (first || 1) - 1));
            return;
        }
        if (final === "H" || final === "f") {
            const row = Number.isFinite(values[0]) && values[0] > 0 ? values[0] - 1 : 0;
            const col = Number.isFinite(values[1]) && values[1] > 0 ? values[1] - 1 : 0;
            // Cursor addressing is VIEWPORT-relative: row 1 is the top of the
            // visible screen, not the top of the cumulative buffer (scrollback).
            const viewportStart = Math.max(0, this.lines.length - this.rows);
            this.cursorRow = viewportStart + row;
            this.cursorCol = Math.max(0, Math.min(this.columns - 1, col));
            this.ensureLine(this.cursorRow);
            return;
        }
        if (final === "J") {
            if (first === 2 || first === 3) {
                this.lines = [[]];
                this.cursorRow = 0;
                this.cursorCol = 0;
            }
            else if (first === 0) {
                // ED 0 — erase from cursor to end of screen, in place (no scrollback
                // push): clear the rest of the current line and drop lines below.
                this.ensureLine(this.cursorRow);
                this.lines[this.cursorRow] = this.lines[this.cursorRow].slice(0, this.cursorCol);
                this.lines.length = this.cursorRow + 1;
            }
            return;
        }
        if (final === "K") {
            this.ensureLine(this.cursorRow);
            if (first === 2) {
                this.lines[this.cursorRow] = [];
                this.cursorCol = 0;
            }
            else {
                this.lines[this.cursorRow] = this.lines[this.cursorRow].slice(0, this.cursorCol);
            }
        }
    }
    putChar(char) {
        if (this.cursorCol >= this.columns)
            this.newLine();
        this.ensureLine(this.cursorRow);
        const line = this.lines[this.cursorRow];
        while (line.length < this.cursorCol)
            line.push(" ");
        line[this.cursorCol] = char;
        this.cursorCol += 1;
    }
    newLine() {
        this.cursorRow += 1;
        this.cursorCol = 0;
        this.ensureLine(this.cursorRow);
    }
    ensureLine(row) {
        while (this.lines.length <= row)
            this.lines.push([]);
    }
}
/**
 * A stdout-shaped object that replays every write into a {@link ScreenRecorder}.
 * Mirrors the minimal surface Ink touches on a TTY stream.
 */
export function makeRecordingStdout(recorder) {
    return {
        columns: recorder.columns,
        rows: recorder.rows,
        isTTY: true,
        writable: true,
        writableEnded: false,
        destroyed: false,
        writableLength: 0,
        write(chunk, callback) {
            recorder.write(chunk);
            callback?.(null);
            return true;
        },
        on() { },
        off() { },
    };
}
//# sourceMappingURL=screen-recorder.js.map