<script lang="ts">
  import { onMount, onDestroy } from "svelte";
  import { Terminal } from "@xterm/xterm";
  import { FitAddon } from "@xterm/addon-fit";
  import type { UnlistenFn } from "@tauri-apps/api/event";
  import {
    spawnPane,
    writeToPane,
    resizePane,
    killPane,
    onPaneData,
    onPaneExit,
    type PaneId,
  } from "../ipc";

  let {
    distro = undefined,
    cwd = undefined,
    onStatus = (_s: string, _ok: boolean) => {},
    onSpawn = undefined,
    onInput = undefined,
    onDataReceived = undefined,
    focusTrigger = 0,
  }: {
    distro?: string;
    cwd?: string;
    onStatus?: (msg: string, ok: boolean) => void;
    /** Fired once when the backend PTY is alive and we have its PaneId. */
    onSpawn?: (paneId: PaneId) => void;
    /** Fired AFTER each writeToPane on user keypress. Used by broadcasting. */
    onInput?: (dataB64: string) => void;
    /** Fired whenever output arrives from the PTY. Used for idle detection. */
    onDataReceived?: () => void;
    /** Increment to refocus the terminal programmatically (palette etc.). */
    focusTrigger?: number;
  } = $props();

  let containerEl: HTMLDivElement;
  let term: Terminal | null = null;
  let fit: FitAddon | null = null;
  let paneId: PaneId | null = null;
  let unlistenData: UnlistenFn | null = null;
  let unlistenExit: UnlistenFn | null = null;
  let ro: ResizeObserver | null = null;

  // Decode base64 -> Uint8Array. xterm.js accepts both strings and Uint8Array;
  // bytes is preferred to avoid double-decoding UTF-8.
  function b64ToBytes(b64: string): Uint8Array {
    const bin = atob(b64);
    const out = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
    return out;
  }

  function bytesToB64(bytes: Uint8Array): string {
    let s = "";
    for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
    return btoa(s);
  }

  function stringToB64(s: string): string {
    // xterm.js's onData emits a JS string; need to UTF-8 encode before base64.
    return bytesToB64(new TextEncoder().encode(s));
  }

  onMount(async () => {
    term = new Terminal({
      fontFamily: '"Cascadia Mono", "JetBrains Mono", "Consolas", monospace',
      fontSize: 13,
      cursorBlink: true,
      theme: {
        background: "#0c0c0c",
        foreground: "#e6e6e6",
      },
      scrollback: 5000,
      convertEol: false,
      allowProposedApi: true,
    });
    fit = new FitAddon();
    term.loadAddon(fit);
    term.open(containerEl);

    // Initial size — fit before we ask the PTY for its dimensions.
    fit.fit();
    const cols = term.cols;
    const rows = term.rows;

    try {
      paneId = await spawnPane({ distro, cwd, cols, rows });
      onStatus(`pane ${paneId} alive`, true);
      onSpawn?.(paneId);
    } catch (e) {
      const msg = `spawn_pane failed: ${e}`;
      term.write(`\r\n\x1b[31m${msg}\x1b[0m\r\n`);
      onStatus(msg, false);
      return;
    }

    unlistenData = await onPaneData(paneId, (b64) => {
      term?.write(b64ToBytes(b64));
      onDataReceived?.();
    });
    unlistenExit = await onPaneExit(paneId, () => {
      term?.write("\r\n\x1b[33m[pane exited]\x1b[0m\r\n");
      onStatus(`pane ${paneId} exited`, false);
    });

    term.onData((data) => {
      if (paneId == null) return;
      const b64 = stringToB64(data);
      void writeToPane(paneId, b64);
      onInput?.(b64);
    });

    // Re-fit on container resize; forward new size to the PTY.
    ro = new ResizeObserver(() => {
      try {
        fit?.fit();
        if (paneId != null && term) {
          void resizePane(paneId, term.cols, term.rows);
        }
      } catch (e) {
        console.warn("resize failed", e);
      }
    });
    ro.observe(containerEl);

    // Focus so typing immediately lands in the terminal.
    term.focus();
  });

  onDestroy(() => {
    ro?.disconnect();
    unlistenData?.();
    unlistenExit?.();
    if (paneId != null) {
      void killPane(paneId);
    }
    term?.dispose();
  });

  // Refocus the terminal whenever the parent bumps focusTrigger.
  $effect(() => {
    // Reactive read on focusTrigger so this effect re-runs when it changes.
    focusTrigger;
    if (term && focusTrigger > 0) term.focus();
  });
</script>

<div class="xterm-host" bind:this={containerEl}></div>

<style>
  .xterm-host {
    width: 100%;
    height: 100%;
  }

  /* xterm.js sets inline padding=0 on its container; ensure the viewport
     fills the host with no scrollbar gap. */
  :global(.xterm) {
    height: 100%;
  }
  :global(.xterm-viewport) {
    background: #0c0c0c !important;
  }
</style>
