/* =====================================================================
   THE CAREER DRUM — standalone page
   A large, opaque, keyboard-operable slot-machine drum. The ball is the
   spin button. Drum and ball are driven by one Framer Motion value.
   ===================================================================== */

import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createRoot } from 'react-dom/client';
import { motion, animate, useMotionValue, useMotionValueEvent, useReducedMotion } from 'framer-motion';

/* ---------------------------------------------------------------------
   Careers. Mirrors the twelve occupations in app.jsx (same ids and SOC
   codes) so a selection made here carries over to the main lab.
   `tone` is the panel's solid background; `ink` is its text colour.
   --------------------------------------------------------------------- */
const CAREERS = [
  { id: 'paralegal',   title: 'Paralegal',              cluster: 'Legal',          soc: '23-2011', full: 'Paralegals and Legal Assistants',            tone: '#F6C94C', ink: '#161616' },
  { id: 'teacher',     title: 'Elementary Teacher',     cluster: 'Education',      soc: '25-2021', full: 'Elementary School Teachers',                 tone: '#BFD7CC', ink: '#161616' },
  { id: 'bookkeeper',  title: 'Bookkeeper',             cluster: 'Business',       soc: '43-3031', full: 'Bookkeeping, Accounting & Auditing Clerks',  tone: '#F4E7C8', ink: '#161616' },
  { id: 'designer',    title: 'Graphic Designer',       cluster: 'Arts & Media',   soc: '27-1024', full: 'Graphic Designers',                          tone: '#FF6658', ink: '#161616' },
  { id: 'nurse',       title: 'Registered Nurse',       cluster: 'Health',         soc: '29-1141', full: 'Registered Nurses',                          tone: '#DDEAE4', ink: '#161616' },
  { id: 'software',    title: 'Software Developer',     cluster: 'Technology',     soc: '15-1252', full: 'Software Developers',                        tone: '#6E69F8', ink: '#FFFFFF' },
  { id: 'csr',         title: 'Customer Service Rep',   cluster: 'Service',        soc: '43-4051', full: 'Customer Service Representatives',           tone: '#FF806F', ink: '#161616' },
  { id: 'analyst',     title: 'Market Research Analyst',cluster: 'Business',       soc: '13-1161', full: 'Market Research Analysts',                   tone: '#FFF7E7', ink: '#161616' },
  { id: 'writer',      title: 'Writer / Author',        cluster: 'Arts & Media',   soc: '27-3043', full: 'Writers and Authors',                        tone: '#F6C94C', ink: '#161616' },
  { id: 'accountant',  title: 'Accountant / Auditor',   cluster: 'Business',       soc: '13-2011', full: 'Accountants and Auditors',                   tone: '#BFD7CC', ink: '#161616' },
  { id: 'electrician', title: 'Electrician',            cluster: 'Skilled Trades', soc: '47-2111', full: 'Electricians',                               tone: '#8C86FF', ink: '#FFFFFF' },
  { id: 'support',     title: 'Computer Support Spec.', cluster: 'Technology',     soc: '15-1232', full: 'Computer User Support Specialists',          tone: '#F4E7C8', ink: '#161616' },
];

const N = CAREERS.length;
const STEP = 360 / N;
const SEG_H = 108;                                            // big panels
const RADIUS = Math.round((SEG_H / 2) / Math.tan(Math.PI / N));
const WIN_H = Math.round(SEG_H * 4.6);                        // shows ~5 panels

/* Deliberately slow: low stiffness + high mass so a spin takes several
   seconds and every panel is readable on its way past the payline. */
const SPRING = { type: 'spring', stiffness: 13, damping: 11.5, mass: 2.4, restDelta: 0.004 };
const AUTO_MS = 2800;                                         // dwell between auto-rotate steps

const clamp = (v, a, b) => Math.max(a, Math.min(b, v));

const store = {
  get(k, f) { try { const v = localStorage.getItem('cf.' + k); return v ? JSON.parse(v) : f; } catch (e) { return f; } },
  set(k, v) { try { localStorage.setItem('cf.' + k, JSON.stringify(v)); } catch (e) { /* private mode */ } },
};

/* =====================================================================
   Ball button — a real <button>, painted on canvas with gradients.
   It stays put (a moving target would be unusable), and spins in place:
   the sheen and specular are rotated by the same value that turns the drum.
   ===================================================================== */
function BallButton({ rot, onSpin, spinning, size }) {
  const ref = useRef(null);
  const dims = useRef({ w: 0, h: 0 });

  const paint = useCallback((r) => {
    const cv = ref.current;
    if (!cv) return;
    const { w, h } = dims.current;
    if (!w) return;
    const ctx = cv.getContext('2d');
    ctx.setTransform(cv.width / w, 0, 0, cv.height / h, 0, 0);
    ctx.clearRect(0, 0, w, h);

    const cx = w / 2, cy = h / 2, R = Math.min(w, h) / 2 - 3;
    const a = r * Math.PI / 180;

    /* body */
    const body = ctx.createRadialGradient(cx - R * 0.34, cy - R * 0.4, R * 0.05, cx, cy, R * 1.08);
    body.addColorStop(0.00, '#FFFDF6');
    body.addColorStop(0.16, '#FFE9A8');
    body.addColorStop(0.44, '#F6C94C');
    body.addColorStop(0.78, '#D9932C');
    body.addColorStop(1.00, '#6E3F0C');
    ctx.fillStyle = body;
    ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.fill();

    /* rotating sheen — this is what makes the ball read as spinning */
    ctx.save();
    ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.clip();
    if (ctx.createConicGradient) {
      const cg = ctx.createConicGradient(a, cx, cy);
      cg.addColorStop(0.00, 'rgba(255,255,255,0.34)');
      cg.addColorStop(0.18, 'rgba(255,255,255,0.02)');
      cg.addColorStop(0.42, 'rgba(110,105,248,0.20)');
      cg.addColorStop(0.62, 'rgba(255,255,255,0.02)');
      cg.addColorStop(0.84, 'rgba(255,102,88,0.22)');
      cg.addColorStop(1.00, 'rgba(255,255,255,0.34)');
      ctx.fillStyle = cg;
    } else {
      const lg = ctx.createLinearGradient(cx - R, cy - R, cx + R, cy + R);
      lg.addColorStop(0, 'rgba(255,255,255,0.28)');
      lg.addColorStop(1, 'rgba(255,102,88,0.20)');
      ctx.fillStyle = lg;
    }
    ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
    ctx.restore();

    /* seam, so rotation is legible even at a glance */
    ctx.save();
    ctx.translate(cx, cy); ctx.rotate(a);
    ctx.strokeStyle = 'rgba(122,74,18,0.45)'; ctx.lineWidth = Math.max(2, R * 0.045);
    ctx.beginPath(); ctx.ellipse(0, 0, R * 0.66, R * 0.9, 0, 0, Math.PI * 2); ctx.stroke();
    ctx.restore();

    /* rim darkening + specular highlight */
    const rim = ctx.createRadialGradient(cx, cy, R * 0.68, cx, cy, R);
    rim.addColorStop(0, 'rgba(110,63,12,0)');
    rim.addColorStop(1, 'rgba(110,63,12,0.55)');
    ctx.fillStyle = rim;
    ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.fill();

    const sx = cx - R * 0.38, sy = cy - R * 0.44;
    const spec = ctx.createRadialGradient(sx, sy, 0, sx, sy, R * 0.5);
    spec.addColorStop(0, 'rgba(255,255,255,0.95)');
    spec.addColorStop(1, 'rgba(255,255,255,0)');
    ctx.fillStyle = spec;
    ctx.beginPath(); ctx.arc(sx, sy, R * 0.5, 0, Math.PI * 2); ctx.fill();

    ctx.strokeStyle = 'rgba(22,22,22,0.6)'; ctx.lineWidth = 2;
    ctx.beginPath(); ctx.arc(cx, cy, R - 1, 0, Math.PI * 2); ctx.stroke();
  }, []);

  useEffect(() => {
    const cv = ref.current;
    if (!cv) return;
    const fit = () => {
      const r = cv.getBoundingClientRect();
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      cv.width = Math.round(r.width * dpr);
      cv.height = Math.round(r.height * dpr);
      dims.current = { w: r.width, h: r.height };
      paint(rot.get());
    };
    fit();
    const ro = new ResizeObserver(fit);
    ro.observe(cv);
    return () => ro.disconnect();
  }, [paint, rot]);

  useMotionValueEvent(rot, 'change', paint);
  useEffect(() => { paint(rot.get()); });

  return (
    <div className="ballwrap">
      <button
        type="button"
        className="dr-ball"
        style={{ '--ball': size }}
        onClick={onSpin}
        aria-label={spinning ? 'Spinning the drum' : 'Spin the drum to pick a career'}
      >
        <canvas ref={ref} aria-hidden="true" />
        <span className="cap">{spinning ? '•••' : 'Spin'}</span>
      </button>
      <p className="ballhint">
        Press the ball to spin. It turns with the drum — same spring, same tick.
      </p>
    </div>
  );
}

/* =====================================================================
   The drum
   ===================================================================== */
function Drum({ selected, onSelect, auto, setAuto }) {
  const reduce = useReducedMotion();
  const cylRef = useRef(null);
  const segRefs = useRef([]);
  const winRef = useRef(null);
  const drag = useRef(null);
  const anim = useRef(null);
  const watchdog = useRef(null);
  const renderRef = useRef(null);

  const startAngle = CAREERS.findIndex((c) => c.id === selected) * STEP;
  const rot = useMotionValue(startAngle);
  const aim = useRef(startAngle);
  const [dragging, setDragging] = useState(false);
  const [spinning, setSpinning] = useState(false);

  const settle = useCallback((to) => {
    anim.current?.stop();
    clearTimeout(watchdog.current);
    aim.current = to;
    const land = () => {
      anim.current?.stop();
      rot.set(to);
      renderRef.current?.(to);
      setSpinning(false);
    };
    if (reduce) { land(); return; }

    setSpinning(true);
    anim.current = animate(rot, to, SPRING);
    anim.current.then(() => { if (aim.current === to) setSpinning(false); }).catch(() => {});

    /* The spring runs on requestAnimationFrame, which browsers suspend for
       hidden tabs. Poll on a timer instead: if the value stops moving before
       it arrives, land it, so the drum is never stranded between panels. */
    let last = NaN;
    const tick = () => {
      if (drag.current || aim.current !== to) return;
      const cur = rot.get();
      if (Math.abs(cur - to) < 0.4 || cur === last) { land(); return; }
      last = cur;
      watchdog.current = setTimeout(tick, 600);
    };
    watchdog.current = setTimeout(tick, 1400);
  }, [reduce]); // eslint-disable-line

  useEffect(() => () => { anim.current?.stop(); clearTimeout(watchdog.current); }, []);

  /* one render pass: transform, per-panel emphasis, active flag */
  const render = useCallback((r) => {
    if (cylRef.current) cylRef.current.style.transform =
      `translateY(-50%) translateZ(${-RADIUS}px) rotateX(${-r}deg)`;
    const active = ((Math.round(r / STEP) % N) + N) % N;
    segRefs.current.forEach((el, i) => {
      if (!el) return;
      const d = ((i * STEP - r) % 360 + 540) % 360 - 180;
      const ad = Math.abs(d);
      /* Panels stay fully opaque. Depth is shown with brightness, never
         with transparency, so no panel ever washes out into the housing. */
      el.style.filter = `brightness(${clamp(1 - ad / 200, 0.42, 1).toFixed(3)})`;
      el.style.visibility = ad > 96 ? 'hidden' : 'visible';
      el.classList.toggle('on', i === active);
    });
  }, []);
  renderRef.current = render;

  useMotionValueEvent(rot, 'change', render);
  useEffect(() => { render(rot.get()); }, [render]); // eslint-disable-line

  const commit = useCallback((v) => {
    const snapped = Math.round(v / STEP) * STEP;
    settle(snapped);
    onSelect(CAREERS[((Math.round(snapped / STEP) % N) + N) % N].id);
  }, [settle, onSelect]);

  /* keep in step when the career is set from the <select> */
  useEffect(() => {
    const i = CAREERS.findIndex((c) => c.id === selected);
    const aimed = ((Math.round(aim.current / STEP) % N) + N) % N;
    if (aimed === i || drag.current) return;
    settle(i * STEP + Math.round((aim.current - i * STEP) / 360) * 360);
  }, [selected, settle]); // eslint-disable-line

  /* ---- slow auto-rotate: one panel at a time, never faster than reading ---- */
  const autoRef = useRef(auto);
  useEffect(() => { autoRef.current = auto; }, [auto]);
  useEffect(() => {
    if (!auto || reduce) return;
    const id = setInterval(() => {
      /* autoRef, not `auto` — so a tick already queued when the reader presses
         Stop does not sneak one more career past them. */
      if (!autoRef.current || drag.current) return;
      commit(aim.current + STEP);
    }, AUTO_MS);
    return () => clearInterval(id);
  }, [auto, reduce, commit]);

  const stopAuto = () => { autoRef.current = false; if (auto) setAuto(false); };

  /* ---- pointer drag ---- */
  const onDown = (e) => {
    if (e.target.closest('button')) return;
    stopAuto();
    e.currentTarget.setPointerCapture?.(e.pointerId);
    anim.current?.stop();
    clearTimeout(watchdog.current);
    aim.current = rot.get();
    drag.current = { y: e.clientY, base: rot.get(), vy: 0, lastDy: 0, t: performance.now() };
    setDragging(true);
    setSpinning(false);
  };
  const onMove = (e) => {
    const d = drag.current;
    if (!d) return;
    const dy = e.clientY - d.y;
    const now = performance.now();
    d.vy = ((dy - d.lastDy) / Math.max(1, now - d.t)) * 16.7;
    d.lastDy = dy; d.t = now;
    rot.set(d.base - dy * 0.30);          // gentler than 1:1 — the drum turns slowly
  };
  const onUp = () => {
    const d = drag.current;
    if (!d) return;
    drag.current = null;
    setDragging(false);
    commit(rot.get() - clamp(d.vy, -30, 30) * 4);
  };

  /* ---- keyboard ---- */
  const onKey = (e) => {
    const k = e.key;
    if (k === 'ArrowDown' || k === 'ArrowRight') { e.preventDefault(); stopAuto(); commit(aim.current + STEP); }
    else if (k === 'ArrowUp' || k === 'ArrowLeft') { e.preventDefault(); stopAuto(); commit(aim.current - STEP); }
    else if (k === 'PageDown') { e.preventDefault(); stopAuto(); commit(aim.current + STEP * 3); }
    else if (k === 'PageUp') { e.preventDefault(); stopAuto(); commit(aim.current - STEP * 3); }
    else if (k === 'Home') { e.preventDefault(); stopAuto(); commit(Math.round(aim.current / 360) * 360); }
    else if (k === 'End') { e.preventDefault(); stopAuto(); commit(Math.round(aim.current / 360) * 360 + STEP * (N - 1)); }
  };

  const spin = () => {
    stopAuto();
    const turns = 2 + Math.floor(Math.random() * 2);
    commit(aim.current + turns * 360 + Math.floor(Math.random() * N) * STEP);
  };

  const ballSize = 'clamp(150px, 22vw, 210px)';

  return (
    <div className="machine">
      <div
        ref={winRef}
        className={'dr-window' + (dragging ? ' dragging' : '')}
        style={{ '--win-h': WIN_H + 'px', '--seg-h': SEG_H + 'px' }}
        tabIndex={0}
        role="group"
        aria-label="Career drum. Use the arrow keys to move one career at a time, or press the spin button."
        onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp}
        onKeyDown={onKey}
      >
        <motion.div ref={cylRef} className="dr-cyl">
          {CAREERS.map((c, i) => (
            <div
              key={c.id}
              ref={(el) => (segRefs.current[i] = el)}
              className="dr-seg"
              style={{
                transform: `rotateX(${i * STEP}deg) translateZ(${RADIUS}px)`,
                background: c.tone,
                color: c.ink,
              }}
              aria-hidden="true"
            >
              <span className="n">{String(i + 1).padStart(2, '0')}</span>
              <span className="txt">
                <span className="t">{c.title}</span>
                <span className="c" style={{ color: c.ink === '#FFFFFF' ? 'rgba(255,255,255,.88)' : 'rgba(22,22,22,.66)' }}>
                  {c.cluster}
                </span>
              </span>
            </div>
          ))}
        </motion.div>
        <div className="bezel t" />
        <div className="bezel b" />
        <div className="payline"><i className="l" /><i className="r" /></div>
      </div>

      <BallButton rot={rot} onSpin={spin} spinning={spinning} size={ballSize} />
    </div>
  );
}

/* =====================================================================
   Page
   ===================================================================== */
const SIZES = [
  { v: 1, label: 'A', name: 'Normal text size', cls: 't1' },
  { v: 1.15, label: 'A', name: 'Large text size', cls: 't2' },
  { v: 1.3, label: 'A', name: 'Extra large text size', cls: 't3' },
];

function App() {
  const [selected, setSelected] = useState(() => store.get('selected', 'paralegal'));
  const [auto, setAuto] = useState(false);
  const [tscale, setTscale] = useState(() => store.get('tscale', 1));
  const reduce = useReducedMotion();

  useEffect(() => { store.set('selected', selected); }, [selected]);
  useEffect(() => {
    document.documentElement.style.setProperty('--tscale', String(tscale));
    store.set('tscale', tscale);
  }, [tscale]);
  useEffect(() => { document.getElementById('boot')?.setAttribute('hidden', ''); }, []);

  const c = CAREERS.find((x) => x.id === selected) || CAREERS[0];

  return (
    <>
      <header className="dr-top">
        <img className="mark" src="assets/svg/brand/app-mark.svg" alt="" />
        <div>
          <b>The Career Drum</b>
          <span>Career Futures</span>
        </div>
        <div className="grow" />
        <a className="backlink" href="index.html">
          <svg viewBox="0 0 64 64" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="5"
            strokeLinecap="round" strokeLinejoin="round"><path d="M10 32h44M38 16l16 16-16 16" /></svg>
          Back to the lab
        </a>
      </header>

      <main className="dr-shell" id="drum-main">
        <div className="a11ybar" role="group" aria-label="Display settings">
          <div className="grp">
            <span className="lab" id="tsz">Text size</span>
            {SIZES.map((s) => (
              <button key={s.v} className={s.cls} aria-pressed={tscale === s.v}
                aria-describedby="tsz" title={s.name}
                onClick={() => setTscale(s.v)}>
                {s.label}<span className="sr-only"> — {s.name}</span>
              </button>
            ))}
          </div>
          <div className="grp">
            <span className="lab">Motion</span>
            <button aria-pressed={auto} onClick={() => setAuto((v) => !v)} disabled={!!reduce}>
              {auto ? 'Stop slow rotation' : 'Rotate slowly'}
            </button>
            {reduce && <span className="tiny">Your system asks for reduced motion, so the drum moves instantly.</span>}
          </div>
        </div>

        <div className="dr-head">
          <p className="eyebrow">Step one</p>
          <h1>Pick a career</h1>
          <p>
            Twelve occupations. Spin the ball, drag the drum, use the arrow keys, or choose
            from the list — then take that career into the lab and work out how exposed it is
            to AI and automation.
          </p>
        </div>

        <Drum selected={selected} onSelect={setSelected} auto={auto} setAuto={setAuto} />

        <div className="dr-picker">
          <label htmlFor="pick">Or choose a career from the list</label>
          <select id="pick" value={selected} onChange={(e) => { setAuto(false); setSelected(e.target.value); }}>
            {CAREERS.map((x) => <option key={x.id} value={x.id}>{x.title} — {x.cluster}</option>)}
          </select>
        </div>

        <p className="sr-only" role="status" aria-live="polite">
          Selected career: {c.title}, {c.cluster}. Number {CAREERS.indexOf(c) + 1} of {N}.
        </p>

        <section className="readcard" aria-label="Selected career">
          <div className="band" style={{ background: c.tone, color: c.ink }}>
            <p className="k">Selected career · {CAREERS.indexOf(c) + 1} of {N}</p>
            <h2>{c.title}</h2>
            <p>{c.full} · SOC {c.soc} · {c.cluster}</p>
          </div>
          <div className="foot">
            <p>
              Open the lab to see live Bureau of Labor Statistics employment and pay for this
              occupation, sort its tasks, and build your own exposure estimate.
            </p>
            <a className="btn" href="index.html">
              Open the career file
              <span className="knob">
                <svg viewBox="0 0 64 64" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="5"
                  strokeLinecap="round" strokeLinejoin="round"><path d="M10 32h44M38 16l16 16-16 16" /></svg>
              </span>
            </a>
          </div>
        </section>

        <section className="keyhelp">
          <h3>Keyboard and screen reader</h3>
          <dl>
            <dt><kbd>Tab</kbd></dt><dd>Move between the text-size buttons, the drum, the spin ball and the list.</dd>
            <dt><kbd>↑</kbd> <kbd>↓</kbd></dt><dd>Move the drum one career at a time, with the drum focused.</dd>
            <dt><kbd>Page Up</kbd> <kbd>Page Down</kbd></dt><dd>Move three careers at a time.</dd>
            <dt><kbd>Home</kbd> <kbd>End</kbd></dt><dd>Jump to the first or last career.</dd>
            <dt><kbd>Enter</kbd> <kbd>Space</kbd></dt><dd>Spin, when the ball has focus.</dd>
          </dl>
          <p className="tiny" style={{ marginTop: 14 }}>
            Every change of career is announced politely to screen readers. The list below the
            drum does the same job as the drum and is never hidden. If your system asks for
            reduced motion the drum stops animating and jumps straight to the chosen career.
          </p>
        </section>

        <footer style={{ marginTop: 40, paddingTop: 22, borderTop: '2px solid rgba(255,247,231,.12)' }}>
          <p className="tiny" style={{ maxWidth: '74ch' }}>
            <b style={{ color: 'var(--mint)' }}>Career Futures — The Career Drum.</b> Occupation
            titles and SOC codes follow the U.S. Bureau of Labor Statistics Standard
            Occupational Classification. Design system from the Career Futures site asset
            package. Built with Claude (Anthropic).
          </p>
        </footer>
      </main>
    </>
  );
}

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