/// Microsecond-precision timestamp for audio synchronization.
pub type SyncTime = u64;

/// Timing boundary types for step and pattern scheduling.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepTiming {
    /// Fire when a beat boundary is crossed.
    NextBeat,
    /// Fire when a bar/quantum boundary is crossed.
    NextBar,
}

impl StepTiming {
    /// Returns true if the boundary was crossed between prev_beat and curr_beat.
    pub fn crossed(&self, prev_beat: f64, curr_beat: f64, quantum: f64) -> bool {
        if prev_beat < 0.0 {
            return false;
        }
        match self {
            Self::NextBeat => prev_beat.floor() as i64 != curr_beat.floor() as i64,
            Self::NextBar => {
                (prev_beat / quantum).floor() as i64 != (curr_beat / quantum).floor() as i64
            }
        }
    }
}

/// Return the beat positions of all substeps in the window [frontier, end).
/// Each entry is the exact beat at which that substep fires.
/// Clamped to 64 results max to prevent runaway.
pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec<f64> {
    if frontier < 0.0 || end <= frontier || speed <= 0.0 {
        return Vec::new();
    }
    let substeps_per_beat = 4.0 * speed;
    let first = (frontier * substeps_per_beat).floor() as i64 + 1;
    let last = (end * substeps_per_beat).floor() as i64;
    let count = (last - first + 1).clamp(0, 64) as usize;
    let mut result = Vec::with_capacity(count);
    for i in 0..count as i64 {
        result.push((first + i) as f64 / substeps_per_beat);
    }
    result
}

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

    fn beats_to_micros(beats: f64, tempo: f64) -> SyncTime {
        if tempo <= 0.0 {
            return 0;
        }
        ((beats / tempo) * 60_000_000.0).round() as SyncTime
    }

    fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize {
        if prev_beat < 0.0 {
            return 0;
        }
        let prev_substep = (prev_beat * 4.0 * speed).floor() as i64;
        let curr_substep = (curr_beat * 4.0 * speed).floor() as i64;
        (curr_substep - prev_substep).clamp(0, 16) as usize
    }

    fn micros_until_next_substep(current_beat: f64, speed: f64, tempo: f64) -> SyncTime {
        if tempo <= 0.0 || speed <= 0.0 {
            return 0;
        }
        let substeps_per_beat = 4.0 * speed;
        let current_substep = (current_beat * substeps_per_beat).floor();
        let next_substep_beat = (current_substep + 1.0) / substeps_per_beat;
        let beats_until = next_substep_beat - current_beat;
        beats_to_micros(beats_until, tempo)
    }

    #[test]
    fn test_beats_to_micros_at_120_bpm() {
        // At 120 BPM, one beat = 0.5 seconds = 500,000 microseconds
        assert_eq!(beats_to_micros(1.0, 120.0), 500_000);
        assert_eq!(beats_to_micros(2.0, 120.0), 1_000_000);
        assert_eq!(beats_to_micros(0.5, 120.0), 250_000);
    }

    #[test]
    fn test_zero_tempo() {
        assert_eq!(beats_to_micros(1.0, 0.0), 0);
    }

    #[test]
    fn test_step_timing_beat_crossed() {
        // Crossing from beat 0 to beat 1
        assert!(StepTiming::NextBeat.crossed(0.9, 1.1, 4.0));
        // Not crossing (both in same beat)
        assert!(!StepTiming::NextBeat.crossed(0.5, 0.9, 4.0));
        // Negative prev_beat returns false
        assert!(!StepTiming::NextBeat.crossed(-1.0, 1.0, 4.0));
    }

    #[test]
    fn test_step_timing_bar_crossed() {
        // Crossing from bar 0 to bar 1 (quantum=4)
        assert!(StepTiming::NextBar.crossed(3.9, 4.1, 4.0));
        // Not crossing (both in same bar)
        assert!(!StepTiming::NextBar.crossed(2.0, 3.0, 4.0));
        // Crossing with different quantum
        assert!(StepTiming::NextBar.crossed(7.9, 8.1, 8.0));
    }

    #[test]
    fn test_substeps_crossed_normal() {
        // One substep crossed at 1x speed
        assert_eq!(substeps_crossed(0.0, 0.26, 1.0), 1);
        // Two substeps crossed
        assert_eq!(substeps_crossed(0.0, 0.51, 1.0), 2);
        // No substep crossed
        assert_eq!(substeps_crossed(0.1, 0.2, 1.0), 0);
    }

    #[test]
    fn test_substeps_crossed_with_speed() {
        // At 2x speed, 0.5 beats = 4 substeps
        assert_eq!(substeps_crossed(0.0, 0.5, 2.0), 4);
        // At 0.5x speed, 0.5 beats = 1 substep
        assert_eq!(substeps_crossed(0.0, 0.5, 0.5), 1);
    }

    #[test]
    fn test_substeps_crossed_negative_prev() {
        // Negative prev_beat returns 0
        assert_eq!(substeps_crossed(-1.0, 0.5, 1.0), 0);
    }

    #[test]
    fn test_substeps_crossed_clamp() {
        // Large jump clamped to 16
        assert_eq!(substeps_crossed(0.0, 100.0, 1.0), 16);
    }

    #[test]
    fn test_micros_until_next_substep_at_beat_zero() {
        // At beat 0.0, speed 1.0, tempo 120 BPM
        // Next substep is at beat 0.25 (1/4 beat)
        // 1/4 beat at 120 BPM = 0.25 / 120 * 60_000_000 = 125_000 μs
        let micros = micros_until_next_substep(0.0, 1.0, 120.0);
        assert_eq!(micros, 125_000);
    }

    #[test]
    fn test_micros_until_next_substep_near_boundary() {
        // At beat 0.24, almost at the substep boundary (0.25)
        // Next substep at 0.25, so 0.01 beats away
        let micros = micros_until_next_substep(0.24, 1.0, 120.0);
        // 0.01 beats at 120 BPM = 5000 μs
        assert_eq!(micros, 5000);
    }

    #[test]
    fn test_micros_until_next_substep_with_speed() {
        // At 2x speed, substeps are at 0.125, 0.25, 0.375...
        // At beat 0.0, next substep is at 0.125
        let micros = micros_until_next_substep(0.0, 2.0, 120.0);
        // 0.125 beats at 120 BPM = 62_500 μs
        assert_eq!(micros, 62_500);
    }

    #[test]
    fn test_micros_until_next_substep_zero_tempo() {
        assert_eq!(micros_until_next_substep(0.0, 1.0, 0.0), 0);
    }

    #[test]
    fn test_micros_until_next_substep_zero_speed() {
        assert_eq!(micros_until_next_substep(0.0, 0.0, 120.0), 0);
    }

    #[test]
    fn test_substeps_in_window_basic() {
        // At 1x speed, substeps at 0.25, 0.5, 0.75, 1.0...
        // Window [0.0, 0.5) should contain 0.25 and 0.5
        let result = substeps_in_window(0.0, 0.5, 1.0);
        assert_eq!(result, vec![0.25, 0.5]);
    }

    #[test]
    fn test_substeps_in_window_2x_speed() {
        // At 2x speed, substeps at 0.125, 0.25, 0.375, 0.5...
        // Window [0.0, 0.5) should contain 4 substeps
        let result = substeps_in_window(0.0, 0.5, 2.0);
        assert_eq!(result, vec![0.125, 0.25, 0.375, 0.5]);
    }

    #[test]
    fn test_substeps_in_window_mid_beat() {
        // Window [0.3, 0.6): should contain 0.5
        let result = substeps_in_window(0.3, 0.6, 1.0);
        assert_eq!(result, vec![0.5]);
    }

    #[test]
    fn test_substeps_in_window_empty() {
        // Window too small to contain any substep
        let result = substeps_in_window(0.1, 0.2, 1.0);
        assert!(result.is_empty());
    }

    #[test]
    fn test_substeps_in_window_negative_frontier() {
        let result = substeps_in_window(-1.0, 0.5, 1.0);
        assert!(result.is_empty());
    }
}
