import { isKeyRelease } from "@earendil-works/pi-tui";
import { TUI } from "@earendil-works/pi-tui";
import { runCallback } from "../../core/callback-activity.js";
import { theme } from "../interactive/theme/theme.js";
import { INTERACTIVE_ENGINE_MAX_FRAME_BYTES, isJsonValue, parseInteractiveEngineCommand, serializeInteractiveEngineMessage, } from "./protocol.js";
class RemoteTerminal {
    constructor(invalidate, control) {
        this.columns = 80;
        this.rows = 24;
        this.kittyProtocolActive = false;
        this.invalidate = invalidate;
        this.control = control;
    }
    start() { }
    stop() { }
    async drainInput() { }
    write() { this.invalidate(); }
    moveBy() { }
    hideCursor() { }
    showCursor() { }
    clearLine() { }
    clearFromCursor() { }
    clearScreen() { }
    setTitle() { }
    setProgress() { }
    /** Ask the host to toggle host-TTY mouse-scroll reporting for this component. */
    setMouseScrollTracking(enabled) {
        this.control?.({ kind: "mouse-scroll-tracking", enabled });
    }
    /** Ask the host to toggle host-TTY autowrap (DECAWM) for this component. */
    setAutowrap(enabled) {
        this.control?.({ kind: "autowrap", enabled });
    }
}
function serializableOverlayOptions(options) {
    const value = typeof options === "function" ? options() : options;
    if (!value)
        return undefined;
    const { anchor, col, margin, maxHeight, minWidth, offsetX, offsetY, row, width } = value;
    return { anchor, col, margin, maxHeight, minWidth, offsetX, offsetY, row, width };
}
function jsonResult(value) {
    if (value === undefined)
        return undefined;
    const encoded = JSON.stringify(value);
    if (encoded === undefined)
        return undefined;
    const decoded = JSON.parse(encoded);
    if (!isJsonValue(decoded))
        throw new Error("Custom UI result is not JSON-safe");
    return decoded;
}
function boundedLines(lines) {
    let remaining = INTERACTIVE_ENGINE_MAX_FRAME_BYTES - 512;
    const bounded = [];
    for (const line of lines) {
        if (remaining <= 0)
            break;
        const bytes = Buffer.from(line, "utf8");
        const next = bytes.length <= remaining ? line : bytes.subarray(0, remaining).toString("utf8");
        bounded.push(next);
        remaining -= Buffer.byteLength(next, "utf8");
    }
    return bounded;
}
export class EngineCustomUiService {
    setWidget(key, factory, placement) {
        const previous = this.widgetIds.get(key);
        if (previous)
            this.disposeComponent(previous, false);
        if (!factory)
            return;
        const componentId = `remote_widget_${++this.nextId}`;
        this.widgetIds.set(key, componentId);
        const terminal = new RemoteTerminal(() => this.send({ type: "engine_custom_invalidate", componentId }));
        const tui = new TUI(terminal);
        void runCallback({ kind: "renderer", name: `widget:${key}` }, () => factory(tui, theme)).then((component) => {
            if (this.widgetIds.get(key) !== componentId) {
                component.dispose?.();
                return;
            }
            tui.addChild(component);
            this.active.set(componentId, {
                component,
                resolve: () => { },
                overlay: false,
                terminal,
                tui,
                widgetKey: key,
            });
            this.send({ type: "engine_custom_open", componentId, overlay: false, widgetKey: key, widgetPlacement: placement });
        }).catch((error) => {
            if (this.widgetIds.get(key) === componentId)
                this.widgetIds.delete(key);
            this.send({ type: "engine_custom_frame", componentId, requestId: 0, lines: [`Widget ${key} failed: ${error.message}`] });
        });
    }
    constructor(write, keybindings) {
        this.widgetIds = new Map();
        this.active = new Map();
        this.nextId = 0;
        this.stateListeners = new Set();
        this.write = write;
        this.keybindings = keybindings;
    }
    async custom(factory, options) {
        const componentId = `remote_component_${++this.nextId}`;
        let resolveCompletion;
        const completion = new Promise((resolve) => { resolveCompletion = resolve; });
        let opened = false;
        let pendingDone;
        let doneCalled = false;
        const done = (result) => {
            if (doneCalled)
                return;
            doneCalled = true;
            pendingDone = result;
            resolveCompletion(result);
            if (opened)
                this.sendDone(componentId, result);
        };
        const terminal = new RemoteTerminal(() => this.send({ type: "engine_custom_invalidate", componentId }), (control) => this.send({ type: "engine_custom_terminal", componentId, control }));
        const tui = new TUI(terminal);
        const component = await factory(tui, theme, this.keybindings, done);
        tui.addChild(component);
        tui.setFocus(component);
        const record = {
            component,
            resolve: (value) => resolveCompletion(value),
            terminal,
            tui,
            overlay: options?.overlay === true,
        };
        this.active.set(componentId, record);
        this.notifyState();
        opened = true;
        this.send({
            type: "engine_custom_open",
            componentId,
            overlay: options?.overlay === true,
            deferInlineCustomUiFocus: options?.deferInlineCustomUiFocus,
            overlayOptions: serializableOverlayOptions(options?.overlayOptions),
        });
        if (options?.onHandle)
            options.onHandle(this.remoteHandle(componentId));
        if (doneCalled)
            this.sendDone(componentId, pendingDone);
        const onAbort = () => this.disposeComponent(componentId, true);
        options?.signal?.addEventListener("abort", onAbort, { once: true });
        try {
            return await completion;
        }
        finally {
            options?.signal?.removeEventListener("abort", onAbort);
            this.disposeComponent(componentId, false);
        }
    }
    getHostCustomUiState() {
        const depth = [...this.active.values()].filter((record) => !record.overlay && !record.widgetKey).length;
        return { blockingInlineCustomUiDepth: depth, blockingInlineCustomUiActive: depth > 0 };
    }
    onHostCustomUiStateChange(listener) {
        this.stateListeners.add(listener);
        return () => this.stateListeners.delete(listener);
    }
    focusHostInlineCustomUi() { return this.getHostCustomUiState().blockingInlineCustomUiActive; }
    requestRender() {
        for (const [componentId, record] of this.active) {
            if (record.hidden === true)
                continue;
            this.send({ type: "engine_custom_invalidate", componentId });
        }
    }
    handleLine(line) {
        const command = parseInteractiveEngineCommand(line);
        if (!command)
            return false;
        if (!command.type.startsWith("engine_custom_"))
            return false;
        const record = this.active.get(command.componentId);
        if (!record)
            return true;
        switch (command.type) {
            case "engine_custom_render":
                record.terminal.columns = Math.max(1, command.width);
                record.terminal.rows = Math.max(1, command.rows);
                void runCallback({ kind: "renderer", name: command.componentId }, () => record.component.render(record.terminal.columns)).then((lines) => this.send({
                    type: "engine_custom_frame",
                    componentId: command.componentId,
                    requestId: command.requestId,
                    lines: boundedLines(lines),
                })).catch((error) => this.send({
                    type: "engine_custom_frame",
                    componentId: command.componentId,
                    requestId: command.requestId,
                    lines: [`Remote component render failed: ${error.message}`],
                }));
                break;
            case "engine_custom_input":
                void runCallback({ kind: "renderer", name: command.componentId }, () => {
                    if (isKeyRelease(command.data) && record.component.wantsKeyRelease !== true)
                        return;
                    record.component.handleInput?.(command.data);
                }).catch(() => undefined);
                break;
            case "engine_custom_dispose":
                this.disposeComponent(command.componentId, true);
                break;
        }
        return true;
    }
    dispose() {
        for (const componentId of [...this.active.keys()])
            this.disposeComponent(componentId, true);
    }
    disposeComponent(componentId, resolve) {
        const record = this.active.get(componentId);
        if (!record)
            return;
        this.active.delete(componentId);
        record.component.dispose?.();
        record.tui.stop();
        if (record.widgetKey) {
            if (this.widgetIds.get(record.widgetKey) === componentId)
                this.widgetIds.delete(record.widgetKey);
            this.send({ type: "engine_custom_close", componentId });
        }
        this.notifyState();
        if (resolve)
            record.resolve(undefined);
    }
    notifyState() {
        const state = this.getHostCustomUiState();
        for (const listener of this.stateListeners)
            listener(state);
    }
    remoteHandle(componentId) {
        let focused = true;
        const setHiddenFlag = (value) => {
            const record = this.active.get(componentId);
            if (record)
                record.hidden = value;
        };
        return {
            hide: () => {
                setHiddenFlag(true);
                this.send({ type: "engine_custom_control", componentId, action: "hide" });
            },
            setHidden: (value) => {
                setHiddenFlag(value);
                this.send({ type: "engine_custom_control", componentId, action: value ? "hide" : "show" });
            },
            isHidden: () => this.active.get(componentId)?.hidden === true,
            focus: () => {
                focused = true;
                this.send({ type: "engine_custom_control", componentId, action: "focus" });
            },
            unfocus: () => {
                focused = false;
                this.send({ type: "engine_custom_control", componentId, action: "unfocus" });
            },
            isFocused: () => focused,
        };
    }
    sendDone(componentId, result) {
        this.send({
            type: "engine_custom_done",
            componentId,
            result: jsonResult(result),
        });
    }
    send(message) {
        this.write(serializeInteractiveEngineMessage(message));
    }
}
//# sourceMappingURL=engine-custom-ui.js.map