import React, { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';

type SectionId = 'home' | 'projects' | 'mission' | 'behind-cave';

const sectionIds: SectionId[] = ['home', 'projects', 'mission', 'behind-cave'];

const legacyRoutes: Record<string, SectionId> = {
  '/home': 'home',
  '/projects': 'projects',
  '/mission': 'mission',
  '/behind-cave': 'behind-cave',
};

const titles: Record<SectionId, string> = {
  home: 'Cave Productions',
  projects: 'Projects — Cave Productions',
  mission: 'Mission — Cave Productions',
  'behind-cave': 'Behind Cave — Cave Productions',
};

const navItems: Array<{ label: string; section: Exclude<SectionId, 'home'> }> = [
  { label: 'PROJECTS', section: 'projects' },
  { label: 'MISSION', section: 'mission' },
  { label: 'BEHIND CAVE', section: 'behind-cave' },
];

let suppressObservedUrlUntil = 0;

function sectionFromHash(hash: string): SectionId | null {
  const id = hash.replace(/^#/, '') as SectionId;
  return sectionIds.includes(id) ? id : null;
}

function sectionUrl(section: SectionId) {
  return section === 'home' ? '/' : `/#${section}`;
}

function scrollToSection(section: SectionId, behavior: ScrollBehavior = 'smooth') {
  const target = document.getElementById(section);
  if (!target) return;
  if (behavior === 'auto') {
    const root = document.documentElement;
    const previousBehavior = root.style.scrollBehavior;
    root.style.scrollBehavior = 'auto';
    window.scrollTo({ top: target.offsetTop });
    requestAnimationFrame(() => { root.style.scrollBehavior = previousBehavior; });
    return;
  }
  target.scrollIntoView({ behavior, block: 'start' });
}

function SectionLink({ section, children, className, ariaLabel, ariaCurrent, onNavigate }: {
  section: SectionId;
  children: React.ReactNode;
  className?: string;
  ariaLabel?: string;
  ariaCurrent?: 'location';
  onNavigate?: () => void;
}) {
  return (
    <a
      className={className}
      href={sectionUrl(section)}
      aria-label={ariaLabel}
      aria-current={ariaCurrent}
      onClick={(event) => {
        if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
        event.preventDefault();
        suppressObservedUrlUntil = Date.now() + 1200;
        const nextUrl = sectionUrl(section);
        if (`${window.location.pathname}${window.location.hash}` !== nextUrl) {
          window.history.pushState({ section }, '', nextUrl);
        }
        scrollToSection(section);
        onNavigate?.();
      }}
    >
      {children}
    </a>
  );
}

function Wordmark({ home, onNavigate }: { home: boolean; onNavigate?: () => void }) {
  return (
    <SectionLink section="home" className={`wordmark${home ? ' wordmark--home' : ''}`} ariaLabel="Cave Productions home" onNavigate={onNavigate}>
      <span>CAVE</span>
      <small>PRODUCTIONS</small>
    </SectionLink>
  );
}

function Header({ activeSection }: { activeSection: SectionId }) {
  const [open, setOpen] = useState(false);

  useEffect(() => setOpen(false), [activeSection]);

  return (
    <header className="site-header">
      <Wordmark home={activeSection === 'home'} onNavigate={() => setOpen(false)} />
      <button
        className="menu-toggle"
        type="button"
        aria-expanded={open}
        aria-controls="primary-navigation"
        aria-label={open ? 'Close navigation' : 'Open navigation'}
        onClick={() => setOpen((value) => !value)}
      >
        <span />
        <span />
      </button>
      <nav id="primary-navigation" className={open ? 'is-open' : ''} aria-label="Primary navigation">
        {navItems.map((item) => (
          <SectionLink
            key={item.section}
            section={item.section}
            className={`nav-link${activeSection === item.section ? ' is-active' : ''}`}
            ariaCurrent={activeSection === item.section ? 'location' : undefined}
            onNavigate={() => setOpen(false)}
          >
            {item.label}
          </SectionLink>
        ))}
      </nav>
    </header>
  );
}

function SectionShell({ id, className, children, footer = false }: {
  id: SectionId;
  className: string;
  children: React.ReactNode;
  footer?: boolean;
}) {
  return (
    <section id={id} className={`page site-section ${className}`} aria-labelledby={`${id}-title`}>
      <div className="page__backdrop" aria-hidden="true" />
      <div className="page__content">{children}</div>
      {footer && <SiteFooter />}
    </section>
  );
}

function SiteFooter() {
  return (
    <footer className="site-footer">
      <p>© 2026 CAVE PRODUCTIONS LLC</p>
      <a href="mailto:caveproductionscontact@gmail.com">caveproductionscontact@gmail.com</a>
    </footer>
  );
}

/* Fire palette: heat 0..1 -> RGBA ramp, deep red through vivid orange to a
   near-white core. Alpha stays at zero until heat 0.3 and then climbs hard,
   reaching 0.78 by heat 0.52. A gentler ramp left the flame as a small bright
   core inside a glow four times its width, which read as a halo and washed
   out the flame's impact; the steep climb puts most of the body at full
   strength and keeps the dim skirt narrow. */
function buildFirePalette() {
  const stops: [number, number, number, number, number][] = [
    [0.0, 0, 0, 0, 0.0],
    [0.3, 110, 20, 3, 0.0],
    [0.4, 190, 52, 8, 0.45],
    [0.52, 236, 96, 16, 0.78],
    [0.64, 252, 140, 30, 0.92],
    [0.78, 255, 190, 72, 0.98],
    [1.0, 255, 244, 206, 1.0],
  ];
  const pal = new Uint8ClampedArray(256 * 4);
  for (let i = 0; i < 256; i++) {
    const h = i / 255;
    let a = stops[0];
    let b = stops[stops.length - 1];
    for (let s = 0; s < stops.length - 1; s++) {
      if (h >= stops[s][0] && h <= stops[s + 1][0]) {
        a = stops[s];
        b = stops[s + 1];
        break;
      }
    }
    const f = (h - a[0]) / (b[0] - a[0] || 1);
    pal[i * 4] = a[1] + (b[1] - a[1]) * f;
    pal[i * 4 + 1] = a[2] + (b[2] - a[2]) * f;
    pal[i * 4 + 2] = a[3] + (b[3] - a[3]) * f;
    pal[i * 4 + 3] = (a[4] + (b[4] - a[4]) * f) * 255;
  }
  return pal;
}

/* The campfire inside the "C". The artwork's own flame is a blown-out white
   core (lum 252), so light added over it is invisible — this canvas sits on
   the log line and burns upward into the dark cave air, where there is
   headroom for it to read. Heat rises, cools and sways; the surrounding
   glow layers track --fire so cast light matches the flame frame by frame. */
function CampFire() {
  const hostRef = useRef<HTMLSpanElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const host = hostRef.current;
    if (!canvas || !host) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    // Simulated finer than it is drawn: downsampling the extra rows into the
    // display size resolves individual licks instead of chunky cells.
    const W = 64;
    const H = 108;
    const STEP = 18; // ~55Hz — hard-burning, not a settled campfire
    canvas.width = W;
    canvas.height = H;

    const heat = new Float32Array(W * H);
    const frame = ctx.createImageData(W, H);
    const data = frame.data;
    const pal = buildFirePalette();
    const bottom = (H - 1) * W;

    // Fuel bed: hottest mid-pile, tapering to the edges of the logs.
    const bed = new Float32Array(W);
    const coolMul = new Float32Array(W);
    const seed = (t: number, ignition: number) => {
      // Slow and shallow on purpose. This dims every column at once, so if it
      // swings hard on a period near the ~1.9s heat takes to cross the grid,
      // each dip leaves a full-width band of cold that rises as a detached
      // puff — the fire reads as smoke balls leaving the logs rather than
      // tongues rooted in them. Kept well below that, the whole flame just
      // breathes up and down together.
      const env = 0.86 + 0.09 * Math.sin(t * 0.0011) + 0.05 * Math.sin(t * 0.0019 + 1.7);
      for (let x = 0; x < W; x++) {
        const nx = (x / (W - 1)) * 2 - 1;
        // A clean gaussian burns as a clean cone. Letting the pile bulge and
        // lean on its own slow schedule means the flame's envelope is never
        // the same symmetric triangle twice.
        const bulge =
          1 + 0.34 * Math.sin(t * 0.0035 + nx * 2.1) + 0.21 * Math.sin(t * 0.007 - nx * 3.3);
        const shape = Math.exp(-nx * nx * 4.4) * bulge;
        // Three waves at unrelated speeds and wavelengths, summed and then
        // CLIPPED AT ZERO. The clip is the whole point: where they cancel the
        // column starves completely, the heat above it loses its supply, and
        // that parcel pinches off and rises away on its own. A floored gate
        // (the old 0.3 + 0.7*|sin|) never starves, which is exactly why the
        // flame could only ever wave about as one connected cone.
        // Wavelengths kept long on purpose: narrow gaps are diffused shut
        // within ~20 rows of the bed and never climb clear of the logs.
        const g =
          0.38 * Math.sin(x * 0.3 + t * 0.0075) +
          0.32 * Math.sin(x * 0.62 - t * 0.019 + 1.3) +
          0.22 * Math.sin(x * 0.22 + t * 0.037) +
          0.06;
        const gate = g > 0 ? Math.pow(g, 0.8) : 0;
        const flare = 0.78 + 0.26 * Math.sin(t * 0.038 + x * 0.8) + 0.14 * Math.random();
        // High floor: the fuel bed itself stays substantial, so the fire is
        // dense and continuous where it leaves the logs. Driving the gaps
        // from down here instead split the flame from the bed upward, which
        // read as two thin columns rather than one fire. The tearing now
        // happens during the climb (see rise) — solid at the base, breaking
        // into tongues higher up, which is how a real one burns.
        const target = Math.min(1, shape * (0.32 + 0.72 * gate) * flare * env * 2.9 * ignition);
        // Heat climbs exactly one row per frame, so an unsmoothed seed writes
        // each frame's random value as a hard horizontal band that then
        // marches up the column. Easing the bed spreads any jump over several
        // rows instead.
        bed[x] += (target - bed[x]) * 0.55;
        const v = bed[x];
        heat[bottom + x] = v;
        if (v * 0.92 > heat[bottom - W + x]) heat[bottom - W + x] = v * 0.92;
      }
    };

    // Each cell draws from the three below it, cooling as it climbs. Sway
    // grows with height so the tips lick sideways while the base stays put.
    const rise = (t: number) => {
      // Kept small: the offset is re-applied every row, so it accumulates up
      // the column and a large value bends the whole flame over sideways.
      const sway = Math.sin(t * 0.0012) * 0.74 + Math.sin(t * 0.0027 + 0.8) * 0.4;
      // Slow per-column drift in how fast heat cools, stored as a deviation
      // from 1 so it can be scaled by height below. Precomputed per frame —
      // evaluating this per cell would be 7k sines.
      for (let x = 0; x < W; x++) {
        coolMul[x] = 0.62 * Math.sin(t * 0.009 + x * 0.42) + 0.4 * Math.sin(t * 0.015 + x * 0.93);
      }
      for (let y = 0; y < H - 1; y++) {
        const up = y * W;
        const dn = (y + 1) * W;
        const lift = 1 - y / H;
        const off = sway * lift * lift;
        // How hard the per-column cooling bites, ramped by height. Cubed
        // rather than squared so it stays near zero through the lower two
        // thirds: tongues stay rooted in the fuel bed and only separate near
        // the tips. Squared let it bite mid-flame and cut tongues loose from
        // the fire they were growing out of.
        const tear = 0.1 + 3.4 * lift * lift * lift;
        // Tuned so the flame dies out with a tip rather than filling the box.
        // The gaussian fuel bed leaves the edges cooler, so they burn out
        // first and the flame tapers on its own.
        const cool = 0.0047 + 0.0086 * lift;
        const i0 = Math.floor(off);
        const fr = off - i0;
        for (let x = 0; x < W; x++) {
          const at = (k: number) => {
            const c = x + k;
            return heat[dn + (c < 0 ? 0 : c > W - 1 ? W - 1 : c)];
          };
          const l = at(i0 - 1) * (1 - fr) + at(i0) * fr;
          const c0 = at(i0) * (1 - fr) + at(i0 + 1) * fr;
          const r = at(i0 + 1) * (1 - fr) + at(i0 + 2) * fr;
          // Low lateral mixing lets a gap in the fuel bed survive the climb
          // instead of diffusing shut ~20 rows up, but too little and each
          // column decays independently.
          //
          // The narrow random range matters more than it looks: a wide spread
          // here (it was 0.3 + 1.45*random) re-rolls every cell every frame,
          // so a rising parcel is shredded into speckle and the fire reads as
          // pixels flying upward rather than flame. Keeping the spread tight
          // lets parcels hold together and stretch into tongues as they climb.
          const v =
            l * 0.085 +
            c0 * 0.83 +
            r * 0.085 -
            cool * Math.max(0.15, 1 + tear * coolMul[x]) * (0.78 + 0.44 * Math.random());
          // Ease toward the new value so per-frame noise blends across rows
          // rather than freezing into stripes. Kept high enough that the
          // cooling noise still tears the flame into separate licks.
          const prev = heat[up + x];
          const next = prev + (v - prev) * 0.94;
          heat[up + x] = next > 0 ? next : 0;
        }
      }
    };

    const paint = () => {
      let energy = 0;
      for (let i = 0; i < W * H; i++) {
        const h = heat[i] > 1 ? 1 : heat[i];
        const p = (h * 255) | 0;
        data[i * 4] = pal[p * 4];
        data[i * 4 + 1] = pal[p * 4 + 1];
        data[i * 4 + 2] = pal[p * 4 + 2];
        data[i * 4 + 3] = pal[p * 4 + 3];
      }
      for (let y = H - 24; y < H; y++) for (let x = 0; x < W; x++) energy += heat[y * W + x];
      ctx.putImageData(frame, 0, 0);
      // Averaged heat only spans ~0.245-0.325, so stretch it into a range the
      // eye can actually read as the wall glow swelling and dipping.
      const e = energy / (24 * W);
      host.style.setProperty('--fire', Math.max(0.45, Math.min(1, e * 5.7 - 0.85)).toFixed(3));
    };

    const motionOff = window.matchMedia('(prefers-reduced-motion: reduce)');

    // Catch on load: nothing, then a hard surge of fuel that overshoots well
    // past the steady rate and falls back. Starting from a cold grid means the
    // flame visibly climbs out of the logs rather than being there already.
    const HOLD_MS = 260; // let the hero start resolving before it takes
    const IGNITE_MS = 1250;
    const ignitionAt = (ms: number) => {
      if (ms <= HOLD_MS) return 0;
      const p = (ms - HOLD_MS) / IGNITE_MS;
      if (p >= 1) return 1;
      if (p < 0.3) return 1.9 * (1 - Math.pow(1 - p / 0.3, 2.2));
      return 1 + 0.9 * Math.pow(1 - (p - 0.3) / 0.7, 2);
    };

    // Settle the sim so the first painted frame is a developed flame.
    const settle = () => {
      for (let i = 0; i < 150; i++) {
        seed(i * STEP, 1);
        rise(i * STEP);
      }
      paint();
    };

    let raf = 0;
    let last = 0;
    const start = performance.now();

    const loop = (now: number) => {
      raf = requestAnimationFrame(loop);
      if (now - last < STEP) return;
      last = now;
      const t = now - start;
      seed(t, ignitionAt(t));
      rise(t);
      paint();
    };

    // Reduced motion keeps the fire lit but holds it on a settled frame — no
    // catch, no flicker. Otherwise start from a cold grid so the flame has to
    // climb out of the logs.
    const run = () => {
      cancelAnimationFrame(raf);
      if (motionOff.matches) {
        settle();
        return;
      }
      heat.fill(0);
      bed.fill(0);
      last = 0;
      raf = requestAnimationFrame(loop);
    };

    run();
    motionOff.addEventListener('change', run);
    return () => {
      cancelAnimationFrame(raf);
      motionOff.removeEventListener('change', run);
    };
  }, []);

  return (
    <span className="campfire" ref={hostRef}>
      <span className="campfire__patch" />
      <span className="campfire__embers" />
      <canvas className="campfire__flames" ref={canvasRef} />
    </span>
  );
}

function HomeSection() {
  return (
    <SectionShell id="home" className="home-page">
      <h1 id="home-title" className="visually-hidden">Cave Productions</h1>
      <div className="home-hero" aria-hidden="true">
        <img className="home-hero__art" src="/assets/cave-wordmark-alpha.png" alt="" />
        <CampFire />
      </div>
      <p className="home-tagline">BELIEVE. BECOME.</p>
      <SectionLink section="projects" className="down-link" ariaLabel="View projects">
        <span aria-hidden="true" />
      </SectionLink>
    </SectionShell>
  );
}

function ProjectsSection() {
  return (
    <SectionShell id="projects" className="projects-page">
      <div className="projects-copy">
        <p className="eyebrow">CURRENT PROJECT</p>
        <h2 id="projects-title">PALATINE HILL</h2>
        <p className="project-status">FEATURE FILM <span>·</span> IN DEVELOPMENT</p>
        <p className="project-description">
          Legendary charioteer Gaius Diocles pursues a glory that<br className="desktop-break" />
          collides with Rome’s persecution of the early Christian movement.
        </p>
      </div>
    </SectionShell>
  );
}

function MissionSection() {
  return (
    <SectionShell id="mission" className="mission-page">
      <div className="mission-copy">
        <h2 id="mission-title">BELIEVE. BECOME.</h2>
        <p>
          Cave Productions helps create cinematic films that meet audiences where<br className="desktop-break" />
          they are and speak to the questions people carry today:<br className="desktop-break" />
          identity, success, purpose, and faith.
        </p>
        <p>
          We believe great stories can entertain without leaving meaning behind.<br className="desktop-break" />
          Through spectacle, character, consequence, and transformation,<br className="desktop-break" />
          we seek to tell stories that endure and inspire audiences to consider<br className="desktop-break" />
          not only who they are, but who they become.
        </p>
      </div>
    </SectionShell>
  );
}

const leaders = [
  {
    name: 'JORDAN TOLLNER',
    title: 'FOUNDER & CEO',
    bio: [
      <>Jordan Tollner is the founder and CEO of Cave Productions, a writer, producer, and actor focused on cinematic stories that explore identity, success, purpose, and faith.</>,
      <>He leads Cave’s creative vision and development, building projects designed to carry meaning beyond the screen and inspire audiences toward who they can become.</>,
    ],
  },
  {
    name: 'RON McNAIR',
    title: 'CHIEF FINANCIAL OFFICER',
    bio: [
      <>Ron McNair is a veteran finance, systems, and operations executive with more than 30 years of experience across media, entertainment, and business.</>,
      <>He previously served as President of Global Business Services and Chief Administrative Officer at Sony Pictures Entertainment, where he led global finance, HR, systems, and treasury operations.</>,
      <>As CFO of Cave Productions, Ron guides the company’s financial strategy, structure, and long-term stewardship.</>,
    ],
  },
  {
    name: 'BRUCE TOLLNER',
    title: 'CHAIRMAN',
    bio: [
      <>Bruce Tollner is an attorney and veteran sports executive with more than three decades of experience representing professional athletes and coaches.</>,
      <>His career has centered on complex negotiation, contracts, representation, and long-term client strategy.</>,
      <>As Chairman of Cave Productions, Bruce provides legal and business perspective as the company builds its foundation and develops its projects.</>,
    ],
  },
];

function BehindSection() {
  return (
    <SectionShell id="behind-cave" className="behind-page" footer>
      <div className="behind-copy">
        <h2 id="behind-cave-title">BEHIND CAVE</h2>
        <span className="title-rule" aria-hidden="true" />
        <p className="behind-intro">Cave Productions is led by a team united by a shared purpose: to create stories that endure.</p>
        <div className="leadership">
          {leaders.map((leader) => (
            <article className="leader" key={leader.name}>
              <h3>{leader.name}</h3>
              <h4>{leader.title}</h4>
              <span className="leader-rule" aria-hidden="true" />
              <div className="leader-bio">
                {leader.bio.map((paragraph, index) => <p key={index}>{paragraph}</p>)}
              </div>
            </article>
          ))}
        </div>
      </div>
    </SectionShell>
  );
}

function App() {
  const [activeSection, setActiveSection] = useState<SectionId>('home');
  const activeRef = useRef<SectionId>('home');
  const historyNavigationRef = useRef(false);

  useEffect(() => {
    const legacySection = legacyRoutes[window.location.pathname.replace(/\/$/, '')];
    const hashSection = sectionFromHash(window.location.hash);
    const initialSection = legacySection ?? hashSection ?? 'home';

    if (legacySection) {
      window.history.replaceState({ section: legacySection }, '', sectionUrl(legacySection));
    }

    requestAnimationFrame(() => scrollToSection(initialSection, 'auto'));

    const onPopState = () => {
      historyNavigationRef.current = true;
      suppressObservedUrlUntil = Date.now() + 1200;
      const target = sectionFromHash(window.location.hash) ?? 'home';
      scrollToSection(target);
      window.setTimeout(() => { historyNavigationRef.current = false; }, 700);
    };

    window.addEventListener('popstate', onPopState);
    return () => window.removeEventListener('popstate', onPopState);
  }, []);

  useEffect(() => {
    const sections = sectionIds
      .map((id) => document.getElementById(id))
      .filter((section): section is HTMLElement => Boolean(section));

    const visibility = new Map<SectionId, number>();
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => visibility.set(entry.target.id as SectionId, entry.intersectionRatio));
      const next = sectionIds.reduce((best, id) => (
        (visibility.get(id) ?? 0) > (visibility.get(best) ?? 0) ? id : best
      ), activeRef.current);

      if (next === activeRef.current || (visibility.get(next) ?? 0) < .25) return;
      activeRef.current = next;
      setActiveSection(next);
      document.title = titles[next];

      if (!historyNavigationRef.current && Date.now() >= suppressObservedUrlUntil) {
        const nextUrl = sectionUrl(next);
        if (`${window.location.pathname}${window.location.hash}` !== nextUrl) {
          window.history.replaceState({ section: next }, '', nextUrl);
        }
      }
    }, { threshold: [0, .2, .42, .55, .7, .9, 1] });

    sections.forEach((section) => observer.observe(section));
    return () => observer.disconnect();
  }, []);

  return (
    <>
      <Header activeSection={activeSection} />
      <main className="scroll-story">
        <HomeSection />
        <ProjectsSection />
        <MissionSection />
        <BehindSection />
      </main>
    </>
  );
}

createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
