// Qualifier Playground — interactive terminal for the qualifier site
// Runs the real qualifier CLI compiled to wasm via Emscripten.

(function () {
  "use strict";

  // ---------------------------------------------------------------
  // 1. VirtualFS
  // ---------------------------------------------------------------

  function VirtualFS(fileMap) {
    this._files = {};
    if (fileMap) {
      for (var k in fileMap) {
        if (fileMap.hasOwnProperty(k)) {
          this._files[k] = fileMap[k];
        }
      }
    }
  }

  VirtualFS.prototype.list = function () {
    var names = [];
    for (var k in this._files) {
      if (this._files.hasOwnProperty(k)) names.push(k);
    }
    names.sort();
    return names;
  };

  VirtualFS.prototype.get = function (name) {
    return this._files.hasOwnProperty(name) ? this._files[name] : null;
  };

  VirtualFS.prototype.has = function (name) {
    return this._files.hasOwnProperty(name);
  };

  VirtualFS.prototype.size = function (name) {
    var content = this.get(name);
    if (content === null) return 0;
    return new Blob([content]).size;
  };

  VirtualFS.prototype.formatSize = function (bytes) {
    if (bytes < 1024) return bytes + " B";
    if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
    return (bytes / 1048576).toFixed(1) + " MB";
  };

  // ---------------------------------------------------------------
  // 2. Command Parser
  // ---------------------------------------------------------------

  function parseCommand(line) {
    var args = [];
    var current = "";
    var inSingle = false;
    var inDouble = false;
    var escape = false;

    for (var i = 0; i < line.length; i++) {
      var ch = line[i];

      if (escape) {
        current += ch;
        escape = false;
        continue;
      }

      if (ch === "\\") {
        escape = true;
        continue;
      }

      if (ch === "'" && !inDouble) {
        inSingle = !inSingle;
        continue;
      }

      if (ch === '"' && !inSingle) {
        inDouble = !inDouble;
        continue;
      }

      if ((ch === " " || ch === "\t") && !inSingle && !inDouble) {
        if (current.length > 0) {
          args.push(current);
          current = "";
        }
        continue;
      }

      current += ch;
    }

    if (current.length > 0) args.push(current);
    return args;
  }

  // ---------------------------------------------------------------
  // 3. ANSI helpers
  // ---------------------------------------------------------------

  var ANSI = {
    reset: "\x1b[0m",
    bold: "\x1b[1m",
    dim: "\x1b[2m",
    accent: "\x1b[34m",
    red: "\x1b[31m",
    green: "\x1b[32m",
    flint: "\x1b[90m",
    white: "\x1b[37m",
    cyan: "\x1b[36m",
    magenta: "\x1b[35m",
  };

  function copperBold(s) {
    return ANSI.accent + ANSI.bold + s + ANSI.reset;
  }

  function red(s) {
    return ANSI.red + s + ANSI.reset;
  }

  function green(s) {
    return ANSI.green + s + ANSI.reset;
  }

  function dim(s) {
    return ANSI.dim + s + ANSI.reset;
  }

  function pencil(s) {
    return ANSI.flint + s + ANSI.reset;
  }

  function bold(s) {
    return ANSI.bold + s + ANSI.reset;
  }

  function pad(s, width, right) {
    s = String(s);
    while (s.length < width) {
      if (right) {
        s = s + " ";
      } else {
        s = " " + s;
      }
    }
    return s;
  }

  // ---------------------------------------------------------------
  // 4. WASM layer — real qualifier CLI via Emscripten
  // ---------------------------------------------------------------

  var compiledWasm = null;
  var wasmFiles = null;
  var wasmReady = false;
  var wasmError = null;

  function loadWasm(files) {
    wasmFiles = files;
    if (typeof createQualifierModule !== "function") {
      wasmError = "Wasm module not loaded";
      return Promise.reject(new Error(wasmError));
    }
    return fetch("/wasm/qualifier.wasm")
      .then(function (resp) {
        return WebAssembly.compileStreaming(resp);
      })
      .then(function (mod) {
        compiledWasm = mod;
        wasmReady = true;
      })
      .catch(function (err) {
        wasmError = err.message || String(err);
        throw err;
      });
  }

  // Fresh Emscripten instance per call — exit() kills the instance, not us
  function runQualifier(args) {
    var stdout = "";
    var stderr = "";
    return createQualifierModule({
      noInitialRun: true,
      instantiateWasm: function (imports, callback) {
        WebAssembly.instantiate(compiledWasm, imports).then(
          function (instance) {
            callback(instance);
          },
        );
        return {};
      },
      print: function (text) {
        stdout += text + "\n";
      },
      printErr: function (text) {
        stderr += text + "\n";
      },
    }).then(function (mod) {
      for (var name in wasmFiles) {
        if (wasmFiles.hasOwnProperty(name)) {
          mod.FS.writeFile("/" + name, wasmFiles[name]);
        }
      }
      try {
        mod.callMain(args);
      } catch (e) {
        // exit() throws to unwind — expected
      }
      return { stdout: stdout.trimEnd(), stderr: stderr.trimEnd() };
    });
  }

  // ---------------------------------------------------------------
  // 5. Shell builtins
  // ---------------------------------------------------------------

  function builtinLs(args, fs) {
    var files = fs.list();
    var lines = [];
    for (var i = 0; i < files.length; i++) {
      var sz = fs.formatSize(fs.size(files[i]));
      lines.push("  " + pad(sz, 8) + "  " + files[i]);
    }
    return { output: lines.join("\r\n") };
  }

  function builtinCat(args, fs) {
    if (args.length < 1) {
      return { output: red("usage: ") + "cat <file>" };
    }
    var name = args[0];
    if (!fs.has(name)) {
      return { output: red("cat: ") + name + ": No such file" };
    }
    var content = fs.get(name);
    // Replace \n with \r\n for terminal display
    return { output: content.replace(/\n/g, "\r\n") };
  }

  function builtinHelp(fs) {
    var files = fs.list();
    var fileList = files.join(", ");

    var lines = [];
    lines.push("");
    lines.push(copperBold("QUALIFIER PLAYGROUND"));
    lines.push("");
    lines.push(bold("Shell builtins:"));
    lines.push("  " + copperBold(pad("ls", 38, true)) + "List files");
    lines.push(
      "  " + copperBold(pad("cat <file>", 38, true)) + "Display file contents",
    );
    lines.push("  " + copperBold(pad("clear", 38, true)) + "Clear terminal");
    lines.push("  " + copperBold(pad("help", 38, true)) + "Show this message");
    lines.push("");
    lines.push(bold("CLI commands:"));
    lines.push(
      "  " +
        copperBold(pad("qualifier show <artifact>", 38, true)) +
        "Show annotations for an artifact",
    );
    lines.push(
      "  " +
        copperBold(pad("qualifier ls [--kind K]", 38, true)) +
        "List artifacts (optionally by kind)",
    );
    lines.push(
      "  " + copperBold(pad("qualifier --help", 38, true)) + "Full CLI usage",
    );
    lines.push("");
    lines.push(dim("Files: ") + fileList);
    lines.push("");
    return { output: lines.join("\r\n") };
  }

  // ---------------------------------------------------------------
  // 6. Command dispatcher
  // ---------------------------------------------------------------

  function dispatch(line, fs) {
    var trimmed = line.trim();
    if (!trimmed) return { output: "" };

    var args = parseCommand(trimmed);
    if (args.length === 0) return { output: "" };

    var cmd = args[0];

    // Shell builtins
    switch (cmd) {
      case "ls":
        return builtinLs(args.slice(1), fs);
      case "cat":
        return builtinCat(args.slice(1), fs);
      case "clear":
        return { clear: true };
      case "help":
        return builtinHelp(fs);
    }

    // qualifier commands — delegate to real wasm binary
    if (cmd === "qualifier") {
      if (!wasmReady) {
        if (wasmError) {
          return { output: red("Wasm failed to load: " + wasmError) };
        }
        return { output: dim("Loading CLI... try again in a moment.") };
      }
      var qualArgs = args.slice(1);
      return runQualifier(qualArgs).then(function (result) {
        var output = "";
        if (result.stdout) output += result.stdout.replace(/\n/g, "\r\n");
        if (result.stderr) {
          if (output) output += "\r\n";
          output += result.stderr.replace(/\n/g, "\r\n");
        }
        return { output: output };
      });
    }

    // cargo install qualifier — special case for boot
    if (cmd === "cargo" && args[1] === "install" && args[2] === "qualifier") {
      return {
        output: dim("  qualifier is already installed."),
      };
    }

    return {
      output:
        red("command not found: ") +
        cmd +
        "\r\n" +
        dim("Type 'help' for available commands."),
    };
  }

  // ---------------------------------------------------------------
  // 7. Word boundary helpers
  // ---------------------------------------------------------------

  function wordBoundaryRight(buf, pos) {
    var i = pos;
    // skip non-word chars
    while (i < buf.length && /\s/.test(buf[i])) i++;
    // skip word chars
    while (i < buf.length && !/\s/.test(buf[i])) i++;
    return i;
  }

  function wordBoundaryLeft(buf, pos) {
    var i = pos;
    // skip spaces to the left
    while (i > 0 && /\s/.test(buf[i - 1])) i--;
    // skip word chars to the left
    while (i > 0 && !/\s/.test(buf[i - 1])) i--;
    return i;
  }

  function wordEndRight(buf, pos) {
    var i = pos;
    if (i < buf.length) i++;
    // skip spaces
    while (i < buf.length && /\s/.test(buf[i])) i++;
    // skip word chars
    while (i < buf.length && !/\s/.test(buf[i])) i++;
    if (i > pos && i <= buf.length) i--;
    return i;
  }

  function WORDBoundaryRight(buf, pos) {
    var i = pos;
    while (i < buf.length && buf[i] !== " ") i++;
    while (i < buf.length && buf[i] === " ") i++;
    return i;
  }

  function WORDBoundaryLeft(buf, pos) {
    var i = pos;
    while (i > 0 && buf[i - 1] === " ") i--;
    while (i > 0 && buf[i - 1] !== " ") i--;
    return i;
  }

  function WORDEndRight(buf, pos) {
    var i = pos;
    if (i < buf.length) i++;
    while (i < buf.length && buf[i] === " ") i++;
    while (i < buf.length && buf[i] !== " ") i++;
    if (i > pos && i <= buf.length) i--;
    return i;
  }

  // ---------------------------------------------------------------
  // 8. TermShell class
  // ---------------------------------------------------------------

  function TermShell(container, fs) {
    this.fs = fs;
    this.history = [];
    this.historyIndex = -1;
    this.savedLine = "";
    this.buffer = "";
    this.cursor = 0;
    var isMobile = "ontouchstart" in window || navigator.maxTouchPoints > 0;
    this.mode = isMobile ? "emacs" : "vi";
    this.viMode = "insert"; // insert or command
    this.killRing = "";
    this.viOperator = null;
    this.pinnedText = "";
    this.busy = false;

    var self = this;

    // --- xterm terminal ---
    this.term = new Terminal({
      cursorBlink: true,
      cursorStyle: "block",
      fontSize: 14,
      fontFamily: '"JetBrains Mono", monospace',
      rows: 20,
      theme: {
        background: "#141720",
        foreground: "#d0d5e3",
        cursor: "#818cf8",
        cursorAccent: "#141720",
        selectionBackground: "#818cf844",
        selectionForeground: "#eef0f6",
        black: "#0c0e11",
        red: "#f87171",
        green: "#34d399",
        yellow: "#fbbf24",
        blue: "#60a5fa",
        magenta: "#c084fc",
        cyan: "#22d3ee",
        white: "#d0d5e3",
        brightBlack: "#6b7394",
        brightRed: "#fca5a5",
        brightGreen: "#6ee7b7",
        brightYellow: "#fcd34d",
        brightBlue: "#93c5fd",
        brightMagenta: "#d8b4fe",
        brightCyan: "#67e8f9",
        brightWhite: "#eef0f6",
      },
      allowTransparency: false,
      scrollback: 1000,
    });

    // --- FitAddon ---
    this.fitAddon = new FitAddon.FitAddon();
    this.term.loadAddon(this.fitAddon);

    this.term.open(container);
    this.fitAddon.fit();

    // Refit on resize
    this._resizeHandler = function () {
      self.fitAddon.fit();
    };
    window.addEventListener("resize", this._resizeHandler);

    // Mobile: tap terminal to open virtual keyboard
    if (isMobile) {
      container.addEventListener("touchstart", function () {
        self.term.focus();
      });
    }

    // --- Mode toggle button ---
    this.modeBtn = document.createElement("button");
    this.modeBtn.className =
      "playground-mode-toggle" + (this.mode === "vi" ? " vi" : "");
    this.modeBtn.textContent = this.mode === "vi" ? "VI" : "EMACS";
    this.modeBtn.addEventListener("click", function () {
      self.setMode(self.mode === "vi" ? "emacs" : "vi");
    });
    container.appendChild(this.modeBtn);

    // --- Pinned command header ---
    this.pinnedEl = document.createElement("div");
    this.pinnedEl.className = "playground-pinned-cmd";
    this.pinnedEl.style.display = "none";
    this.pinnedEl.innerHTML =
      '<span class="pinned-prompt">qual</span>' +
      '<span class="pinned-dollar"> $ </span>' +
      '<span class="pinned-text"></span>';
    container.appendChild(this.pinnedEl);

    // --- Input handler (onData handles all input: keyboard, mobile, paste) ---
    this.term.onData(function (data) {
      if (self.busy) return;
      self.handleData(data);
    });
  }

  TermShell.prototype.setMode = function (mode) {
    this.mode = mode;
    if (mode === "vi") {
      this.modeBtn.textContent = "VI";
      this.modeBtn.classList.add("vi");
      this.viMode = "insert";
    } else {
      this.modeBtn.textContent = "EMACS";
      this.modeBtn.classList.remove("vi");
    }
  };

  TermShell.prototype.viEnterInsert = function () {
    this.viMode = "insert";
    this.viOperator = null;
    this.term.options.cursorStyle = "block";
  };

  TermShell.prototype.viEnterCommand = function () {
    this.viMode = "command";
    this.viOperator = null;
    this.term.options.cursorStyle = "underline";
    // In vi, entering command mode moves cursor back one if possible
    if (this.cursor > 0 && this.cursor >= this.buffer.length) {
      this.cursor = this.buffer.length - 1;
      this.refreshLine();
    }
  };

  TermShell.prototype.killRange = function (from, to) {
    if (from === to) return;
    var start = Math.min(from, to);
    var end = Math.max(from, to);
    this.killRing = this.buffer.substring(start, end);
    this.buffer = this.buffer.substring(0, start) + this.buffer.substring(end);
    this.cursor = start;
    this.refreshLine();
  };

  TermShell.prototype.prompt = function () {
    this.buffer = "";
    this.cursor = 0;
    this.historyIndex = -1;
    this.savedLine = "";
    if (this.mode === "vi") {
      this.viMode = "insert";
      this.viOperator = null;
      this.term.options.cursorStyle = "block";
    }
    this.term.write(copperBold("qual") + " " + pencil("$") + " ");
  };

  TermShell.prototype.refreshLine = function () {
    var out =
      "\r" +
      copperBold("qual") +
      " " +
      pencil("$") +
      " " +
      this.buffer +
      "\x1b[K";
    var cursorOffset = this.buffer.length - this.cursor;
    if (cursorOffset > 0) {
      out += "\x1b[" + cursorOffset + "D";
    }
    this.term.write(out);
  };

  TermShell.prototype.insertChar = function (ch) {
    this.buffer =
      this.buffer.substring(0, this.cursor) +
      ch +
      this.buffer.substring(this.cursor);
    this.cursor++;
    this.refreshLine();
  };

  TermShell.prototype.pinCommand = function (cmd) {
    this.pinnedText = cmd;
    this.updatePinned();
  };

  TermShell.prototype.updatePinned = function () {
    if (!this.pinnedText) {
      this.pinnedEl.style.display = "none";
      return;
    }

    var viewport = this.term.element
      ? this.term.element.querySelector(".xterm-viewport")
      : null;

    // Show pinned header if scrolled past the original command
    if (viewport && viewport.scrollTop > 30) {
      this.pinnedEl.style.display = "";
      this.pinnedEl.querySelector(".pinned-text").textContent = this.pinnedText;
    } else {
      this.pinnedEl.style.display = "none";
    }
  };

  TermShell.prototype.submit = function () {
    var line = this.buffer;
    this.term.write("\r\n");

    if (line.trim()) {
      this.history.unshift(line);
      if (this.history.length > 200) this.history.pop();
    }

    this.historyIndex = -1;
    this.savedLine = "";

    // Pin command
    this.pinCommand(line.trim());

    var self = this;
    var result = dispatch(line, this.fs);

    if (result && typeof result.then === "function") {
      // Async (wasm command) — disable input until done
      this.busy = true;
      result.then(function (r) {
        if (r && r.output) {
          self.term.write(r.output);
          self.term.write("\r\n");
        }
        self.busy = false;
        self.prompt();
        self._attachScrollWatcher();
      });
    } else {
      if (result.clear) {
        this.term.clear();
        this.pinnedText = "";
        this.updatePinned();
        this.prompt();
        return;
      }

      if (result.output) {
        this.term.write(result.output);
        this.term.write("\r\n");
      }

      this.prompt();
      this._attachScrollWatcher();
    }
  };

  TermShell.prototype._attachScrollWatcher = function () {
    var self = this;
    var viewport = this.term.element
      ? this.term.element.querySelector(".xterm-viewport")
      : null;
    if (viewport) {
      viewport.addEventListener("scroll", function () {
        self.updatePinned();
      });
    }
  };

  TermShell.prototype.historyPrev = function () {
    if (this.history.length === 0) return;
    if (this.historyIndex === -1) {
      this.savedLine = this.buffer;
    }
    if (this.historyIndex < this.history.length - 1) {
      this.historyIndex++;
      this.buffer = this.history[this.historyIndex];
      this.cursor = this.buffer.length;
      this.refreshLine();
    }
  };

  TermShell.prototype.historyNext = function () {
    if (this.historyIndex === -1) return;
    this.historyIndex--;
    if (this.historyIndex === -1) {
      this.buffer = this.savedLine;
    } else {
      this.buffer = this.history[this.historyIndex];
    }
    this.cursor = this.buffer.length;
    this.refreshLine();
  };

  // --- Data dispatch ---
  // All input comes through onData as terminal byte sequences:
  //   Ctrl+A = \x01 ... Ctrl+Z = \x1a
  //   Enter = \r, Backspace = \x7f, Tab = \t, Escape = \x1b
  //   Arrows = \x1b[A/B/C/D, Home = \x1b[H, End = \x1b[F, Delete = \x1b[3~
  //   Alt+key = \x1b + char
  //   Paste = multiple printable chars at once

  TermShell.prototype.handleData = function (data) {
    // Multi-char non-escape data = paste
    if (data.length > 1 && data[0] !== "\x1b") {
      if (this.mode === "vi" && this.viMode === "command") {
        this.viEnterInsert();
      }
      for (var i = 0; i < data.length; i++) {
        var ch = data[i];
        if (ch === "\r" || ch === "\n") {
          this.submit();
          return;
        }
        this.insertChar(ch);
      }
      return;
    }

    if (this.mode === "emacs") {
      this.handleEmacs(data);
    } else if (this.viMode === "insert") {
      this.handleViInsert(data);
    } else {
      this.handleViCommand(data);
    }
  };

  // --- Emacs bindings ---
  TermShell.prototype.handleEmacs = function (data) {
    switch (data) {
      case "\r":
        this.submit();
        return;
      case "\x03": // Ctrl+C
        this.term.write("^C\r\n");
        this.prompt();
        return;
      case "\x01": // Ctrl+A — beginning of line
        this.cursor = 0;
        this.refreshLine();
        return;
      case "\x05": // Ctrl+E — end of line
        this.cursor = this.buffer.length;
        this.refreshLine();
        return;
      case "\x02": // Ctrl+B — back one char
      case "\x1b[D": // Left arrow
        if (this.cursor > 0) {
          this.cursor--;
          this.refreshLine();
        }
        return;
      case "\x06": // Ctrl+F — forward one char
      case "\x1b[C": // Right arrow
        if (this.cursor < this.buffer.length) {
          this.cursor++;
          this.refreshLine();
        }
        return;
      case "\x04": // Ctrl+D — delete char under cursor
        if (this.buffer.length === 0) return;
        if (this.cursor < this.buffer.length) {
          this.buffer =
            this.buffer.substring(0, this.cursor) +
            this.buffer.substring(this.cursor + 1);
          this.refreshLine();
        }
        return;
      case "\x08": // Ctrl+H — backspace
      case "\x7f": // Backspace
        if (this.cursor > 0) {
          this.buffer =
            this.buffer.substring(0, this.cursor - 1) +
            this.buffer.substring(this.cursor);
          this.cursor--;
          this.refreshLine();
        }
        return;
      case "\x0b": // Ctrl+K — kill to end of line
        this.killRing = this.buffer.substring(this.cursor);
        this.buffer = this.buffer.substring(0, this.cursor);
        this.refreshLine();
        return;
      case "\x15": // Ctrl+U — kill to beginning of line
        this.killRing = this.buffer.substring(0, this.cursor);
        this.buffer = this.buffer.substring(this.cursor);
        this.cursor = 0;
        this.refreshLine();
        return;
      case "\x17": // Ctrl+W — kill word backward
        this.killRange(wordBoundaryLeft(this.buffer, this.cursor), this.cursor);
        return;
      case "\x19": // Ctrl+Y — yank
        if (this.killRing) {
          this.buffer =
            this.buffer.substring(0, this.cursor) +
            this.killRing +
            this.buffer.substring(this.cursor);
          this.cursor += this.killRing.length;
          this.refreshLine();
        }
        return;
      case "\x14": // Ctrl+T — transpose characters
        if (this.cursor > 0 && this.buffer.length >= 2) {
          var pos = this.cursor;
          if (pos === this.buffer.length) pos--;
          if (pos > 0) {
            var chars = this.buffer.split("");
            var tmp = chars[pos - 1];
            chars[pos - 1] = chars[pos];
            chars[pos] = tmp;
            this.buffer = chars.join("");
            this.cursor = pos + 1;
            this.refreshLine();
          }
        }
        return;
      case "\x10": // Ctrl+P — history prev
      case "\x1b[A": // Up arrow
        this.historyPrev();
        return;
      case "\x0e": // Ctrl+N — history next
      case "\x1b[B": // Down arrow
        this.historyNext();
        return;
      case "\x1bb": // Alt+B — word backward
        this.cursor = wordBoundaryLeft(this.buffer, this.cursor);
        this.refreshLine();
        return;
      case "\x1bf": // Alt+F — word forward
        this.cursor = wordBoundaryRight(this.buffer, this.cursor);
        this.refreshLine();
        return;
      case "\x1bd": // Alt+D — kill word forward
        this.killRange(
          this.cursor,
          wordBoundaryRight(this.buffer, this.cursor),
        );
        return;
      case "\x1b[H": // Home
      case "\x1bOH":
        this.cursor = 0;
        this.refreshLine();
        return;
      case "\x1b[F": // End
      case "\x1bOF":
        this.cursor = this.buffer.length;
        this.refreshLine();
        return;
      case "\x1b[3~": // Delete
        if (this.cursor < this.buffer.length) {
          this.buffer =
            this.buffer.substring(0, this.cursor) +
            this.buffer.substring(this.cursor + 1);
          this.refreshLine();
        }
        return;
      case "\t": // Tab — ignore
      case "\x1b": // Escape — ignore in emacs mode
        return;
    }

    // Regular printable characters
    if (data.length === 1 && data >= " ") {
      this.insertChar(data);
    }
  };

  // --- Vi insert mode ---
  TermShell.prototype.handleViInsert = function (data) {
    switch (data) {
      case "\r":
        this.submit();
        return;
      case "\x1b": // Escape — enter command mode
        this.viEnterCommand();
        return;
      case "\x03": // Ctrl+C
        this.term.write("^C\r\n");
        this.prompt();
        return;
      case "\x15": // Ctrl+U — kill to beginning of line
        this.killRing = this.buffer.substring(0, this.cursor);
        this.buffer = this.buffer.substring(this.cursor);
        this.cursor = 0;
        this.refreshLine();
        return;
      case "\x17": // Ctrl+W — kill word backward
        this.killRange(wordBoundaryLeft(this.buffer, this.cursor), this.cursor);
        return;
      case "\x08": // Ctrl+H — backspace
      case "\x7f": // Backspace
        if (this.cursor > 0) {
          this.buffer =
            this.buffer.substring(0, this.cursor - 1) +
            this.buffer.substring(this.cursor);
          this.cursor--;
          this.refreshLine();
        }
        return;
      case "\x1b[D": // Left arrow
        if (this.cursor > 0) {
          this.cursor--;
          this.refreshLine();
        }
        return;
      case "\x1b[C": // Right arrow
        if (this.cursor < this.buffer.length) {
          this.cursor++;
          this.refreshLine();
        }
        return;
      case "\x1b[A": // Up arrow
        this.historyPrev();
        return;
      case "\x1b[B": // Down arrow
        this.historyNext();
        return;
      case "\x1b[3~": // Delete
        if (this.cursor < this.buffer.length) {
          this.buffer =
            this.buffer.substring(0, this.cursor) +
            this.buffer.substring(this.cursor + 1);
          this.refreshLine();
        }
        return;
      case "\t": // Tab — ignore
        return;
    }

    // Regular printable chars
    if (data.length === 1 && data >= " ") {
      this.insertChar(data);
    }
  };

  // --- Vi command mode ---
  TermShell.prototype.handleViCommand = function (data) {
    // If there's a pending operator (d or c), handle the motion
    if (this.viOperator) {
      var op = this.viOperator;
      this.viOperator = null;
      var from = this.cursor;
      var to = from;

      switch (data) {
        case "w":
          to = wordBoundaryRight(this.buffer, from);
          break;
        case "b":
          to = wordBoundaryLeft(this.buffer, from);
          break;
        case "e":
          to = wordEndRight(this.buffer, from);
          if (op === "d" || op === "c") to++;
          break;
        case "W":
          to = WORDBoundaryRight(this.buffer, from);
          break;
        case "B":
          to = WORDBoundaryLeft(this.buffer, from);
          break;
        case "E":
          to = WORDEndRight(this.buffer, from);
          if (op === "d" || op === "c") to++;
          break;
        case "0":
          to = 0;
          break;
        case "$":
          to = this.buffer.length;
          break;
        case "^":
          to = 0;
          while (to < this.buffer.length && this.buffer[to] === " ") to++;
          break;
        case "h":
          to = Math.max(0, from - 1);
          break;
        case "l":
          to = Math.min(this.buffer.length, from + 1);
          break;
        case "d":
          if (op === "d") {
            this.killRing = this.buffer;
            this.buffer = "";
            this.cursor = 0;
            this.refreshLine();
            return;
          }
          break;
        case "c":
          if (op === "c") {
            this.killRing = this.buffer;
            this.buffer = "";
            this.cursor = 0;
            this.refreshLine();
            this.viEnterInsert();
            return;
          }
          break;
        default:
          return;
      }

      this.killRange(Math.min(from, to), Math.max(from, to));
      if (op === "c") {
        this.viEnterInsert();
      }
      return;
    }

    switch (data) {
      case "\x03": // Ctrl+C
        this.term.write("^C\r\n");
        this.prompt();
        return;
      case "\r": // Enter
        this.submit();
        return;
      case "h":
      case "\x1b[D": // Left arrow
        if (this.cursor > 0) {
          this.cursor--;
          this.refreshLine();
        }
        return;
      case "l":
      case "\x1b[C": // Right arrow
        if (this.cursor < this.buffer.length - 1) {
          this.cursor++;
          this.refreshLine();
        }
        return;
      case "w":
        this.cursor = wordBoundaryRight(this.buffer, this.cursor);
        if (this.cursor > this.buffer.length - 1 && this.buffer.length > 0)
          this.cursor = this.buffer.length - 1;
        this.refreshLine();
        return;
      case "b":
        this.cursor = wordBoundaryLeft(this.buffer, this.cursor);
        this.refreshLine();
        return;
      case "e":
        this.cursor = wordEndRight(this.buffer, this.cursor);
        if (this.cursor > this.buffer.length - 1 && this.buffer.length > 0)
          this.cursor = this.buffer.length - 1;
        this.refreshLine();
        return;
      case "W":
        this.cursor = WORDBoundaryRight(this.buffer, this.cursor);
        if (this.cursor > this.buffer.length - 1 && this.buffer.length > 0)
          this.cursor = this.buffer.length - 1;
        this.refreshLine();
        return;
      case "B":
        this.cursor = WORDBoundaryLeft(this.buffer, this.cursor);
        this.refreshLine();
        return;
      case "E":
        this.cursor = WORDEndRight(this.buffer, this.cursor);
        if (this.cursor > this.buffer.length - 1 && this.buffer.length > 0)
          this.cursor = this.buffer.length - 1;
        this.refreshLine();
        return;
      case "0":
        this.cursor = 0;
        this.refreshLine();
        return;
      case "^":
        this.cursor = 0;
        while (
          this.cursor < this.buffer.length &&
          this.buffer[this.cursor] === " "
        )
          this.cursor++;
        this.refreshLine();
        return;
      case "$":
        this.cursor = Math.max(0, this.buffer.length - 1);
        this.refreshLine();
        return;
      case "i":
        this.viEnterInsert();
        return;
      case "a":
        if (this.cursor < this.buffer.length) this.cursor++;
        this.viEnterInsert();
        this.refreshLine();
        return;
      case "I":
        this.cursor = 0;
        this.viEnterInsert();
        this.refreshLine();
        return;
      case "A":
        this.cursor = this.buffer.length;
        this.viEnterInsert();
        this.refreshLine();
        return;
      case "s":
        if (this.cursor < this.buffer.length) {
          this.killRing = this.buffer[this.cursor];
          this.buffer =
            this.buffer.substring(0, this.cursor) +
            this.buffer.substring(this.cursor + 1);
          this.refreshLine();
        }
        this.viEnterInsert();
        return;
      case "S":
        this.killRing = this.buffer;
        this.buffer = "";
        this.cursor = 0;
        this.refreshLine();
        this.viEnterInsert();
        return;
      case "x":
        if (this.cursor < this.buffer.length) {
          this.killRing = this.buffer[this.cursor];
          this.buffer =
            this.buffer.substring(0, this.cursor) +
            this.buffer.substring(this.cursor + 1);
          if (this.cursor >= this.buffer.length && this.buffer.length > 0)
            this.cursor = this.buffer.length - 1;
          this.refreshLine();
        }
        return;
      case "X":
        if (this.cursor > 0) {
          this.killRing = this.buffer[this.cursor - 1];
          this.buffer =
            this.buffer.substring(0, this.cursor - 1) +
            this.buffer.substring(this.cursor);
          this.cursor--;
          this.refreshLine();
        }
        return;
      case "D":
        this.killRing = this.buffer.substring(this.cursor);
        this.buffer = this.buffer.substring(0, this.cursor);
        if (this.cursor > 0 && this.cursor >= this.buffer.length)
          this.cursor = this.buffer.length - 1;
        this.refreshLine();
        return;
      case "C":
        this.killRing = this.buffer.substring(this.cursor);
        this.buffer = this.buffer.substring(0, this.cursor);
        this.refreshLine();
        this.viEnterInsert();
        return;
      case "d":
        this.viOperator = "d";
        return;
      case "c":
        this.viOperator = "c";
        return;
      case "p":
        if (this.killRing) {
          var pos = Math.min(this.cursor + 1, this.buffer.length);
          this.buffer =
            this.buffer.substring(0, pos) +
            this.killRing +
            this.buffer.substring(pos);
          this.cursor = pos + this.killRing.length - 1;
          this.refreshLine();
        }
        return;
      case "P":
        if (this.killRing) {
          this.buffer =
            this.buffer.substring(0, this.cursor) +
            this.killRing +
            this.buffer.substring(this.cursor);
          this.cursor += this.killRing.length - 1;
          this.refreshLine();
        }
        return;
      case "k":
      case "\x1b[A": // Up arrow
        this.historyPrev();
        return;
      case "j":
      case "\x1b[B": // Down arrow
        this.historyNext();
        return;
    }
  };

  // --- Programmatic execution (async-aware) ---
  TermShell.prototype.exec = function (line, callback) {
    this.buffer = line;
    this.cursor = line.length;
    this.refreshLine();
    var self = this;
    setTimeout(function () {
      self.term.write("\r\n");
      if (line.trim()) {
        self.history.unshift(line);
      }
      self.pinCommand(line.trim());
      var result = dispatch(line, self.fs);
      if (result && typeof result.then === "function") {
        result.then(function (r) {
          if (r && r.output) {
            self.term.write(r.output);
            self.term.write("\r\n");
          }
          if (callback) callback();
        });
      } else {
        if (result) {
          if (result.clear) {
            self.term.clear();
          } else if (result.output) {
            self.term.write(result.output);
            self.term.write("\r\n");
          }
        }
        if (callback) callback();
      }
    }, 50);
  };

  // --- Character-by-character typing animation (async-aware) ---
  TermShell.prototype.autoType = function (line, callback) {
    var self = this;
    var i = 0;
    var speed = 35;
    var variance = 25;

    function typeNext() {
      if (i < line.length) {
        self.insertChar(line[i]);
        i++;
        var delay = speed + Math.floor(Math.random() * variance);
        setTimeout(typeNext, delay);
      } else {
        setTimeout(function () {
          self.term.write("\r\n");
          if (line.trim()) {
            self.history.unshift(line);
          }
          self.pinCommand(line.trim());
          var result = dispatch(line, self.fs);
          if (result && typeof result.then === "function") {
            result.then(function (r) {
              if (r && r.output) {
                self.term.write(r.output);
                self.term.write("\r\n");
              }
              if (callback) callback();
            });
          } else {
            if (result && result.output) {
              self.term.write(result.output);
              self.term.write("\r\n");
            }
            if (callback) callback();
          }
        }, 300);
      }
    }

    typeNext();
  };

  // ---------------------------------------------------------------
  // 9. Boot sequence
  // ---------------------------------------------------------------

  function boot() {
    var container = document.getElementById("playground-terminal");
    if (!container) return;

    var fileMap = window.__PLAYGROUND_FILES__ || {};
    var fs = new VirtualFS(fileMap);
    var shell = new TermShell(container, fs);

    shell.term.write(dim("  Loading CLI...") + "\r\n\r\n");

    loadWasm(fileMap)
      .then(function () {
        shell.prompt();
        shell.autoType("qualifier show src/parser.rs", function () {
          shell.prompt();
        });
      })
      .catch(function () {
        shell.term.write(red("  Failed to load wasm binary.") + "\r\n");
        shell.term.write(
          dim("  Shell builtins (ls, cat, help) are still available.") +
            "\r\n\r\n",
        );
        shell.prompt();
      });
  }

  // ---------------------------------------------------------------
  // 10. Init — wire up "Try in browser" button
  // ---------------------------------------------------------------

  function init() {
    var btn = document.getElementById("try-it-btn");
    var section = document.getElementById("playground-section");
    if (!btn || !section) return;

    btn.addEventListener("click", function () {
      section.hidden = false;
      section.scrollIntoView({ behavior: "smooth" });
      setTimeout(boot, 80);
      // Disable button so boot only runs once
      btn.disabled = true;
      btn.style.opacity = "0.5";
      btn.style.cursor = "default";
    });
  }

  // Run init when DOM is ready
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();
