import { TerminalModeController } from "./terminal-mode-controller.js";
import { RemoteFrameWidthClamp } from "./remote-frame-clamp.js";
class RemoteComponent {
    constructor(componentId, runtime, requestRender, getRows) {
        this.wantsKeyRelease = true;
        this.lines = ["Loading remote component…"];
        this.width = 0;
        this.requestId = 0;
        this.appliedRequestId = 0;
        this.dirty = true;
        this.disposed = false;
        this.frameClamp = new RemoteFrameWidthClamp();
        this.componentId = componentId;
        this.runtime = runtime;
        this.requestRender = requestRender;
        this.getRows = getRows;
    }
    render(width) {
        if (!this.disposed && (this.dirty || width !== this.width)) {
            this.width = width;
            this.dirty = false;
            this.runtime.sendEngineCommand({
                type: "engine_custom_render",
                componentId: this.componentId,
                requestId: ++this.requestId,
                width,
                rows: this.getRows(),
            });
        }
        // The engine child re-renders asynchronously; until the fresh frame
        // arrives, the previous frame may be wrapped for an older terminal
        // width. Clamp so a resize never replays overflowing lines (pi-tui
        // crashes on any rendered line wider than the terminal).
        return this.frameClamp.clamp(this.lines, width);
    }
    handleInput(data) {
        if (this.disposed)
            return;
        this.runtime.sendEngineCommand({ type: "engine_custom_input", componentId: this.componentId, data });
        // Engine commands are delivered in order, so a frame requested now is
        // rendered by the child only AFTER it has applied this input. Pipelining
        // the request keeps keypress latency at a single round trip and repaints
        // components that never self-invalidate, instead of waiting for a
        // child-side invalidate (or an unrelated refresh) to trigger a frame.
        this.dirty = true;
        this.requestRender();
    }
    invalidate() {
        this.dirty = true;
    }
    applyFrame(requestId, lines) {
        if (this.disposed || requestId < this.appliedRequestId)
            return;
        this.appliedRequestId = requestId;
        this.lines = lines;
        this.requestRender();
    }
    requestRemoteRender() {
        if (this.disposed)
            return;
        this.dirty = true;
        this.requestRender();
    }
    dispose() {
        if (this.disposed)
            return;
        this.disposed = true;
        this.runtime.sendEngineCommand({ type: "engine_custom_dispose", componentId: this.componentId });
    }
}
function overlayOptions(options) {
    return options;
}
/**
 * Resolve the real host terminal from the pi-tui TUI handed to the overlay
 * factory. Optional: some hosts / test seams do not surface `tui.terminal`, in
 * which case terminal-mode controls harmlessly no-op.
 */
function hostTerminal(tui) {
    const terminal = tui.terminal;
    return typeof terminal?.write === "function"
        ? terminal
        : { write: () => { } };
}
export class RemoteComponentController {
    constructor(runtime, ui) {
        this.mounted = new Map();
        this.terminalModes = new TerminalModeController();
        this.runtime = runtime;
        this.ui = ui;
        this.unsubscribe = runtime.onEngineMessage((message) => this.handleMessage(message));
    }
    dispose() {
        this.unsubscribe();
        this.terminalModes.resetAll();
        for (const record of this.mounted.values()) {
            if (record.widgetKey)
                this.ui.setWidget(record.widgetKey, undefined);
            record.component.dispose();
        }
        this.mounted.clear();
    }
    handleMessage(message) {
        switch (message.type) {
            case "engine_ready":
                // A fresh engine generation replaced the child: any terminal modes
                // the dead generation left on are stale. Reset them so a crashed or
                // restarted overlay never strands the host TTY in mouse-reporting
                // or autowrap-off mode; the new generation re-asserts on remount.
                this.terminalModes.resetAll();
                break;
            case "engine_custom_open":
                this.open(message.componentId, message.overlay, message.deferInlineCustomUiFocus, message.overlayOptions, message.widgetKey, message.widgetPlacement);
                break;
            case "engine_custom_close":
                this.close(message.componentId);
                break;
            case "engine_custom_frame":
                this.mounted.get(message.componentId)?.component.applyFrame(message.requestId, message.lines);
                break;
            case "engine_custom_invalidate":
                this.mounted.get(message.componentId)?.component.requestRemoteRender();
                break;
            case "engine_custom_terminal":
                this.terminalModes.applyControl(message.componentId, message.control);
                break;
            case "engine_custom_done": {
                const record = this.mounted.get(message.componentId);
                if (record) {
                    record.engineDone = true;
                    record.done(message.result);
                }
                break;
            }
            case "engine_custom_control":
                this.control(message.componentId, message.action);
                break;
        }
    }
    open(componentId, overlay, deferInlineCustomUiFocus, options, widgetKey, widgetPlacement) {
        if (this.mounted.has(componentId))
            return;
        if (widgetKey) {
            let rows = 24;
            const component = new RemoteComponent(componentId, this.runtime, () => this.ui.requestRender(), () => rows);
            this.mounted.set(componentId, { component, done: () => { }, engineDone: false, widgetKey });
            this.ui.setWidget(widgetKey, (tui) => {
                rows = tui.terminal.rows;
                return component;
            }, { placement: widgetPlacement });
            return;
        }
        let mounted;
        void this.ui.custom((tui, _theme, _keybindings, done) => {
            const component = new RemoteComponent(componentId, this.runtime, () => this.ui.requestRender(), () => tui.terminal.rows);
            mounted = { component, done, engineDone: false };
            this.mounted.set(componentId, mounted);
            // Bind this component to the real host terminal so buffered/pending
            // terminal-mode controls (e.g. mouse-scroll reporting the overlay
            // enabled before the mount frame) apply to the host TTY.
            this.terminalModes.onMount(componentId, hostTerminal(tui));
            return component;
        }, {
            overlay,
            deferInlineCustomUiFocus,
            overlayOptions: overlayOptions(options),
            onHandle: (handle) => { if (mounted)
                mounted.handle = handle; },
        }).catch(() => undefined).finally(() => {
            this.terminalModes.onUnmount(componentId);
            const record = this.mounted.get(componentId);
            if (!record)
                return;
            this.mounted.delete(componentId);
            if (!record.engineDone)
                record.component.dispose();
        });
    }
    close(componentId) {
        const record = this.mounted.get(componentId);
        if (!record)
            return;
        this.terminalModes.onUnmount(componentId);
        this.mounted.delete(componentId);
        if (record.widgetKey)
            this.ui.setWidget(record.widgetKey, undefined);
        record.component.dispose();
    }
    control(componentId, action) {
        const handle = this.mounted.get(componentId)?.handle;
        if (!handle)
            return;
        switch (action) {
            case "focus":
                handle.focus();
                break;
            case "hide":
                handle.setHidden(true);
                break;
            case "show":
                handle.setHidden(false);
                break;
            case "unfocus":
                handle.unfocus();
                break;
        }
    }
}
//# sourceMappingURL=remote-component.js.map