/* Novel layout: orbital mixer.
   Sounds are "pebbles" arranged in orbits. Drag a pebble toward the
   glowing center to raise its volume; drag it to the edge to silence.
   The sleep timer occupies the center as a breathing glow. */

function NovelLayout({ state, api }) {
  const { mix, timer, scenes } = state;
  const containerRef = useRef(null);

  const W = 360, H = 360;
  const cx = W / 2, cy = H / 2;
  const rInner = 68;
  const trackGap = 14;
  const rTrackStart = rInner + trackGap;
  const rOuter = 168;

  const pebbles = useMemo(() => {
    return SOUNDS.map((s, i) => {
      const angle = (i / SOUNDS.length) * Math.PI * 2 - Math.PI / 2;
      const volume = mix[s.id] || 0;
      const r = rOuter - (rOuter - rTrackStart) * volume;
      return { sound: s, angle, r, volume };
    });
  }, [mix]);

  const [drag, setDrag] = useState(null);
  const dragMovedRef = useRef(false);
  const suppressClickRef = useRef(false);
  const onPointerDown = (id) => (e) => {
    e.preventDefault();
    dragMovedRef.current = false;
    setDrag({ id });
  };
  useEffect(() => {
    if (!drag) return;
    const handle = (e) => {
      const evt = e.touches ? e.touches[0] : e;
      const rect = containerRef.current.getBoundingClientRect();
      const scale = rect.width / W;
      const x = (evt.clientX - rect.left) / scale - cx;
      const y = (evt.clientY - rect.top) / scale - cy;
      const dist = Math.hypot(x, y);
      const clamped = Math.max(rTrackStart, Math.min(rOuter, dist));
      const vol = 1 - (clamped - rTrackStart) / (rOuter - rTrackStart);
      dragMovedRef.current = true;
      api.setVolume(drag.id, Math.round(vol * 100) / 100);
    };
    const up = () => {
      if (dragMovedRef.current) suppressClickRef.current = true;
      setDrag(null);
    };
    window.addEventListener('mousemove', handle);
    window.addEventListener('mouseup', up);
    window.addEventListener('touchmove', handle, { passive: false });
    window.addEventListener('touchend', up);
    return () => {
      window.removeEventListener('mousemove', handle);
      window.removeEventListener('mouseup', up);
      window.removeEventListener('touchmove', handle);
      window.removeEventListener('touchend', up);
    };
  }, [drag, api]);

  const onTap = (id, curVol) => () => {
    if (suppressClickRef.current) {
      suppressClickRef.current = false;
      return;
    }
    api.setVolume(id, curVol > 0.01 ? 0 : 0.6);
  };

  const totalActive = Object.values(mix).filter(v => v > 0.01).length;

  const pct = timer.running && timer.minutes > 0
    ? timer.remaining / (timer.minutes * 60)
    : timer.minutes > 0 ? 1 : 0;

  /* Scene drawer: edge-fade + drag-to-pan + progress indicator */
  const sceneScrollRef = useRef(null);
  const [scrollEdge, setScrollEdge] = useState({ left: false, right: false, ratio: 0, thumb: 0 });
  const updateEdges = useCallback(() => {
    const el = sceneScrollRef.current;
    if (!el) return;
    const max = el.scrollWidth - el.clientWidth;
    if (max <= 1) {
      setScrollEdge({ left: false, right: false, ratio: 0, thumb: 1 });
      return;
    }
    const x = el.scrollLeft;
    const ratio = x / max;
    const thumb = el.clientWidth / el.scrollWidth;
    setScrollEdge({ left: x > 1, right: x < max - 1, ratio, thumb });
  }, []);
  useEffect(() => {
    updateEdges();
    const el = sceneScrollRef.current;
    if (!el) return;
    el.addEventListener('scroll', updateEdges, { passive: true });
    const ro = new ResizeObserver(updateEdges);
    ro.observe(el);
    return () => {
      el.removeEventListener('scroll', updateEdges);
      ro.disconnect();
    };
  }, [updateEdges, scenes.length]);

  const dragPan = useRef({ active: false, startX: 0, startLeft: 0, moved: false });
  const onSceneRowMouseDown = (e) => {
    if (e.button !== 0) return;
    const el = sceneScrollRef.current;
    if (!el) return;
    dragPan.current = { active: true, startX: e.clientX, startLeft: el.scrollLeft, moved: false };
  };
  useEffect(() => {
    let pendingSwallow = null;
    const removeSwallow = () => {
      if (pendingSwallow) {
        window.removeEventListener('click', pendingSwallow, true);
        pendingSwallow = null;
      }
    };
    const move = (e) => {
      if (!dragPan.current.active) return;
      const dx = e.clientX - dragPan.current.startX;
      if (Math.abs(dx) > 4) dragPan.current.moved = true;
      sceneScrollRef.current.scrollLeft = dragPan.current.startLeft - dx;
    };
    const up = () => {
      if (dragPan.current.moved) {
        removeSwallow();
        pendingSwallow = (ev) => {
          ev.stopPropagation();
          ev.preventDefault();
          removeSwallow();
        };
        window.addEventListener('click', pendingSwallow, true);
      }
      dragPan.current.active = false;
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    return () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
      removeSwallow();
    };
  }, []);

  /* Long-press scene delete */
  const longPressTimer = useRef(null);
  const longPressFired = useRef(false);
  const onSceneDown = (id) => () => {
    longPressFired.current = false;
    longPressTimer.current = setTimeout(() => {
      longPressFired.current = true;
      api.requestDeleteScene(id);
    }, 600);
  };
  const onSceneUp = () => clearTimeout(longPressTimer.current);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div className="neu" style={{ padding: 20, position: 'relative' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
          <div>
            <div className="t-label">Orbital Mixer</div>
            <div className="t-title" style={{ marginTop: 2 }}>Drag inward to layer</div>
          </div>
          <NeuToggle on={state.soundOn} onChange={api.toggleSound} label={state.soundOn ? 'On' : 'Off'} ariaLabel={state.soundOn ? 'Mute all sound' : 'Unmute sound'} />
        </div>

        <div
          ref={containerRef}
          className="orbit-wrap"
        >
          {pebbles.map(p => {
            const trackLen = rOuter - rTrackStart;
            const angleDeg = (p.angle * 180) / Math.PI;
            const x1 = cx + rTrackStart * Math.cos(p.angle);
            const y1 = cy + rTrackStart * Math.sin(p.angle);
            return (
              <div key={'track-' + p.sound.id}
                style={{
                  position: 'absolute',
                  left: `${(x1 / W) * 100}%`, top: `${(y1 / H) * 100}%`,
                  width: `${(trackLen / W) * 100}%`, height: 14,
                  marginTop: -7,
                  borderRadius: 999,
                  background: 'var(--bg)',
                  boxShadow: 'inset -2px -2px 4px var(--hl), inset 2px 2px 4px var(--sh)',
                  transform: `rotate(${angleDeg}deg)`,
                  transformOrigin: '0 50%',
                  pointerEvents: 'none',
                  overflow: 'hidden',
                }}
              >
                {p.volume > 0.01 && (
                  <div style={{
                    position: 'absolute',
                    left: 2, top: 2, bottom: 2,
                    width: `calc(${p.volume * 100}% - 4px)`,
                    borderRadius: 999,
                    background: 'linear-gradient(to right, oklch(0.82 0.06 var(--accent-h)), oklch(0.72 0.1 var(--accent-h)))',
                    transition: 'width 200ms var(--ease)',
                  }} />
                )}
              </div>
            );
          })}

          <svg width="100%" height="100%" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
            <defs>
              <radialGradient id="core-glow" cx="50%" cy="50%" r="50%">
                <stop offset="0%" stopColor="oklch(0.78 0.12 var(--accent-h))" stopOpacity="0.55" />
                <stop offset="60%" stopColor="oklch(0.78 0.12 var(--accent-h))" stopOpacity="0.08" />
                <stop offset="100%" stopColor="oklch(0.78 0.12 var(--accent-h))" stopOpacity="0" />
              </radialGradient>
            </defs>

            <circle cx={cx} cy={cy} r={rOuter} fill="none" stroke="var(--ink-3)" strokeOpacity="0.12" strokeDasharray="2 4" />

            <circle
              cx={cx} cy={cy}
              r={rInner + Math.min(40, totalActive * 8)}
              fill="url(#core-glow)"
              style={{ transition: 'r 500ms var(--ease)' }}
            />

            {timer.minutes > 0 && (() => {
              const r = rInner + 8;
              const end = -Math.PI / 2 + (1 - pct) * Math.PI * 2;
              const startA = -Math.PI / 2;
              const large = (Math.PI * 2 * pct) > Math.PI ? 1 : 0;
              const x1 = cx + r * Math.cos(startA), y1 = cy + r * Math.sin(startA);
              const x2 = cx + r * Math.cos(end), y2 = cy + r * Math.sin(end);
              return (
                <path
                  d={`M ${x1} ${y1} A ${r} ${r} 0 ${large} 0 ${x2} ${y2}`}
                  fill="none"
                  stroke="oklch(0.7 0.1 var(--accent-h))"
                  strokeOpacity="0.6"
                  strokeWidth="2"
                  strokeLinecap="round"
                />
              );
            })()}
          </svg>

          <div className="neu-inset-lg" style={{
            position: 'absolute',
            left: `${((cx - rInner) / W) * 100}%`, top: `${((cy - rInner) / H) * 100}%`,
            width: `${(rInner * 2 / W) * 100}%`, height: `${(rInner * 2 / H) * 100}%`,
            borderRadius: '50%',
            display: 'grid', placeItems: 'center',
            textAlign: 'center',
          }}>
            <div>
              <div className="t-label">
                {timer.running ? 'Sleeping in' : totalActive === 0 ? 'Silent' : 'Mix'}
              </div>
              <div className="t-num" style={{ fontSize: 22, fontWeight: 700, marginTop: 4, color: 'var(--ink-1)' }}>
                {timer.running ? formatShortTime(timer.remaining)
                  : totalActive === 0 ? '—'
                  : `${totalActive}`}
              </div>
              {!timer.running && totalActive > 0 && (
                <div className="t-label" style={{ marginTop: 2 }}>layer{totalActive > 1 ? 's' : ''}</div>
              )}
            </div>
          </div>

          {pebbles.map(p => {
            const px = cx + p.r * Math.cos(p.angle);
            const py = cy + p.r * Math.sin(p.angle);
            const size = 48 + p.volume * 10;
            const active = p.volume > 0.01;
            return (
              <div key={p.sound.id}
                onMouseDown={onPointerDown(p.sound.id)}
                onTouchStart={onPointerDown(p.sound.id)}
                onClick={onTap(p.sound.id, p.volume)}
                onKeyDown={(e) => {
                  const step = e.shiftKey ? 0.2 : 0.1;
                  if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
                    e.preventDefault();
                    api.setVolume(p.sound.id, Math.min(1, p.volume + step));
                  } else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
                    e.preventDefault();
                    api.setVolume(p.sound.id, Math.max(0, p.volume - step));
                  } else if (e.key === 'Home') {
                    e.preventDefault();
                    api.setVolume(p.sound.id, 0);
                  } else if (e.key === 'End') {
                    e.preventDefault();
                    api.setVolume(p.sound.id, 1);
                  } else if (e.key === ' ' || e.key === 'Enter') {
                    e.preventDefault();
                    api.setVolume(p.sound.id, p.volume > 0.01 ? 0 : 0.6);
                  }
                }}
                role="slider"
                tabIndex={0}
                aria-label={`${p.sound.name} volume`}
                aria-valuemin={0} aria-valuemax={100}
                aria-valuenow={Math.round(p.volume * 100)}
                aria-valuetext={`${Math.round(p.volume * 100)} percent`}
                className={`pebble ${active ? 'accent-glow' : ''}`}
                style={{
                  position: 'absolute',
                  left: `${(px / W) * 100}%`, top: `${(py / H) * 100}%`,
                  width: size, height: size, borderRadius: '50%',
                  marginLeft: -size / 2, marginTop: -size / 2,
                  background: 'var(--bg)',
                  boxShadow: active
                    ? 'inset -2px -2px 4px var(--hl), inset 2px 2px 4px var(--sh), 0 0 0 2px oklch(0.72 0.08 var(--accent-h) / 0.3)'
                    : '-3px -3px 6px var(--hl), 3px 3px 6px var(--sh)',
                  display: 'grid', placeItems: 'center',
                  cursor: drag?.id === p.sound.id ? 'grabbing' : 'grab',
                  color: active ? 'oklch(0.68 0.1 var(--accent-h))' : 'var(--ink-2)',
                  transition: drag?.id === p.sound.id ? 'none' : 'left 260ms var(--ease), top 260ms var(--ease), box-shadow 200ms, width 200ms, height 200ms',
                  touchAction: 'none',
                  userSelect: 'none',
                }}
                title={p.sound.name}
              >
                <div style={{ width: 22, height: 22 }}>{ICONS[p.sound.id]}</div>
                {active && (
                  <div style={{
                    position: 'absolute', bottom: -18, left: 0, right: 0,
                    fontSize: 10, fontWeight: 600, textAlign: 'center',
                    color: 'var(--ink-2)',
                  }}>
                    {p.sound.name.split(' ')[0]}
                  </div>
                )}
              </div>
            );
          })}
        </div>

        <div style={{ display: 'flex', gap: 6, justifyContent: 'center', alignItems: 'center', marginTop: 18, flexWrap: 'wrap' }}>
          {[0, 15, 30, 60, 480].map(m => (
            <button key={m}
              className="btn"
              onClick={() => api.setTimerMinutes(m)}
              aria-pressed={timer.minutes === m}
              aria-label={m === 0 ? 'No timer' : m === 480 ? '8 hours' : m >= 60 ? `${m/60} hour${m/60>1?'s':''}` : `${m} minutes`}
              style={{
                padding: '8px 12px', fontSize: 11, fontWeight: 600,
                color: timer.minutes === m ? 'var(--ink-1)' : 'var(--ink-3)',
                boxShadow: timer.minutes === m
                  ? 'inset -2px -2px 4px var(--hl), inset 2px 2px 4px var(--sh)'
                  : '-2px -2px 4px var(--hl), 2px 2px 4px var(--sh)',
              }}>
              {m === 0 ? 'No timer' : m === 480 ? '8h' : m >= 60 ? `${m/60}h` : `${m}m`}
            </button>
          ))}
          <button
            className="btn"
            onClick={() => api.toggleTimer(!timer.running)}
            aria-pressed={timer.running}
            aria-label={timer.running ? 'Stop sleep timer' : 'Start sleep timer'}
            title={timer.running ? 'Stop timer' : 'Start timer'}
            disabled={timer.minutes === 0}
            style={{
              width: 34, height: 34, padding: 0, marginLeft: 4,
              display: 'grid', placeItems: 'center',
              color: timer.running ? 'oklch(0.68 0.1 var(--accent-h))' : 'var(--ink-3)',
              opacity: timer.minutes === 0 ? 0.4 : 1,
              boxShadow: timer.running
                ? 'inset -2px -2px 4px var(--hl), inset 2px 2px 4px var(--sh)'
                : '-2px -2px 4px var(--hl), 2px 2px 4px var(--sh)',
            }}>
            <svg width="14" height="14" viewBox="0 0 10 10" fill="none" aria-hidden="true">
              <path d="M5 1v4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
              <path d="M2.2 3.2a3.5 3.5 0 105.6 0" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" fill="none"/>
            </svg>
          </button>
        </div>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        <SectionHead title="Scenes"
          action={
            <button className="btn" onClick={api.saveScene} style={{ padding: '4px 10px', fontSize: 11, fontWeight: 600, color: 'var(--ink-2)', boxShadow: '-2px -2px 4px var(--hl), 2px 2px 4px var(--sh)' }}>+ Save mix</button>
          }
        />
        <div
          ref={sceneScrollRef}
          onMouseDown={onSceneRowMouseDown}
          className={
            'no-scrollbar scene-row' +
            (scrollEdge.left ? ' fade-left' : '') +
            (scrollEdge.right ? ' fade-right' : '')
          }
          style={{
            display: 'flex', gap: 14, overflowX: 'auto', padding: '10px 4px 14px',
            cursor: scrollEdge.right || scrollEdge.left ? 'grab' : 'default',
          }}>
          {scenes.map(sc => {
            const count = Object.values(sc.mix).filter(v => v > 0.01).length;
            return (
              <button key={sc.id} className="neu"
                onMouseDown={onSceneDown(sc.id)}
                onMouseUp={onSceneUp}
                onMouseLeave={onSceneUp}
                onTouchStart={onSceneDown(sc.id)}
                onTouchEnd={onSceneUp}
                onContextMenu={(e) => { e.preventDefault(); api.requestDeleteScene(sc.id); }}
                style={{
                  flex: '0 0 auto', width: 140, padding: 14,
                  display: 'flex', flexDirection: 'column', gap: 6, cursor: 'pointer',
                  marginLeft: 4, marginRight: 4, textAlign: 'left',
                  border: 'none', font: 'inherit', color: 'inherit',
                }}
                onClick={() => { if (!longPressFired.current) api.loadScene(sc.id); }}
                aria-label={`Load scene ${sc.name}, ${count} layer${count!==1?'s':''}. Long-press to delete.`}>
                <div className="neu-sm" style={{ width: 36, height: 36, borderRadius: 10, display: 'grid', placeItems: 'center', color: 'var(--ink-2)' }}>
                  {sc.icon}
                </div>
                <div className="t-title" style={{ fontSize: 13, marginTop: 4 }}>{sc.name}</div>
                <div className="t-body" style={{ fontSize: 10 }}>{count} layer{count!==1?'s':''}</div>
              </button>
            );
          })}
        </div>
        {(scrollEdge.left || scrollEdge.right) && (
          <div className="scroll-thumb" aria-hidden="true">
            <div
              className="scroll-thumb-bar"
              style={{
                width: `${Math.max(18, scrollEdge.thumb * 100)}%`,
                left: `${scrollEdge.ratio * (100 - Math.max(18, scrollEdge.thumb * 100))}%`,
              }}
            />
          </div>
        )}
      </div>

      <div className="neu-inset-lg" style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 12 }}>
        <div style={{ flex: 1 }}>
          <div className="t-label">Fade on start</div>
          <NeuSlider value={state.fadeIn} onChange={api.setFadeIn} min={0} max={8} step={0.5} />
        </div>
        <IconBtn size={38} onClick={api.stopAll} title="Stop all" ariaLabel="Stop all sounds">
          <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="1.5"/></svg>
        </IconBtn>
      </div>
    </div>
  );
}

function formatShortTime(sec) {
  if (sec <= 0) return '—';
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  const s = Math.floor(sec % 60);
  if (h > 0) return `${h}h ${m}m`;
  if (m > 0) return `${m}:${String(s).padStart(2,'0')}`;
  return `${s}s`;
}

window.NovelLayout = NovelLayout;
