import ansiEscapes from 'ansi-escapes';
import cliCursor from 'cli-cursor';
import { cursorPositionChanged, buildCursorSuffix, buildCursorOnlySequence, buildReturnToBottomPrefix, hideCursorEscape, } from './cursor-helpers.js';
// Count visible lines in a string, ignoring the trailing empty element
// that `split('\n')` produces when the string ends with '\n'.
const visibleLineCount = (lines, str) => str.endsWith('\n') ? lines.length - 1 : lines.length;
const createStandard = (stream, { showCursor = false } = {}) => {
    let previousLineCount = 0;
    let previousOutput = '';
    let hasHiddenCursor = false;
    let cursorPosition;
    let cursorDirty = false;
    let previousCursorPosition;
    let cursorWasShown = false;
    const getActiveCursor = () => (cursorDirty ? cursorPosition : undefined);
    const hasChanges = (str, activeCursor) => {
        const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
        return str !== previousOutput || cursorChanged;
    };
    const render = (str) => {
        if (!showCursor && !hasHiddenCursor) {
            cliCursor.hide(stream);
            hasHiddenCursor = true;
        }
        // Only use cursor if setCursorPosition was called since last render.
        // This ensures stale positions don't persist after component unmount.
        const activeCursor = getActiveCursor();
        cursorDirty = false;
        const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
        if (!hasChanges(str, activeCursor)) {
            return false;
        }
        const lines = str.split('\n');
        const visibleCount = visibleLineCount(lines, str);
        const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor);
        if (str === previousOutput && cursorChanged) {
            stream.write(buildCursorOnlySequence({
                cursorWasShown,
                previousLineCount,
                previousCursorPosition,
                visibleLineCount: visibleCount,
                cursorPosition: activeCursor,
            }));
        }
        else {
            previousOutput = str;
            const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition);
            stream.write(returnPrefix +
                ansiEscapes.eraseLines(previousLineCount) +
                str +
                cursorSuffix);
            previousLineCount = lines.length;
        }
        previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
        cursorWasShown = activeCursor !== undefined;
        return true;
    };
    render.clear = () => {
        const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition);
        stream.write(prefix + ansiEscapes.eraseLines(previousLineCount));
        previousOutput = '';
        previousLineCount = 0;
        previousCursorPosition = undefined;
        cursorWasShown = false;
    };
    render.done = () => {
        previousOutput = '';
        previousLineCount = 0;
        previousCursorPosition = undefined;
        cursorWasShown = false;
        if (!showCursor) {
            cliCursor.show(stream);
            hasHiddenCursor = false;
        }
    };
    render.reset = () => {
        previousOutput = '';
        previousLineCount = 0;
        previousCursorPosition = undefined;
        cursorWasShown = false;
    };
    render.sync = (str) => {
        const activeCursor = cursorDirty ? cursorPosition : undefined;
        cursorDirty = false;
        const lines = str.split('\n');
        previousOutput = str;
        previousLineCount = lines.length;
        if (!activeCursor && cursorWasShown) {
            stream.write(hideCursorEscape);
        }
        if (activeCursor) {
            stream.write(buildCursorSuffix(visibleLineCount(lines, str), activeCursor));
        }
        previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
        cursorWasShown = activeCursor !== undefined;
    };
    render.setCursorPosition = (position) => {
        cursorPosition = position;
        cursorDirty = true;
    };
    render.isCursorDirty = () => cursorDirty;
    render.willRender = (str) => hasChanges(str, getActiveCursor());
    return render;
};
const createIncremental = (stream, { showCursor = false } = {}) => {
    let previousLines = [];
    let previousOutput = '';
    let hasHiddenCursor = false;
    let cursorPosition;
    let cursorDirty = false;
    let previousCursorPosition;
    let cursorWasShown = false;
    const getActiveCursor = () => (cursorDirty ? cursorPosition : undefined);
    const hasChanges = (str, activeCursor) => {
        const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
        return str !== previousOutput || cursorChanged;
    };
    const render = (str) => {
        if (!showCursor && !hasHiddenCursor) {
            cliCursor.hide(stream);
            hasHiddenCursor = true;
        }
        // Only use cursor if setCursorPosition was called since last render.
        // This ensures stale positions don't persist after component unmount.
        const activeCursor = getActiveCursor();
        cursorDirty = false;
        const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
        if (!hasChanges(str, activeCursor)) {
            return false;
        }
        const nextLines = str.split('\n');
        const visibleCount = visibleLineCount(nextLines, str);
        const previousVisible = visibleLineCount(previousLines, previousOutput);
        if (str === previousOutput && cursorChanged) {
            stream.write(buildCursorOnlySequence({
                cursorWasShown,
                previousLineCount: previousLines.length,
                previousCursorPosition,
                visibleLineCount: visibleCount,
                cursorPosition: activeCursor,
            }));
            previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
            cursorWasShown = activeCursor !== undefined;
            return true;
        }
        const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition);
        if (str === '\n' || previousOutput.length === 0) {
            const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor);
            stream.write(returnPrefix +
                ansiEscapes.eraseLines(previousLines.length) +
                str +
                cursorSuffix);
            cursorWasShown = activeCursor !== undefined;
            previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
            previousOutput = str;
            previousLines = nextLines;
            return true;
        }
        const hasTrailingNewline = str.endsWith('\n');
        // We aggregate all chunks for incremental rendering into a buffer, and then write them to stdout at the end.
        const buffer = [];
        buffer.push(returnPrefix);
        // Clear extra lines if the current content's line count is lower than the previous.
        if (visibleCount < previousVisible) {
            const previousHadTrailingNewline = previousOutput.endsWith('\n');
            const extraSlot = previousHadTrailingNewline ? 1 : 0;
            buffer.push(ansiEscapes.eraseLines(previousVisible - visibleCount + extraSlot), ansiEscapes.cursorUp(visibleCount));
        }
        else {
            buffer.push(ansiEscapes.cursorUp(previousLines.length - 1));
        }
        for (let i = 0; i < visibleCount; i++) {
            const isLastLine = i === visibleCount - 1;
            // We do not write lines if the contents are the same. This prevents flickering during renders.
            if (nextLines[i] === previousLines[i]) {
                // Don't move past the last line when there's no trailing newline,
                // otherwise the cursor overshoots the rendered block.
                if (!isLastLine || hasTrailingNewline) {
                    buffer.push(ansiEscapes.cursorNextLine);
                }
                continue;
            }
            buffer.push(ansiEscapes.cursorTo(0) +
                nextLines[i] +
                ansiEscapes.eraseEndLine +
                // Don't append newline after the last line when the input
                // has no trailing newline (fullscreen mode).
                (isLastLine && !hasTrailingNewline ? '' : '\n'));
        }
        const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor);
        buffer.push(cursorSuffix);
        stream.write(buffer.join(''));
        cursorWasShown = activeCursor !== undefined;
        previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
        previousOutput = str;
        previousLines = nextLines;
        return true;
    };
    render.clear = () => {
        const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition);
        stream.write(prefix + ansiEscapes.eraseLines(previousLines.length));
        previousOutput = '';
        previousLines = [];
        previousCursorPosition = undefined;
        cursorWasShown = false;
    };
    render.done = () => {
        previousOutput = '';
        previousLines = [];
        previousCursorPosition = undefined;
        cursorWasShown = false;
        if (!showCursor) {
            cliCursor.show(stream);
            hasHiddenCursor = false;
        }
    };
    render.reset = () => {
        previousOutput = '';
        previousLines = [];
        previousCursorPosition = undefined;
        cursorWasShown = false;
    };
    render.sync = (str) => {
        const activeCursor = cursorDirty ? cursorPosition : undefined;
        cursorDirty = false;
        const lines = str.split('\n');
        previousOutput = str;
        previousLines = lines;
        if (!activeCursor && cursorWasShown) {
            stream.write(hideCursorEscape);
        }
        if (activeCursor) {
            stream.write(buildCursorSuffix(visibleLineCount(lines, str), activeCursor));
        }
        previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
        cursorWasShown = activeCursor !== undefined;
    };
    render.setCursorPosition = (position) => {
        cursorPosition = position;
        cursorDirty = true;
    };
    render.isCursorDirty = () => cursorDirty;
    render.willRender = (str) => hasChanges(str, getActiveCursor());
    return render;
};
const create = (stream, { showCursor = false, incremental = false } = {}) => {
    if (incremental) {
        return createIncremental(stream, { showCursor });
    }
    return createStandard(stream, { showCursor });
};
const logUpdate = { create };
export default logUpdate;
//# sourceMappingURL=log-update.js.map