// ============================================================================
// CWR SYNTAX HIGHLIGHTER  ·  field-level color-coded preview
// ----------------------------------------------------------------------------
// Renders a CWR transmission as a color-coded document. Unlike the legacy
// LinesView (plain monospace with only the 3-char record type colored), this
// component tokenizes each line into named fields using CwrDecode.fieldsFor()
// and colors every field by its semantic kind:
//
//   • text  →  default ink
//   • num   →  numeric tint
//   • date  →  date tint
//   • time  →  time tint
//   • flag  →  flag tint (Y/N/U/etc)
//   • ipi   →  identifier tint (with mod-101 ring on hover)
//   • iswc  →  identifier tint (with mod-10 ring on hover)
//
// Record-type prefix (HDR/GRH/NWR/SPU/...) is colored by family:
//   control (HDR/GRH/GRT/TRL), work (NWR/REV/ISW), publisher (SPU/OPU/SPT/OPT),
//   writer (SWR/OWR/SWT/OWT), link (PWR), title (ALT/EWT/VER), perf (PER/REC),
//   non-roman (NAT/NCT/NPN/NWN/NVT/NRA), other (ORN/INS/IND/COM/MSG/ARI/XRF/ACK).
//
// Empty fields (space-padded only) render at 30% opacity so the eye skips them.
// Hover any field for tooltip with field-name + decoded value. Click any line
// to drive the right-hand Inspector (preserves existing flow).
//
// Three themes:
//   editorial  — subtle, low-contrast, ASTRO default (paper bg)
//   vibrant    — high-contrast saturated tints
//   terminal   — green-amber-red on near-black bg (VT100 vibe)
//
// EXPORT: window.CwrHighlight = { Viewer, tokenizeLine, THEMES }
// ============================================================================

(function () {
  'use strict';
  const { useState, useMemo, useRef, useEffect, useCallback } = React;

  // ─── Record-type families ─────────────────────────────────────────────────
  const RECORD_FAMILIES = {
    control:   ['HDR','GRH','GRT','TRL'],
    work:      ['NWR','REV','ISW','EXC','AGR'],
    publisher: ['SPU','OPU','SPT','OPT'],
    writer:    ['SWR','OWR','SWT','OWT'],
    link:      ['PWR','IPA'],
    title:     ['ALT','EWT','VER','COM'],
    perf:      ['PER','REC','ORN'],
    nonroman:  ['NAT','NCT','NPN','NWN','NVT','NRA','NPA','NOW'],
    detail:    ['INS','IND','MSG','ARI','XRF','ACK','TER'],
  };
  const FAMILY_OF = (() => {
    const m = {};
    Object.entries(RECORD_FAMILIES).forEach(([fam, codes]) => codes.forEach((c) => { m[c] = fam; }));
    return m;
  })();

  // ─── Themes ───────────────────────────────────────────────────────────────
  const THEMES = {
    editorial: {
      name: 'Editorial',
      bg: 'var(--paper)',
      gutter: 'var(--ink-4)',
      gutterBg: 'transparent',
      controlBg: 'rgba(0,0,0,0.025)',
      activeBg: 'var(--bg-2)',
      activeBar: 'var(--ink)',
      ink: 'var(--ink)',
      empty: 'rgba(0,0,0,0.20)',
      kinds: {
        text:  'var(--ink)',
        num:   '#1a5fb4',
        date:  '#7c4dff',
        time:  '#9333a8',
        flag:  '#d68910',
        ipi:   '#0c8a7b',
        iswc:  '#0c8a7b',
      },
      family: {
        control:   '#0f5c66',
        work:      '#9c2a48',
        publisher: '#1a5fb4',
        writer:    '#0c8a4d',
        link:      '#7c4dff',
        title:     '#a05000',
        perf:      '#9333a8',
        nonroman:  '#7a6300',
        detail:    'var(--ink-2)',
      },
    },
    vibrant: {
      name: 'Vibrant',
      bg: '#fafaf7',
      gutter: '#999',
      gutterBg: '#f0efea',
      controlBg: '#fff4d6',
      activeBg: '#dceaff',
      activeBar: '#1a5fb4',
      ink: '#1c1c1c',
      empty: 'rgba(0,0,0,0.18)',
      kinds: {
        text:  '#1c1c1c',
        num:   '#0050b0',
        date:  '#6a1b9a',
        time:  '#ad1457',
        flag:  '#e65100',
        ipi:   '#00695c',
        iswc:  '#00695c',
      },
      family: {
        control:   '#004d40',
        work:      '#b71c1c',
        publisher: '#0050b0',
        writer:    '#1b5e20',
        link:      '#4527a0',
        title:     '#bf360c',
        perf:      '#880e4f',
        nonroman:  '#827717',
        detail:    '#37474f',
      },
    },
    terminal: {
      name: 'Terminal',
      bg: '#0d1117',
      gutter: '#4a5568',
      gutterBg: '#0a0d12',
      controlBg: 'rgba(255,255,255,0.04)',
      activeBg: 'rgba(120,200,255,0.10)',
      activeBar: '#58a6ff',
      ink: '#c9d1d9',
      empty: 'rgba(201,209,217,0.22)',
      kinds: {
        text:  '#c9d1d9',
        num:   '#79c0ff',
        date:  '#d2a8ff',
        time:  '#ff7b72',
        flag:  '#ffa657',
        ipi:   '#7ee787',
        iswc:  '#7ee787',
      },
      family: {
        control:   '#39d0d8',
        work:      '#ff7b72',
        publisher: '#79c0ff',
        writer:    '#7ee787',
        link:      '#d2a8ff',
        title:     '#ffa657',
        perf:      '#f778ba',
        nonroman:  '#e3b341',
        detail:    '#8b949e',
      },
    },
  };

  // ─── Tokenizer ────────────────────────────────────────────────────────────
  // Returns array of { name, value, raw, kind, start, end, empty, lookup }
  function tokenizeLine(line, version) {
    if (!line || line.length < 3) {
      return [{ name: 'Raw', value: line || '', raw: line || '', kind: 'text', start: 0, end: (line || '').length, empty: !line }];
    }
    const recType = line.slice(0, 3);
    const fields = window.CwrDecode && window.CwrDecode.fieldsFor
      ? window.CwrDecode.fieldsFor(recType, version)
      : null;
    if (!fields) {
      return [
        { name: 'Record type', value: recType, raw: recType, kind: 'rec', start: 0, end: 3 },
        { name: 'Body', value: line.slice(3), raw: line.slice(3), kind: 'text', start: 3, end: line.length },
      ];
    }
    const out = [];
    let pos = 0;
    fields.forEach((f) => {
      const [name, len, kind, lookup] = f;
      const raw = line.slice(pos, pos + len);
      const value = raw.replace(/\s+$/, '');
      const empty = value.length === 0;
      out.push({
        name, value, raw, kind: name === 'Record type' ? 'rec' : kind,
        start: pos, end: pos + len, empty, lookup,
      });
      pos += len;
    });
    if (pos < line.length) {
      out.push({ name: 'Trailing', value: line.slice(pos), raw: line.slice(pos), kind: 'text', start: pos, end: line.length });
    }
    return out;
  }

  // ─── Field tooltip — value + decoded code if lookup present ───────────────
  function tokenTitle(tok, recType) {
    const parts = [`${tok.name} · cols ${tok.start + 1}-${tok.end}`];
    if (tok.value) parts.push(`"${tok.value}"`);
    if (tok.lookup && tok.value && window.CwrDecode && window.CwrDecode.lookupCode) {
      const decoded = window.CwrDecode.lookupCode(tok.lookup, tok.value);
      if (decoded) parts.push(`→ ${decoded}`);
    }
    return parts.join(' · ');
  }

  // ─── Single-line renderer ─────────────────────────────────────────────────
  function HighlightedLine({ line, idx, version, theme, active, onPick, hideEmpty, fold, search }) {
    const recType = line.slice(0, 3);
    const family = FAMILY_OF[recType] || 'detail';
    const tokens = useMemo(() => tokenizeLine(line, version), [line, version]);
    const familyColor = theme.family[family] || theme.ink;
    const isControl = family === 'control';
    if (fold && isControl && !active) {
      // collapsed control rows still show but compact
    }

    // search highlighting — wrap matches inside text fields
    const searchLc = (search || '').toLowerCase();
    const matchesSearch = searchLc && line.toLowerCase().includes(searchLc);

    return (
      <div onClick={() => onPick(idx)}
        data-line={idx}
        style={{
          display: 'flex', cursor: 'pointer', padding: '0 12px',
          background: active ? theme.activeBg : (matchesSearch ? 'rgba(255, 235, 0, 0.10)' : (isControl ? theme.controlBg : 'transparent')),
          borderLeft: '3px solid ' + (active ? theme.activeBar : 'transparent'),
          minHeight: 22,
        }}>
        {/* gutter */}
        <span style={{
          width: 50, color: theme.gutter, flex: '0 0 50px', userSelect: 'none',
          background: theme.gutterBg, paddingRight: 8, textAlign: 'right',
        }}>{String(idx + 1).padStart(4, '0')}</span>
        {/* tokens */}
        <span style={{ whiteSpace: 'pre', minWidth: 'max-content' }}>
          {tokens.map((tok, ti) => {
            if (tok.empty && hideEmpty) {
              return <span key={ti} title={tokenTitle(tok, recType)} style={{ color: 'transparent' }}>{tok.raw}</span>;
            }
            let color = theme.kinds[tok.kind] || theme.ink;
            let weight = 400;
            if (tok.kind === 'rec') {
              color = familyColor;
              weight = 700;
            }
            const op = tok.empty ? 0.18 : 1;
            return (
              <span key={ti}
                title={tokenTitle(tok, recType)}
                style={{
                  color, fontWeight: weight, opacity: op,
                  borderBottom: tok.kind === 'ipi' || tok.kind === 'iswc' ? '1px dotted currentColor' : 'none',
                }}>{tok.raw}</span>
            );
          })}
        </span>
      </div>
    );
  }

  // ─── Main viewer ──────────────────────────────────────────────────────────
  function Viewer({ lines, version, active, onPick, themeName: themeNameProp, onThemeChange, compact }) {
    const [themeName, setThemeName] = useState(themeNameProp || 'editorial');
    const [hideEmpty, setHideEmpty] = useState(false);
    const [foldControl, setFoldControl] = useState(false);
    const [search, setSearch] = useState('');
    const [showLegend, setShowLegend] = useState(false);
    const scrollRef = useRef(null);

    useEffect(() => { if (themeNameProp) setThemeName(themeNameProp); }, [themeNameProp]);
    const setTheme = useCallback((n) => { setThemeName(n); onThemeChange && onThemeChange(n); }, [onThemeChange]);
    const theme = THEMES[themeName] || THEMES.editorial;

    // record-type counts for the legend
    const counts = useMemo(() => {
      const c = {};
      lines.forEach((l) => { const r = l.slice(0, 3); c[r] = (c[r] || 0) + 1; });
      return c;
    }, [lines]);

    // visible lines (fold collapses runs of control lines? no — keep all visible, just style)
    const visibleLines = lines;

    // Auto-scroll active line into view
    useEffect(() => {
      if (!scrollRef.current || active == null) return;
      const el = scrollRef.current.querySelector(`[data-line="${active}"]`);
      if (el) {
        const box = scrollRef.current;
        const top = el.offsetTop;
        const h = box.clientHeight;
        if (top < box.scrollTop || top > box.scrollTop + h - 22) {
          box.scrollTop = Math.max(0, top - h / 2);
        }
      }
    }, [active]);

    const copyAll = useCallback(() => {
      const text = lines.join('\n');
      if (navigator.clipboard) navigator.clipboard.writeText(text);
    }, [lines]);

    const downloadFile = useCallback(() => {
      const text = lines.join('\r\n');
      const blob = new Blob([text], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url; a.download = `transmission-v${version}.V${(version || '').replace(/\D/g, '').slice(0, 2)}1`;
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    }, [lines, version]);

    return (
      <div style={{ display: 'flex', flexDirection: 'column', minHeight: 0, background: theme.bg, color: theme.ink }}>
        {/* toolbar */}
        {!compact && (
          <div style={{
            display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
            borderBottom: '1px solid var(--rule)', background: 'var(--bg)', flexWrap: 'wrap',
          }}>
            {/* theme picker */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
              <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)' }}>THEME</span>
              {Object.keys(THEMES).map((k) => (
                <button key={k} onClick={() => setTheme(k)} className="ff-mono"
                  style={{
                    padding: '4px 9px', fontSize: 10, fontWeight: 600,
                    background: themeName === k ? 'var(--ink)' : 'var(--paper)',
                    color: themeName === k ? 'var(--paper)' : 'var(--ink)',
                    border: '1px solid var(--rule)', cursor: 'pointer', textTransform: 'lowercase',
                  }}>{THEMES[k].name}</button>
              ))}
            </div>

            <div style={{ width: 1, height: 16, background: 'var(--rule)' }} />

            {/* toggles */}
            <button onClick={() => setHideEmpty((v) => !v)} className="ff-mono"
              style={{ padding: '4px 9px', fontSize: 10, fontWeight: 600,
                background: hideEmpty ? 'var(--ink)' : 'var(--paper)',
                color: hideEmpty ? 'var(--paper)' : 'var(--ink)',
                border: '1px solid var(--rule)', cursor: 'pointer' }}
              title="Hide empty (space-padded) fields">empty fields</button>
            <button onClick={() => setShowLegend((v) => !v)} className="ff-mono"
              style={{ padding: '4px 9px', fontSize: 10, fontWeight: 600,
                background: showLegend ? 'var(--ink)' : 'var(--paper)',
                color: showLegend ? 'var(--paper)' : 'var(--ink)',
                border: '1px solid var(--rule)', cursor: 'pointer' }}>legend</button>

            <div style={{ flex: 1 }} />

            {/* search */}
            <div style={{ position: 'relative' }}>
              <input value={search} onChange={(e) => setSearch(e.target.value)}
                placeholder="filter lines…" className="ff-mono"
                style={{ width: 180, padding: '5px 9px', fontSize: 11,
                  background: 'var(--paper)', border: '1px solid var(--rule)' }} />
            </div>

            {/* copy / download */}
            <button onClick={copyAll} className="ff-mono"
              style={{ padding: '4px 9px', fontSize: 10, fontWeight: 600,
                background: 'var(--paper)', border: '1px solid var(--rule)', cursor: 'pointer' }}>copy</button>
            <button onClick={downloadFile} className="ff-mono"
              style={{ padding: '4px 9px', fontSize: 10, fontWeight: 600,
                background: 'var(--paper)', border: '1px solid var(--rule)', cursor: 'pointer' }}>download</button>
          </div>
        )}

        {/* legend */}
        {showLegend && (
          <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--rule)', background: theme.bg }}>
            <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>RECORD FAMILIES · {Object.keys(counts).length} types in transmission</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 14 }}>
              {Object.entries(RECORD_FAMILIES).map(([fam, codes]) => {
                const present = codes.filter((c) => counts[c]);
                if (!present.length) return null;
                return (
                  <div key={fam} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
                    <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.10em', color: theme.family[fam], fontWeight: 700 }}>{fam}</div>
                    <div style={{ display: 'flex', gap: 6 }}>
                      {present.map((c) => (
                        <span key={c} className="ff-mono" style={{ fontSize: 10, color: theme.family[fam], fontWeight: 600 }}>
                          {c}<span style={{ color: 'var(--ink-3)', fontWeight: 400 }}>·{counts[c]}</span>
                        </span>
                      ))}
                    </div>
                  </div>
                );
              })}
            </div>
            <div style={{ height: 1, background: 'var(--rule)', margin: '10px 0' }} />
            <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>FIELD KINDS</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 14 }}>
              {[
                ['text', 'text'], ['num', 'numeric'], ['date', 'date'], ['time', 'time'],
                ['flag', 'flag'], ['ipi', 'IPI'], ['iswc', 'ISWC'],
              ].map(([k, l]) => (
                <span key={k} className="ff-mono" style={{ fontSize: 10, color: theme.kinds[k], fontWeight: 600 }}>● {l}</span>
              ))}
              <span className="ff-mono" style={{ fontSize: 10, color: theme.empty, fontWeight: 600 }}>● empty</span>
            </div>
          </div>
        )}

        {/* lines */}
        <div ref={scrollRef} style={{
          overflow: 'auto', flex: 1, background: theme.bg,
          fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', fontSize: 11.5, lineHeight: 1.7,
        }}>
          <div style={{ minWidth: 'max-content', padding: '6px 0' }}>
            {visibleLines.map((line, i) => (
              <HighlightedLine
                key={i} line={line} idx={i} version={version} theme={theme}
                active={active === i} onPick={onPick}
                hideEmpty={hideEmpty} fold={foldControl} search={search}
              />
            ))}
          </div>
        </div>

        {/* footer status */}
        {!compact && (
          <div style={{
            padding: '6px 14px', borderTop: '1px solid var(--rule)',
            display: 'flex', justifyContent: 'space-between',
            fontSize: 10, color: 'var(--ink-3)', background: 'var(--bg)',
          }} className="ff-mono">
            <span>{lines.length.toLocaleString()} lines · {lines.reduce((s, l) => s + l.length + 2, 0).toLocaleString()} bytes · CWR v{version}</span>
            <span>{themeName} theme · click any field for inspector</span>
          </div>
        )}
      </div>
    );
  }

  window.CwrHighlight = { Viewer, tokenizeLine, THEMES, FAMILY_OF, RECORD_FAMILIES };
})();
