import { h } from 'preact';
import { useRef, useState } from 'preact/hooks';

// ── Tokenizer ─────────────────────────────────────────────────────────────────

type TT = 'kw' | 'type' | 'str' | 'cmt' | 'num' | 'attr' | 'text';

const CUE_KEYWORDS = new Set([
  'package', 'import', 'for', 'in', 'if', 'let', 'true', 'false', 'null',
]);
const CUE_TYPES = new Set([
  'number', 'int', 'float', 'string', 'bytes', 'bool', 'uint', 'rune',
]);

// Identifiers that make sensible always-available completions.
const CUE_COMPLETION_WORDS = [...CUE_KEYWORDS, ...CUE_TYPES];

interface Tok { t: TT; s: string; }

function isAlpha(c: string) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_' || c === '#'; }
function isAlNum(c: string) { return isAlpha(c) || (c >= '0' && c <= '9'); }
function isDigit(c: string) { return c >= '0' && c <= '9'; }

function tokenize(src: string): Tok[] {
  const out: Tok[] = [];
  let i = 0;

  while (i < src.length) {
    const c = src[i];

    // Line comment
    if (c === '/' && src[i + 1] === '/') {
      const end = src.indexOf('\n', i);
      const stop = end < 0 ? src.length : end;
      out.push({ t: 'cmt', s: src.slice(i, stop) });
      i = stop;
      continue;
    }

    // Multiline string """...""" or '''...'''
    if ((c === '"' || c === "'") && src[i + 1] === c && src[i + 2] === c) {
      const q = c + c + c;
      const end = src.indexOf(q, i + 3);
      const stop = end < 0 ? src.length : end + 3;
      out.push({ t: 'str', s: src.slice(i, stop) });
      i = stop;
      continue;
    }

    // String "..." or '...'
    if (c === '"' || c === "'") {
      let j = i + 1;
      while (j < src.length) {
        if (src[j] === '\\') { j += 2; continue; }
        if (src[j] === c || src[j] === '\n') { j++; break; }
        j++;
      }
      out.push({ t: 'str', s: src.slice(i, j) });
      i = j;
      continue;
    }

    // Number
    if (isDigit(c) || (c === '.' && isDigit(src[i + 1] ?? ''))) {
      let j = i;
      while (j < src.length && (isDigit(src[j]) || src[j] === '.' || src[j] === '_')) j++;
      if (j < src.length && (src[j] === 'e' || src[j] === 'E')) {
        j++;
        if (src[j] === '+' || src[j] === '-') j++;
        while (j < src.length && isDigit(src[j])) j++;
      }
      out.push({ t: 'num', s: src.slice(i, j) });
      i = j;
      continue;
    }

    // Identifier / keyword / field label
    if (isAlpha(c)) {
      let j = i;
      while (j < src.length && isAlNum(src[j])) j++;
      const word = src.slice(i, j);
      // Peek past spaces for a ':' => this identifier is a field label.
      let k = j;
      while (k < src.length && (src[k] === ' ' || src[k] === '\t')) k++;
      let t: TT = 'text';
      if (CUE_KEYWORDS.has(word)) t = 'kw';
      else if (CUE_TYPES.has(word)) t = 'type';
      else if (src[k] === ':') t = 'attr';
      out.push({ t, s: word });
      i = j;
      continue;
    }

    out.push({ t: 'text', s: c });
    i++;
  }

  return out;
}

function renderTokens(src: string) {
  return tokenize(src).map((tok, idx) =>
    tok.t === 'text'
      ? <span key={idx}>{tok.s}</span>
      : <span key={idx} class={`cue-${tok.t}`}>{tok.s}</span>
  );
}

// ── Caret coordinates (mirror technique) ──────────────────────────────────────

function caretXY(ta: HTMLTextAreaElement): { left: number; top: number; lineHeight: number } {
  const style = getComputedStyle(ta);
  const div = document.createElement('div');
  const props = [
    'boxSizing', 'width', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
    'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
    'fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'tabSize',
  ] as const;
  for (const p of props) (div.style as any)[p] = (style as any)[p];
  div.style.position = 'absolute';
  div.style.visibility = 'hidden';
  div.style.whiteSpace = 'pre-wrap';
  div.style.wordWrap = 'break-word';
  div.style.overflow = 'hidden';

  div.textContent = ta.value.slice(0, ta.selectionStart);
  const marker = document.createElement('span');
  marker.textContent = ta.value.slice(ta.selectionStart) || '.';
  div.appendChild(marker);
  document.body.appendChild(div);
  const lineHeight = parseFloat(style.lineHeight) || (parseFloat(style.fontSize) * 1.4);
  const top = marker.offsetTop - ta.scrollTop;
  const left = marker.offsetLeft - ta.scrollLeft;
  document.body.removeChild(div);
  return { left, top, lineHeight };
}

// ── Component ─────────────────────────────────────────────────────────────────

interface Props {
  value: string;
  onChange: (v: string) => void;
  rows?: number;
  /** Parameter/signal names offered alongside the CUE vocabulary. */
  completions?: string[];
}

interface AC { items: string[]; index: number; from: number; left: number; top: number; }

export default function CueEditor({ value, onChange, rows = 14, completions = [] }: Props) {
  const preRef = useRef<HTMLPreElement>(null);
  const taRef = useRef<HTMLTextAreaElement>(null);
  const [ac, setAc] = useState<AC | null>(null);

  const vocab = [...new Set([...completions, ...CUE_COMPLETION_WORDS])];

  function syncScroll(e: Event) {
    const ta = e.target as HTMLTextAreaElement;
    if (preRef.current) {
      preRef.current.scrollTop = ta.scrollTop;
      preRef.current.scrollLeft = ta.scrollLeft;
    }
  }

  function wordPrefix(ta: HTMLTextAreaElement): { word: string; from: number } {
    const caret = ta.selectionStart;
    let from = caret;
    while (from > 0 && /[A-Za-z0-9_#]/.test(ta.value[from - 1])) from--;
    return { word: ta.value.slice(from, caret), from };
  }

  function refreshAC(ta: HTMLTextAreaElement) {
    const { word, from } = wordPrefix(ta);
    if (word.length < 1) { setAc(null); return; }
    const lower = word.toLowerCase();
    const items = vocab.filter((w) => w.toLowerCase().startsWith(lower) && w !== word).slice(0, 8);
    if (items.length === 0) { setAc(null); return; }
    const { left, top, lineHeight } = caretXY(ta);
    const rect = ta.getBoundingClientRect();
    setAc({ items, index: 0, from, left: rect.left + left, top: rect.top + top + lineHeight });
  }

  function accept(item: string) {
    const ta = taRef.current;
    if (!ta || !ac) return;
    const caret = ta.selectionStart;
    const next = value.slice(0, ac.from) + item + value.slice(caret);
    onChange(next);
    setAc(null);
    const pos = ac.from + item.length;
    requestAnimationFrame(() => {
      ta.focus();
      ta.setSelectionRange(pos, pos);
    });
  }

  function onKeyDown(e: KeyboardEvent) {
    if (!ac) {
      // Ctrl+Space forces the completion popup open.
      if (e.key === ' ' && e.ctrlKey) {
        e.preventDefault();
        refreshAC(e.target as HTMLTextAreaElement);
      }
      return;
    }
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setAc({ ...ac, index: (ac.index + 1) % ac.items.length });
        break;
      case 'ArrowUp':
        e.preventDefault();
        setAc({ ...ac, index: (ac.index - 1 + ac.items.length) % ac.items.length });
        break;
      case 'Enter':
      case 'Tab':
        e.preventDefault();
        accept(ac.items[ac.index]);
        break;
      case 'Escape':
        e.preventDefault();
        setAc(null);
        break;
    }
  }

  return (
    <div class="cue-editor" style={`height:${rows * 1.5}em;`}>
      <pre ref={preRef} class="cue-pre" aria-hidden="true">
        {renderTokens(value)}
        {'\n'}
      </pre>
      <textarea
        ref={taRef}
        class="cue-textarea"
        value={value}
        spellcheck={false}
        autocomplete="off"
        onInput={(e) => { onChange((e.target as HTMLTextAreaElement).value); refreshAC(e.target as HTMLTextAreaElement); }}
        onKeyDown={onKeyDown}
        onScroll={syncScroll}
        onBlur={() => setTimeout(() => setAc(null), 120)}
      />
      {ac && (
        <ul class="cue-ac" style={`left:${ac.left}px;top:${ac.top}px;`}>
          {ac.items.map((it, i) => (
            <li
              key={it}
              class={i === ac.index ? 'cue-ac-item active' : 'cue-ac-item'}
              onMouseDown={(e) => { e.preventDefault(); accept(it); }}
            >
              {it}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
