/* === JitterFree — Timeline ===
   Multi-track timeline. Each layer gets a row with an intro block and an outro block.
   Intro and outro move independently — dragging one does not affect the other.
   - Drag intro middle      → shift only the intro (startTime). Clamped: intro end ≤ outro start.
   - Drag intro left edge   → shift startTime (keeps intro right edge fixed)
   - Drag intro right edge  → resize intro.duration
   - Drag outro middle      → shift only the outro (outroStart). Clamped: outro start ≥ intro end.
   - Drag outro left edge   → shift outroStart (keeps outro right edge fixed)
   - Drag outro right edge  → resize outro.duration
   Snap: to playhead and to other layers' edges within ±SNAP_PX.
*/

const SNAP_PX = 6;

const Timeline = ({
  comp, currentTime, playing, looping,
  selectedId, onSelect,
  onPlay, onPause, onLoop, onRestart, onSeek,
  onUpdateLayer,
  zoom, setZoom,
  manualDuration, setManualDuration,
}) => {
  const tracksRef = useRef(null);
  const total = window.compositionDuration(comp);
  const [trackWidth, setTrackWidth] = useState(800);
  const [snapMarker, setSnapMarker] = useState(null); // px x position

  useLayoutEffect(() => {
    const el = tracksRef.current;
    if (!el) return;
    const ro = new ResizeObserver(() => setTrackWidth(el.clientWidth));
    ro.observe(el);
    setTrackWidth(el.clientWidth);
    return () => ro.disconnect();
  }, []);

  // pixels per second, with zoom applied. Base = trackWidth/total at zoom=1.
  const basePps = Math.max(40, trackWidth / Math.max(0.1, total));
  const pps = basePps * zoom;
  const fullWidth = total * pps;

  const timeToPx = (t) => t * pps;
  const pxToTime = (px) => px / pps;

  // Snap candidates (in time)
  const snapCandidates = useMemo(() => {
    const set = new Set([0, total, currentTime]);
    for (const L of comp.layers) {
      set.add(L.startTime);
      set.add(L.startTime + L.intro.duration);
      set.add(window.layerOutroStart(L));
      set.add(window.layerEnd(L));
    }
    return Array.from(set);
  }, [comp.layers, currentTime, total]);

  const trySnap = (timeVal, excludeIds = []) => {
    const px = timeToPx(timeVal);
    let best = null;
    for (const c of snapCandidates) {
      const cpx = timeToPx(c);
      const d = Math.abs(cpx - px);
      if (d <= SNAP_PX && (!best || d < best.d)) best = { time: c, d, px: cpx };
    }
    if (best) { setSnapMarker(best.px); return best.time; }
    setSnapMarker(null);
    return timeVal;
  };

  // ===== Block drag handlers =====
  const startDrag = (e, layer, op) => {
    e.preventDefault(); e.stopPropagation();
    onSelect(layer.id);
    const startX = e.clientX;
    const orig = JSON.parse(JSON.stringify({
      startTime: layer.startTime,
      intro: layer.intro,
      outroStart: window.layerOutroStart(layer),
      outro: layer.outro,
    }));

    const move = (ev) => {
      const dx = ev.clientX - startX;
      const dt = pxToTime(dx);
      let next = { ...orig, intro: { ...orig.intro }, outro: { ...orig.outro } };

      if (op === "move") {
        // Move only the intro block. Outro stays where it is.
        const introDur = orig.intro.duration;
        let newStart = Math.max(0, orig.startTime + dt);
        const snapped = trySnap(newStart, [layer.id]);
        newStart = Math.max(0, snapped);
        // Don't let intro overrun the outro start.
        newStart = Math.min(newStart, Math.max(0, orig.outroStart - introDur));
        next.startTime = newStart;
      } else if (op === "intro-left") {
        // Move intro start; keep intro right edge fixed. Outro is independent.
        const introEnd = orig.startTime + orig.intro.duration;
        let newStart = orig.startTime + dt;
        newStart = Math.max(0, Math.min(introEnd - 0.05, newStart));
        const snapped = trySnap(newStart);
        newStart = Math.max(0, Math.min(introEnd - 0.05, snapped));
        next.startTime = newStart;
        next.intro.duration = Math.max(0.05, introEnd - newStart);
      } else if (op === "intro-right") {
        // Resize intro from the right. Don't push outroStart; just clamp so intro doesn't overrun outro.
        let dur = Math.max(0.05, orig.intro.duration + dt);
        const introEndAbs = orig.startTime + dur;
        const snapped = trySnap(introEndAbs);
        dur = Math.max(0.05, snapped - orig.startTime);
        // Clamp so intro end doesn't exceed outro start.
        dur = Math.min(dur, orig.outroStart - orig.startTime);
        next.intro.duration = Math.max(0.05, dur);
      } else if (op === "outro-move") {
        // Move outro block (left edge = outroStart). Stays after intro end.
        const introEnd = orig.startTime + orig.intro.duration;
        let newOutroStart = orig.outroStart + dt;
        newOutroStart = Math.max(introEnd, newOutroStart);
        const snapped = trySnap(newOutroStart, [layer.id]);
        next.outroStart = Math.max(introEnd, snapped);
      } else if (op === "outro-left") {
        // Move outro start; keep outro right edge fixed (resize from the left).
        const introEnd = orig.startTime + orig.intro.duration;
        const outroEnd = orig.outroStart + orig.outro.duration;
        let newOutroStart = orig.outroStart + dt;
        newOutroStart = Math.max(introEnd, Math.min(outroEnd - 0.05, newOutroStart));
        const snapped = trySnap(newOutroStart);
        newOutroStart = Math.max(introEnd, Math.min(outroEnd - 0.05, snapped));
        next.outroStart = newOutroStart;
        next.outro.duration = Math.max(0.05, outroEnd - newOutroStart);
      } else if (op === "outro-right") {
        let dur = Math.max(0.05, orig.outro.duration + dt);
        const outroEndAbs = orig.outroStart + dur;
        const snapped = trySnap(outroEndAbs);
        dur = Math.max(0.05, snapped - orig.outroStart);
        next.outro.duration = dur;
      }
      onUpdateLayer(layer.id, next);
    };
    const up = () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
      setSnapMarker(null);
    };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };

  // ===== Playhead drag =====
  const playheadDown = (e) => {
    e.preventDefault();
    const rect = tracksRef.current.getBoundingClientRect();
    const move = (ev) => {
      const x = ev.clientX - rect.left + tracksRef.current.scrollLeft;
      onSeek(Math.max(0, Math.min(total, pxToTime(x))));
    };
    const up = () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
    };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };

  // ===== Click on track to seek =====
  const onTrackBgClick = (e) => {
    if (e.target.closest(".tl-block")) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    onSeek(Math.max(0, Math.min(total, pxToTime(x))));
  };

  // ===== Wheel zoom =====
  const onWheel = (e) => {
    if (e.ctrlKey || e.metaKey) {
      e.preventDefault();
      const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
      setZoom((z) => Math.max(0.25, Math.min(8, z * factor)));
    }
  };

  // ===== Ruler ticks =====
  const ruler = useMemo(() => {
    const ticks = [];
    // pick step to keep labels readable: aim for major ticks ~80px apart
    const targetPx = 80;
    const targetT = targetPx / pps;
    const candidates = [0.1, 0.2, 0.5, 1, 2, 5, 10, 30];
    let step = candidates[0];
    for (const c of candidates) { if (c >= targetT) { step = c; break; } step = c; }
    const subStep = step / 5;
    for (let t = 0; t <= total + 1e-6; t += subStep) {
      const major = Math.abs(t / step - Math.round(t / step)) < 1e-3;
      ticks.push(
        <div key={`t${t}`}
             className={`tl-ruler-tick${major ? " major" : ""}`}
             style={{ left: timeToPx(t) }}/>
      );
      if (major) {
        ticks.push(
          <div key={`l${t}`} className="tl-ruler-label" style={{ left: timeToPx(t) }}>
            {t.toFixed(step < 1 ? 1 : 0)}s
          </div>
        );
      }
    }
    return ticks;
  }, [total, pps]);

  const fmtTime = (t) => {
    const s = Math.floor(t);
    const ms = Math.floor((t - s) * 100);
    return `${s.toString().padStart(2, "0")}:${ms.toString().padStart(2, "0")}`;
  };

  return (
    <div className="timeline">
      <div className="tl-bar">
        <div className="transport">
          <button className="transport-btn" onClick={onRestart} title="Restart"><Icon name="skip-back" size={12}/></button>
          <button className={`transport-btn${playing ? " active" : ""}`}
                  onClick={playing ? onPause : onPlay}
                  title="Play / Pause (Space)">
            <Icon name={playing ? "pause" : "play"} size={12}/>
          </button>
          <button className={`transport-btn toggle${looping ? " on" : ""}`} onClick={onLoop} title="Loop"><Icon name="loop" size={12}/></button>
        </div>
        <div className="timecode">
          <span>{fmtTime(currentTime)}</span><span className="sep">/</span><span className="total">{fmtTime(total)}</span>
        </div>
        <div className="tl-spacer"/>
        <div className="tl-meta">
          <div className="tl-meta-item">
            <label>Duration</label>
            <input className="numeric-input" type="number" step="0.1" min="0.5" max="60"
                   value={(manualDuration != null ? manualDuration : total).toFixed(1)}
                   onChange={(e) => {
                     const v = parseFloat(e.target.value);
                     setManualDuration(isFinite(v) ? Math.max(0.5, v) : null);
                   }}
                   onBlur={(e) => { if (e.target.value === "" || !isFinite(parseFloat(e.target.value))) setManualDuration(null); }}/>
            {manualDuration != null && (
              <button className="btn ghost" style={{ padding: "2px 6px", fontSize: 10 }} onClick={() => setManualDuration(null)}>Auto</button>
            )}
          </div>
        </div>
        <div className="zoom-ctrl">
          <button onClick={() => setZoom((z) => Math.max(0.25, z / 1.25))} title="Zoom out"><Icon name="minus" size={12}/></button>
          <span className="zoom-value">{Math.round(zoom * 100)}%</span>
          <button onClick={() => setZoom((z) => Math.min(8, z * 1.25))} title="Zoom in"><Icon name="plus" size={12}/></button>
        </div>
      </div>

      <div className="tl-body">
        <div className="tl-col-labels">
          <div className="tl-label-row-head">Tracks</div>
          {[...comp.layers].slice().reverse().map((layer) => (
            <div key={layer.id}
                 className={"tl-label-row" + (selectedId === layer.id ? " selected" : "")}
                 onClick={() => onSelect(layer.id)}>
              <div className="layer-thumb" style={{ backgroundImage: `url(${layer.src})` }}/>
              <div className="layer-name">{layer.name}</div>
            </div>
          ))}
        </div>

        <div className="tl-col-tracks" ref={tracksRef} onWheel={onWheel}>
          <div style={{ width: Math.max(fullWidth, trackWidth), position: "relative" }}>
            <div className="tl-ruler" onClick={(e) => {
              const r = e.currentTarget.getBoundingClientRect();
              onSeek(Math.max(0, Math.min(total, pxToTime(e.clientX - r.left))));
            }}>
              {ruler}
              <div className="tl-playhead-hit" style={{ left: timeToPx(currentTime) }} onPointerDown={playheadDown}/>
              <div className="tl-playhead" style={{ left: timeToPx(currentTime) }}>
                <div className="tl-playhead-head" onPointerDown={playheadDown}/>
              </div>
            </div>

            <div className="tl-tracks">
              {[...comp.layers].slice().reverse().map((layer) => {
                const outroStartT = window.layerOutroStart(layer);
                const introX = timeToPx(layer.startTime);
                const introW = timeToPx(layer.intro.duration);
                const outroX = timeToPx(outroStartT);
                const outroW = timeToPx(layer.outro.duration);
                return (
                  <div key={layer.id}
                       className={"tl-track" + (selectedId === layer.id ? " selected" : "")}
                       onClick={(e) => { onSelect(layer.id); onTrackBgClick(e); }}>
                    {introW > 1 && (
                      <div className="tl-block intro" style={{ left: introX, width: introW }}>
                        <div className="resize-handle left"  onPointerDown={(e) => startDrag(e, layer, "intro-left")}/>
                        <div className="move-handle"         onPointerDown={(e) => startDrag(e, layer, "move")}/>
                        <div className="resize-handle right" onPointerDown={(e) => startDrag(e, layer, "intro-right")}/>
                        <span className="tl-block-label">{window.PRESET_BY_ID[layer.intro.preset].name}</span>
                        <span className="tl-block-meta">{layer.intro.duration.toFixed(1)}s</span>
                      </div>
                    )}
                    {outroW > 1 && (
                      <div className="tl-block outro" style={{ left: outroX, width: outroW }}>
                        <div className="resize-handle left"  onPointerDown={(e) => startDrag(e, layer, "outro-left")}/>
                        <div className="move-handle"         onPointerDown={(e) => startDrag(e, layer, "outro-move")}/>
                        <div className="resize-handle right" onPointerDown={(e) => startDrag(e, layer, "outro-right")}/>
                        <span className="tl-block-label">{window.PRESET_BY_ID[layer.outro.preset].name}</span>
                        <span className="tl-block-meta">{layer.outro.duration.toFixed(1)}s</span>
                      </div>
                    )}
                  </div>
                );
              })}
              {snapMarker != null && <div className="tl-snap-line" style={{ left: snapMarker }}/>}
              {/* Continuation of playhead through the tracks for visual alignment */}
              <div className="tl-playhead" style={{ left: timeToPx(currentTime), top: 0, height: comp.layers.length * 36 }}/>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

window.Timeline = Timeline;
