import { store } from '../core/store';

/**
 * Soft-fill autofill: as the user types into a text input or an on-canvas text
 * box, the best-matching saved term is shown as a muted GHOST at the caret.
 * Tab / Enter accepts it; typing anything else ignores it; clicking out keeps
 * exactly what was typed.
 *
 * The completion is NEVER part of the element's value/text — it lives in a
 * separate `pointer-events:none` <span> pinned to the caret — which is what
 * makes "click out keeps only what you typed" true. Accepting programmatically
 * writes the full term and fires `input`/`change` so any bindings update.
 */

// ---------- matching ----------

/**
 * Among `terms` whose lowercase starts with `prefix` (case-insensitive), return
 * the SHORTEST one (earliest in the list wins ties), in its original casing.
 * Shortest-first is the rule the product spec's example implies: with
 * [Apple, App, Barry, Cargo], "a" → "App" (3 < 5), "appl" → "Apple".
 */
export function bestMatch(prefix: string, terms: string[]): string | null {
  if (!prefix.trim()) return null;
  const lo = prefix.toLowerCase();
  let best: string | null = null;
  for (const t of terms) {
    if (t.toLowerCase().startsWith(lo) && (best === null || t.length < best.length)) best = t;
  }
  return best;
}

function pad2(n: number): string {
  return String(n).padStart(2, '0');
}

/**
 * Today's date as virtual autofill terms (MM/DD/YY and MM/DD/YYYY), so typing
 * "07" on 2026-07-11 suggests "07/11/26" (shortest wins over the 4-digit form).
 * Recomputed on every keystroke, never persisted, never counted against the 10.
 */
export function dateTerms(now: Date = new Date()): string[] {
  const mm = pad2(now.getMonth() + 1);
  const dd = pad2(now.getDate());
  const yyyy = now.getFullYear();
  return [`${mm}/${dd}/${pad2(yyyy % 100)}`, `${mm}/${dd}/${yyyy}`];
}

/** The effective match pool = the user's saved terms + today's date terms. */
export function autofillPool(): string[] {
  if (!store.autofillEnabled) return [];
  return [...store.autofillTerms, ...dateTerms()];
}

// ---------- controller ----------

export interface AutofillOptions {
  /** Override the match pool (defaults to `autofillPool`). */
  getTerms?: () => string[];
  /** Gate the whole behavior (defaults to `store.autofillEnabled`). */
  enabled?: () => boolean;
  /** Optional extra hook fired after a term is accepted. */
  onAccept?: (full: string) => void;
}

type Editable = HTMLInputElement | HTMLTextAreaElement | HTMLElement;

/** Build a canvas-usable / CSS font string from computed style. */
function fontString(cs: CSSStyleDeclaration, withLineHeight: boolean): string {
  const lh = withLineHeight && cs.lineHeight && cs.lineHeight !== 'normal' ? `/${cs.lineHeight}` : '';
  return `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize}${lh} ${cs.fontFamily}`;
}

let measureCtx: CanvasRenderingContext2D | null = null;
function measureWidth(text: string, font: string): number {
  if (!measureCtx) measureCtx = document.createElement('canvas').getContext('2d');
  if (!measureCtx) return 0;
  measureCtx.font = font;
  return measureCtx.measureText(text).width;
}

/**
 * Attach soft-fill to an <input>/<textarea> (value mode) or a contenteditable
 * element (text mode). Returns a `detach()` that tears everything down.
 *
 * Listener hygiene: element-local listeners die with the node, and the global
 * scroll/resize/pointerdown listeners only exist while the element is focused
 * (the ghost only while shown), so an input the properties panel creates and
 * later discards without calling detach() leaks nothing.
 */
export function attachAutofill(el: Editable, opts: AutofillOptions = {}): () => void {
  const isCE = el.isContentEditable;
  const input = el as HTMLInputElement | HTMLTextAreaElement;
  const getTerms = opts.getTerms ?? autofillPool;
  const enabled = opts.enabled ?? (() => store.autofillEnabled);

  let ghost: HTMLSpanElement | null = null;
  let activeMatch: string | null = null; // full term when a completion is showing
  let composing = false;
  let detached = false;

  const readTyped = (): string => (isCE ? (el.textContent ?? '').replace(/\n$/, '') : input.value);

  const caretAtEnd = (): boolean => {
    if (!isCE) return input.selectionStart === input.value.length && input.selectionEnd === input.value.length;
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return false;
    const caret = sel.getRangeAt(0);
    const r = document.createRange();
    r.selectNodeContents(el);
    r.setStart(caret.endContainer, caret.endOffset);
    return r.toString().length === 0;
  };

  const removeGhost = () => { ghost?.remove(); ghost = null; };
  const hide = () => { activeMatch = null; removeGhost(); };

  const reposition = () => {
    if (!ghost) return;
    if (!document.contains(el)) { teardown(); return; } // node destroyed mid-edit (zoom/tool change)
    const cs = getComputedStyle(el);
    if (!isCE) {
      const r = el.getBoundingClientRect();
      const padL = parseFloat(cs.paddingLeft) || 0;
      const padT = parseFloat(cs.paddingTop) || 0;
      const padR = parseFloat(cs.paddingRight) || 0;
      const bL = parseFloat(cs.borderLeftWidth) || 0;
      const bT = parseFloat(cs.borderTopWidth) || 0;
      const tw = measureWidth(input.value, fontString(cs, false));
      const x = r.left + padL + bL - input.scrollLeft + tw;
      if (x >= r.right - padR) { removeGhost(); return; } // no room — don't overflow the box
      ghost.style.left = `${x}px`;
      ghost.style.top = `${r.top + padT + bT}px`;
    } else {
      const sel = window.getSelection();
      let rect: DOMRect | null = null;
      if (sel && sel.rangeCount) {
        const cr = sel.getRangeAt(0).cloneRange();
        cr.collapse(false);
        rect = cr.getBoundingClientRect();
      }
      if (!rect || (rect.left === 0 && rect.top === 0 && rect.width === 0)) {
        // empty box / no caret rect — fall back to the element's content box
        const r = el.getBoundingClientRect();
        ghost.style.left = `${r.left + (parseFloat(cs.paddingLeft) || 0)}px`;
        ghost.style.top = `${r.top + (parseFloat(cs.paddingTop) || 0)}px`;
      } else {
        ghost.style.left = `${rect.right}px`;
        ghost.style.top = `${rect.top}px`;
      }
    }
  };

  const showGhost = (suffix: string) => {
    if (!ghost) {
      ghost = document.createElement('span');
      ghost.className = 'autofill-ghost';
      document.body.appendChild(ghost);
    }
    const cs = getComputedStyle(el);
    ghost.style.font = fontString(cs, true);
    ghost.style.letterSpacing = cs.letterSpacing;
    ghost.textContent = suffix;
    reposition();
  };

  const recompute = () => {
    if (composing) return;
    if (!enabled()) { hide(); return; }
    const prefix = readTyped();
    if (!caretAtEnd()) { hide(); return; }
    const match = bestMatch(prefix, getTerms());
    const suffix = match && match.length > prefix.length ? match.slice(prefix.length) : '';
    if (suffix) { activeMatch = match; showGhost(suffix); }
    else hide();
  };

  const accept = (): boolean => {
    if (!activeMatch) return false;
    const full = activeMatch;
    if (!isCE) {
      input.value = full;
      input.setSelectionRange(full.length, full.length);
      hide();
      input.dispatchEvent(new Event('input', { bubbles: true }));
      input.dispatchEvent(new Event('change', { bubbles: true }));
    } else {
      el.textContent = full;
      const r = document.createRange();
      r.selectNodeContents(el);
      r.collapse(false);
      const sel = window.getSelection();
      sel?.removeAllRanges();
      sel?.addRange(r);
      hide();
      el.dispatchEvent(new Event('input', { bubbles: true }));
    }
    opts.onAccept?.(full);
    return true;
  };

  // ---- events ----
  const onKeydown = (ev: Event) => {
    const e = ev as KeyboardEvent;
    if (e.isComposing) return;
    const active = activeMatch !== null;
    if (active && (e.key === 'Tab' || e.key === 'Enter')) {
      e.preventDefault(); e.stopPropagation(); accept(); return;
    }
    if (active && e.key === 'ArrowRight' && caretAtEnd()) {
      e.preventDefault(); accept(); return;
    }
    // Escape hides the ghost but does NOT consume — the overlay's blur / a
    // modal's close still fire as before.
    if (e.key === 'Escape') { if (active) hide(); return; }
    // any caret-moving key drops the ghost; the next `input` recomputes it
    if (e.key.startsWith('Arrow') || e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown') hide();
  };
  const onInput = () => recompute();
  const onCompositionStart = () => { composing = true; hide(); };
  const onCompositionEnd = () => { composing = false; recompute(); };

  // global (only while focused)
  const onScroll = () => reposition();
  const onResize = () => reposition();
  const onDocPointerDown = () => hide(); // clicking anywhere never auto-fills

  let globalsOn = false;
  const addGlobals = () => {
    if (globalsOn) return;
    globalsOn = true;
    window.addEventListener('scroll', onScroll, { capture: true, passive: true });
    window.addEventListener('resize', onResize);
    document.addEventListener('pointerdown', onDocPointerDown, true);
  };
  const removeGlobals = () => {
    if (!globalsOn) return;
    globalsOn = false;
    window.removeEventListener('scroll', onScroll, { capture: true });
    window.removeEventListener('resize', onResize);
    document.removeEventListener('pointerdown', onDocPointerDown, true);
  };

  const onFocus = () => { addGlobals(); recompute(); };
  const onBlur = () => { removeGlobals(); hide(); };

  el.addEventListener('keydown', onKeydown);
  el.addEventListener('input', onInput);
  el.addEventListener('focus', onFocus);
  el.addEventListener('blur', onBlur);
  el.addEventListener('compositionstart', onCompositionStart);
  el.addEventListener('compositionend', onCompositionEnd);

  // the overlay focuses the box BEFORE attaching, so run focus setup now
  if (document.activeElement === el) onFocus();

  function teardown() {
    if (detached) return;
    detached = true;
    removeGlobals();
    hide();
    el.removeEventListener('keydown', onKeydown);
    el.removeEventListener('input', onInput);
    el.removeEventListener('focus', onFocus);
    el.removeEventListener('blur', onBlur);
    el.removeEventListener('compositionstart', onCompositionStart);
    el.removeEventListener('compositionend', onCompositionEnd);
  }
  return teardown;
}
