// app.jsx — Main shell: scroll loop, scroll-driven 3D blend, HUD, rail, cursor, tweaks.

const { useState, useEffect, useRef, useCallback } = React;

// TWEAK_DEFAULTS lives in Hecstrim.html (so the loader can read it before
// Babel transpiles this file). We just pull from window here.
const TWEAK_DEFAULTS = window.TWEAK_DEFAULTS || {
  cursorTrail: true, neonGrain: true, loopBanner: true, magneticUI: true,
  loaderStyle: 'malla',
};

// Hero look/feel — variants previously surfaced as tweaks. The selected
// combination is baked in; tweaks panel no longer exposes these.
const HERO_BAKED = {
  cursorReach:      'medium',  // ← cursor halo size
  panelSeparation:  'lift',    // ← how panels react to the cursor
  wirePulse:        'soft',    // ← mesh breathing intensity
  aboutTransition:  'warp',    // ← hero→about dive style
};

const SECTION_NAMES = [
  { key: 'hero',         label: 'inicio',      num: '01' },
  { key: 'about',        label: 'perfil',      num: '02' },
  { key: 'deliverables', label: 'descargas',   num: '03' },
  { key: 'shop',         label: 'tienda',      num: '04' },
  { key: 'links',        label: 'enlaces',     num: '05' },
];

// ─────────────────────────────────────────────────────────
// Custom cursor — follows mouse with slight lag, scales on
// pointer hover over interactive elements.
// ─────────────────────────────────────────────────────────
function useCustomCursor() {
  useEffect(() => {
    const cursor = document.getElementById('cursor');
    if (!cursor) return;
    let tx = window.innerWidth / 2, ty = window.innerHeight / 2;
    let cx = tx, cy = ty;
    let raf;
    const onMove = (e) => { tx = e.clientX; ty = e.clientY; };
    const onOver = (e) => {
      const interactive = e.target.closest('a, button, .btn, .chip, .card, [role="button"]');
      cursor.classList.toggle('large', !!interactive);
    };
    const onLeave = () => { cursor.style.opacity = '0'; };
    const onEnter = () => { cursor.style.opacity = '1'; };
    const loop = () => {
      cx += (tx - cx) * 0.25;
      cy += (ty - cy) * 0.25;
      cursor.style.left = cx + 'px';
      cursor.style.top = cy + 'px';
      raf = requestAnimationFrame(loop);
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerover', onOver);
    window.addEventListener('pointerleave', onLeave);
    window.addEventListener('pointerenter', onEnter);
    loop();
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerover', onOver);
      window.removeEventListener('pointerleave', onLeave);
      window.removeEventListener('pointerenter', onEnter);
    };
  }, []);
}

// ─────────────────────────────────────────────────────────
// Scroll loop: render [tail-clone, ...real, head-clone].
// When user lands on a clone, silently snap to its real
// counterpart. Continuously emits scroll fraction so the
// 3D scene and background can crossfade.
// ─────────────────────────────────────────────────────────
function useScrollLoop(deckRef, count, onProgress, onLoopWrap) {
  const fireLoopBanner = useCallback(() => {
    const el = document.querySelector('.loop-banner');
    if (!el) return;
    el.classList.remove('fire');
    void el.offsetWidth;
    el.classList.add('fire');
  }, []);

  useEffect(() => {
    const deck = deckRef.current;
    if (!deck) return;
    let scrollTimer = null;
    let raf;

    const emitProgress = () => {
      // Map raw scrollTop to a continuous position `p` in [0, count]. The
      // wrap zone [count-1, count] represents the smooth Links→Hero loop
      // — both clones (tail at f∈[0,1] and head at f∈[count, count+1])
      // resolve into the same wrap range, so downstream consumers can
      // crossfade through it without ever knowing the snap happened.
      const f = deck.scrollTop / window.innerHeight;
      let p;
      if (f >= 1 && f <= count) {
        p = f - 1;                                            // main range 0..count-1
      } else if (f > count) {
        p = (count - 1) + Math.min(1, f - count);             // head-clone → wrap
      } else {
        p = (count - 1) + Math.max(0, Math.min(1, f));        // tail-clone → wrap
      }

      // Convert continuous position to (idx, frac). In the wrap zone we
      // saturate at idx=count-1, frac up to 1 — Option A's contract with
      // __setSection / __setBgAccent / camera blend, which read idx=count-1
      // & frac>0 as "Links blending toward Hero".
      let idx, frac;
      if (p >= count - 1) {
        idx = count - 1;
        frac = Math.min(1, p - idx);
      } else {
        idx = Math.floor(p);
        frac = p - idx;
      }
      onProgress(idx, frac);
    };

    const handleScroll = () => {
      emitProgress();
      clearTimeout(scrollTimer);
      scrollTimer = setTimeout(() => {
        const settledIdx = Math.round(deck.scrollTop / window.innerHeight);
        if (settledIdx === 0) {
          // tail clone — jump to real last
          fireLoopBanner();
          if (onLoopWrap) onLoopWrap();
          requestAnimationFrame(() => {
            deck.classList.add('no-snap');
            deck.scrollTop = count * window.innerHeight;
            requestAnimationFrame(() => {
              deck.classList.remove('no-snap');
              emitProgress();
            });
          });
        } else if (settledIdx === count + 1) {
          fireLoopBanner();
          if (onLoopWrap) onLoopWrap();
          requestAnimationFrame(() => {
            deck.classList.add('no-snap');
            deck.scrollTop = 1 * window.innerHeight;
            requestAnimationFrame(() => {
              deck.classList.remove('no-snap');
              emitProgress();
            });
          });
        }
      }, 150);
    };

    deck.addEventListener('scroll', handleScroll, { passive: true });

    // Start at real section 0 (at scrollTop = 1 * vh). Wait a tick to ensure
    // the deck has its scroll height (sections rendered + sized) before we set.
    const init = () => {
      deck.classList.add('no-snap');
      deck.scrollTop = window.innerHeight;
      // Use a longer timeout so the browser doesn't auto-snap back to 0
      setTimeout(() => {
        deck.scrollTop = window.innerHeight; // re-set in case snap reset
        deck.classList.remove('no-snap');
        emitProgress();
      }, 80);
    };
    // Ensure layout pass complete
    setTimeout(init, 60);

    return () => {
      deck.removeEventListener('scroll', handleScroll);
      clearTimeout(scrollTimer);
      cancelAnimationFrame(raf);
    };
  }, [deckRef, count, onProgress, fireLoopBanner, onLoopWrap]);

  const scrollTo = useCallback((visualIdx) => {
    const deck = deckRef.current;
    if (!deck) return;
    const target = (visualIdx + 1) * window.innerHeight;
    deck.scrollTo({ top: target, behavior: 'smooth' });
  }, [deckRef]);

  return { scrollTo };
}

// ─────────────────────────────────────────────────────────
// Keyboard navigation
// ─────────────────────────────────────────────────────────
function useKeyboardNav(active, count, scrollTo) {
  useEffect(() => {
    const handler = (e) => {
      if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
      if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ') {
        e.preventDefault();
        scrollTo((active + 1) % count);
      } else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
        e.preventDefault();
        scrollTo((active - 1 + count) % count);
      } else if (e.key === 'Home') {
        e.preventDefault();
        scrollTo(0);
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [active, count, scrollTo]);
}

// ─────────────────────────────────────────────────────────
// HUD — top bar with brand mark, section index, live clock
// ─────────────────────────────────────────────────────────
function HUD({ active, fraction }) {
  const [time, setTime] = useState(() => new Date());
  useEffect(() => {
    const t = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(t);
  }, []);
  const stamp = time.toISOString().slice(11, 19);
  const meterFill = (active + fraction) / SECTION_NAMES.length;
  const meterBars = 12;
  return (
    <div className="hud">
      <div className="left">
        <span className="brand">
          <span className="square"></span>
          hecstrim
        </span>
        <span className="mono" style={{ color: 'var(--text-faint)' }}>| gráficos en movimiento</span>
      </div>
      <div className="right">
        <span className="meter" aria-hidden>
          {Array.from({ length: meterBars }).map((_, i) => (
            <i key={i} className={i / meterBars < meterFill ? 'on' : ''}></i>
          ))}
        </span>
        <span className="time">{stamp}</span>
        <span style={{ color: 'var(--text)' }}>
          {SECTION_NAMES[active]?.num}/{String(SECTION_NAMES.length).padStart(2, '0')}
        </span>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────
// Rail — right-side section nav
// ─────────────────────────────────────────────────────────
function Rail({ active, scrollTo }) {
  return (
    <div className="rail">
      {SECTION_NAMES.map((s, i) => (
        <button key={s.key} className={active === i ? 'active' : ''}
          onClick={(e) => { scrollTo(i); window.emitSparks && window.emitSparks(e.clientX, e.clientY, 10); }}>
          <span className="label">{s.num} {s.label}</span>
          <span className="tick"></span>
        </button>
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────────────────
// App
// ─────────────────────────────────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const deckRef = useRef(null);
  const [active, setActive] = useState(0);
  const [fraction, setFraction] = useState(0);

  useCustomCursor();

  // Apply tweak: grain toggle
  useEffect(() => {
    const el = document.getElementById('grain');
    if (el) el.style.display = t.neonGrain ? '' : 'none';
  }, [t.neonGrain]);

  // Apply tweak: cursor visibility
  useEffect(() => {
    const el = document.getElementById('cursor');
    if (el) el.style.display = t.cursorTrail ? '' : 'none';
    document.body.style.cursor = t.cursorTrail ? 'none' : 'default';
  }, [t.cursorTrail]);

  // Sync hero→about transition variant into three-scene click handler
  useEffect(() => {
    window.__heroAboutVariant = HERO_BAKED.aboutTransition;
  }, []);

  // Orchestrate scroll position around the 3D transition. Visual handover is
  // entirely CSS — both sections are pinned as fixed overlays by .hx-transitioning.
  // We snap scrollTop to about's slot only at the very end.
  const cooldownRef = useRef(0);
  const pinScrollRef = useRef(-1); // -1 = don't pin; otherwise the scrollTop to enforce
  useEffect(() => {
    const onT = (e) => {
      const deck = deckRef.current;
      if (!deck) return;
      const phase = e.detail && e.detail.phase;
      if (phase === 'start') {
        deck.classList.add('hx-transitioning');
      } else if (phase === 'end') {
        // Update the pin to the DESTINATION before snapping, so the scroll
        // listener doesn't immediately revert our snap back to the origin.
        const dest = 2 * window.innerHeight;
        pinScrollRef.current = dest;
        deck.scrollTop = dest;
        // Clear the captured hero offset so subsequent transitions re-measure.
        deck.style.removeProperty('--hx-hero-top');
        cooldownRef.current = performance.now() + 600;
        setTimeout(() => deck.classList.remove('hx-transitioning'), 120);
        // Release the pin shortly after cooldown ends so the user can scroll freely.
        setTimeout(() => { pinScrollRef.current = -1; }, 720);
      }
    };
    window.addEventListener('hxtransition', onT);
    return () => window.removeEventListener('hxtransition', onT);
  }, []);

  // Intercept scroll-down on hero to fire the warp transition. We use a TINY
  // threshold so the deck only has a chance to drift a pixel or two before we
  // take over — and on fire, we measure the hero's CURRENT viewport position
  // and pin position:fixed to that exact spot, so there is NO visual snap-back
  // to origin when the overlay class activates. The transition then animates
  // from wherever the scroll left things.
  useEffect(() => {
    const deck = deckRef.current;
    if (!deck) return;
    let touchStartY = 0;

    const isLocked = () =>
      deck.classList.contains('hx-transitioning') ||
      performance.now() < cooldownRef.current;

    const onScroll = () => {
      const pin = pinScrollRef.current;
      if (pin >= 0 && deck.scrollTop !== pin) {
        deck.scrollTop = pin;
      }
    };

    const onWheel = (e) => {
      if (isLocked()) { e.preventDefault(); return; }
      // Tiny threshold: any meaningful downward intent fires immediately.
      if (active === 0 && e.deltaY > 0 && window.__startHeroAboutTransition) {
        e.preventDefault();
        fire();
      }
    };
    const fire = () => {
      // Measure hero's live viewport offset BEFORE we add the transitioning
      // class. Whatever pixels the scroll has eaten get baked into a CSS
      // variable so position:fixed lands hero at the same visual spot.
      const heroEl = deck.querySelector('.screen[data-screen-label="01 Hero"]');
      const heroTop = heroEl ? heroEl.getBoundingClientRect().top : 0;
      deck.style.setProperty('--hx-hero-top', heroTop + 'px');
      pinScrollRef.current = deck.scrollTop;
      window.__startHeroAboutTransition(HERO_BAKED.aboutTransition);
    };

    const onTouchStart = (e) => { touchStartY = e.touches[0].clientY; };
    const onTouchMove = (e) => {
      if (isLocked()) { e.preventDefault(); return; }
      if (active !== 0) return;
      const dy = touchStartY - e.touches[0].clientY;
      if (dy > 4 && window.__startHeroAboutTransition) {
        e.preventDefault();
        fire();
      }
    };
    const onKey = (e) => {
      if (!isLocked()) return;
      if (['ArrowDown','ArrowUp','PageDown','PageUp','Home','End',' '].includes(e.key)) {
        e.preventDefault();
      }
    };

    deck.addEventListener('wheel', onWheel, { passive: false });
    deck.addEventListener('touchstart', onTouchStart, { passive: true });
    deck.addEventListener('touchmove', onTouchMove, { passive: false });
    deck.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('keydown', onKey, { capture: true });
    return () => {
      deck.removeEventListener('wheel', onWheel);
      deck.removeEventListener('touchstart', onTouchStart);
      deck.removeEventListener('touchmove', onTouchMove);
      deck.removeEventListener('scroll', onScroll);
      window.removeEventListener('keydown', onKey, { capture: true });
    };
  }, [active]);

  // Push the baked hero look into three-scene. Runs once on mount; if the
  // scene loads after this effect we retry until __setHeroTweaks is ready.
  useEffect(() => {
    const reach = { cursorRadius: 1.7, cursorBoost: 1.0 };           // 'medium'
    const sep   = { repel: 1.0, panelGap: 0.30, reactStyle: 0 };     // 'lift'
    const pulse = { wireBase: 0.40, wireAmp: 0.14, wireSpeed: 1.2 }; // 'soft'
    const payload = { ...reach, ...sep, ...pulse };
    const push = () => {
      if (window.__setHeroTweaks) { window.__setHeroTweaks(payload); return true; }
      return false;
    };
    if (!push()) {
      const id = setInterval(() => { if (push()) clearInterval(id); }, 80);
      setTimeout(() => clearInterval(id), 4000);
      return () => clearInterval(id);
    }
  }, []);

  // Tell the loader the app has mounted (one signal flips it to 'ready').
  useEffect(() => {
    // Wait a tick so the first paint has actually rendered the deck.
    const id = requestAnimationFrame(() => {
      if (window.__signalReady) window.__signalReady('app');
    });
    return () => cancelAnimationFrame(id);
  }, []);

  // Scroll progress pushes into the global 3D scene + 2D background
  const onProgress = useCallback((idx, frac) => {
    setActive(idx);
    setFraction(frac);
    if (window.__setSection) window.__setSection(idx, frac);
    if (window.__setBgAccent) window.__setBgAccent(idx, frac);
  }, []);

  // When the loop wraps (tail-clone → real last, or head-clone → real first)
  // we silence the hero→about wheel handler for a moment. Otherwise the
  // wheel-event residue from a forceful scroll keeps firing AFTER the snap
  // lands us on hero, immediately triggering the dive-to-about transition.
  // Symptom this fixes: scrolling down from 05 links flashed past hero and
  // landed on 02 about instead of stopping cleanly at 01 hero.
  const onLoopWrap = useCallback(() => {
    cooldownRef.current = performance.now() + 650;
  }, []);

  const { scrollTo } = useScrollLoop(deckRef, SECTION_NAMES.length, onProgress, onLoopWrap);
  useKeyboardNav(active, SECTION_NAMES.length, scrollTo);

  const renderSection = (key, suffix = '', sectionIdx = -1) => {
    const isActive = active === sectionIdx;
    switch (key) {
      case 'hero':         return <Hero key={'hero'+suffix} active={isActive} />;
      case 'about':        return <About key={'about'+suffix} active={isActive} />;
      case 'deliverables': return <Deliverables key={'deliv'+suffix} active={isActive} />;
      case 'shop':         return <Shop key={'shop'+suffix} active={isActive} />;
      case 'links':        return <Links key={'links'+suffix} active={isActive} />;
      default: return null;
    }
  };

  return (
    <>
      <div className="deck" ref={deckRef}>
        {/* tail clone — duplicate of last section, sits before real first */}
        <div aria-hidden="true">{renderSection(SECTION_NAMES[SECTION_NAMES.length - 1].key, '-tail', -1)}</div>
        {SECTION_NAMES.map((s, i) => renderSection(s.key, '', i))}
        {/* head clone — duplicate of first, sits after real last */}
        <div aria-hidden="true">{renderSection(SECTION_NAMES[0].key, '-head', -1)}</div>
      </div>

      <HUD active={active} fraction={fraction} />
      <Rail active={active} scrollTo={scrollTo} />
      {t.loopBanner && <div className="loop-banner">↻ LOOP · CONTINUAR</div>}

      <TweaksPanel>
        <TweakSection label="Carga" />
        <TweakRadio label="Estilo del loader" value={t.loaderStyle}
          options={['malla', 'escaneo', 'pulso']}
          onChange={(v) => setTweak('loaderStyle', v)} />
        <div style={{
          font: 'inherit', fontSize: 10, color: '#5a544a',
          padding: '0 2px 8px', letterSpacing: '0.06em',
        }}>
          se aplica al recargar la página
        </div>

        <TweakSection label="Movimiento" />
        <TweakToggle label="Cursor personalizado" value={t.cursorTrail}
          onChange={(v) => setTweak('cursorTrail', v)} />
        <TweakToggle label="UI magnética" value={t.magneticUI}
          onChange={(v) => setTweak('magneticUI', v)} />
        <TweakToggle label="Granulado" value={t.neonGrain}
          onChange={(v) => setTweak('neonGrain', v)} />
        <TweakToggle label="Banner del loop" value={t.loopBanner}
          onChange={(v) => setTweak('loopBanner', v)} />

        <TweakSection label="Navegar" />
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
          {SECTION_NAMES.map((s, i) => (
            <button key={s.key}
              onClick={() => scrollTo(i)}
              style={{
                appearance: 'none',
                border: '1px solid rgba(0,0,0,0.1)',
                background: active === i ? '#29261b' : 'transparent',
                color: active === i ? '#fff' : '#29261b',
                padding: '6px 8px',
                font: 'inherit', fontSize: 10,
                cursor: 'pointer', letterSpacing: '0.12em',
                textTransform: 'uppercase',
              }}>
              {s.num} {s.label}
            </button>
          ))}
        </div>
      </TweaksPanel>
    </>
  );
}

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