// Main app — owns endDate, rows, zoom/pan, filters, hover, playback state.

const { useState: useStateA, useEffect: useEffectA, useRef: useRefA, useMemo: useMemoA, useCallback: useCallbackA } = React;

const DAYS = 60;
const SHIFT = 14;
const MIN_ZOOM = 1;
const MAX_ZOOM = 60;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "mood": "nocturnal",
  "blockStyle": "bars",
  "density": 28
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Apply mood + density to root via CSS class + variable
  useEffectA(() => {
    document.body.dataset.mood = t.mood;
    document.body.dataset.blockStyle = t.blockStyle;
    document.documentElement.style.setProperty('--row-h', t.density + 'px');
  }, [t.mood, t.blockStyle, t.density]);

  // endDate = the most-recent day shown (its row covers noon→noon).
  // Skip the in-progress window: the most recent row must be one whose
  // noon→noon span has fully elapsed.
  const [endDate, setEndDate] = useStateA(() => {
    const d = new Date();
    d.setHours(12, 0, 0, 0);
    if (new Date().getHours() < 12) d.setDate(d.getDate() - 1);
    d.setDate(d.getDate() - 1);
    return d;
  });

  // Rows are loaded asynchronously from the API when endDate changes.
  const [rows, setRows] = useStateA([]);
  const [loading, setLoading] = useStateA(true);
  useEffectA(() => {
    let cancelled = false;
    setLoading(true);
    MeowData.generateRange(endDate, DAYS)
      .then((r) => { if (!cancelled) { setRows(r); setLoading(false); } })
      .catch((err) => {
        console.error('Failed to load rows', err);
        if (!cancelled) { setRows([]); setLoading(false); }
      });
    return () => { cancelled = true; };
  }, [endDate]);

  // Zoom + pan (shared across all rows). Pan is in pixels, in the
  // un-transformed track space *after* it has been scaled by zoom.
  // i.e. we render track at width = zoom*100% then translateX(-pan).
  const [zoom, setZoom] = useStateA(1);
  const [pan, setPan] = useStateA(0);

  // Filters
  const [scoreThreshold, setScoreThreshold] = useStateA(0.25);
  const [enabledLabels, setEnabledLabels] = useStateA(() => new Set(MeowData.LABELS));

  // Hover & playback
  const [hover, setHover] = useStateA(null); // { meow, row, x, y }
  const [player, setPlayer] = useStateA(null); // { meow, row, dur, x, y }

  // Refs for measuring the scrollable track area
  const gridRef = useRefA(null); // axis-header element — track viewport bounds
  const wrapRef = useRefA(null); // grid-wrap — wheel/drag listener target (covers axis + rows)

  function trackWidthPx() {
    const el = gridRef.current;
    if (!el) return 0;
    return el.clientWidth;
  }

  // Clamp pan so we never reveal beyond the track edges.
  function clampPan(p, z) {
    const w = trackWidthPx();
    const max = Math.max(0, w * (z - 1));
    return Math.max(0, Math.min(max, p));
  }

  // Compute the smallest fractional [start, end] window that covers every
  // recording period across all loaded rows. Returns null if no periods.
  function recordingExtent(rowsArg) {
    let lo = Infinity, hi = -Infinity;
    for (const row of rowsArg) {
      for (const p of row.periods || []) {
        if (p.start < lo) lo = p.start;
        if (p.end > hi) hi = p.end;
      }
    }
    if (!isFinite(lo) || !isFinite(hi) || hi <= lo) return null;
    return { lo, hi };
  }

  // Set zoom + pan so the viewport spans only the recording extent.
  // Falls back to a full 1× view if no periods are available.
  function fitToRecordings(rowsArg) {
    const ext = recordingExtent(rowsArg);
    if (!ext) { setZoom(1); setPan(0); return; }
    // Tiny padding so the very first/last recordings aren't flush to the edges.
    const PAD = 0.01;
    const fStart = Math.max(0, ext.lo - PAD);
    const fEnd = Math.min(1, ext.hi + PAD);
    const range = Math.max(1e-6, fEnd - fStart);
    const z = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, 1 / range));
    // Pan needs the track viewport width, which only exists after layout.
    // Retry on the next frame if the ref isn't measurable yet (initial mount).
    function apply() {
      const w = trackWidthPx();
      if (!w) { requestAnimationFrame(apply); return; }
      setZoom(z);
      setPan(fStart * w * z);
    }
    apply();
  }

  useEffectA(() => {
    setPan((p) => clampPan(p, zoom));
    // re-clamp on resize too
    function onResize() { setPan((p) => clampPan(p, zoom)); }
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [zoom]);

  // Whenever the loaded rows change (initial load or date navigation),
  // default the viewport to the span of recording periods.
  useEffectA(() => {
    if (rows.length === 0) return;
    fitToRecordings(rows);
  }, [rows]);

  // ----- Wheel zoom (centered on cursor) -----
  useEffectA(() => {
    function onWheel(e) {
      // Zoom only when Ctrl (or Cmd on mac) is held; otherwise let the
      // page scroll vertically as normal.
      if (!e.ctrlKey && !e.metaKey) return;
      // Suppress the browser's native Ctrl+wheel page-zoom site-wide.
      // Must call preventDefault BEFORE any other work — and the listener
      // must be passive:false (we register it that way below).
      e.preventDefault();
      e.stopPropagation();
      const trackEl = gridRef.current;
      if (!trackEl) return;
      const rect = trackEl.getBoundingClientRect();
      // Cursor X within the track viewport, clamped so zoom anchors
      // behave sensibly when the wheel happens over the row-header gutter.
      const w = trackEl.clientWidth;
      const cursorX = Math.max(0, Math.min(w, e.clientX - rect.left));
      // Fraction along the *content* under the cursor:
      // visible offset = pan + cursorX, content width = w * zoom
      setZoom((z) => {
        const dy = e.deltaY;
        // exponential zoom; faster with bigger deltas
        const factor = Math.exp(-dy * 0.0015);
        let z2 = z * factor;
        z2 = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, z2));
        // Adjust pan so the point under cursor stays put.
        // Let frac = (pan + cursorX) / (w * z). We want
        // (pan2 + cursorX) / (w * z2) === frac
        setPan((p) => {
          const contentW = w * z;
          const frac = (p + cursorX) / contentW;
          const p2 = frac * (w * z2) - cursorX;
          const max = Math.max(0, w * (z2 - 1));
          return Math.max(0, Math.min(max, p2));
        });
        return z2;
      });
    }
    // Attach at document level with capture+non-passive so we win the race
    // against the browser's native Ctrl+wheel page-zoom behaviour regardless
    // of which descendant element the wheel event actually targets.
    document.addEventListener('wheel', onWheel, { passive: false, capture: true });
    return () => document.removeEventListener('wheel', onWheel, { capture: true });
  }, []);

  // ----- Drag to pan -----
  useEffectA(() => {
    const el = wrapRef.current;
    if (!el) return;
    let dragging = false;
    let startX = 0;
    let startPan = 0;
    function onDown(e) {
      // Don't start drag on a block (let click pass through)
      if (e.target.closest('.block')) return;
      // Only left button
      if (e.button !== 0) return;
      dragging = true;
      startX = e.clientX;
      startPan = panRef.current;
      el.classList.add('dragging');
      e.preventDefault();
    }
    function onMove(e) {
      if (!dragging) return;
      const dx = e.clientX - startX;
      setPan(() => clampPan(startPan - dx, zoomRef.current));
    }
    function onUp() {
      if (dragging) {
        dragging = false;
        el.classList.remove('dragging');
      }
    }
    el.addEventListener('mousedown', onDown);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      el.removeEventListener('mousedown', onDown);
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, []);

  // Refs for current zoom/pan so the drag handler isn't stale
  const zoomRef = useRefA(zoom); useEffectA(() => { zoomRef.current = zoom; }, [zoom]);
  const panRef = useRefA(pan);   useEffectA(() => { panRef.current = pan; }, [pan]);

  // ----- Toolbar handlers -----
  function shiftDays(delta) {
    setEndDate((d) => {
      const n = new Date(d);
      n.setDate(n.getDate() + delta);
      // Don't let user navigate into the future past today
      const today = new Date();
      today.setHours(12, 0, 0, 0);
      if (n > today) return today;
      return n;
    });
    setHover(null);
  }
  function jumpToDate(d) {
    const target = new Date(d);
    target.setHours(12, 0, 0, 0);
    const today = new Date();
    today.setHours(12, 0, 0, 0);
    if (target > today) target.setTime(today.getTime());
    setEndDate(target);
    setHover(null);
  }
  function toggleLabel(label) {
    setEnabledLabels((s) => {
      const n = new Set(s);
      if (n.has(label)) n.delete(label); else n.add(label);
      return n;
    });
  }

  // ----- Block hover / click -----
  const onBlockHover = useCallbackA((meow, row, e) => {
    setHover({ meow, row, x: e.clientX, y: e.clientY });
  }, []);
  const onBlockLeave = useCallbackA(() => setHover(null), []);
  const onBlockClick = useCallbackA((meow, row, e) => {
    const dur = MeowAudio.play(meow, () => {
      setPlayer((p) => (p && p.meow.id === meow.id ? null : p));
    });
    setPlayer({ meow, row, dur, startedAt: performance.now() });
  }, []);

  function closePlayer() {
    MeowAudio.stopAll();
    setPlayer(null);
  }

  // Counts
  const totalMeows = useMemoA(() => rows.reduce((acc, r) => acc + r.meows.length, 0), [rows]);
  const visibleMeows = useMemoA(() => rows.reduce((acc, r) => acc + r.meows.filter(m => m.score >= scoreThreshold && enabledLabels.has(m.label)).length, 0), [rows, scoreThreshold, enabledLabels]);

  const today = new Date();

  return (
    <div className="app">
      <Toolbar
        endDate={endDate}
        onShiftDays={shiftDays}
        onJumpToDate={jumpToDate}
        scoreThreshold={scoreThreshold}
        onScoreThreshold={setScoreThreshold}
        enabledLabels={enabledLabels}
        onToggleLabel={toggleLabel}
        rows={rows}
        totalMeows={totalMeows}
        visibleMeows={visibleMeows}
        zoom={zoom}
        onResetZoom={() => fitToRecordings(rows)}
        player={player}
        onClosePlayer={closePlayer}
      />
      <div className="grid-wrap" ref={wrapRef}>
        <div className="grid-header">
          <div className="grid-header-stack">
            <div className="grid-header-row">
              <div className="grid-header-spacer grid-header-spacer-heatmap">
                <span className="grid-header-spacer-label">activity</span>
              </div>
              <div className="grid-header-heatmap">
                <Heatmap
                  rows={rows}
                  scoreThreshold={scoreThreshold}
                  enabledLabels={enabledLabels}
                  zoom={zoom}
                  pan={pan}
                />
              </div>
            </div>
            <div className="grid-header-row">
              <div className="grid-header-spacer" />
              <div className="grid-header-axis" ref={gridRef}>
                <TimeAxis zoom={zoom} pan={pan} />
              </div>
            </div>
          </div>
        </div>
        <div className="grid-rows">
          {loading && rows.length === 0 ? (
            <div className="grid-empty">Loading…</div>
          ) : rows.length === 0 ? (
            <div className="grid-empty">No data</div>
          ) : (
            rows.map((row) => (
              <TimelineRow
                key={row.key}
                row={row}
                zoom={zoom}
                pan={pan}
                scoreThreshold={scoreThreshold}
                enabledLabels={enabledLabels}
                isToday={MeowUtil.isSameDay(row.startDate, today)}
                onBlockHover={onBlockHover}
                onBlockLeave={onBlockLeave}
                onBlockClick={onBlockClick}
                activeMeowId={player ? player.meow.id : null}
              />
            ))
          )}
        </div>
      </div>
      <Tooltip data={hover} />
      <TweaksPanel>
        <TweakSection label="Mood" />
        <TweakRadio
          label="Atmosphere"
          value={t.mood}
          options={['nocturnal', 'aurora', 'graphite']}
          onChange={(v) => setTweak('mood', v)}
        />
        <TweakSection label="Visualization" />
        <TweakRadio
          label="Block style"
          value={t.blockStyle}
          options={['bars', 'dots', 'ridges']}
          onChange={(v) => setTweak('blockStyle', v)}
        />
        <TweakSection label="Density" />
        <TweakSlider
          label="Row height"
          value={t.density}
          min={18} max={48} step={1} unit="px"
          onChange={(v) => setTweak('density', v)}
        />
      </TweaksPanel>
      <div className="hint-bar">
        <span><kbd>ctrl</kbd>+<kbd>scroll</kbd> zoom</span>
        <span><kbd>drag</kbd> pan</span>
        <span><kbd>click</kbd> play meow</span>
      </div>
    </div>
  );
}

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