use crate::*;

pub struct Metronome {
    // Audio playback
    click_samples: Arc<AudioChunk>,
    click_volume: f32,

    // Timing state
    frames_per_beat: usize,
    frames_since_last_beat: usize, // Where we are in the current beat cycle
    last_frame_time: Option<u32>,  // For xrun detection
}

#[derive(Debug, Clone, PartialEq)]
pub struct BufferTiming {
    /// Beat index within the current buffer (if any)
    pub beat_in_buffer: Option<u32>,

    /// Number of frames missed due to xrun (0 if no xrun)
    pub missed_frames: usize,

    /// Beat index within the missed frames (if any)
    pub beat_in_missed: Option<u32>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct PpqnTick {
    /// Sample offset within the buffer where this tick occurs
    pub sample_offset: usize,
    /// PPQN tick index (0-23)
    pub tick_index: usize,
    /// Position within the beat (0.0 - 1.0)
    pub position: f32,
}

impl Metronome {
    pub fn new(click_samples: Arc<AudioChunk>, state: &State) -> Self {
        Self {
            click_samples,
            click_volume: state.metronome.click_volume,
            frames_per_beat: state.metronome.frames_per_beat,
            frames_since_last_beat: 0,
            last_frame_time: None,
        }
    }

    /// Process audio for current buffer, writing to output slice
    pub fn process(
        &mut self,
        ps: &jack::ProcessScope,
        ports: &mut JackPorts,
        osc_controller: &OscController,
    ) -> Result<BufferTiming> {
        let buffer_size = ps.n_frames();
        let current_frame_time = ps.last_frame_time();

        // Process PPQN ticks for this buffer
        self.process_ppqn_ticks(ps, ports, osc_controller)?;

        // Calculate timing for this buffer
        let timing = self.calculate_timing(current_frame_time, buffer_size)?;

        // Get output buffer for click track
        let click_output = ports.click_track_out.as_mut_slice(ps);

        self.render_click(buffer_size, &timing, click_output);

        Ok(timing)
    }

    pub fn set_click_volume(
        &mut self,
        click_volume: f32,
        persistence: &mut PersistenceManagerController,
    ) -> Result<()> {
        let clamped = click_volume.clamp(0.0, 1.0);
        persistence.update_metronome_volume(clamped)?;
        self.click_volume = clamped;
        Ok(())
    }

    pub fn set_frames_per_beat(&mut self, frames_per_beat: usize) {
        self.frames_per_beat = frames_per_beat;
        self.frames_since_last_beat = self.frames_since_last_beat % self.frames_per_beat;
    }

    fn process_ppqn_ticks(
        &mut self,
        ps: &jack::ProcessScope,
        ports: &mut JackPorts,
        osc_controller: &OscController,
    ) -> Result<()> {
        let buffer_size = ps.n_frames() as usize;

        // Use the pure function to find tick (allocation-free)
        if let Some(tick) = Self::find_next_ppqn_tick_in_buffer(
            self.frames_since_last_beat,
            self.frames_per_beat,
            buffer_size,
        ) {
            // Send MIDI clock
            let midi_clock_message = [0xF8];
            let mut midi_writer = ports.midi_out.writer(ps);
            midi_writer
                .write(&jack::RawMidi {
                    time: tick.sample_offset as u32,
                    bytes: &midi_clock_message,
                })
                .map_err(|_| LooperError::Midi(std::panic::Location::caller()))?;

            // Send OSC position message
            osc_controller.metronome_position_changed(tick.position)?;
        }

        Ok(())
    }

    fn render_click(&mut self, buffer_size: u32, timing: &BufferTiming, click_output: &mut [f32]) {
        let click_length = self.click_samples.sample_count;

        // Calculate our position at the START of this buffer (before calculate_timing updated it)
        // We need to go back by: buffer_size + any missed samples
        let total_advancement = buffer_size as usize + timing.missed_frames;
        let start_position = (self.frames_since_last_beat + self.frames_per_beat
            - total_advancement)
            % self.frames_per_beat;

        if let Some(beat_offset) = timing.beat_in_buffer {
            let beat_offset = beat_offset as usize;

            // Check if we're still playing a click from before this beat
            if start_position < click_length {
                // Continue click until beat offset
                let click_samples_remaining = click_length as usize - start_position as usize;
                let samples_to_write = beat_offset.min(click_samples_remaining);

                let dest = &mut click_output[0..samples_to_write];
                if self
                    .click_samples
                    .copy_samples(dest, start_position as usize)
                    .is_ok()
                {
                    dest.iter_mut()
                        .for_each(|sample| *sample *= self.click_volume);
                }

                // Fill gap between end of click and new beat with silence
                click_output[samples_to_write..beat_offset].fill(0.0);
            } else {
                // Write silence up to beat
                click_output[0..beat_offset].fill(0.0);
            }

            // Start new click at beat offset
            if beat_offset < buffer_size as usize {
                let remaining_buffer = buffer_size as usize - beat_offset;
                let samples_to_write = remaining_buffer.min(click_length as usize);

                let dest = &mut click_output[beat_offset..beat_offset + samples_to_write];
                if self.click_samples.copy_samples(dest, 0).is_ok() {
                    dest.iter_mut()
                        .for_each(|sample| *sample *= self.click_volume);
                }

                // Fill any remaining buffer with silence
                click_output[(beat_offset + samples_to_write)..].fill(0.0);
            }
        } else {
            // No beat in this buffer - check if we're continuing a click
            if start_position < click_length {
                let click_offset = start_position as usize;
                let remaining_click = click_length as usize - click_offset;
                let samples_to_write = (buffer_size as usize).min(remaining_click);

                let dest = &mut click_output[0..samples_to_write];
                if self.click_samples.copy_samples(dest, click_offset).is_ok() {
                    dest.iter_mut()
                        .for_each(|sample| *sample *= self.click_volume);
                }

                // Fill remaining with silence
                click_output[samples_to_write..].fill(0.0);
            } else {
                // No click playing - all silence
                click_output.fill(0.0);
            }
        }
    }

    pub fn calculate_timing(
        &mut self,
        current_frame_time: u32,
        buffer_size: u32,
    ) -> Result<BufferTiming> {
        // Detect xrun
        let (missed_frames, beat_in_missed) = if let Some(last) = self.last_frame_time {
            let expected = last.wrapping_add(buffer_size); // Handle u32 wrap
            if current_frame_time != expected {
                // We have a gap
                let missed = current_frame_time.wrapping_sub(expected) as usize;

                // Check if we missed multiple beats
                let total_samples =
                    self.frames_since_last_beat + missed as usize + buffer_size as usize;
                if total_samples >= 2 * self.frames_per_beat {
                    return Err(LooperError::Xrun(std::panic::Location::caller()));
                }

                // Check if a beat occurred in the missed section
                let beat_in_missed =
                    if self.frames_since_last_beat + missed as usize >= self.frames_per_beat {
                        Some((self.frames_per_beat - self.frames_since_last_beat) as u32)
                    } else {
                        None
                    };

                (missed, beat_in_missed)
            } else {
                (0, None)
            }
        } else {
            // First call
            (0, None)
        };

        // Check for beat in current buffer
        // We need to account for any missed samples here too
        let start_position = (self.frames_since_last_beat + missed_frames) % self.frames_per_beat;
        let beat_in_buffer = if start_position + buffer_size as usize >= self.frames_per_beat {
            Some((self.frames_per_beat - start_position) as u32)
        } else {
            None
        };

        // Update state - advance by total samples (missed + buffer)
        self.frames_since_last_beat =
            (self.frames_since_last_beat + missed_frames + buffer_size as usize)
                % self.frames_per_beat;
        self.last_frame_time = Some(current_frame_time);

        Ok(BufferTiming {
            beat_in_buffer,
            missed_frames: missed_frames,
            beat_in_missed,
        })
    }

    /// Pure function to find the next PPQN tick in a buffer (allocation-free)
    /// Returns Some(tick) if there's a tick in this buffer, None otherwise
    pub fn find_next_ppqn_tick_in_buffer(
        frames_since_last_beat: usize,
        frames_per_beat: usize,
        buffer_size: usize,
    ) -> Option<PpqnTick> {
        const TICKS_PER_BEAT: usize = 24;
        let frames_per_tick = frames_per_beat / TICKS_PER_BEAT;

        // Find distance to next tick boundary
        let frames_since_last_tick = frames_since_last_beat % frames_per_tick;
        let frames_to_next_tick = if frames_since_last_tick == 0 {
            0 // We're exactly on a tick
        } else {
            frames_per_tick - frames_since_last_tick
        };

        // Check if this tick occurs within our buffer
        if frames_to_next_tick < buffer_size {
            let absolute_tick_position = frames_since_last_beat + frames_to_next_tick;
            let tick_index = (absolute_tick_position / frames_per_tick) % TICKS_PER_BEAT;
            let position = (tick_index as f32) / (TICKS_PER_BEAT as f32);

            Some(PpqnTick {
                sample_offset: frames_to_next_tick,
                tick_index,
                position,
            })
        } else {
            None
        }
    }
}

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

    fn create_test_metronome(frames_per_beat: usize) -> Metronome {
        let beep_samples = Arc::new(AudioChunk {
            samples: vec![1.0; 100].into_boxed_slice(),
            sample_count: 100,
            next: None,
        });

        Metronome {
            click_samples: beep_samples,
            click_volume: 1.0,
            frames_per_beat,
            frames_since_last_beat: 0,
            last_frame_time: None,
        }
    }

    #[test]
    fn test_first_call_initialization() {
        let mut metronome = create_test_metronome(1000);

        let result = metronome.calculate_timing(5000, 128).unwrap();

        assert_eq!(metronome.frames_since_last_beat, 128);
        assert_eq!(metronome.last_frame_time, Some(5000));
        assert_eq!(result.missed_frames, 0);
        assert_eq!(result.beat_in_missed, None);
        assert_eq!(result.beat_in_buffer, None);
    }

    #[test]
    fn test_normal_buffer_no_beat() {
        let mut metronome = create_test_metronome(1000);

        // Initialize at time 1000
        metronome.calculate_timing(1000, 128).unwrap();
        assert_eq!(metronome.frames_since_last_beat, 128);

        // Next buffer at 1128 - no beat expected
        let result = metronome.calculate_timing(1128, 128).unwrap();

        assert_eq!(metronome.frames_since_last_beat, 256);
        assert_eq!(result.missed_frames, 0);
        assert_eq!(result.beat_in_missed, None);
        assert_eq!(result.beat_in_buffer, None);
    }

    #[test]
    fn test_beat_in_buffer() {
        let mut metronome = create_test_metronome(1000);

        // Initialize at time 0
        metronome.calculate_timing(0, 512).unwrap();
        assert_eq!(metronome.frames_since_last_beat, 512);

        // Next buffer: 512 -> 1024, beat at 1000 (offset 488)
        let result = metronome.calculate_timing(512, 512).unwrap();

        assert_eq!(result.missed_frames, 0);
        assert_eq!(result.beat_in_missed, None);
        assert_eq!(result.beat_in_buffer, Some(488)); // 1000 - 512
        assert_eq!(metronome.frames_since_last_beat, 24); // (512 + 512) % 1000
    }

    #[test]
    fn test_xrun_no_missed_beat() {
        let mut metronome = create_test_metronome(1000);

        // Initialize at time 1000
        metronome.calculate_timing(1000, 128).unwrap();
        assert_eq!(metronome.frames_since_last_beat, 128);

        // Normal buffer at 1128
        metronome.calculate_timing(1128, 128).unwrap();
        assert_eq!(metronome.frames_since_last_beat, 256);

        // Xrun: expected 1256 but got 1428 (172 samples missed)
        let result = metronome.calculate_timing(1428, 128).unwrap();

        assert_eq!(result.missed_frames, 172);
        assert_eq!(result.beat_in_missed, None);
        assert_eq!(result.beat_in_buffer, None);
        assert_eq!(metronome.frames_since_last_beat, 556); // 256 + 172 + 128
    }

    #[test]
    fn test_xrun_with_missed_beat() {
        let mut metronome = create_test_metronome(1000);

        // Initialize at time 1000
        metronome.calculate_timing(1000, 128).unwrap();
        assert_eq!(metronome.frames_since_last_beat, 128);

        // Normal buffer at 1128
        metronome.calculate_timing(1128, 128).unwrap();
        assert_eq!(metronome.frames_since_last_beat, 256);

        // Xrun: expected 1256 but got 2228 (972 samples missed)
        // We're at position 256, miss 972 samples = 1228 total
        // Beat occurs at position 1000, so beat_in_missed = 1000 - 256 = 744
        let result = metronome.calculate_timing(2228, 128).unwrap();

        assert_eq!(result.missed_frames, 972);
        assert_eq!(result.beat_in_missed, Some(744)); // 1000 - 256
        assert_eq!(result.beat_in_buffer, None);
        assert_eq!(metronome.frames_since_last_beat, 356); // (256 + 972 + 128) % 1000
    }

    #[test]
    fn test_xrun_with_missed_beat_and_upcoming_beat() {
        let mut metronome = create_test_metronome(1000);

        // Initialize at time 1000
        metronome.calculate_timing(1000, 128).unwrap();

        // Normal buffer at 1128
        metronome.calculate_timing(1128, 128).unwrap();
        assert_eq!(metronome.frames_since_last_beat, 256);

        // Xrun: expected 1256 but got 2078 (822 samples missed)
        // We're at position 256, miss 822 samples = 1078 total
        // Beat occurs at position 1000, so beat_in_missed = 1000 - 256 = 744
        // After missed: position = 78, buffer = 128, no beat in buffer
        let result = metronome.calculate_timing(2078, 128).unwrap();

        assert_eq!(result.missed_frames, 822);
        assert_eq!(result.beat_in_missed, Some(744)); // 1000 - 256
        assert_eq!(result.beat_in_buffer, None); // 78 + 128 < 1000
        assert_eq!(metronome.frames_since_last_beat, 206); // (256 + 822 + 128) % 1000
    }

    #[test]
    fn test_xrun_multiple_beats_error() {
        let mut metronome = create_test_metronome(1000);

        // Initialize at time 1000
        metronome.calculate_timing(1000, 128).unwrap();

        // Normal buffer at 1128
        metronome.calculate_timing(1128, 128).unwrap();

        // Xrun: expected 1256 but got 3328 (2072 samples missed)
        // Total advancement would be 256 + 2072 + 128 = 2456 samples
        // That's more than 2 beats (2000 samples), so error
        let result = metronome.calculate_timing(3328, 128);

        assert!(result.is_err());
    }

    #[test]
    fn test_consecutive_buffers_with_beat() {
        let mut metronome = create_test_metronome(1000);

        // First buffer - initialization
        let result1 = metronome.calculate_timing(0, 512).unwrap();
        assert_eq!(result1.beat_in_buffer, None);
        assert_eq!(metronome.frames_since_last_beat, 512);

        // Second buffer - beat should occur at position 1000
        let result2 = metronome.calculate_timing(512, 512).unwrap();
        assert_eq!(result2.beat_in_buffer, Some(488)); // 1000 - 512
        assert_eq!(metronome.frames_since_last_beat, 24); // (512 + 512) % 1000

        // Third buffer - no beat
        let result3 = metronome.calculate_timing(1024, 512).unwrap();
        assert_eq!(result3.beat_in_buffer, None);
        assert_eq!(metronome.frames_since_last_beat, 536);

        // Fourth buffer - next beat at position 1000 again
        let result4 = metronome.calculate_timing(1536, 512).unwrap();
        assert_eq!(result4.beat_in_buffer, Some(464)); // 1000 - 536
        assert_eq!(metronome.frames_since_last_beat, 48); // (536 + 512) % 1000
    }

    #[test]
    fn test_u32_wrapping() {
        let mut metronome = create_test_metronome(1000);

        // Initialize near u32::MAX
        let start_time = u32::MAX - 100;
        metronome.calculate_timing(start_time, 128).unwrap();

        // Next buffer wraps around
        let next_time = start_time.wrapping_add(128);
        let result = metronome.calculate_timing(next_time, 128).unwrap();

        assert_eq!(result.missed_frames, 0);
        assert_eq!(metronome.frames_since_last_beat, 256);
    }

    // Tests for the pure PPQN calculation function
    #[test]
    fn test_ppqn_no_tick_in_buffer() {
        let tick = Metronome::find_next_ppqn_tick_in_buffer(100, 960000, 128);
        assert_eq!(tick, None);
    }

    #[test]
    fn test_ppqn_tick_at_buffer_start() {
        let frames_per_beat = 960000;

        // Position exactly at tick boundary
        let tick = Metronome::find_next_ppqn_tick_in_buffer(0, frames_per_beat, 128).unwrap();
        assert_eq!(tick.sample_offset, 0);
        assert_eq!(tick.tick_index, 0);
        assert_eq!(tick.position, 0.0);
    }

    #[test]
    fn test_ppqn_tick_in_middle_of_buffer() {
        let frames_per_beat = 960000;
        let frames_per_tick = frames_per_beat / 24; // 40000

        // Position 64 frames before tick 1
        let frames_since_last_beat = frames_per_tick - 64;
        let tick =
            Metronome::find_next_ppqn_tick_in_buffer(frames_since_last_beat, frames_per_beat, 128)
                .unwrap();

        assert_eq!(tick.sample_offset, 64);
        assert_eq!(tick.tick_index, 1);
        assert_eq!(tick.position, 1.0 / 24.0);
    }

    #[test]
    fn test_ppqn_tick_wrapping() {
        let frames_per_beat = 960000;
        let frames_per_tick = frames_per_beat / 24; // 40000

        // Position near end of beat cycle
        let frames_since_last_beat = 23 * frames_per_tick - 64; // 64 frames before tick 23
        let tick =
            Metronome::find_next_ppqn_tick_in_buffer(frames_since_last_beat, frames_per_beat, 128)
                .unwrap();

        assert_eq!(tick.sample_offset, 64);
        assert_eq!(tick.tick_index, 23);
        assert_eq!(tick.position, 23.0 / 24.0);
    }

    #[test]
    fn test_ppqn_real_world_scenario() {
        // Your actual values
        let frames_per_beat = 960000;
        let frames_since_last_beat = 39936;
        let buffer_size = 128;

        let tick = Metronome::find_next_ppqn_tick_in_buffer(
            frames_since_last_beat,
            frames_per_beat,
            buffer_size,
        );

        if let Some(tick) = tick {
            println!(
                "Tick {} at offset {} (position {})",
                tick.tick_index, tick.sample_offset, tick.position
            );
            assert_eq!(tick.sample_offset, 64); // 40000 - 39936 = 64
            assert_eq!(tick.tick_index, 1);
        }
    }
}
