//! Adjacent-segment merging for the edit list.
//!
//! Two temporally-adjacent segments are coalesced into one output clip when the
//! gap between them is smaller than `merge_gap_ms` and the resulting clip would
//! not exceed `max_merged_ms` — i.e. `segment + gap + segment -> clip`, keeping
//! the in-between gap's data rather than dropping it. Merging is greedy and
//! chains: a third segment folds into a growing clip as long as each new gap and
//! the running total stay within bounds. Set `merge_gap_ms = 0` to disable it
//! (a gap is never `< 0`, so every segment stays its own clip).

use crate::json::Segment;

/// One output clip: one or more original edit-list segments merged together.
/// `start_ts` / `end_ts` are milliseconds from the recording start and span the
/// whole clip (gaps between merged segments included); `sources` holds the
/// original segments, in order, to be echoed into `metadata.json`.
#[derive(Debug)]
pub struct Clip {
    pub start_ts: u64,
    pub end_ts: u64,
    pub sources: Vec<Segment>,
}

/// Greedily coalesce temporally-adjacent segments per the merge rule. Segments
/// are first ordered by `start_ts`; each is folded into the current clip when
/// the gap to it is `< merge_gap_ms` **and** the merged span is `<=
/// max_merged_ms`, otherwise it starts a new clip.
pub fn merge_segments(
    mut segments: Vec<Segment>,
    merge_gap_ms: u64,
    max_merged_ms: u64,
) -> Vec<Clip> {
    segments.sort_by_key(|s| s.start_ts);

    let mut clips: Vec<Clip> = Vec::new();
    for seg in segments {
        if let Some(last) = clips.last_mut() {
            // saturating_sub tolerates overlapping/degenerate segments (gap 0).
            let gap = seg.start_ts.saturating_sub(last.end_ts);
            let new_end = last.end_ts.max(seg.end_ts);
            let merged_span = new_end.saturating_sub(last.start_ts);
            if gap < merge_gap_ms && merged_span <= max_merged_ms {
                last.end_ts = new_end;
                last.sources.push(seg);
                continue;
            }
        }
        clips.push(Clip {
            start_ts: seg.start_ts,
            end_ts: seg.end_ts,
            sources: vec![seg],
        });
    }
    clips
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::json::Segment;

    const MIN: u64 = 60_000; // 1 minute in ms (default --merge-gap-ms)
    const MAX: u64 = 300_000; // 5 minutes in ms (default --max-merged-ms)

    fn seg(index: u32, start_ts: u64, end_ts: u64) -> Segment {
        Segment {
            index,
            start_ts,
            end_ts,
            raw: serde_json::json!({"index": index, "start_ts": start_ts, "end_ts": end_ts}),
        }
    }

    /// (start_ts, end_ts, [source indices]) for each clip — the shape we assert.
    fn shape(clips: &[Clip]) -> Vec<(u64, u64, Vec<u32>)> {
        clips
            .iter()
            .map(|c| {
                (
                    c.start_ts,
                    c.end_ts,
                    c.sources.iter().map(|s| s.index).collect(),
                )
            })
            .collect()
    }

    #[test]
    fn small_gap_and_small_result_merges() {
        // 30 s gap, merged span 4 min <= 5 min → one clip spanning both.
        let clips = merge_segments(vec![seg(0, 0, 120_000), seg(1, 150_000, 240_000)], MIN, MAX);
        assert_eq!(shape(&clips), vec![(0, 240_000, vec![0, 1])]);
    }

    #[test]
    fn gap_of_exactly_a_minute_does_not_merge() {
        // Exactly 60 s gap is not "less than a minute" → stays two clips.
        let clips = merge_segments(vec![seg(0, 0, 120_000), seg(1, 180_000, 240_000)], MIN, MAX);
        assert_eq!(
            shape(&clips),
            vec![(0, 120_000, vec![0]), (180_000, 240_000, vec![1])]
        );
    }

    #[test]
    fn merge_that_would_exceed_five_minutes_is_blocked() {
        // Gap tiny, but merged span 0..301_000 > 5 min → stays two clips.
        let clips = merge_segments(vec![seg(0, 0, 200_000), seg(1, 200_500, 301_000)], MIN, MAX);
        assert_eq!(
            shape(&clips),
            vec![(0, 200_000, vec![0]), (200_500, 301_000, vec![1])]
        );
    }

    #[test]
    fn span_of_exactly_five_minutes_merges() {
        // Merged span exactly 300_000 ("not larger than 5 min") → merges.
        let clips = merge_segments(vec![seg(0, 0, 200_000), seg(1, 200_500, 300_000)], MIN, MAX);
        assert_eq!(shape(&clips), vec![(0, 300_000, vec![0, 1])]);
    }

    #[test]
    fn merging_chains_across_a_run() {
        let clips = merge_segments(
            vec![
                seg(0, 0, 60_000),
                seg(1, 90_000, 150_000),
                seg(2, 160_000, 250_000),
            ],
            MIN,
            MAX,
        );
        assert_eq!(shape(&clips), vec![(0, 250_000, vec![0, 1, 2])]);
    }

    #[test]
    fn chain_stops_when_running_total_would_exceed_cap() {
        // 0..260k merges (span 260k), but adding 270k..320k → span 320k > cap.
        let clips = merge_segments(
            vec![
                seg(0, 0, 120_000),
                seg(1, 130_000, 260_000),
                seg(2, 270_000, 320_000),
            ],
            MIN,
            MAX,
        );
        assert_eq!(
            shape(&clips),
            vec![(0, 260_000, vec![0, 1]), (270_000, 320_000, vec![2])]
        );
    }

    #[test]
    fn zero_gap_setting_disables_merging() {
        // Even back-to-back segments (gap 0) stay separate when disabled.
        let clips = merge_segments(vec![seg(0, 0, 120_000), seg(1, 120_000, 240_000)], 0, MAX);
        assert_eq!(
            shape(&clips),
            vec![(0, 120_000, vec![0]), (120_000, 240_000, vec![1])]
        );
    }

    #[test]
    fn unsorted_input_is_ordered_before_merging() {
        let clips = merge_segments(vec![seg(1, 150_000, 240_000), seg(0, 0, 120_000)], MIN, MAX);
        assert_eq!(shape(&clips), vec![(0, 240_000, vec![0, 1])]);
    }

    #[test]
    fn readme_example_five_segments_to_four_clips() {
        // The shipped 11_43_33_ego.mix.json under the defaults: only segments
        // 2 and 3 satisfy both bounds (gap 15 s, span 232 s), so they merge.
        let clips = merge_segments(
            vec![
                seg(0, 30_000, 266_500),
                seg(1, 266_500, 443_000),
                seg(2, 488_000, 630_000),
                seg(3, 645_000, 720_000),
                seg(4, 720_000, 790_000),
            ],
            MIN,
            MAX,
        );
        assert_eq!(
            shape(&clips),
            vec![
                (30_000, 266_500, vec![0]),
                (266_500, 443_000, vec![1]),
                (488_000, 720_000, vec![2, 3]),
                (720_000, 790_000, vec![4]),
            ]
        );
    }
}
