/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import process from 'node:process';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { fork } from 'node:child_process';
import { StyledLine } from './styled-line.js';
import { Serializer } from './serialization.js';
import { TerminalBufferWorker } from './worker/render-worker.js';
import { linesEqual } from './worker/terminal-writer.js';
import { flattenRegion, regionLayoutProperties, copyRegionProperty, treesEqual, } from './output.js';
const debugEdits = false;
export default class TerminalBuffer {
    get lines() {
        if (this._cachedLines) {
            return this._cachedLines;
        }
        if (this.lastRootRegion) {
            this._cachedLines = flattenRegion(this.lastRootRegion, {
                skipScrollbars: true,
                skipStickyHeaders: true,
            });
            return this._cachedLines;
        }
        return [];
    }
    _cachedLines;
    lastRootRegion;
    serializer = new Serializer();
    worker;
    workerInstance;
    resizeListener;
    stdout;
    // Track previous state of all regions by ID
    lastRegions = new Map();
    lastCursorPosition;
    lastTree;
    lastOptions;
    optionsChanged = false;
    columns;
    rows;
    constructor(columns, rows, options) {
        this.lastOptions = {
            isAlternateBufferEnabled: options?.isAlternateBufferEnabled,
            stickyHeadersInBackbuffer: options?.stickyHeadersInBackbuffer,
            animatedScroll: options?.animatedScroll,
            animationInterval: options?.animationInterval,
            backbufferUpdateDelay: options?.backbufferUpdateDelay,
            maxScrollbackLength: options?.maxScrollbackLength,
            forceScrollToBottomOnBackbufferRefresh: options?.forceScrollToBottomOnBackbufferRefresh,
        };
        this.columns = columns;
        this.rows = rows;
        this.stdout = options?.stdout ?? process.stdout;
        const createWorkerInstance = () => {
            const instance = new TerminalBufferWorker(columns, rows, {
                debugRainbowEnabled: options?.debugRainbowEnabled,
                stdout: this.stdout,
                isAlternateBufferEnabled: options?.isAlternateBufferEnabled,
                stickyHeadersInBackbuffer: options?.stickyHeadersInBackbuffer,
                animatedScroll: options?.animatedScroll,
                animationInterval: options?.animationInterval,
                backbufferUpdateDelay: options?.backbufferUpdateDelay,
                maxScrollbackLength: options?.maxScrollbackLength,
                forceScrollToBottomOnBackbufferRefresh: options?.forceScrollToBottomOnBackbufferRefresh,
            });
            void instance.render();
            return instance;
        };
        let renderInProcess = options?.renderInProcess ?? false;
        let workerUrl;
        if (!renderInProcess) {
            // eslint-disable-next-line unicorn/relative-url-style
            workerUrl = new URL('./worker/worker-entry.js', import.meta.url);
            let workerPath = workerUrl.protocol === 'file:' ? fileURLToPath(workerUrl) : null;
            // Fallback for ts-node testing environments
            if (workerPath &&
                !fs.existsSync(workerPath) &&
                workerPath.endsWith('.js')) {
                const tsPath = workerPath.replace(/\.js$/, '.ts');
                if (fs.existsSync(tsPath)) {
                    workerPath = tsPath;
                    // eslint-disable-next-line unicorn/relative-url-style
                    workerUrl = new URL('./worker/worker-entry.ts', import.meta.url);
                }
            }
            if (workerPath && !fs.existsSync(workerPath)) {
                console.warn(`Unable to launch render process at ${workerPath}`);
                // Fallback to in-process rendering if the worker file was not bundled.
                renderInProcess = true;
            }
        }
        if (renderInProcess) {
            this.workerInstance = createWorkerInstance();
        }
        else {
            this.worker = fork(workerUrl, {
                env: {
                    ...process.env,
                    INK_WORKER: 'true',
                },
            });
            this.worker.on('error', error => {
                console.error('Render worker error:', error);
            });
            this.sendToWorker({
                type: 'init',
                columns,
                rows,
                debugRainbowEnabled: options?.debugRainbowEnabled,
                isAlternateBufferEnabled: options?.isAlternateBufferEnabled,
                stickyHeadersInBackbuffer: options?.stickyHeadersInBackbuffer,
                animatedScroll: options?.animatedScroll,
                animationInterval: options?.animationInterval,
                backbufferUpdateDelay: options?.backbufferUpdateDelay,
                maxScrollbackLength: options?.maxScrollbackLength,
                forceScrollToBottomOnBackbufferRefresh: options?.forceScrollToBottomOnBackbufferRefresh,
                cacheToStyledCharacters: options?.cacheToStyledCharacters,
            }, 'Failed to send init message to worker:');
        }
        this.resizeListener = () => {
            if (this.stdout.columns && this.stdout.rows) {
                this.resize(this.stdout.columns, this.stdout.rows);
            }
        };
        this.stdout.on('resize', this.resizeListener);
    }
    resize(columns, rows) {
        if (this.columns === columns && this.rows === rows) {
            return;
        }
        this.columns = columns;
        this.rows = rows;
        if (this.workerInstance) {
            this.workerInstance.resize(columns, rows);
        }
        else {
            this.sendToWorker({
                type: 'resize',
                columns,
                rows,
            }, 'Failed to send resize message to worker:');
        }
    }
    updateOptions(options) {
        const keys = [
            'isAlternateBufferEnabled',
            'stickyHeadersInBackbuffer',
            'animatedScroll',
            'animationInterval',
            'backbufferUpdateDelay',
            'maxScrollbackLength',
            'forceScrollToBottomOnBackbufferRefresh',
        ];
        for (const key of keys) {
            if (options[key] !== this.lastOptions?.[key]) {
                this.optionsChanged = true;
                break;
            }
        }
        this.lastOptions = { ...options };
        if (this.workerInstance) {
            this.workerInstance.updateOptions(options);
        }
        else {
            this.sendToWorker({
                type: 'updateOptions',
                options,
            }, 'Failed to send updateOptions message to worker:');
        }
    }
    startRecording(filename) {
        if (this.workerInstance) {
            this.workerInstance.startRecording(filename);
        }
        else {
            this.sendToWorker({
                type: 'startRecording',
                filename,
            }, 'Failed to send startRecording message to worker:');
        }
    }
    stopRecording() {
        if (this.workerInstance) {
            this.workerInstance.stopRecording();
        }
        else {
            this.sendToWorker({
                type: 'stopRecording',
            }, 'Failed to send stopRecording message to worker:');
        }
    }
    dumpCurrentFrame(filename) {
        if (this.workerInstance) {
            this.workerInstance.dumpCurrentFrame(filename);
        }
        else {
            this.sendToWorker({
                type: 'dumpCurrentFrame',
                filename,
            }, 'Failed to send dumpCurrentFrame message to worker:');
        }
    }
    update(_start, _end, root, cursorPosition) {
        this.lastRootRegion = root;
        this._cachedLines = undefined;
        const currentRegionsMap = new Map();
        const nodeIdToElement = new Map();
        const updates = [];
        // Traverse tree to collect all current regions and build structure
        const buildTree = (r) => {
            currentRegionsMap.set(r.id, r);
            // Populate nodeIdToElement map
            if (r.nodeId !== undefined && r.node !== undefined) {
                nodeIdToElement.set(r.nodeId, r.node);
            }
            // Diff this region
            const update = this.diffRegion(r, nodeIdToElement);
            if (update) {
                updates.push(update);
            }
            return {
                id: r.id,
                children: r.children.map(child => buildTree(child)),
            };
        };
        const tree = buildTree(root);
        const treeChanged = !this.lastTree || !treesEqual(this.lastTree, tree);
        this.lastTree = tree;
        // Update local state to current frame
        this.lastRegions = currentRegionsMap;
        const cursorChanged = cursorPosition !== this.lastCursorPosition &&
            (!cursorPosition ||
                !this.lastCursorPosition ||
                cursorPosition.row !== this.lastCursorPosition.row ||
                cursorPosition.col !== this.lastCursorPosition.col);
        this.lastCursorPosition = cursorPosition;
        if (updates.length > 0 ||
            cursorChanged ||
            this.optionsChanged ||
            treeChanged) {
            this.optionsChanged = false;
            this.sendEdits(tree, updates, cursorPosition);
            return true;
        }
        return false;
    }
    async render() {
        if (this.workerInstance) {
            await this.workerInstance.render();
        }
        else if (this.worker?.connected) {
            try {
                this.worker.send({
                    type: 'render',
                });
            }
            catch (error) {
                console.error('Failed to send render message to worker:', error);
            }
        }
    }
    async fullRender() {
        if (this.workerInstance) {
            await this.workerInstance.fullRender();
        }
        else if (this.worker?.connected) {
            try {
                this.worker.send({
                    type: 'fullRender',
                });
            }
            catch (error) {
                console.error('Failed to send fullRender message to worker:', error);
            }
        }
    }
    done() {
        if (this.workerInstance) {
            this.workerInstance.done();
        }
        else if (this.worker?.connected) {
            try {
                this.worker.send({
                    type: 'done',
                });
            }
            catch {
                // Silently fail on exit errors as the worker might already be gone
            }
        }
    }
    async getLinesUpdated() {
        if (this.workerInstance) {
            return this.workerInstance.getLinesUpdated();
        }
        if (!this.worker?.connected) {
            return 0;
        }
        return new Promise(resolve => {
            const handler = (message) => {
                if (message.type === 'linesUpdated') {
                    this.worker?.off('message', handler);
                    resolve(message.count);
                }
            };
            this.worker?.on('message', handler);
            try {
                this.worker?.send({ type: 'getLinesUpdated' });
            }
            catch (error) {
                this.worker?.off('message', handler);
                console.error('Failed to send getLinesUpdated message to worker:', error);
                resolve(0);
            }
        });
    }
    resetLinesUpdated() {
        if (this.workerInstance) {
            this.workerInstance.resetLinesUpdated();
        }
        else if (this.worker?.connected) {
            try {
                this.worker.send({ type: 'resetLinesUpdated' });
            }
            catch (error) {
                console.error('Failed to send resetLinesUpdated message to worker:', error);
            }
        }
    }
    destroy() {
        if (this.worker) {
            this.worker.kill();
        }
        if (this.resizeListener) {
            this.stdout.off('resize', this.resizeListener);
        }
    }
    sendToWorker(message, errorMessage) {
        if (this.worker?.connected) {
            try {
                // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
                this.worker.send(message);
            }
            catch (error) {
                console.error(errorMessage, error);
            }
        }
    }
    diffRegion(current, nodeIdToElement) {
        const last = this.lastRegions.get(current.id);
        const update = { id: current.id };
        let hasChanges = false;
        if (!last) {
            // New region, send everything
            hasChanges = true;
            for (const key of regionLayoutProperties) {
                copyRegionProperty(update, current, key);
            }
            update.stickyHeaders = current.stickyHeaders.map(h => ({
                ...h,
                node: undefined,
                lines: this.serializer.serialize(h.lines),
                stuckLines: h.stuckLines
                    ? this.serializer.serialize(h.stuckLines)
                    : undefined,
                styledOutput: this.serializer.serialize(h.styledOutput),
            }));
            // Send all lines
            const serialized = this.serializer.serialize(current.lines);
            update.lines = {
                updates: [
                    {
                        start: 0,
                        end: current.lines.length,
                        data: serialized,
                    },
                ],
                totalLength: current.lines.length,
            };
            return update;
        }
        // Check properties
        for (const key of regionLayoutProperties) {
            if (current[key] !== last[key]) {
                copyRegionProperty(update, current, key);
                hasChanges = true;
            }
        }
        // Deep compare sticky headers? For now assuming reference change or simple length change is enough,
        // or we can rely on the fact they are rebuilt every frame.
        // Let's just resend if length differs or assume they might change.
        // To be safe and simple: always send sticky headers if they exist or existed.
        if (current.stickyHeaders !== last.stickyHeaders ||
            current.stickyHeaders.length > 0 ||
            last.stickyHeaders.length > 0) {
            update.stickyHeaders = current.stickyHeaders.map(h => ({
                ...h,
                node: undefined,
                lines: this.serializer.serialize(h.lines),
                stuckLines: h.stuckLines
                    ? this.serializer.serialize(h.stuckLines)
                    : undefined,
                styledOutput: this.serializer.serialize(h.styledOutput),
            }));
            hasChanges = true;
        }
        // Diff lines
        const lineUpdates = this.diffLines(last.lines, current.lines);
        if (lineUpdates.length > 0 || last.lines.length !== current.lines.length) {
            hasChanges = true;
            update.lines = {
                updates: lineUpdates,
                totalLength: current.lines.length,
            };
            if (current.stableScrollback && current.nodeId !== undefined) {
                const element = nodeIdToElement.get(current.nodeId);
                if (element) {
                    const scrollTop = current.scrollTop ?? 0;
                    for (const chunk of lineUpdates) {
                        if (chunk.start < scrollTop) {
                            element.internalIsScrollbackDirty = true;
                            break;
                        }
                    }
                    // Also check if lines were removed from the end of the content but still within the scrollback
                    if (!element.internalIsScrollbackDirty &&
                        current.lines.length < last.lines.length &&
                        current.lines.length < scrollTop) {
                        element.internalIsScrollbackDirty = true;
                    }
                }
            }
        }
        return hasChanges ? update : undefined;
    }
    diffLines(oldLines, newLines) {
        if (oldLines === newLines) {
            return [];
        }
        const updates = [];
        const limit = Math.max(oldLines.length, newLines.length);
        let chunkStart = -1;
        let chunkLines = [];
        let chunkSource = [];
        for (let i = 0; i < limit; i++) {
            const newLine = newLines[i];
            const oldLine = oldLines[i];
            const areEqual = linesEqual(oldLine, newLine);
            if (areEqual) {
                if (chunkStart !== -1) {
                    updates.push({
                        start: chunkStart,
                        end: chunkStart + chunkLines.length,
                        data: this.serializer.serialize(chunkLines),
                        source: debugEdits
                            ? this.serializer.serialize(chunkSource)
                            : undefined,
                    });
                    chunkStart = -1;
                    chunkLines = [];
                    chunkSource = [];
                }
            }
            else {
                if (chunkStart === -1) {
                    chunkStart = i;
                }
                chunkLines.push(newLine ?? new StyledLine());
                if (debugEdits) {
                    chunkSource.push(oldLine ?? new StyledLine());
                }
            }
        }
        if (chunkStart !== -1) {
            updates.push({
                start: chunkStart,
                end: chunkStart + chunkLines.length,
                data: this.serializer.serialize(chunkLines),
                source: debugEdits ? this.serializer.serialize(chunkSource) : undefined,
            });
        }
        return updates;
    }
    sendEdits(tree, updates, cursorPosition) {
        if (this.workerInstance) {
            this.workerInstance.update(tree, updates, cursorPosition);
        }
        else if (this.worker?.connected) {
            try {
                this.worker.send({
                    type: 'edits',
                    tree,
                    updates,
                    cursorPosition,
                });
            }
            catch (error) {
                console.error('Failed to send edits to worker:', error);
            }
        }
    }
}
//# sourceMappingURL=terminal-buffer.js.map