import { flattenVisibleRows } from "./render.js";
import { formatKey } from "../tui/input/format.js";
export function handleKeyEx(state, event) {
    // Help overlay swallows the next keystroke — any key closes it.
    if (state.helpOpen) {
        return { state: { ...state, helpOpen: false } };
    }
    // Any keystroke clears a transient status message.
    const cleared = state.messageBar ? { ...state, messageBar: undefined } : state;
    const fmt = formatKey(event);
    const next = handleKey(cleared, event);
    // Promote special keys into commands the run loop has to action.
    if (fmt === "/")
        return { state: next, command: { kind: "search" } };
    if (fmt === "y")
        return { state: next, command: { kind: "copy" } };
    if (fmt === "f")
        return { state: next, command: { kind: "toggleFollow" } };
    return { state: next };
}
export function handleKey(state, event) {
    const rows = flattenVisibleRows(state);
    if (rows.length === 0)
        return state;
    const idx = rows.findIndex((r) => r.node.id === state.cursorId);
    // `formatKey` returns canonical strings like "j", "G", "Up",
    // "Ctrl+C". It is case-preserving for single letters, so 'g' and
    // 'G' stay distinct here.
    switch (formatKey(event)) {
        case "j":
        case "Down":
        case "Ctrl+N":
            return moveCursor(state, rows, Math.min(idx + 1, rows.length - 1));
        case "k":
        case "Up":
        case "Ctrl+P":
            return moveCursor(state, rows, Math.max(idx - 1, 0));
        case "g":
            return { ...state, cursorId: rows[0].node.id, scrollTop: 0 };
        case "G":
            return moveCursor(state, rows, rows.length - 1);
        case "l":
        case "Right":
        case "Enter":
            return expand(state, rows, idx);
        case "h":
        case "Left":
            return collapseOrParent(state, rows, idx);
        case "e":
            return expandAll(state);
        case "E":
            return collapseAll(state);
        case "Tab":
            return cycleTrace(state, +1);
        case "Shift+Tab":
            return cycleTrace(state, -1);
        case "n":
            return jumpMatch(state, +1);
        case "N":
            return jumpMatch(state, -1);
        case "Escape":
            return clearSearch(state);
        case "?":
            return { ...state, helpOpen: true };
        case "q":
        case "Ctrl+C":
            return { ...state, quit: true };
        default:
            return state;
    }
}
// Jump cursor to next/previous match. Wraps. No-op if no active query.
function jumpMatch(state, direction) {
    const matches = state.matches ?? [];
    if (matches.length === 0)
        return state;
    const cur = state.matchIdx ?? -1;
    const next = (cur + direction + matches.length) % matches.length;
    return { ...state, matchIdx: next, cursorId: matches[next] };
}
function clearSearch(state) {
    if (!state.query && !state.matches?.length)
        return state;
    return { ...state, query: undefined, matches: undefined, matchIdx: undefined };
}
// Expand every span and trace in the forest. Leaves stay leaves.
function expandAll(state) {
    const next = new Set(state.expanded);
    const walk = (node) => {
        if (node.nodeKind !== "event")
            next.add(node.id);
        for (const c of node.children)
            walk(c);
    };
    for (const r of state.roots)
        walk(r);
    return { ...state, expanded: next };
}
// Collapse everything. Per the v1 default-expand-only-trace rule,
// auto-expand the lone trace when there is exactly one — keeps the
// "press E to see the top-level view" behavior intuitive.
function collapseAll(state) {
    const onlyTrace = state.roots.length === 1 ? state.roots[0].id : undefined;
    const expanded = new Set();
    if (onlyTrace !== undefined)
        expanded.add(onlyTrace);
    // Keep the cursor on something visible; if its node is now hidden,
    // move it to the trace root containing it (or the first trace).
    const cursorTraceRoot = traceRootOf(state.roots, state.cursorId);
    const cursorId = expanded.has(state.cursorId)
        ? state.cursorId
        : cursorTraceRoot ?? state.roots[0]?.id ?? state.cursorId;
    return { ...state, expanded, cursorId, scrollTop: 0 };
}
// Move the cursor to the previous/next trace root, wrapping at the
// edges. No-op when there's only one trace.
function cycleTrace(state, direction) {
    const traceIds = state.roots.map((r) => r.id);
    if (traceIds.length <= 1)
        return state;
    const cur = traceRootOf(state.roots, state.cursorId);
    const startIdx = cur ? traceIds.indexOf(cur) : -1;
    const nextIdx = (startIdx + direction + traceIds.length) % traceIds.length;
    return { ...state, cursorId: traceIds[nextIdx] };
}
function traceRootOf(roots, id) {
    for (const r of roots) {
        if (containsId(r, id))
            return r.id;
    }
    return undefined;
}
function containsId(node, id) {
    if (node.id === id)
        return true;
    for (const c of node.children) {
        if (containsId(c, id))
            return true;
    }
    return false;
}
function moveCursor(state, rows, newIdx) {
    if (newIdx < 0 || newIdx >= rows.length)
        return state;
    return { ...state, cursorId: rows[newIdx].node.id };
}
function expand(state, rows, idx) {
    if (idx < 0)
        return state;
    const node = rows[idx].node;
    // Synthetic text rows aren't expandable and have no children to
    // descend into; leave them alone.
    if (node.nodeKind === "jsonLine" || node.nodeKind === "convoLine") {
        return state;
    }
    const isLeafWithPayload = node.nodeKind === "event" && !!node.event;
    const isRawToggle = node.nodeKind === "rawDataToggle";
    if (node.children.length === 0 && !isLeafWithPayload && !isRawToggle) {
        return state;
    }
    // If the node is already expanded, descend into its first child
    // (only for true tree nodes — leaves' "children" are synthetic
    // jsonLine rows that aren't worth landing the cursor on first).
    if (state.expanded.has(node.id)) {
        if (node.children.length > 0) {
            return { ...state, cursorId: node.children[0].id };
        }
        return state;
    }
    const next = new Set(state.expanded);
    next.add(node.id);
    return { ...state, expanded: next };
}
function collapseOrParent(state, rows, idx) {
    if (idx < 0)
        return state;
    const node = rows[idx].node;
    if (state.expanded.has(node.id)) {
        const next = new Set(state.expanded);
        next.delete(node.id);
        return { ...state, expanded: next };
    }
    if (node.parentId) {
        return { ...state, cursorId: node.parentId };
    }
    return state;
}
