/* ============================================================
   femtoAI EVK — Main App (React)
   Wires SerialController → ProtocolController → UI
   Tabs: Console | File Manager
   ============================================================ */
const { useState, useEffect, useRef, useCallback, useLayoutEffect } = React;

/* ---- Inline icons ---- */
const ICONS = {
  plug:       '<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/>',
  power:      '<path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/>',
  terminal:   '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
  folder:     '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
  settings:   '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
  trash:      '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
  download:   '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/>',
  scrolldown: '<path d="M12 17V3"/><path d="m6 11 6 6 6-6"/><path d="M19 21H5"/>',
  clock:      '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
  hash:       '<line x1="4" x2="20" y1="9" y2="9"/><line x1="4" x2="20" y1="15" y2="15"/><line x1="10" x2="8" y1="3" y2="21"/><line x1="14" x2="12" y1="3" y2="21"/>',
  send:       '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
  alert:      '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
  audio:      '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>',
};
function Icon({ name, className, style }) {
  return (
    <svg className={"ico " + (className || "")} style={style} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"
      dangerouslySetInnerHTML={{ __html: ICONS[name] || "" }} />
  );
}

const MAX_LINES = 1000;
const LS_KEY = "femto-evk-serial-settings";

function loadSettings() {
  try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch (_) { return {}; }
}
function fmtBytes(n) {
  if (n < 1024) return n + " B";
  if (n < 1048576) return (n / 1024).toFixed(1) + " KB";
  return (n / 1048576).toFixed(2) + " MB";
}
function fmtTime(d) {
  const p = (x, n = 2) => String(x).padStart(n, "0");
  return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
}
function hexDump(bytes) {
  return Array.from(bytes).map(b => b.toString(16).toUpperCase().padStart(2, "0")).join(" ");
}
function parseHexInput(str) {
  const clean = str.replace(/0x/gi, " ").replace(/[^0-9a-fA-F]/g, " ").trim().replace(/\s+/g, "");
  if (!clean.length) return null;
  const padded = clean.length % 2 ? "0" + clean : clean;
  const out = new Uint8Array(padded.length / 2);
  for (let i = 0; i < out.length; i++) out[i] = parseInt(padded.substr(i * 2, 2), 16);
  return out;
}

/* ---- Console line ---- */
function ConsoleLine({ entry, showTs, hexView }) {
  const cls = "line " + entry.dir;
  const arrow = entry.dir === "rx" ? "‹" : entry.dir === "tx" ? "›" : "";
  let body;
  if (entry.dir === "sys" || entry.dir === "err") {
    body = <span className="txt">{entry.text}</span>;
  } else if (hexView && entry.bytes) {
    body = <span className="txt hexdump">{hexDump(entry.bytes)}</span>;
  } else {
    body = <span className="txt">{entry.text === null ? hexDump(entry.bytes) : (entry.text || " ")}</span>;
  }
  return (
    <div className={cls}>
      {showTs && entry.t && <span className="ts">{fmtTime(entry.t)}</span>}
      {arrow && <span className="arrow">{arrow}</span>}
      {body}
    </div>
  );
}

/* =================== App =================== */
function App() {
  const supported = window.SerialSupported;
  const isIframe = window.self !== window.top;
  const serialRef = useRef(null);
  const protoRef = useRef(null);

  if (!serialRef.current && supported) serialRef.current = new window.SerialController();
  if (!protoRef.current && supported) protoRef.current = new window.ProtocolController(serialRef.current);

  const saved = loadSettings();
  const [connState, setConnState] = useState("closed");
  const [lines, setLines] = useState([]);
  const [counters, setCounters] = useState({ rx: 0, tx: 0 });
  const [knownPorts, setKnownPorts] = useState([]);
  const [sendText, setSendText] = useState("");
  const [activeTab, setActiveTab] = useState("console");
  const [dfuPhase, setDfuPhase] = useState(null); // null | 'flashing' | 'reconnecting' | 'done' | 'timeout'
  const [spuActive, setSpuActive] = useState(null); // null=unknown, true=ACTIVE, false=LOOPBACK

  const settings = { baudRate: 115200, dataBits: 8, stopBits: 1, parity: "none", flowControl: "none" };
  const settingsRef = useRef(settings);
  const dfuPollRef  = useRef(null);

  const [sendEnding, setSendEnding] = useState(saved.sendEnding || "lf");
  const [opts, setOpts] = useState({
    timestamps: saved.timestamps !== undefined ? saved.timestamps : true,
    autoscroll: saved.autoscroll !== undefined ? saved.autoscroll : true,
    hexView:    saved.hexView    || false,
    hexSend:    saved.hexSend    || false,
  });

  const outRef     = useRef(null);
  const pendingRef = useRef([]);
  const rafRef     = useRef(0);
  const histRef    = useRef([]);
  const histIdxRef = useRef(-1);

  const isOpen = connState === "open";
  const isBusy = connState === "connecting" || connState === "closing";

  /* persist settings */
  useEffect(() => {
    localStorage.setItem(LS_KEY, JSON.stringify({ sendEnding, ...opts }));
  }, [sendEnding, opts]);

  /* batched line flush */
  const pushEntry = useCallback((entry) => {
    pendingRef.current.push(entry);
    if (!rafRef.current) {
      rafRef.current = requestAnimationFrame(() => {
        rafRef.current = 0;
        const batch = pendingRef.current;
        pendingRef.current = [];
        setLines(prev => {
          const next = prev.concat(batch);
          return next.length > MAX_LINES ? next.slice(next.length - MAX_LINES) : next;
        });
      });
    }
  }, []);

  /* wire controllers */
  useEffect(() => {
    const serial = serialRef.current;
    const proto  = protoRef.current;
    if (!serial || !proto) return;

    // SerialController → ProtocolController (intercepts ACK/NAK) → console
    serial.onLine     = e => proto.handleLine(e);
    proto.onLine      = e => pushEntry(e);

    serial.onState    = s => {
      setConnState(s);
      if (s === "closed" || s === "error") {
        proto.abortAll("Connection closed");
        setSpuActive(null);
      }
    };
    proto.onSpuChange = active => setSpuActive(active);
    serial.onInfo     = t => pushEntry({ dir: "sys", t: new Date(), text: t,  bytes: null });
    serial.onError    = t => pushEntry({ dir: "err", t: new Date(), text: t,  bytes: null });
    serial.onCounters = n => setCounters(n);

    refreshKnown();

    return () => {
      serial.onLine = serial.onState = serial.onInfo = serial.onError = serial.onCounters = null;
      proto.onLine = proto.onSpuChange = null;
      // Closes any open port when App unmounts without a full page reload
      // (e.g. embedded as a widget and navigated away from via client-side
      // routing) — a full page unload already releases the port for free,
      // this only matters for the SPA-embedding case.
      serial.disconnect().catch(() => {});
    };
  }, [pushEntry]); // eslint-disable-line

  const refreshKnown = useCallback(async () => {
    const ports = await (serialRef.current?.getKnownPorts() || []);
    setKnownPorts(ports);
  }, []);

  /* Query SPU state on connect */
  useEffect(() => {
    if (isOpen) {
      protoRef.current.spuStatus().then(active => setSpuActive(active)).catch(() => {});
    }
  }, [isOpen]); // eslint-disable-line

  /* autoscroll */
  useLayoutEffect(() => {
    if (opts.autoscroll && outRef.current) outRef.current.scrollTop = outRef.current.scrollHeight;
  }, [lines, opts.autoscroll]);

  /* ---- actions ---- */
  const doConnect = useCallback(async (port) => {
    const serial = serialRef.current;
    try {
      const p = port || (await serial.requestPort());
      await serial.connect(p, settings);
      refreshKnown();
    } catch (err) {
      if (!err) return;
      if (err.name === "NotFoundError") return;
      const isPolicy = err.name === "SecurityError" || (err.message || "").toLowerCase().includes("permissions policy");
      const msg = isPolicy
        ? "Serial blocked — open this file directly in Chrome from localhost, not inside an iframe."
        : (err.message || String(err));
      setConnState("error");
      pushEntry({ dir: "err", t: new Date(), text: msg, bytes: null });
      setTimeout(() => setConnState(s => s === "error" ? "closed" : s), 80);
      setConnState("closed");
    }
  }, [settings, pushEntry, refreshKnown]);

  const doDisconnect = useCallback(async () => {
    if (dfuPollRef.current) { clearTimeout(dfuPollRef.current); dfuPollRef.current = null; }
    setDfuPhase(null);
    await serialRef.current.disconnect();
  }, []);

  const handleDfuTriggered = useCallback(async () => {
    if (dfuPollRef.current) { clearTimeout(dfuPollRef.current); dfuPollRef.current = null; }
    setDfuPhase("flashing");

    // Close port — device may have already dropped
    try { await serialRef.current.disconnect(); } catch (_) {}

    let attemptsLeft = 25; // 25 × 2.5 s ≈ 62 s total

    const poll = async () => {
      if (attemptsLeft-- <= 0) { setDfuPhase("timeout"); dfuPollRef.current = null; return; }
      setDfuPhase(prev => prev === "flashing" ? "reconnecting" : prev);
      try {
        const ports = await serialRef.current.getKnownPorts();
        if (ports.length > 0) {
          await serialRef.current.connect(ports[0], settingsRef.current);
          await new Promise(r => setTimeout(r, 500)); // let device settle
          await protoRef.current.ping();
          setDfuPhase("done");
          dfuPollRef.current = null;
          return;
        }
      } catch (_) {
        try { await serialRef.current.disconnect(); } catch (__) {}
      }
      dfuPollRef.current = setTimeout(poll, 2500);
    };

    dfuPollRef.current = setTimeout(poll, 2500);
  }, []); // eslint-disable-line

  const doSend = useCallback(async () => {
    const serial = serialRef.current;
    if (!isOpen || !sendText.length) return;
    try {
      if (opts.hexSend) {
        const bytes = parseHexInput(sendText);
        if (!bytes) return;
        await serial.writeBytes(bytes);
      } else {
        await serial.write(sendText, sendEnding);
      }
      histRef.current.unshift(sendText);
      histRef.current = histRef.current.slice(0, 50);
      histIdxRef.current = -1;
      setSendText("");
    } catch (err) {
      pushEntry({ dir: "err", t: new Date(), text: err.message || String(err), bytes: null });
    }
  }, [isOpen, sendText, sendEnding, opts.hexSend, pushEntry]);

  const onSendKey = (e) => {
    if (e.key === "Enter") { e.preventDefault(); doSend(); return; }
    const h = histRef.current;
    if (e.key === "ArrowUp" && h.length) {
      e.preventDefault();
      histIdxRef.current = Math.min(histIdxRef.current + 1, h.length - 1);
      setSendText(h[histIdxRef.current]);
    } else if (e.key === "ArrowDown") {
      e.preventDefault();
      histIdxRef.current = Math.max(histIdxRef.current - 1, -1);
      setSendText(histIdxRef.current === -1 ? "" : h[histIdxRef.current]);
    }
  };

  const clearConsole = () => { pendingRef.current = []; setLines([]); };
  const downloadLog = () => {
    const text = lines.map(e => {
      const ts   = e.t ? "[" + fmtTime(e.t) + "] " : "";
      const dir  = { rx: "< ", tx: "> ", err: "! ", sys: "# " }[e.dir] || "  ";
      const body = e.text === null ? hexDump(e.bytes) : e.text;
      return ts + dir + body;
    }).join("\n");
    const blob = new Blob([text], { type: "text/plain" });
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement("a");
    a.href = url; a.download = "femto-serial-" + new Date().toISOString().slice(0, 19).replace(/:/g, "-") + ".log";
    a.click(); URL.revokeObjectURL(url);
  };

  const setOpt = k => e => setOpts(o => ({ ...o, [k]: e.target.checked }));

  /* ---- unsupported / iframe ---- */
  if (!supported || isIframe) {
    return (
      <div className="unsupported-screen">
        <div className="unsupported-card">
          <Icon name="alert" />
          <h2>{!supported ? "Web Serial isn't available" : "Run this from localhost"}</h2>
          {!supported ? (
            <p>This tool requires the <b>Web Serial API</b> — open it in <b>Chrome</b>, <b>Edge</b>, or <b>Arc</b>.</p>
          ) : (
            <>
              <p>The Web Serial API is blocked inside embedded previews. Open the file directly in Chrome from a local server.</p>
              <p style={{ marginTop:16, fontFamily:"var(--font-mono)", fontSize:"0.85rem", textAlign:"left", background:"var(--bg-subtle)", borderRadius:"var(--radius-lg)", padding:"16px 20px", lineHeight:1.9 }}>
                <span style={{color:"var(--fg-muted)"}}># from this project's folder:</span><br/>
                <b>python3 -m http.server 8080</b><br/>
                <span style={{color:"var(--fg-muted)"}}># then open in Chrome:</span><br/>
                <b>http://localhost:8080/EVK%20Serial%20Console.html</b>
              </p>
              <p style={{color:"var(--fg-subtle)",fontSize:"var(--text-sm)"}}>Or use <code>npx serve .</code> if you have Node.</p>
            </>
          )}
        </div>
      </div>
    );
  }

  const stateLabel = { closed:"Disconnected", connecting:"Connecting…", open:"Connected", closing:"Closing…", error:"Error" }[connState] || connState;

  return (
    <div className="app">
      {/* Header */}
      <header className="app-header">
        <img className="logo" src="assets/femtoai-logo.png" alt="femtoAI" />
        <span className="divider"></span>
        <span className="app-title">EVK Serial Console<span className="sub">device interface</span></span>
        <span className="spacer"></span>
        <span className="status" data-state={connState}>
          <span className="dot"></span>{stateLabel}
        </span>
      </header>

      {/* Toolbar */}
      <div className="toolbar">
        {!isOpen ? (
          <button className="btn btn-primary" disabled={isBusy} onClick={() => doConnect(null)}>
            <Icon name="plug" />Connect device
          </button>
        ) : (
          <button className="btn btn-danger" disabled={isBusy} onClick={doDisconnect}>
            <Icon name="power" />Disconnect
          </button>
        )}
        <span className="spacer"></span>
        <div className="stat-strip">
          {isOpen && spuActive !== null && (
            <div className="stat">
              <span className="k">SPU</span>
              <span className={`spu-badge spu-badge--${spuActive ? 'active' : 'loopback'}`}>
                {spuActive ? ' ACTIVE ' : 'LOOPBACK'}
              </span>
            </div>
          )}
          <div className="stat"><span className="k">RX</span><span className="v rx">{fmtBytes(counters.rx)}</span></div>
          <div className="stat"><span className="k">TX</span><span className="v tx">{fmtBytes(counters.tx)}</span></div>
          <div className="stat"><span className="k">Lines</span><span className="v">{lines.length.toLocaleString()}</span></div>
        </div>
      </div>

      {/* Tab bar */}
      <div className="tab-bar">
        {knownPorts.length > 0 && !isOpen && (
          <div className="known-ports" style={{ marginRight: "auto" }}>
            {knownPorts.map((p, i) => (
              <button key={i} className="chip" disabled={isBusy} onClick={() => doConnect(p)}>
                <Icon name="plug" />{window.serialPortLabel(p)}
              </button>
            ))}
          </div>
        )}
        <button className={"tab-btn" + (activeTab === "console" ? " tab-btn--active" : "")} onClick={() => setActiveTab("console")}>
          <Icon name="terminal" />Console
        </button>
        <button className={"tab-btn" + (activeTab === "files" ? " tab-btn--active" : "")} onClick={() => setActiveTab("files")}>
          <Icon name="folder" />File Manager
        </button>
        <button className={"tab-btn" + (activeTab === "config" ? " tab-btn--active" : "")} onClick={() => setActiveTab("config")}>
          <Icon name="settings" />Config
        </button>
        <button className={"tab-btn" + (activeTab === "audio" ? " tab-btn--active" : "")} onClick={() => setActiveTab("audio")}>
          <Icon name="audio" />Audio
        </button>
      </div>

      {/* Tab content */}
      <div className="tab-content">
        {/* Console tab */}
        <div className="console-wrap" style={{ display: activeTab === "console" ? "flex" : "none", flexDirection: "column" }}>
          <div className="console">
            <img className="brand-mark" src="assets/spu-icon.svg" alt=""
              style={{ filter:"brightness(0) invert(1)", width:420, height:420 }} />

            <div className="console-bar">
              <span className="title"><Icon name="terminal" />Serial monitor</span>
              <span className="spacer"></span>
              <label className="toggle">
                <input type="checkbox" checked={opts.timestamps} onChange={setOpt("timestamps")} />
                <span className="sw"></span>Timestamps
              </label>
              <label className="toggle">
                <input type="checkbox" checked={opts.hexView} onChange={setOpt("hexView")} />
                <span className="sw"></span>Hex view
              </label>
              <label className="toggle">
                <input type="checkbox" checked={opts.autoscroll} onChange={setOpt("autoscroll")} />
                <span className="sw"></span>Autoscroll
              </label>
              <button className="btn-icon" title="Download log" onClick={downloadLog} disabled={!lines.length}><Icon name="download" /></button>
              <button className="btn-icon" title="Clear console" onClick={clearConsole} disabled={!lines.length}><Icon name="trash" /></button>
            </div>

            <div className="console-out" ref={outRef}>
              {lines.length === 0 ? (
                <div className="console-empty">
                  <Icon name="terminal" />
                  <div className="big">{isOpen ? "Listening for data…" : "No device connected"}</div>
                  <div className="small">
                    {isOpen
                      ? "Incoming serial data streams here. Type below to send commands."
                      : <span>Click <b>Connect device</b> and pick your EVK2 from the browser prompt.</span>}
                  </div>
                </div>
              ) : (
                lines.map((e, i) => <ConsoleLine key={i} entry={e} showTs={opts.timestamps} hexView={opts.hexView} />)
              )}
            </div>

            <div className="send-bar">
              {opts.hexSend && <span className="hexmode-tag">hex</span>}
              <input className="send-input" type="text" value={sendText} disabled={!isOpen}
                placeholder={isOpen ? (opts.hexSend ? "Bytes to send, e.g. FF 01 0A" : "Type a command and press Enter…") : "Connect a device to send data"}
                onChange={e => setSendText(e.target.value)} onKeyDown={onSendKey} />
              <label className="toggle" style={{ color:"var(--color-lilac)" }}>
                <input type="checkbox" checked={opts.hexSend} onChange={setOpt("hexSend")} />
                <span className="sw"></span>Hex
              </label>
              <select className="le-select" value={sendEnding} disabled={!isOpen||opts.hexSend} onChange={e => setSendEnding(e.target.value)}>
                <option value="lf">\n</option>
                <option value="cr">\r</option>
                <option value="crlf">\r\n</option>
                <option value="none">none</option>
              </select>
              <button className="btn btn-send" disabled={!isOpen||!sendText.length} onClick={doSend}>
                <Icon name="send" />Send
              </button>
            </div>
          </div>
        </div>

        {/* File Manager tab */}
        <div className="fm-tab-wrap" style={{ display: activeTab === "files" ? "flex" : "none", flexDirection: "column" }}>
          <window.FileManager
            protocolCtrl={protoRef.current}
            connState={connState}
            addLog={msg => pushEntry({ dir:"sys", t:new Date(), text:msg, bytes:null })}
            dfuPhase={dfuPhase}
            onDfuTriggered={handleDfuTriggered}
            onDfuDismiss={() => setDfuPhase(null)}
          />
        </div>

        {/* Config tab */}
        <div className="fm-tab-wrap" style={{ display: activeTab === "config" ? "flex" : "none", flexDirection: "column" }}>
          <window.ConfigEditor
            protocolCtrl={protoRef.current}
            connState={connState}
            addLog={msg => pushEntry({ dir:"sys", t:new Date(), text:msg, bytes:null })}
          />
        </div>

        {/* Audio tab */}
        <div className="fm-tab-wrap" style={{ display: activeTab === "audio" ? "flex" : "none", flexDirection: "column" }}>
          <window.AudioPanel
            protocolCtrl={protoRef.current}
            connState={connState}
            addLog={msg => pushEntry({ dir:"sys", t:new Date(), text:msg, bytes:null })}
          />
        </div>
      </div>
    </div>
  );
}

// Exposed so this app can also be mounted as an embedded widget (see
// src/index.js) instead of only self-mounting into a static page's #root.
window.mountEvkWebGui = function mountEvkWebGui(container) {
  const root = ReactDOM.createRoot(container);
  root.render(<App />);
  return { unmount: () => root.unmount() };
};

if (document.getElementById("root")) {
  window.mountEvkWebGui(document.getElementById("root"));
}
