// Auto-Zoom on Speech (V22.1)
//
// Idea: use Whisper word timestamps to find when speech is happening, then
// apply a `zoompan` filter during those windows so the speaker's face
// "pops in" — same trick CapCut's auto-zoom uses.
//
// We analyse captions to produce a list of "zoom windows": [start, end]
// inside which the video is scaled up by `zoomFactor`. Multiple windows
// chain via `enable='between(t,a,b)'` so we can share one zoompan filter.

export interface ZoomWindow {
  /** Seconds in clip timeline. */
  start: number;
  /** Seconds in clip timeline. */
  end: number;
  /** Zoom factor 1.0..2.5 (1 = no zoom, 1.3 = typical "pop in"). */
  factor: number;
}

export interface AutoZoomOptions {
  /** Zoom factor when speech detected (default 1.25). */
  factor?: number;
  /** Minimum zoom window duration in seconds (default 0.6). */
  minWindowSec?: number;
  /** Maximum zoom window duration (default 4). */
  maxWindowSec?: number;
  /** Pad each zoom window with N seconds before & after (default 0.15). */
  padSec?: number;
  /** Skip zoom on windows shorter than N seconds total (default 0.5). */
  minSpeechSec?: number;
}

export interface SubtitleLike {
  start: number;
  end: number;
  text: string;
}

/**
 * Derive zoom windows from a list of subtitle cues.
 * - Filters out very short cues
 * - Pads each window slightly
 * - Splits windows longer than maxWindowSec so zoom doesn't feel stuck
 */
export function deriveZoomWindows(
  cues: SubtitleLike[],
  options: AutoZoomOptions = {},
): ZoomWindow[] {
  const factor = options.factor ?? 1.25;
  const minWindow = options.minWindowSec ?? 0.6;
  const maxWindow = options.maxWindowSec ?? 4;
  const pad = options.padSec ?? 0.15;
  const minSpeech = options.minSpeechSec ?? 0.5;

  const raw: ZoomWindow[] = [];
  for (const c of cues) {
    const dur = c.end - c.start;
    if (dur < minSpeech) continue;
    let start = Math.max(0, c.start - pad);
    let end = c.end + pad;
    // Split too-long windows
    while (end - start > maxWindow) {
      raw.push({ start, end: start + maxWindow, factor });
      start += maxWindow;
    }
    raw.push({ start, end, factor });
  }
  // Drop tiny windows after padding and merge overlapping
  raw.sort((a, b) => a.start - b.start);
  const merged: ZoomWindow[] = [];
  for (const w of raw) {
    const last = merged[merged.length - 1];
    if (last && w.start <= last.end + 0.05) {
      last.end = Math.max(last.end, w.end);
      last.factor = Math.max(last.factor, w.factor);
    } else if (w.end - w.start >= minWindow) {
      merged.push({ ...w });
    } else if (merged.length > 0) {
      merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, w.end);
    } else {
      merged.push({ ...w });
    }
  }
  return merged;
}

/**
 * Build the FFmpeg zoompan expression that activates only inside the given
 * windows. The pipeline assumes the clip is already trimmed and sized;
 * zoompan needs `d=1` (1 frame per input) so we wrap in `enable`.
 *
 * Output format:
 *   zoompan=z='if(<expr>,<factor>,1)':d=1:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=WxH:enable='<expr>'
 *
 * Single combined `enable` is built from `between(t,a,b)` OR'd together.
 */
export function buildZoomPanFilter(
  windows: ZoomWindow[],
  outWidth: number,
  outHeight: number,
): string | null {
  if (windows.length === 0) return null;
  const expr = windows
    .map((w) => `between(t,${w.start.toFixed(2)},${w.end.toFixed(2)})`)
    .join('+');
  // The zoom factor alternates between 1 (no zoom) and `factor` based on t.
  // Use `if(expr, F, 1)` so the pan is only active inside zoom windows.
  // (avg factor 1 when expr=0, max factor when expr=1)
  const f = windows[0]?.factor ?? 1.25;
  return (
    `zoompan=z='if(${expr},${f.toFixed(2)},1)':d=1:` +
    `x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=${outWidth}x${outHeight}`
  );
}