use crate::*;

/// Audio data representation supporting sync offsets for column-based timing
#[derive(Debug)]
pub enum AudioData {
    /// No audio data present
    Empty,

    /// Unconsolidated linked chunks with sync offset (used during/after recording)
    Unconsolidated {
        chunks: Arc<AudioChunk>,
        sync_offset: usize, // samples from column start to recording start
        length: usize,      // total samples in recording
    },

    /// Consolidated single buffer, reordered for optimal playback
    Consolidated {
        buffer: Box<[f32]>, // single optimized array
    },
}

impl AudioData {
    /// Create new empty audio data
    pub fn new_empty() -> Self {
        Self::Empty
    }

    /// Create new unconsolidated audio data from initial chunk
    pub fn new_unconsolidated<F: ChunkFactory>(
        chunk_factory: &mut F,
        sync_offset: usize,
    ) -> Result<Self> {
        Ok(Self::Unconsolidated {
            chunks: chunk_factory.create_chunk()?,
            sync_offset,
            length: 0,
        })
    }

    /// Get total length in samples
    pub fn len(&self) -> usize {
        match self {
            Self::Empty => 0,
            Self::Unconsolidated { length, .. } => *length,
            Self::Consolidated { buffer } => buffer.len(),
        }
    }

    /// Check if audio data is empty
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Append samples during recording (only valid for Unconsolidated state)
    pub fn append_samples<F: ChunkFactory>(
        &mut self,
        samples: &[f32],
        chunk_factory: &mut F,
    ) -> Result<()> {
        match self {
            Self::Unconsolidated { chunks, length, .. } => {
                chunks.append_samples(samples, chunk_factory)?;
                *length += samples.len();
                Ok(())
            }
            _ => Err(LooperError::OutOfBounds(std::panic::Location::caller())),
        }
    }

    /// Copy samples to output buffer with volume scaling and looping
    /// logical_position is the position within the column cycle (0 = beat 1)
    pub fn copy_samples_to_output(
        &self,
        output_slice: &mut [f32],
        logical_position: usize,
        volume: f32,
    ) -> Result<()> {
        match self {
            Self::Empty => {
                output_slice.fill(0.0);
                Ok(())
            }
            Self::Unconsolidated {
                chunks,
                sync_offset,
                length,
            } => self.copy_unconsolidated_samples(
                chunks,
                *sync_offset,
                *length,
                output_slice,
                logical_position,
                volume,
            ),
            Self::Consolidated { buffer } => {
                self.copy_consolidated_samples(buffer, output_slice, logical_position, volume)
            }
        }
    }

    /// Get underlying chunk for post-record processing
    pub fn get_chunk_for_processing(&self) -> Result<(Arc<AudioChunk>, usize)> {
        match self {
            Self::Unconsolidated {
                chunks,
                sync_offset,
                ..
            } => Ok((chunks.clone(), *sync_offset)),
            _ => Err(LooperError::ChunkOwnership(std::panic::Location::caller())),
        }
    }

    /// Replace with consolidated buffer from post-record processing
    pub fn set_consolidated_buffer(&mut self, buffer: Box<[f32]>) -> Result<()> {
        match self {
            Self::Unconsolidated {
                length: old_length, ..
            } => {
                if buffer.len() != *old_length {
                    return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
                }
                *self = Self::Consolidated { buffer };
                Ok(())
            }
            _ => Err(LooperError::OutOfBounds(std::panic::Location::caller())),
        }
    }

    /// Clear all audio data
    pub fn clear(&mut self) -> Result<()> {
        *self = Self::Empty;
        Ok(())
    }

    /// Append silence frames to audio data (RT-safe, uses fixed-size buffer and loops)
    pub fn append_silence<F: ChunkFactory>(
        &mut self,
        frame_count: usize,
        chunk_factory: &mut F,
    ) -> Result<()> {
        const SILENCE_BUFFER_SIZE: usize = 1024;
        let silence_buffer = [0.0; SILENCE_BUFFER_SIZE];

        let mut remaining_frames = frame_count;
        while remaining_frames > 0 {
            let frames_this_iteration = remaining_frames.min(SILENCE_BUFFER_SIZE);
            self.append_samples(&silence_buffer[..frames_this_iteration], chunk_factory)?;
            remaining_frames -= frames_this_iteration;
        }
        Ok(())
    }
}

impl AudioData {
    /// Copy samples from unconsolidated chunks with sync offset handling
    fn copy_unconsolidated_samples(
        &self,
        chunks: &Arc<AudioChunk>,
        sync_offset: usize,
        length: usize,
        output_slice: &mut [f32],
        mut logical_position: usize,
        volume: f32,
    ) -> Result<()> {
        if length == 0 {
            output_slice.fill(0.0);
            return Ok(());
        }

        let mut samples_written = 0;
        let samples_needed = output_slice.len();

        while samples_written < samples_needed {
            // Map logical position to buffer position using sync offset
            let buffer_position = (logical_position + sync_offset) % length;

            let samples_remaining_in_output = samples_needed - samples_written;
            // This is the crucial change: determine how many contiguous samples can be read from the current buffer_position
            let readable_samples_from_here = length - buffer_position;

            let samples_to_copy = samples_remaining_in_output.min(readable_samples_from_here);

            // Copy from chunks to output slice
            chunks.copy_samples(
                &mut output_slice[samples_written..samples_written + samples_to_copy],
                buffer_position,
            )?;

            // Apply volume scaling in-place
            for sample in &mut output_slice[samples_written..samples_written + samples_to_copy] {
                *sample *= volume;
            }

            samples_written += samples_to_copy;
            logical_position = (logical_position + samples_to_copy) % length;
        }

        Ok(())
    }

    /// Copy samples from consolidated buffer (already reordered)
    fn copy_consolidated_samples(
        &self,
        buffer: &[f32],
        output_slice: &mut [f32],
        mut logical_position: usize,
        volume: f32,
    ) -> Result<()> {
        let length = buffer.len();
        if length == 0 {
            output_slice.fill(0.0);
            return Ok(());
        }

        let mut samples_written = 0;
        let samples_needed = output_slice.len();

        while samples_written < samples_needed {
            let samples_remaining = samples_needed - samples_written;
            let samples_until_loop = length - logical_position;
            let samples_to_copy = samples_remaining.min(samples_until_loop);

            // Direct copy since consolidated buffer is already reordered
            let src = &buffer[logical_position..logical_position + samples_to_copy];
            let dest = &mut output_slice[samples_written..samples_written + samples_to_copy];
            dest.copy_from_slice(src);

            // Apply volume scaling in-place
            for sample in dest {
                *sample *= volume;
            }

            samples_written += samples_to_copy;
            logical_position += samples_to_copy;

            // Handle looping
            if logical_position >= length {
                logical_position = 0;
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chunk_factory::mock::MockFactory;

    #[test]
    fn test_new_empty() {
        let audio_data = AudioData::new_empty();

        assert_eq!(audio_data.len(), 0);
        assert!(audio_data.is_empty());
    }

    #[test]
    fn test_new_unconsolidated() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(1024)]);
        let sync_offset = 100;

        let audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();

        match audio_data {
            AudioData::Unconsolidated {
                sync_offset: offset,
                length,
                ..
            } => {
                assert_eq!(offset, 100);
                assert_eq!(length, 0);
            }
            _ => panic!("Expected Unconsolidated variant"),
        }
    }

    #[test]
    fn test_append_samples_unconsolidated() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(1024)]);
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();

        let samples = vec![1.0, 2.0, 3.0, 4.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();

        assert_eq!(audio_data.len(), 4);
        assert!(!audio_data.is_empty());
    }

    #[test]
    fn test_append_samples_invalid_state() {
        let mut factory = MockFactory::new(vec![]);
        let mut audio_data = AudioData::new_empty();

        let samples = vec![1.0, 2.0];
        let result = audio_data.append_samples(&samples, &mut factory);

        assert!(result.is_err());
    }

    #[test]
    fn test_copy_samples_empty() {
        let audio_data = AudioData::new_empty();
        let mut output = vec![99.0; 4]; // Fill with non-zero to verify silence

        audio_data
            .copy_samples_to_output(&mut output, 0, 1.0)
            .unwrap();

        assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]);
    }

    #[test]
    fn test_copy_samples_unconsolidated_no_offset() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();

        // Add some test samples
        let samples = vec![1.0, 2.0, 3.0, 4.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();

        let mut output = vec![0.0; 4];
        audio_data
            .copy_samples_to_output(&mut output, 0, 1.0)
            .unwrap();

        assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0]);
    }

    #[test]
    fn test_copy_samples_unconsolidated_with_offset() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let sync_offset = 2;
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();

        // Recording order: [A, B, C, D] but recorded starting at beat 3 (sync_offset=2)
        // So logical mapping should be: beat1=C, beat2=D, beat3=A, beat4=B
        let samples = vec![10.0, 20.0, 30.0, 40.0]; // A, B, C, D
        audio_data.append_samples(&samples, &mut factory).unwrap();

        let mut output = vec![0.0; 4];
        audio_data
            .copy_samples_to_output(&mut output, 0, 1.0)
            .unwrap(); // Start at logical beat 1

        // Should get: [C, D, A, B] = [30.0, 40.0, 10.0, 20.0]
        assert_eq!(output, vec![30.0, 40.0, 10.0, 20.0]);
    }

    #[test]
    fn test_copy_samples_unconsolidated_with_volume() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();

        let samples = vec![1.0, 2.0, 3.0, 4.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();

        let mut output = vec![0.0; 4];
        audio_data
            .copy_samples_to_output(&mut output, 0, 0.5)
            .unwrap(); // 50% volume

        assert_eq!(output, vec![0.5, 1.0, 1.5, 2.0]);
    }

    #[test]
    fn test_copy_samples_unconsolidated_looping() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();

        let samples = vec![1.0, 2.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();

        let mut output = vec![0.0; 6]; // Request more samples than available
        audio_data
            .copy_samples_to_output(&mut output, 0, 1.0)
            .unwrap();

        // Should loop: [1.0, 2.0, 1.0, 2.0, 1.0, 2.0]
        assert_eq!(output, vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
    }

    #[test]
    fn test_copy_samples_unconsolidated_offset_with_looping() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let sync_offset = 1;
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();

        // Recording: [A, B, C] starting at beat 2 (sync_offset=1)
        // Logical mapping: beat1=B, beat2=C, beat3=A
        let samples = vec![10.0, 20.0, 30.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();

        let mut output = vec![0.0; 6]; // Request 2 full loops
        audio_data
            .copy_samples_to_output(&mut output, 0, 1.0)
            .unwrap();

        // Should get: [B, C, A, B, C, A] = [20.0, 30.0, 10.0, 20.0, 30.0, 10.0]
        assert_eq!(output, vec![20.0, 30.0, 10.0, 20.0, 30.0, 10.0]);
    }

    #[test]
    fn test_copy_samples_consolidated() {
        // Create consolidated data directly
        let buffer = vec![1.0, 2.0, 3.0, 4.0].into_boxed_slice();

        let audio_data = AudioData::Consolidated { buffer };

        let mut output = vec![0.0; 4];
        audio_data
            .copy_samples_to_output(&mut output, 0, 1.0)
            .unwrap();

        assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0]);
    }

    #[test]
    fn test_get_chunk_for_processing() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let sync_offset = 42;
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();

        let samples = vec![1.0, 2.0, 3.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();

        let result = audio_data.get_chunk_for_processing();
        assert!(result.is_ok());

        let (chunk, offset) = result.unwrap();
        assert_eq!(offset, 42);
        assert_eq!(chunk.len(), 3);
    }

    #[test]
    fn test_get_chunk_for_processing_wrong_state() {
        let audio_data = AudioData::new_empty();
        let result = audio_data.get_chunk_for_processing();
        assert!(result.is_err());
    }

    #[test]
    fn test_set_consolidated_buffer() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, 100).unwrap();

        let samples = vec![1.0, 2.0, 3.0, 4.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();

        // Create consolidated buffer with same length
        let consolidated_buffer = vec![10.0, 20.0, 30.0, 40.0].into_boxed_slice();

        audio_data
            .set_consolidated_buffer(consolidated_buffer)
            .unwrap();

        // Should now be Consolidated variant
        match audio_data {
            AudioData::Consolidated { buffer } => {
                assert_eq!(buffer.len(), 4);
            }
            _ => panic!("Expected Consolidated variant"),
        }
    }

    #[test]
    fn test_set_consolidated_buffer_length_mismatch() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();

        let samples = vec![1.0, 2.0]; // Length 2
        audio_data.append_samples(&samples, &mut factory).unwrap();

        // Try to set buffer with different length
        let wrong_length_buffer = vec![10.0, 20.0, 30.0, 40.0].into_boxed_slice(); // Length 4

        let result = audio_data.set_consolidated_buffer(wrong_length_buffer);
        assert!(result.is_err());
    }

    #[test]
    fn test_clear() {
        let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
        let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();

        let samples = vec![1.0, 2.0, 3.0];
        audio_data.append_samples(&samples, &mut factory).unwrap();
        assert_eq!(audio_data.len(), 3);

        audio_data.clear().unwrap();

        assert_eq!(audio_data.len(), 0);
        assert!(audio_data.is_empty());

        match audio_data {
            AudioData::Empty => (), // Expected
            _ => panic!("Expected Empty variant after clear"),
        }
    }
}
