// console-autocomplete.js
// Vanilla dropdown autocomplete for [data-console-form] inputs.
// Reads ranked completions from window.__rankVocab (loaded via
// vocab-rank.bundle.js). Owns: Tab, Shift+Tab, Esc, mouse events.
// Leaves: ArrowUp, ArrowDown, Enter (console-history.js owns those).
//
// First-token only: the dropdown is hidden as soon as the cursor
// is past the first space in the input.
//
// Listeners are delegated to the form (not the input) so the input
// element can be swapped via HTMX without breaking autocomplete.
// Document- and window-level handlers are module-scoped and operate
// on the currently-open dropdown via the activeBinding pointer.

const VOCAB_URL = "/static/data/srccfg-vocab.json";
const MAX_RENDERED = 8;

// Module-scoped state.
let vocabPromise = null;
let vocab = null;
// activeBinding assumes a single dropdown open at a time. Today the
// project has one console form per page; multi-form support would
// need per-form open-state tracking (e.g. a Set) here.
let activeBinding = null;

function loadVocab() {
  if (vocabPromise) return vocabPromise;
  vocabPromise = fetch(VOCAB_URL, { credentials: "same-origin" })
    .then(r => r.ok ? r.json() : Promise.reject(new Error("vocab fetch failed: " + r.status)))
    .then(v => { vocab = v; return v; })
    .catch(err => { console.warn("[console-autocomplete] vocab load failed", err); return null; });
  return vocabPromise;
}

function firstTokenSlice(value, caret) {
  // Returns the substring [0, end-of-first-token) if the caret is
  // within the first token; otherwise null.
  const spaceIdx = value.indexOf(" ");
  if (spaceIdx === -1) {
    return { token: value, from: 0, to: value.length };
  }
  if (caret > spaceIdx) return null;
  return { token: value.slice(0, spaceIdx), from: 0, to: spaceIdx };
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, c => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
  }[c]));
}

// Module-scoped document/window listeners installed once.
// Operate on whichever binding has its dropdown currently open.
document.addEventListener("mousedown", (event) => {
  if (!activeBinding) return;
  const dropdown = activeBinding.getDropdown();
  if (!dropdown || dropdown.style.display === "none") return;
  const row = event.target.closest(".console-autocomplete-row");
  if (!row || !dropdown.contains(row)) return;
  event.preventDefault();
  activeBinding.acceptRow(row);
});

window.addEventListener("resize", () => {
  if (activeBinding) activeBinding.position();
});

window.addEventListener("scroll", () => {
  if (activeBinding) activeBinding.position();
}, true);

function bindConsoleAutocomplete(form) {
  if (form.dataset.consoleAutocompleteBound === "true") return;
  form.dataset.consoleAutocompleteBound = "true";

  // Per-binding state.
  let dropdown = null;
  let items = [];
  let highlightIdx = 0;
  let focusHandledOnce = false;

  function getInput() {
    return form.querySelector("input[name='command']");
  }

  function ensureDropdown() {
    if (dropdown) return dropdown;
    dropdown = document.createElement("div");
    dropdown.className = "console-autocomplete-dropdown";
    dropdown.setAttribute("role", "listbox");
    dropdown.style.display = "none";
    document.body.appendChild(dropdown);
    return dropdown;
  }

  function position() {
    const input = getInput();
    if (!input || !dropdown) return;
    const rect = input.getBoundingClientRect();
    dropdown.style.left = `${rect.left + window.scrollX}px`;
    dropdown.style.top = `${rect.bottom + window.scrollY}px`;
    dropdown.style.minWidth = `${rect.width}px`;
  }

  function close() {
    if (!dropdown) {
      if (activeBinding === binding) activeBinding = null;
      return;
    }
    dropdown.style.display = "none";
    items = [];
    highlightIdx = 0;
    if (activeBinding === binding) activeBinding = null;
  }

  function render() {
    ensureDropdown();
    if (items.length === 0) { close(); return; }
    const rows = items.slice(0, MAX_RENDERED).map((e, i) => {
      const selected = i === highlightIdx ? " aria-selected='true'" : "";
      // SourceMod commands (sm_*) get a distinct class so the user
      // can see at a glance that the command depends on a plugin
      // being loaded on the target server.
      const kindClass = e.kind === "command"
        ? (e.name.startsWith("sm_") ? "kind-sourcemod" : "kind-command")
        : "kind-cvar";
      const desc = e.desc ? `<span class="console-autocomplete-desc">${escapeHtml(e.desc)}</span>` : "";
      return `<div class="console-autocomplete-row ${kindClass}"${selected} role="option" data-idx="${i}"><span class="console-autocomplete-name">${escapeHtml(e.name)}</span>${desc}</div>`;
    }).join("");
    dropdown.innerHTML = rows;
    dropdown.style.display = "block";
    activeBinding = binding;
    position();
  }

  function acceptHighlighted() {
    if (items.length === 0) return;
    const input = getInput();
    if (!input) return;
    const chosen = items[highlightIdx];
    const slice = firstTokenSlice(input.value, input.selectionStart || 0);
    if (!slice) return;
    if (slice.token === chosen.name) { close(); return; }
    const before = input.value.slice(0, slice.from);
    const after = input.value.slice(slice.to);
    input.value = before + chosen.name + after;
    const caret = before.length + chosen.name.length;
    input.setSelectionRange(caret, caret);
    recompute();
  }

  function acceptRow(rowEl) {
    highlightIdx = parseInt(rowEl.dataset.idx, 10) || 0;
    acceptHighlighted();
    const input = getInput();
    if (input) input.focus();
  }

  function recompute() {
    if (!vocab) return;
    if (typeof window.__rankVocab !== "function") {
      console.warn("[console-autocomplete] window.__rankVocab unavailable — vocab-rank.bundle.js failed to load?");
      return;
    }
    const input = getInput();
    if (!input) return;
    const slice = firstTokenSlice(input.value, input.selectionStart || 0);
    if (!slice || !slice.token) { close(); return; }
    items = window.__rankVocab(slice.token, vocab);
    if (items.length === 0) { close(); return; }
    highlightIdx = 0;
    render();
  }

  // Binding object exposed to module-scope listeners.
  // getDropdown() returns the current dropdown reference, which is
  // assigned lazily by ensureDropdown(); the getter is needed because
  // the dropdown variable's binding changes after construction.
  const binding = {
    getDropdown: () => dropdown,
    position,
    close,
    acceptRow,
  };

  // Form-level event delegation. event.target.matches gates each
  // handler to the command input, so the input element can be
  // swapped via HTMX without rebinding.

  form.addEventListener("focusin", async (event) => {
    if (!event.target.matches('input[name="command"]')) return;
    if (focusHandledOnce) return;
    focusHandledOnce = true;
    if (!vocab) {
      await loadVocab();
      // If the user typed during the fetch, rank now so the dropdown
      // doesn't appear to lag a keystroke behind on cold load.
      if (vocab && document.activeElement === event.target) recompute();
    }
  });

  form.addEventListener("input", (event) => {
    if (!event.target.matches('input[name="command"]')) return;
    if (!vocab) return; // fetch may not have resolved yet
    recompute();
  });

  form.addEventListener("keydown", (event) => {
    if (!event.target.matches('input[name="command"]')) return;
    if (event.key === "Tab" && !event.shiftKey) {
      if (items.length > 0) {
        event.preventDefault();
        acceptHighlighted();
      }
    } else if (event.key === "Tab" && event.shiftKey) {
      if (items.length > 0) {
        event.preventDefault();
        highlightIdx = (highlightIdx - 1 + Math.min(items.length, MAX_RENDERED))
                       % Math.min(items.length, MAX_RENDERED);
        render();
      }
    } else if (event.key === "Escape") {
      if (dropdown && dropdown.style.display !== "none") {
        event.preventDefault();
        close();
      }
    }
    // ArrowUp/ArrowDown/Enter intentionally NOT handled here.
  });

  form.addEventListener("focusout", (event) => {
    if (!event.target.matches('input[name="command"]')) return;
    // Delay close so a click on a dropdown row can fire first.
    setTimeout(close, 100);
  });

  // HTMX form submission clears the input; close on submit.
  form.addEventListener("htmx:beforeRequest", close);
}

function bindAll(root) {
  if (!root) return;
  const scope = root.matches && root.matches("[data-console-form]") ? [root] : [];
  if (root.querySelectorAll) {
    root.querySelectorAll("[data-console-form]").forEach((el) => scope.push(el));
  }
  scope.forEach(bindConsoleAutocomplete);
}

document.addEventListener("DOMContentLoaded", () => bindAll(document));
document.addEventListener("htmx:load", (event) => bindAll(event.detail.elt));
