/**
 * Screen: the top-level entry point that ties Solid rendering to the terminal.
 *
 * Creates a root TermNode, starts the frame scheduler, manages input,
 * and handles the terminal lifecycle (raw mode, cleanup on exit).
 *
 * Usage:
 *   const screen = mount(() => <App />, { fps: 30 })
 *   // later:
 *   screen.unmount()
 */

import { flush } from "solid-js";
import { createInputBus, InputContext } from "../input/hooks.jsx";
import { decodePasteBytes, startInput } from "../input/index.jsx";
import { handleInputKey } from "../input/input-handler.jsx";
import { createComponent, insertNode, render } from "../renderer/term-node.jsx";
import { diffFrames, serializeChanges, wrapSynchronized } from "./frame-buffer.jsx";
import { clearDirty, createRenderState, findHandler, hitTest, layout, paint, renderFrame, renderToAnsi, scrollBy } from "./pipeline.jsx";
import { clearSelection, copyToClipboard, getSelectedText } from "./selection.jsx";
import { terminalCaps } from "./terminal-caps.jsx";
import { Node as YogaNode } from "./yoga.jsx";
function getTerminalSize() {
  const cols = process.stdout.columns ?? 80;
  const rows = process.stdout.rows ?? 24;
  return {
    cols,
    rows
  };
}
export function mount(code, options = {}) {
  const termSize = getTerminalSize();
  let cols = options.cols ?? termSize.cols;
  let rows = options.rows ?? termSize.rows;
  const fps = options.fps ?? 30;
  const write = options.write ?? (data => process.stdout.write(data));
  const syncOutput = options.syncOutput ?? terminalCaps.syncOutput;
  const enableInput = options.enableInput ?? true;
  const altScreen = options.altScreen ?? false;
  const contentDriven = options.contentDriven ?? false;

  // Track content-driven state
  let contentRows = 0; // visible rows on terminal (min of content, terminal)
  let prevViewportY = 0; // viewport offset from previous frame

  // Create root node with yoga
  const yogaRoot = new YogaNode();
  yogaRoot.setWidth(cols);
  if (!contentDriven) {
    yogaRoot.setHeight(rows);
  }
  // else: unconstrained height initially — will be set per frame
  const root = {
    type: "box",
    props: {},
    children: [],
    parent: null,
    yogaNode: yogaRoot,
    screenRect: null,
    scrollTop: 0,
    dirty: true,
    _cachedText: null
  };

  // Render state (double-buffered frame buffers)
  let state = createRenderState(cols, rows, syncOutput, terminalCaps.hyperlinks);

  // Input bus — shared between the input system and Solid components via context
  const bus = createInputBus();

  // Mount the Solid tree with InputContext provider
  // In Solid 2.0, createContext() returns the provider component directly
  const dispose = render(() => createComponent(InputContext, {
    value: bus,
    get children() {
      return code();
    }
  }), root);

  // Find the first focused <input> node in the tree
  function findFocusedInput(node) {
    if (node.type === "input" && node.props.focused !== false) {
      return node;
    }
    for (const child of node.children) {
      const found = findFocusedInput(child);
      if (found) return found;
    }
    return null;
  }

  // Route key events to focused <input> elements
  function handleInputElement(key) {
    const inputNode = findFocusedInput(root);
    if (!inputNode) return false;
    const value = inputNode.props.value ?? "";
    const rawCursor = inputNode.props._cursorPos ?? value.length;
    // Clamp cursor — value may have been changed externally (e.g. submit clearing)
    const cursorPos = Math.max(0, Math.min(rawCursor, value.length));
    const inputState = {
      value,
      cursorPos
    };
    const multiline = inputNode.props.multiline;
    const result = handleInputKey(key, inputState, {
      multiline
    });

    // Update cursor position on the node
    inputNode.props._cursorPos = result.cursorPos;
    if (result.changed) {
      const onChange = inputNode.props.onChange;
      onChange?.(result.value);
    }
    if (result.submit) {
      const onSubmit = inputNode.props.onSubmit;
      onSubmit?.(result.value);
    }
    return result.changed || result.submit || result.cursorPos !== cursorPos;
  }

  // Mouse hit-testing state
  let hoveredNode = null;
  function handleMouseEvent(event) {
    // Mouse coords are already 0-indexed from the parser
    const col = event.x;
    const row = event.y;
    const target = hitTest(root, col, row);

    // Text selection handling
    if (event.type === "down" && event.button === 0) {
      state.selection.anchor = {
        col,
        row
      };
      state.selection.focus = {
        col,
        row
      };
    } else if (event.type === "drag" && state.selection.anchor) {
      state.selection.focus = {
        col,
        row
      };
    } else if (event.type === "up" && event.button === 0 && state.selection.anchor) {
      state.selection.focus = {
        col,
        row
      };
      const {
        anchor
      } = state.selection;
      if (anchor.col === col && anchor.row === row) {
        // Click with no drag — clear selection
        clearSelection(state.selection);
      } else {
        // Copy selected text to clipboard via OSC 52
        // Use prevBuffer — it has the last rendered frame (buffer gets swapped)
        const text = getSelectedText(state.prevBuffer, state.selection);
        if (text) copyToClipboard(text, write);
      }
    }

    // Enter/leave tracking
    if (event.type === "move" || event.type === "drag") {
      if (target !== hoveredNode) {
        if (hoveredNode) {
          const leave = findHandler(hoveredNode, "onMouseLeave");
          leave?.handler(event);
        }
        hoveredNode = target;
        if (target) {
          const enter = findHandler(target, "onMouseEnter");
          enter?.handler(event);
        }
      }
    }

    // Click dispatch
    if (event.type === "up" && target) {
      const click = findHandler(target, "onClick");
      click?.handler(event);
    }

    // Scroll dispatch to scroll containers
    if (event.type === "scroll" && target) {
      // Auto-scroll the nearest overflow="scroll" ancestor
      let scrollContainer = target;
      while (scrollContainer) {
        if (scrollContainer.props.overflow === "scroll") {
          const delta = event.scroll?.direction === "up" ? -1 : 1;
          scrollBy(scrollContainer, delta);
          break;
        }
        scrollContainer = scrollContainer.parent;
      }
      // Still fire user's onScroll handler if present
      const scrollTarget = findHandler(target, "onScroll");
      if (scrollTarget) {
        scrollTarget.handler(event);
      }
    }
  }

  // Start input handling
  let inputHandle = null;
  if (enableInput) {
    inputHandle = startInput({
      onKey(key) {
        // Clear text selection on any keypress
        if (state.selection.anchor) clearSelection(state.selection);
        // Route to focused <input> first, then dispatch to bus
        handleInputElement(key);
        bus._dispatchKey(key);
        // Render on next microtask — coalesces rapid keypresses
        scheduleRender();
      },
      onMouse(event) {
        handleMouseEvent(event);
        bus._dispatchMouse(event);
      },
      onPaste(bytes) {
        bus._dispatchPaste(decodePasteBytes(bytes));
      }
    }, {
      mouse: options.mouse ?? false,
      kittyKeyboard: options.kittyKeyboard ?? terminalCaps.kittyKeyboard
    });
  }

  // Frame scheduler
  let frameTimer = null;
  let running = true;
  let renderPending = false;
  let renderedSinceTick = false;

  /** Flush signals and render a frame. */
  function doRender() {
    if (!running) return;
    flush();
    if (contentDriven) {
      if (!root.dirty) return;

      // ── Always unconstrained layout (CC pattern) ──
      yogaRoot.setHeight(undefined);
      layout(root, cols, undefined);
      clearDirty(root);

      // Measure natural content height
      let contentHeight = 0;
      for (const child of root.children) {
        if (child.yogaNode) {
          const bottom = child.yogaNode.getComputedTop() + child.yogaNode.getComputedHeight();
          if (bottom > contentHeight) contentHeight = Math.ceil(bottom);
        }
      }
      contentHeight = Math.max(1, contentHeight);

      // Viewport: show the last `rows` rows of content
      const viewportY = Math.max(0, contentHeight - rows);
      const visibleRows = Math.min(contentHeight, rows);

      // ── Terminal adjustments ──
      const scrollDelta = viewportY - prevViewportY;
      const heightDelta = visibleRows - contentRows;

      // Growth and scroll can happen simultaneously (e.g., opening
      // command palette pushes content past the terminal bottom).
      // Total LFs = new terminal rows (heightDelta) + scroll amount.
      const totalLFs = Math.max(0, heightDelta) + Math.max(0, scrollDelta);
      if (totalLFs > 0 && contentRows > 0) {
        write(`\x1b[${contentRows};1H`);
        write("\n".repeat(totalLFs));
        write(`\x1b[${rows}A`);
      } else if (heightDelta < 0 || scrollDelta < 0) {
        // Content shrunk or viewport shifted up — clear the entire
        // old visible area so stale content (border lines, suggestion
        // text) from the previous frame doesn't linger on screen.
        write(`\x1b[1;1H\x1b[0J`);
      }

      // ── Resize / shift prevBuffer to match terminal state after scroll ──
      if (visibleRows !== contentRows || scrollDelta !== 0) {
        const oldPrev = state.prevBuffer;
        const oldRows = state.rows;
        state = createRenderState(cols, visibleRows, syncOutput, terminalCaps.hyperlinks);
        if (scrollDelta > 0 && oldRows > 0) {
          // Terminal scrolled: shift prevBuffer up by scrollDelta.
          // After scroll, terminal row 1 has what was in old buffer row scrollDelta.
          const copyRows = Math.min(oldRows - scrollDelta, visibleRows);
          for (let row = 0; row < copyRows; row++) {
            const srcRow = row + scrollDelta;
            if (srcRow >= 0 && srcRow < oldRows) {
              for (let col = 0; col < cols; col++) {
                state.prevBuffer.set(col, row, oldPrev.get(col, srcRow));
              }
            }
          }
        } else if (scrollDelta === 0 && heightDelta >= 0) {
          // No scroll, just grew: copy overlapping rows
          const overlap = Math.min(oldRows, visibleRows);
          for (let row = 0; row < overlap; row++) {
            for (let col = 0; col < cols; col++) {
              state.prevBuffer.set(col, row, oldPrev.get(col, row));
            }
          }
        }
        // For shrinking (heightDelta < 0) or unscroll (scrollDelta < 0),
        // prevBuffer starts blank → full repaint. Sync output masks flicker.
      }
      contentRows = visibleRows;
      prevViewportY = viewportY;

      // ── Paint with viewport offset → only visible content in buffer ──
      state.buffer.clearDirtyRows();
      paint(root, state.buffer, 0, -viewportY);
      state.activeHeight = state.buffer.contentHeight();
      const dirtyRows = new Set(state.buffer.dirtyRows);
      for (const row of state.prevBuffer.dirtyRows) dirtyRows.add(row);
      const changes = diffFrames(state.prevBuffer, state.buffer, dirtyRows);
      const ansi = serializeChanges(changes, undefined, state.linksEnabled);
      const tmp = state.prevBuffer;
      state.prevBuffer = state.buffer;
      state.buffer = tmp;
      if (ansi) {
        const out = state.syncSupported ? wrapSynchronized(ansi) : ansi;
        write(out);
      }
      return;
    }
    const ansi = renderFrame(root, state);
    if (ansi) write(ansi);
  }

  /** Schedule a render on the next microtask (coalesces rapid input). */
  function scheduleRender() {
    if (renderPending) return;
    renderPending = true;
    queueMicrotask(() => {
      renderPending = false;
      if (!running) return;
      renderedSinceTick = true;
      doRender();
    });
  }
  function tick() {
    if (!running) return;
    if (renderedSinceTick) {
      renderedSinceTick = false;
      return;
    }
    doRender();
  }
  frameTimer = setInterval(tick, Math.round(1000 / fps));
  if (altScreen) {
    write("\x1b[?1049h\x1b[?25l");
  } else if (contentDriven) {
    // Content-driven: scroll viewport to guarantee content starts at row 1.
    // Same as CC/Ink's primary-screen setup — CUP (absolute positioning)
    // in serializeChanges relies on content at row 1.
    write(`${"\n".repeat(rows)}\x1b[${rows}A\x1b[0J\x1b[?25l`);
  } else {
    // Scroll terminal to make clean space, clear it, hide cursor
    write(`${"\n".repeat(rows)}\x1b[${rows}A\x1b[0J\x1b[?25l`);
  }

  // Initial render
  tick();

  // Handle terminal resize
  const onResize = () => {
    const size = getTerminalSize();
    screen.resize(size.cols, size.rows);
  };
  process.stdout.on("resize", onResize);

  // Safety net: restore terminal on unexpected exit
  const onSignal = () => {
    screen.unmount();
    process.exit();
  };
  const onExit = () => {
    screen.unmount();
  };
  process.on("SIGINT", onSignal);
  process.on("SIGTERM", onSignal);
  process.on("exit", onExit);
  let mouseActive = options.mouse ?? false;
  const screen = {
    root,
    input: bus,
    get mouse() {
      return mouseActive;
    },
    set mouse(enabled) {
      screen.setMouse(enabled);
    },
    setMouse(enabled) {
      if (enabled === mouseActive) return;
      mouseActive = enabled;
      if (enabled) {
        write("\x1b[?1006h\x1b[?1003h");
      } else {
        write("\x1b[?1003l\x1b[?1006l");
      }
    },
    unmount() {
      if (!running) return;
      running = false;
      if (frameTimer) {
        clearInterval(frameTimer);
        frameTimer = null;
      }
      inputHandle?.stop();
      inputHandle = null;
      if (mouseActive) {
        write("\x1b[?1003l\x1b[?1006l");
        mouseActive = false;
      }
      dispose();
      process.stdout.off("resize", onResize);
      process.off("SIGINT", onSignal);
      process.off("SIGTERM", onSignal);
      process.off("exit", onExit);
      if (altScreen) {
        // Leave alternate screen buffer + restore cursor
        write("\x1b[?25h\x1b[?1049l");
      } else {
        // Restore the terminal's native cursor
        write("\x1b[?25h");
      }
    },
    commit(content) {
      if (altScreen) return; // alt-screen has no scrollback

      // 1. Render the component to ANSI
      const {
        ansi
      } = renderToAnsi(content, cols, terminalCaps.hyperlinks);

      // 2. Erase the active region from the viewport
      write("\x1b[1;1H\x1b[0J\x1b[0m");

      // 3. Write rendered content at the viewport top
      write(ansi);

      // 4. Push committed content into scrollback
      write("\n".repeat(rows));
      write("\x1b[H");

      // 5. Reset frame buffers and repaint
      root.dirty = true;
      state = createRenderState(cols, rows, syncOutput, terminalCaps.hyperlinks);
      screen.refresh();
    },
    commitTo(content, parent, anchor) {
      // Render content in an isolated Solid root
      const tempYoga = new YogaNode();
      tempYoga.setWidth(cols);
      const tempRoot = {
        type: "box",
        props: {},
        children: [],
        parent: null,
        yogaNode: tempYoga,
        screenRect: null,
        scrollTop: 0,
        dirty: true,
        _cachedText: null
      };
      const dispose = render(content, tempRoot);

      // Dispose reactivity — effects die, nodes remain frozen
      dispose();

      // Move children from temp root into target parent
      const children = [...tempRoot.children];
      for (const child of children) {
        // Detach from temp root
        const idx = tempRoot.children.indexOf(child);
        tempRoot.children.splice(idx, 1);
        if (tempRoot.yogaNode && child.yogaNode) {
          tempRoot.yogaNode.removeChild(child.yogaNode);
        }
        child.parent = null;

        // Insert into target parent via renderer (handles yoga reparenting + markDirty)
        insertNode(parent, child, anchor);
      }
      tempYoga.free();
      screen.refresh();
    },
    refresh() {
      doRender();
    },
    resize(newCols, newRows) {
      if (newCols < 1 || newRows < 1) return;
      clearSelection(state.selection);
      cols = newCols;
      rows = newRows;
      yogaRoot.setWidth(cols);
      if (!contentDriven) {
        yogaRoot.setHeight(rows);
      } else {
        // Content-driven: reset viewport tracking on resize
        contentRows = 0;
        prevViewportY = 0;
      }
      root.dirty = true;
      state = createRenderState(cols, contentDriven ? 1 : rows, syncOutput, terminalCaps.hyperlinks);
      // Clear the entire viewport to avoid ghost artifacts when terminal grows
      if (!altScreen) {
        write(`\x1b[H\x1b[0J`);
      }
      screen.refresh();
    }
  };
  return screen;
}
//# sourceMappingURL=screen.jsx.map
