/* global React, ReactDOM, Viewer3D, AnnotateTool, Header, Sidebar, RightPanel, BottomBar, ViewportToolbar, CanvasActions, HelpResourcesModal, MobileToolPanel, Toast,
   useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakToggle, TweakColor, TweakSelect */
// ─────────────────────────────────────────────────────────────────
// App — coordinates state across chrome and the 3D scene
// ─────────────────────────────────────────────────────────────────

const { useState, useRef, useEffect } = React;

// Never re-renders once mounted — pressing perspective/view buttons drives
// state elsewhere in the app tree, and this content never changes, so
// memoizing it prevents it from flashing on every unrelated re-render.
const CanvasCaption = React.memo(function CanvasCaption() {
  return (
    <div className="canvas-caption">
      <span>Alexander Hamilton</span>
      <span>Case #1234567890</span>
    </div>
  );
});

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "brand": "glidewell",
  "background": "lightblue",
  "crownMaterial": "zirconia",
  "showCoach": true,
  "compassSide": "right"
}/*EDITMODE-END*/;

const BG_STYLES = {
  lightblue: "linear-gradient(180deg, #F5F5F5 0%, #E6E6E6 100%)",
  paper:     "radial-gradient(ellipse at 50% 35%, #FAF7F1 0%, #ECE4D2 60%, #DCCFB1 100%)",
  slate:     "radial-gradient(ellipse at 50% 35%, #475569 0%, #334155 60%, #1E293B 100%)",
  dark:      "radial-gradient(ellipse at 50% 35%, #1F2937 0%, #111827 60%, #030712 100%)",
  studio:    "linear-gradient(180deg, #2A3340 0%, #131820 100%)",
};

const CROWN_MATERIALS = {
  zirconia: { color: 0xF0E8D8, label: "Zirconia" },
  emax:     { color: 0xCFE2F3, label: "e.max" },
  pmma:     { color: 0xF8C2C2, label: "PMMA" },
  wax:      { color: 0xC8A9F4, label: "Wax" },
  gold:     { color: 0xE5C260, label: "Gold" },
};

const BRAND_OPTIONS = [
  { key: "glidewell",     label: "Glidewell" },
  { key: "crownworld",    label: "Crown World" },
  { key: "riverside",     label: "Riverside" },
  { key: "newwest",       label: "New West" },
  { key: "smithsterling", label: "Smith Sterling" },
  { key: "pacificedge",   label: "Pacific Edge" },
];

// Two-tone themes: distinct gum and teeth colors, blended per vertex by
// how neutral/white the scan's captured color already is (teeth are near-
// white, gum is reddish). "Natural" (gum/teeth: null) keeps the scanner's
// true captured color untouched.
const hexToInt = (h) => parseInt(h.slice(1), 16);

const COLOR_OPTIONS = [
  { key: "natural", label: "Natural", gum: null, teeth: null, icon: (window.__resources && window.__resources.swatchNatural) || "assets/swatch-natural.svg" },
  // Kd values re-derived from user reference screenshots (2026-07-31):
  //   Muted gum 0.824 0.776 0.671 · teeth 0.816 0.816 0.749
  //   High Contrast gum 0.812 0.710 0.557 · teeth 0.663 0.663 0.678
  { key: "muted", label: "Muted", gum: "#D2C6AB", teeth: "#D0D0BF", icon: (window.__resources && window.__resources.swatchMuted) || "assets/swatch-muted.svg" },
  { key: "contrast", label: "High Contrast", gum: "#CFB58E", teeth: "#A9A9AD", icon: (window.__resources && window.__resources.swatchContrast) || "assets/swatch-contrast.svg" },
];
const NATURAL_CROWN_COLOR = "#C9A475";

function App() {
  const viewerRef = useRef(null);
  window.__viewerRef = viewerRef;
  const canvasAreaRef = useRef(null);
  const [zenMode, setZenMode] = useState(false);
  const [introAnimating, setIntroAnimating] = useState(true);
  const [introRevealed, setIntroRevealed] = useState(false);
  // Only true for the one frame replayIntro() snaps the panel/toolbar back
  // to hidden before re-revealing them — see .app-intro-snap in app.css.
  const [introSnapping, setIntroSnapping] = useState(false);
  // Loading splash — covers the canvas + panel + toolbar (like the reference)
  // until the scans have actually arrived. The progress bar is one continuous
  // ease-in fill sized to the real load time, floored at 500ms so the message
  // is never just a flash.
  const loadT0 = useRef(typeof window !== "undefined" && window.__pageT0 || performance.now());
  const [loadingModel, setLoadingModel] = useState(true);
  const [loadOverlayShown, setLoadOverlayShown] = useState(true);
  const [barFilling, setBarFilling] = useState(false);
  const [barDuration, setBarDuration] = useState(500);
  // Chrome (panel + toolbar) and the model's own entrance both wait for the
  // splash to fully dismiss — nothing animates underneath a screen the user
  // can't see yet.
  const startChromeAndModelIntro = () => {
    requestAnimationFrame(() => requestAnimationFrame(() => setIntroRevealed(true)));
    setTimeout(() => setIntroAnimating(false), 710);
    viewerRef.current && viewerRef.current.playEntrance();
  };
  // Replays the chrome (panel/toolbar slide + review-section pop) and the
  // model's scale-in, without the loading splash — used when returning from
  // the confirmation screen so it doesn't just jump back to a static viewer.
  // Re-arms .app-intro-start for a frame so the reveal transition actually
  // has a "before" state to animate from, same as the real first load.
  // introSnapping suppresses the panel/toolbar transition for that one frame
  // — otherwise re-arming .app-intro-start on an already-visible panel would
  // itself transition closed over 630ms before the reveal ever starts,
  // eating half the reveal's apparent length.
  const replayIntro = () => {
    setIntroSnapping(true);
    setIntroAnimating(true);
    setIntroRevealed(false);
    requestAnimationFrame(() => requestAnimationFrame(() => {
      setIntroSnapping(false);
      setIntroRevealed(true);
      setTimeout(() => setIntroAnimating(false), 710);
      viewerRef.current && viewerRef.current.replayEntrance();
    }));
  };
  const handleScansReady = () => {
    const elapsed = performance.now() - loadT0.current;
    const dur = Math.max(500, elapsed);
    setBarDuration(dur);
    requestAnimationFrame(() => requestAnimationFrame(() => setBarFilling(true)));
    setTimeout(() => {
      setLoadOverlayShown(false);
      startChromeAndModelIntro();
      setTimeout(() => setLoadingModel(false), 280);
    }, dur + 300);
  };
  const toggleZenMode = () => setZenMode((z) => !z);
  // Mobile menu (<960px): the right panel becomes a full-screen menu.
  const [menuOpen, setMenuOpen] = useState(false);
  // Mobile only: which floating canvas-overlay tool panel is open (Visibility
  // Controls / Color Options). Hides the header + bottom toolbar while set —
  // see the "app-mobile-tool" CSS class below.
  const [mobileTool, setMobileTool] = useState(null); // null | "visibility" | "color"
  const [helpOpen, setHelpOpen] = useState(false);
  // Device type for Help & Resources: based on primary input mechanism, not
  // viewport width — a desktop browser resized small should still show
  // mouse/keyboard controls, not touch gestures.
  const isTouchDevice = typeof window !== "undefined" && window.matchMedia("(pointer: coarse)").matches;  const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" && window.innerWidth < 960);
  useEffect(() => {
    const onResize = () => {
      const mobile = window.innerWidth < 960;
      setIsMobile(mobile);
      if (!mobile) { setMenuOpen(false); setMobileTool(null); }
    };
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  useEffect(() => {
    // No floating panel on mobile — centre the model instead of biasing it left.
    viewerRef.current?.setPanelShift(zenMode || isMobile ? 0 : 177);
  }, [zenMode, isMobile]);
  useEffect(() => {
    // The mobile Visibility Controls / Color Options panel floats over the
    // bottom of the canvas (in place of the hidden bottom toolbar) — lift the
    // model clear of it while that panel is open.
    viewerRef.current?.setVerticalShift(isMobile && mobileTool ? 150 : 0);
  }, [isMobile, mobileTool]);
  useEffect(() => {
    if (!zenMode) return;
    const onKey = (e) => { if (e.key === "Escape") setZenMode(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [zenMode]);
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // ── 3D view state ──
  const [view, setView] = useState(null);
  const [tool, setTool] = useState("import");
  const [annotating, setAnnotating] = useState(false);
  const [capturing, setCapturing] = useState(false);
  const [annotations, setAnnotations] = useState([]);
  const [reviewStatus, setReviewStatus] = useState(null);
  // Index of the saved annotation being re-opened for editing (null = new capture).
  const [editAnn, setEditAnn] = useState(null);
  // Toast — 0 means none shown; a fresh id both mounts a new Toast (so its
  // entrance animation always restarts, even if one is already showing) and
  // is checked against toastIdRef before auto-dismissing, so an earlier
  // toast's timeout can never clear a newer one out from under it.
  const [toastId, setToastId] = useState(0);
  const toastIdRef = useRef(0);
  const showToast = () => {
    const id = toastIdRef.current + 1;
    toastIdRef.current = id;
    setToastId(id);
    setTimeout(() => { if (toastIdRef.current === id) setToastId(0); }, 2800);
  };
  // Lock the camera while the annotate tool is armed so the captured view
  // matches exactly what the user was looking at.
  const annotatingRef = useRef(false);
  useEffect(() => {
    annotatingRef.current = annotating;
    viewerRef.current?.setControlsEnabled(!annotating);
  }, [annotating]);

  // Model toggles
  const [modelOptions, setModelOptions] = useState({
    color: false, smooth: false, curvature: false, grid: false, undercut: false,
  });
  const [modelColor, setModelColor] = useState(COLOR_OPTIONS[0].key);
  const setModelOption = (k, v) => setModelOptions(o => ({ ...o, [k]: v }));

  const [jaw, setJaw] = useState({
    maxArch:  { shown: true, opacity: 100 },
    manArch:  { shown: true, opacity: 100 },
    manCrown: { shown: true, opacity: 100,
      checks: { margin: true, directions: false, insertion: false } },
  });

  const [contacts, setContacts] = useState({
    occlusal: -200, mesial: 30, distal: 30,
  });

  const [tooth, setTooth] = useState(19);
  const [history] = useState({ canUndo: false, canRedo: false });

  const flashTimer = useRef(null);
  const flashOnTimer = useRef(null);
  // View shortcuts (arrow keys, O, R, +/-) should only act when the pointer
  // is over the 3D viewport — otherwise stray arrow-key events from elsewhere
  // (scrolling, the surrounding tool, focus changes) hijack the camera and
  // snap it to a preset view.
  const canvasHover = useRef(false);
  // Flash the shortcut's active (red) state momentarily, then revert —
  // the user can freely orbit afterward, so a persistent active state is wrong.
  const handleView = (v) => {
    setJaw((j) => ({ ...j,
      maxArch: { ...j.maxArch, shown: true },
      manArch: { ...j.manArch, shown: true },
      manCrown: { ...j.manCrown, shown: true } }));
    viewerRef.current && viewerRef.current.snapToView(v);
    clearTimeout(flashOnTimer.current);
    clearTimeout(flashTimer.current);
    flashOnTimer.current = setTimeout(() => setView(v), 20);
    flashTimer.current = setTimeout(() => setView(null), 300);
  };
  const handleReset = () => {
    clearTimeout(flashOnTimer.current);
    clearTimeout(flashTimer.current);
    setView(null);
    viewerRef.current && viewerRef.current.resetView();
  };
  const handleFit = () => {
    viewerRef.current && viewerRef.current.fitToScreen();
  };
  const handleJawView = (mode) => {
    const v = mode === "maxilla" ? "MAXV" : "MANV";
    const fromJaw = jaw;
    const toJaw = mode === "maxilla"
      ? { ...jaw, maxArch: { ...jaw.maxArch, shown: true }, manArch: { ...jaw.manArch, shown: false }, manCrown: { ...jaw.manCrown, shown: true } }
      : { ...jaw, maxArch: { ...jaw.maxArch, shown: false }, manArch: { ...jaw.manArch, shown: true }, manCrown: { ...jaw.manCrown, shown: false } };
    setJaw(toJaw);
    viewerRef.current && viewerRef.current.snapToJawView(mode, fromJaw, toJaw);
    clearTimeout(flashOnTimer.current);
    clearTimeout(flashTimer.current);
    flashOnTimer.current = setTimeout(() => setView(v), 20);
    flashTimer.current = setTimeout(() => setView(null), 300);
  };

  // Keyboard
  useEffect(() => {
    const onKey = (e) => {
      // Only snap in response to a REAL, deliberate key press. Synthetic key
      // events injected by the surrounding tool (e.g. presentation mode's
      // auto-advance / forwarded ←→ navigation keys) report isTrusted === false;
      // ignoring them stops the camera from jumping to Pt. Right (ArrowRight)
      // on its own while the user is just orbiting the model. Auto-repeat from a
      // held key is also ignored so a single press = a single snap.
      if (!e.isTrusted || e.repeat) return;
      if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.isContentEditable) return;
      // The annotate flow freezes the model so the captured view is preserved.
      if (annotatingRef.current) return;
      // Only act when the pointer is over the 3D viewport.
      if (!canvasHover.current) return;
      const k = e.key.toUpperCase();
      if (e.key === "ArrowUp")         { e.preventDefault(); handleView("L"); }
      else if (e.key === "ArrowDown")  { e.preventDefault(); handleView("L"); }
      else if (e.key === "ArrowLeft")  { e.preventDefault(); handleView("M"); }
      else if (e.key === "ArrowRight") { e.preventDefault(); handleView("D"); }
      else if (k === "O") handleView("O");
      else if (k === "F") handleFit();
      // X/D (jaw view) are handled by ViewportToolbar's own keydown listener,
      // which calls this same handleJawView via the onJawView prop — handling
      // them here too double-invoked it on every press (whenever the pointer
      // happened to be over the canvas), which could make a single keypress
      // skip the "already in this view" shortcut differently than a single
      // button click did.
      else if (e.key === "=" || e.key === "+") viewerRef.current?.zoomIn();
      else if (e.key === "-" || e.key === "_") viewerRef.current?.zoomOut();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  // Seamlessly recolor the model in place when the swatch changes.
  const activeTheme = COLOR_OPTIONS.find((o) => o.key === modelColor) || COLOR_OPTIONS[0];
  useEffect(() => {
    const t = activeTheme.gum == null ? null : { gum: hexToInt(activeTheme.gum), teeth: hexToInt(activeTheme.teeth) };
    viewerRef.current && viewerRef.current.setModelColor(t);
  }, [modelColor]);

  const modelOpts = {
    showCrown: true,
    smooth: modelOptions.smooth,
    color: modelOptions.color,
    modelColor: modelColor,
    crownColor: hexToInt(NATURAL_CROWN_COLOR),
  };

  const isDarkBg = t.background === "slate" || t.background === "dark" || t.background === "studio";

  return (
    <div className={"app" + (zenMode ? " app-zen" : "") + (introAnimating ? " app-intro" : "") + (annotating ? " app-annotating" : "") + (capturing ? " app-capturing" : "") + (!introRevealed ? " app-intro-start" : "") + (introSnapping ? " app-intro-snap" : "") + (menuOpen ? " app-menu-open" : "") + (mobileTool ? " app-mobile-tool" : "")} data-theme={t.brand !== "glidewell" ? t.brand : undefined}>
      <Header onResetPrototype={() => window.location.reload()} menuOpen={menuOpen} onToggleMenu={() => setMenuOpen((m) => !m)} onHelp={() => setHelpOpen(true)} />
      {helpOpen && <HelpResourcesModal onClose={() => setHelpOpen(false)} isMobile={isTouchDevice} />}
      <div className="main">
        <div
          className="canvas-area"
          ref={canvasAreaRef}
          style={{ background: BG_STYLES[t.background] || BG_STYLES.lightblue }}
          onPointerEnter={() => { canvasHover.current = true; }}
          onPointerLeave={() => { canvasHover.current = false; }}>
          <Viewer3D ref={viewerRef}
            modelOpts={modelOpts}
            jaw={jaw}
            showGrid={modelOptions.grid}
            onScansReady={handleScansReady} />

          <div className="canvas-overlay">
            <CanvasActions isZen={zenMode} onToggleZen={toggleZenMode} onZoomIn={() => viewerRef.current?.zoomIn()} onZoomOut={() => viewerRef.current?.zoomOut()} />
            {t.showCoach && (
              <ViewportToolbar
                view={view}
                onView={handleView}
                onReset={handleReset}
                onFit={handleFit}
                onJawView={handleJawView}
                annotating={annotating}
                onAnnotate={() => setAnnotating((a) => !a)}
                locked={!!reviewStatus}
                dark={isDarkBg} />
            )}
            <CanvasCaption />
            {isMobile && mobileTool &&
            <MobileToolPanel
              variant={mobileTool}
              jaw={jaw} setJaw={setJaw}
              colorOptions={COLOR_OPTIONS}
              modelColor={modelColor} setModelColor={setModelColor}
              onDone={() => setMobileTool(null)}
              dark={isDarkBg} />
            }
          </div>
        </div>

        <RightPanel
          view={view}
          onView={handleView}
          onHelp={() => setHelpOpen(true)}
          onReset={handleReset}
          colorOptions={COLOR_OPTIONS}
          modelColor={modelColor}
          setModelColor={setModelColor}
          jaw={jaw} setJaw={setJaw}
          annotations={annotations}
          onEditAnnotation={(i) => { setEditAnn(i); setAnnotating(true); }}
          onCreateAnnotation={() => { setEditAnn(null); setAnnotating(true); setMenuOpen(false); }}
          onDeleteAnnotation={(i) => setAnnotations((a) => a.filter((_, idx) => idx !== i))}
          reviewStatus={reviewStatus}
          onResetPrototype={() => window.location.reload()}
          menuOpen={menuOpen}
          isMobile={isMobile}
          onOpenTool={(v) => { setMenuOpen(false); setMobileTool(v); }}
          onReviewComplete={(mode, date) => { setMenuOpen(false); setAnnotating(false); setCapturing(false); setEditAnn(null); setReviewStatus({ mode, date: date || new Date() }); replayIntro(); }}
          canUndo={history.canUndo} canRedo={history.canRedo}
          onUndo={() => {}} onRedo={() => {}} />
      </div>

      {loadingModel &&
      <div className={"load-overlay" + (loadOverlayShown ? "" : " load-overlay-hide")} style={{ background: BG_STYLES[t.background] || BG_STYLES.lightblue }}>
        <p className="load-overlay-text">We think this will make your patient smile</p>
        <div className="load-bar-track">
          <div className="load-bar-fill" style={{ width: barFilling ? "100%" : "0%", transitionDuration: barDuration + "ms" }} />
        </div>
      </div>
      }

      {annotating && !reviewStatus &&
        <AnnotateTool
          key={editAnn == null ? "new" : "edit-" + editAnn}
          initialShot={editAnn == null ? null : annotations[editAnn]}
          onExit={() => { setAnnotating(false); setCapturing(false); setEditAnn(null); }}
          onCapturingChange={setCapturing}
          onSave={(src) => { setAnnotations((a) => editAnn == null
            ? a.concat([src])
            : a.map((s, i) => i === editAnn ? src : s)); showToast(); }} />
      }

      {toastId > 0 && <Toast key={toastId} message="Annotation saved" />}

      <TweaksPanel>
        <TweakSection label="Brand theme" />
        <TweakSelect
          label="Lab"
          value={t.brand}
          options={BRAND_OPTIONS.map(b => ({ value: b.key, label: b.label }))}
          onChange={v => setTweak("brand", v)} />

        <TweakSection label="Canvas" />
        <TweakSelect
          label="Background"
          value={t.background}
          options={[
            { value: "lightblue", label: "Clinical blue" },
            { value: "paper",     label: "Paper cream" },
            { value: "slate",     label: "Slate" },
            { value: "dark",      label: "Dark" },
            { value: "studio",    label: "Studio gradient" },
          ]}
          onChange={v => setTweak("background", v)} />

        <TweakSection label="Crown material" />
        <TweakRadio
          label="Type"
          value={t.crownMaterial}
          options={[
            { value: "zirconia", label: "Zirc" },
            { value: "emax",     label: "e.max" },
            { value: "pmma",     label: "PMMA" },
            { value: "wax",      label: "Wax" },
          ]}
          onChange={v => setTweak("crownMaterial", v)} />

        <TweakSection label="Overlays" />
        <TweakToggle
          label="Keyboard hints"
          value={t.showCoach}
          onChange={v => setTweak("showCoach", v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
