/*
SPDX-License-Identifier: GPL-3.0-or-later
Copyright © 2024 Mike Robeson [dijksterhuis]
*/

//! Types and ser/de of `*.ot` binary data files.

use crate::slices::{Slice, Slices};
use crate::{
    CalculateChecksum, CheckChecksum, CheckFileVersion, CheckHeader, Decode, Encode, LoopMode,
    OptionEnumValueConvert, RBoxErr, SwapBytes, TimestrechMode, TrigQuantizationMode,
};
use ot_tools_io_derive::{IntegrityChecks, OctatrackFile};
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;

// in `hexdump -C` format:
// ```
// FORM....DPS1SMPA
// ......
// ```
/// Standard header bytes in a sample settings file
pub const SAMPLES_HEADER: [u8; 21] = [
    0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
    0x00, 0x00, 0x00, 0x00, 0x00,
];

/// Current/supported datatype version for a sample settings file
pub const SAMPLES_FILE_VERSION: u8 = 2;

#[derive(Debug)]
pub enum SampleSettingsErrors {
    TempoOutOfBounds,
    GainOutOfBounds,
}
impl std::fmt::Display for SampleSettingsErrors {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::GainOutOfBounds => write!(f, "invalid gain, must be in range -24.0 <= x <= 24.0"),
            Self::TempoOutOfBounds => write!(
                f,
                "invalid tempo value, must be in range 30.0 <= x <= 300.0"
            ),
        }
    }
}
impl std::error::Error for SampleSettingsErrors {}

pub const DEFAULT_TEMPO: u32 = 2880;
pub const DEFAULT_GAIN: u16 = 48;

/// Struct to create a valid Octatrack `.ot` file a.k.a a sample settings file.
/// General metadata for the sample's configuration on the OT
/// and the slice array with pointer positions for the sliced WAV.
///
/// NOTE: On the naming for this -- the Octatrack manual specifically refers to
/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
/// > separate file and link it to the sample currently being edited.
/// > -- page 87.
///
/// So ... this is the SETTINGS file.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, OctatrackFile, IntegrityChecks)]
pub struct SampleSettingsFile {
    /// Header
    pub header: [u8; 21],

    /// Datatype's version ID//
    pub datatype_version: u8,

    /// Unknown data
    pub unknown: u8,

    /// Tempo is always the machine UI's BPM multiplied by 24
    pub tempo: u32,

    /// Number of bars for the sample trim length marker.
    /// By default, trim length should be equal to trim end,
    /// and probably loop length too for drum hit sample chains.
    pub trim_len: u32,

    /// Number of bars for the sample loop length marker.
    /// By default, loop length should be equal to trim length for sample chains.
    pub loop_len: u32,

    /// Default timestrech algorithm applied to the sample.
    /// See the `ot_sample::options::SampleTimestrechModes` enum for suitable choices.
    pub stretch: u32,

    /// Default loop mode applied to the sample.
    /// See the `ot_sample::options::SampleLoopModes` enum for suitable choices.
    pub loop_mode: u32,

    /// Gain of the sample.
    /// -24.0 db <= x <= +24 db range in the machine's UI, with increments of 0.5 db changes.
    /// 0 <= x <= 96 range in binary data file.
    pub gain: u16,

    /// Default trig quantization mode applied to the sample.
    /// See the `ot_sample::options::SampleTrigQuantizationModes` enum for suitable choices.
    pub quantization: u8,

    /// Where the trim start marker is placed for the sample, measured in bars.
    /// Default is 0 (start of sample).
    /// TODO: measured in bars? or samples?
    pub trim_start: u32,

    /// Where the trim end marker is placed for the sample.
    /// When the sample is being played in normal mode (i.e. not using slices),
    /// the Octatrack will not play samples past this point.
    /// By default, trim length should be equal to trim end,
    /// and probably loop length too for drum hit sample chains.
    /// TODO: measured in bars? or samples?
    pub trim_end: u32,

    /// Start position for any loops. Default should be the same as trim start.
    /// Measured in bars.
    /// A note from the Octatrack manual on loop point/start behaviour:
    /// > If a loop point is set, the sample will play from the start point to the
    /// > end point, then loop from the loop point to the end point
    pub loop_start: u32,

    /// 64 length array containing `Slice`s.
    /// See the `Slice` struct for more details.
    /// Any empty slice positions should have zero-valued struct fields.
    #[serde(with = "BigArray")]
    pub slices: [Slice; 64],

    /// Number of usuable `Slice`s in this sample.
    /// Used by the Octatrack to ignore zero-valued `Slice`s in the `slices` array when loading the sample.
    pub slices_len: u32,

    /// Checksum value for the struct.
    /// This must be calculated **after** the struct is created on little-endian systems
    /// (requires byte swapping all struct fields to get the correct checksum value).
    pub checksum: u16,
}

impl SwapBytes for SampleSettingsFile {
    fn swap_bytes(self) -> RBoxErr<Self> {
        let mut bswapped_slices: [Slice; 64] = self.slices;

        for (i, slice) in self.slices.iter().enumerate() {
            bswapped_slices[i] = slice.swap_bytes()?;
        }

        let bswapped = Self {
            header: SAMPLES_HEADER,
            datatype_version: SAMPLES_FILE_VERSION,
            unknown: self.unknown,
            tempo: self.tempo.swap_bytes(),
            trim_len: self.trim_len.swap_bytes(),
            loop_len: self.loop_len.swap_bytes(),
            stretch: self.stretch.swap_bytes(),
            loop_mode: self.loop_mode.swap_bytes(),
            gain: self.gain.swap_bytes(),
            quantization: self.quantization.swap_bytes(),
            trim_start: self.trim_start.swap_bytes(),
            trim_end: self.trim_end.swap_bytes(),
            loop_start: self.loop_start.swap_bytes(),
            slices: bswapped_slices,
            slices_len: self.slices_len.swap_bytes(),
            checksum: self.checksum.swap_bytes(),
        };

        Ok(bswapped)
    }
}

impl SampleSettingsFile {
    /// Create a new `SampleSettingsFile`
    ///
    /// Values like `tempo` and `gain` need to be normalised to relevant OT
    /// bounds before using this method.
    /// ```rust
    /// # use std::array::from_fn;
    /// # use ot_tools_io::samples::{SampleSettingsFile, TrimConfig, LoopConfig};
    /// # use ot_tools_io::{
    /// #    TrigQuantizationMode,
    /// #    LoopMode,
    /// #    TimestrechMode,
    /// # };
    /// # use ot_tools_io::slices::{Slices, Slice};
    /// # let slice_arr: [Slice; 64] = from_fn(|_| Slice::default());
    /// # let slices: Slices = Slices { slices: slice_arr, count: 0 };
    /// let x = SampleSettingsFile::new(
    ///     &2880,
    ///     // ...
    ///     # &TimestrechMode::default(),
    ///     # &TrigQuantizationMode::default(),
    ///     &48,
    ///     // ...
    ///     # &TrimConfig { start: 0, end: 100, length: 25 },
    ///     # &LoopConfig { start: 0, length: 100, mode: LoopMode::Off },
    ///     # &slices,
    /// ).unwrap();
    /// assert_eq!(x.tempo, 2880);
    /// assert_eq!(x.gain, 48);
    /// ```
    pub fn new(
        tempo: &u32,
        stretch: &TimestrechMode,
        quantization: &TrigQuantizationMode,
        gain: &u16,
        trim_config: &TrimConfig,
        loop_config: &LoopConfig,
        slices: &Slices,
    ) -> RBoxErr<Self> {
        #[allow(clippy::manual_range_contains)]
        if tempo < &720 || tempo > &7200 {
            return Err(SampleSettingsErrors::TempoOutOfBounds.into());
        }

        #[allow(clippy::manual_range_contains)]
        if gain < &0 || gain > &96 {
            return Err(SampleSettingsErrors::GainOutOfBounds.into());
        }

        Ok(Self {
            header: SAMPLES_HEADER,
            datatype_version: SAMPLES_FILE_VERSION,
            unknown: 0,
            gain: *gain,
            stretch: stretch.value()?,
            tempo: *tempo,
            quantization: quantization.value()? as u8,
            trim_start: trim_config.start,
            trim_end: trim_config.end,
            trim_len: trim_config.length, // bin lengths are multiplied by 100?
            loop_start: loop_config.start,
            loop_len: loop_config.length, // bin lengths are multiplied by 100?
            loop_mode: loop_config.mode.value()?,
            slices: slices.slices,
            slices_len: slices.count,
            checksum: 0,
        })
    }
}

// For samples, need to run special decode method as need to flip bytes depending on endianness
impl Decode for SampleSettingsFile {
    /// Decode raw bytes of a `.ot` data file into a new struct,
    /// swap byte values if system is little-endian then do some minor
    /// post-processing to get user-friendly settings values.
    fn decode(bytes: &[u8]) -> RBoxErr<Self> {
        let decoded: Self = bincode::deserialize(bytes)?;
        let mut bswapd = decoded.clone();

        // swapping bytes is one required when running on little-endian systems
        if cfg!(target_endian = "little") {
            bswapd = decoded.swap_bytes()?;
        }

        Ok(bswapd)
    }
}

// For sample data, need swap bytes depending on endianness and calculate a checksum
impl Encode for SampleSettingsFile {
    /// Encodes struct data to binary representation, after some pre-processing.
    ///
    /// Before serializing, will:
    /// 1. modify tempo and gain values to machine ranges
    /// 2. swaps bytes of values (when current system is little-endian)
    /// 3. generate checksum value
    fn encode(&self) -> RBoxErr<Vec<u8>> {
        let mut chkd = self.clone();
        chkd.checksum = self.calculate_checksum()?;

        let encoded = if cfg!(target_endian = "little") {
            bincode::serialize(&chkd.swap_bytes()?)?
        } else {
            bincode::serialize(&chkd)?
        };
        Ok(encoded)
    }
}

impl CalculateChecksum for SampleSettingsFile {
    fn calculate_checksum(&self) -> RBoxErr<u16> {
        let bytes = bincode::serialize(&self)?;

        // skip header and checksum byte values
        let checksum_bytes = &bytes[16..bytes.len() - 2];

        let chk: u32 = checksum_bytes
            .iter()
            .map(|x| *x as u32)
            .sum::<u32>()
            .rem_euclid(u16::MAX as u32 + 1);

        Ok(chk as u16)
    }
}

impl CheckHeader for SampleSettingsFile {
    fn check_header(&self) -> bool {
        self.header == SAMPLES_HEADER
    }
}

impl CheckChecksum for SampleSettingsFile {
    fn check_checksum(&self) -> RBoxErr<bool> {
        Ok(self.checksum == self.calculate_checksum()?)
    }
}

impl CheckFileVersion for SampleSettingsFile {
    fn check_file_version(&self) -> RBoxErr<bool> {
        Ok(self.datatype_version == SAMPLES_FILE_VERSION)
    }
}

// impl CheckIntegrity for SampleSettingsFile {}

#[cfg(test)]
mod test {
    use crate::samples::{LoopConfig, TrimConfig};
    use crate::slices::{Slice, Slices};
    use crate::LoopMode;

    fn create_mock_configs_blank() -> (TrimConfig, LoopConfig, Slices) {
        let trim_config = TrimConfig {
            start: 0,
            end: 0,
            length: 0,
        };

        let loop_config = LoopConfig {
            start: 0,
            length: 0,
            mode: LoopMode::Normal,
        };

        let default_slice = Slice {
            trim_start: 0,
            trim_end: 0,
            loop_start: 0xFFFFFFFF,
        };

        let slices: [Slice; 64] = [default_slice; 64];

        let slice_conf = Slices { slices, count: 0 };

        (trim_config, loop_config, slice_conf)
    }

    mod checksum {
        /*
        mod array_based {
            use crate::CalculateChecksum;
            use std::array::from_fn;

            #[test]
            #[ignore]
            fn ok_no_overflow_all_255() {
                let bytes: [u8; 832] = from_fn(|_| 255);
                assert_eq!(
                    get_checksum(&bytes).unwrap(),
                    10962,
                    "overflows should wrap"
                );
            }
            #[test]
            #[ignore]
            fn ok_no_overflow() {
                let bytes: [u8; 832] = from_fn(|_| 78);

                assert_eq!(
                    get_checksum(&bytes).unwrap(),
                    63492,
                    "random value that shouldn't overflow at all"
                );
            }

            #[test]
            #[ignore]
            fn ok_64_slice_len() {
                let bytes: [u8; 832] = from_fn(|_| 64);
                assert_eq!(get_checksum(&bytes).unwrap(), 52096,);
            }

            #[test]
            #[ignore]
            fn ok_one_bytes() {
                let bytes: [u8; 832] = from_fn(|_| 1);
                assert_eq!(
                    get_checksum(&bytes).unwrap(),
                    814,
                    "low values definitely don't overflow"
                );
            }

            #[test]
            #[ignore]
            fn ok_zeroed() {
                let bytes: [u8; 832] = from_fn(|_| 0);
                assert_eq!(
                    get_checksum(&bytes).unwrap(),
                    0,
                    "unknown if this is correct behaviour"
                );
            }
        }
        */

        mod file_read {
            use crate::samples::{CalculateChecksum, SampleSettingsFile};
            use crate::test_utils::get_samples_dirpath;
            use crate::OctatrackFileIO;

            #[test]
            fn zero_slices_trig_quant_one_step() {
                let path = get_samples_dirpath()
                    .join("checksum")
                    .join("0slices-tq1step.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }

            #[test]
            fn zero_slices() {
                let path = get_samples_dirpath().join("checksum").join("0slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }

            #[test]
            fn one_slice() {
                let path = get_samples_dirpath().join("checksum").join("1slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn two_slices() {
                let path = get_samples_dirpath().join("checksum").join("2slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn four_slices() {
                let path = get_samples_dirpath().join("checksum").join("4slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn eight_slices() {
                let path = get_samples_dirpath().join("checksum").join("8slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn sixteen_slices() {
                let path = get_samples_dirpath().join("checksum").join("16slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn thirty_two_slices() {
                let path = get_samples_dirpath().join("checksum").join("32slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn forty_eight_slices() {
                let path = get_samples_dirpath().join("checksum").join("48slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn sixty_two_slices() {
                let path = get_samples_dirpath().join("checksum").join("62slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn sixty_three_slices() {
                let path = get_samples_dirpath().join("checksum").join("63slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
            #[test]
            fn sixty_four_slices_correct() {
                let path = get_samples_dirpath().join("checksum").join("64slices.ot");
                let data = SampleSettingsFile::from_data_file(&path).unwrap();
                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
            }
        }
        mod file_write {
            use crate::samples::SampleSettingsFile;
            use crate::test_utils::get_samples_dirpath;
            use crate::OctatrackFileIO;

            fn helper_write(test_name: String) {
                let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
                src_path.set_extension("ot");

                let test_dir_path = std::env::temp_dir()
                    .join("ot-tools-io")
                    .join("samples")
                    .join(&test_name);

                let mut test_fpath = test_dir_path.join("test");
                test_fpath.set_extension("ot");
                let _ = std::fs::create_dir_all(&test_dir_path);

                let valid = SampleSettingsFile::from_data_file(&src_path).unwrap();

                // need to reset checksum as it gets cacluated during encoding
                let mut to_write = valid.clone();
                to_write.checksum = 0;

                to_write.to_data_file(&test_fpath).unwrap();
                let written = SampleSettingsFile::from_data_file(&test_fpath);

                assert!(written.is_ok());
                assert_eq!(valid, written.unwrap());
            }

            #[test]
            fn zero_slices_trig_quant_one_step() {
                helper_write("0slices-tq1step".to_string());
            }

            #[test]
            fn zero_slices() {
                helper_write("0slices".to_string());
            }

            #[test]
            fn one_slice() {
                helper_write("1slices".to_string());
            }
            #[test]
            fn two_slices() {
                helper_write("2slices".to_string());
            }
            #[test]
            fn four_slices() {
                helper_write("4slices".to_string());
            }
            #[test]
            fn eight_slices() {
                helper_write("8slices".to_string());
            }
            #[test]
            fn sixteen_slices() {
                helper_write("16slices".to_string());
            }
            #[test]
            fn thirty_two_slices() {
                helper_write("32slices".to_string());
            }
            #[test]
            fn forty_eight_slices() {
                helper_write("48slices".to_string());
            }
            #[test]
            fn sixty_two_slices() {
                helper_write("62slices".to_string());
            }
            #[test]
            fn sixty_three_slices() {
                helper_write("63slices".to_string());
            }
            #[test]
            fn sixty_four_slices_correct() {
                helper_write("64slices".to_string());
            }
        }
    }

    mod create_new {

        use super::create_mock_configs_blank;
        use crate::samples::SampleSettingsFile;
        use crate::{TimestrechMode, TrigQuantizationMode};

        #[test]
        fn err_oob_tempo() {
            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();

            let composed_chain = SampleSettingsFile::new(
                &7201,
                &TimestrechMode::Off,
                &TrigQuantizationMode::PatternLength,
                &46,
                &trim_conf,
                &loop_conf,
                &slices,
            );

            assert!(composed_chain.is_err());
        }

        #[test]
        fn err_invalid_gain() {
            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();

            let composed_chain = SampleSettingsFile::new(
                &2880,
                &TimestrechMode::Off,
                &TrigQuantizationMode::PatternLength,
                &97,
                &trim_conf,
                &loop_conf,
                &slices,
            );

            assert!(composed_chain.is_err());
        }
    }

    mod integrity {
        use super::create_mock_configs_blank;
        use crate::samples::{SampleSettingsFile, DEFAULT_GAIN, DEFAULT_TEMPO};
        use crate::CheckHeader;
        use crate::{TimestrechMode, TrigQuantizationMode};
        #[test]
        fn true_valid_header() {
            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
            let new = SampleSettingsFile::new(
                &DEFAULT_TEMPO,
                &TimestrechMode::Off,
                &TrigQuantizationMode::PatternLength,
                &DEFAULT_GAIN,
                &trim_conf,
                &loop_conf,
                &slices,
            );
            assert!(new.unwrap().check_header());
        }

        #[test]
        fn false_invalid_header() {
            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
            let new = SampleSettingsFile::new(
                &DEFAULT_TEMPO,
                &TimestrechMode::Off,
                &TrigQuantizationMode::PatternLength,
                &DEFAULT_GAIN,
                &trim_conf,
                &loop_conf,
                &slices,
            );
            let mut mutated = new.unwrap();
            mutated.header[0] = 0x00;
            mutated.header[20] = 111;
            assert!(!mutated.check_header());
        }
    }
}

/// A helper struct for the OT Sample's Trim settings

#[derive(PartialEq, Debug, Clone, Copy)]
pub struct TrimConfig {
    /// Start of full audio sample (n samples)
    pub start: u32,
    /// End of full audio sample (n samples)
    pub end: u32,
    /// Length of audio sample to play before stopping/looping playback
    /// NOTE: This is measured in number of bars.
    pub length: u32,
}

impl TrimConfig {
    pub fn from_decoded(decoded: &SampleSettingsFile) -> RBoxErr<Self> {
        let new = TrimConfig {
            start: decoded.trim_start,
            end: decoded.trim_end,
            length: decoded.trim_len,
        };
        Ok(new)
    }
}

/// A helper struct for the OT Sample's Loop settings

#[derive(PartialEq, Debug, Clone, Copy)]
pub struct LoopConfig {
    /// Loop start position for the audio sample (n samples).
    pub start: u32,

    /// Length of the loop for the audio sample (n samples).
    pub length: u32,

    /// Type of looping mode.
    pub mode: LoopMode,
}

impl LoopConfig {
    pub fn new(start: u32, length: u32, mode: LoopMode) -> RBoxErr<Self> {
        Ok(Self {
            start,
            length,
            mode,
        })
    }

    pub fn from_decoded(decoded: &SampleSettingsFile) -> RBoxErr<Self> {
        Self::new(
            decoded.loop_start,
            decoded.loop_len,
            LoopMode::from_value(&decoded.loop_mode).unwrap_or(LoopMode::Off),
        )
    }
}

#[cfg(test)]
#[allow(unused_imports)]
mod config_tests {

    mod test_sample_loop_config {
        mod test_new {

            use crate::samples::{LoopConfig, LoopMode};

            #[test]
            fn test_new_sample_loop_config_loop_off() {
                assert_eq!(
                    LoopConfig::new(0, 10, LoopMode::Off).unwrap(),
                    LoopConfig {
                        start: 0,
                        length: 10,
                        mode: LoopMode::Off
                    }
                );
            }

            #[test]
            fn test_new_sample_loop_config_umin_start_umax_length() {
                assert_eq!(
                    LoopConfig::new(u32::MIN, u32::MAX, LoopMode::Off).unwrap(),
                    LoopConfig {
                        start: u32::MIN,
                        length: u32::MAX,
                        mode: LoopMode::Off
                    }
                );
            }

            #[test]
            fn test_new_sample_loop_config_loop_normal() {
                assert_eq!(
                    LoopConfig::new(0, 10, LoopMode::Normal).unwrap(),
                    LoopConfig {
                        start: 0,
                        length: 10,
                        mode: LoopMode::Normal
                    }
                );
            }

            #[test]
            fn test_new_sample_loop_config_loop_pingpong() {
                assert_eq!(
                    LoopConfig::new(0, 10, LoopMode::PingPong).unwrap(),
                    LoopConfig {
                        start: 0,
                        length: 10,
                        mode: LoopMode::PingPong
                    }
                );
            }
        }
    }

    mod test_from_decoded {

        use crate::slices::Slice;
        use crate::{
            samples::{
                LoopConfig, LoopMode, SampleSettingsFile, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
            },
            OptionEnumValueConvert,
        };

        #[test]
        fn test_umin_start_umax_len() {
            let decoded = SampleSettingsFile {
                header: SAMPLES_HEADER,
                datatype_version: SAMPLES_FILE_VERSION,
                unknown: 0,
                tempo: 128000,
                trim_len: 0,
                loop_len: u32::MAX,
                stretch: 0,
                loop_mode: LoopMode::Off.value().unwrap(),
                gain: 0,
                quantization: 0,
                trim_start: 0,
                trim_end: 0,
                loop_start: u32::MIN,
                slices: [Slice {
                    trim_start: 0,
                    trim_end: 0,
                    loop_start: 0,
                }; 64],
                slices_len: 0,
                checksum: 0,
            };

            assert_eq!(
                LoopConfig::from_decoded(&decoded).unwrap(),
                LoopConfig {
                    start: u32::MIN,
                    length: u32::MAX,
                    mode: LoopMode::Off
                }
            );
        }

        #[test]
        fn test_loop_off() {
            let decoded = SampleSettingsFile {
                header: SAMPLES_HEADER,
                datatype_version: SAMPLES_FILE_VERSION,
                unknown: 0,
                tempo: 128000,
                trim_len: 0,
                loop_len: 10,
                stretch: 0,
                loop_mode: LoopMode::Off.value().unwrap(),
                gain: 0,
                quantization: 0,
                trim_start: 0,
                trim_end: 0,
                loop_start: 0,
                slices: [Slice {
                    trim_start: 0,
                    trim_end: 0,
                    loop_start: 0,
                }; 64],
                slices_len: 0,
                checksum: 0,
            };

            assert_eq!(
                LoopConfig::from_decoded(&decoded).unwrap(),
                LoopConfig {
                    start: 0,
                    length: 10,
                    mode: LoopMode::Off
                }
            );
        }

        #[test]
        fn test_loop_normal() {
            let decoded = SampleSettingsFile {
                header: SAMPLES_HEADER,
                datatype_version: SAMPLES_FILE_VERSION,
                unknown: 0,
                tempo: 128000,
                trim_len: 0,
                loop_len: 10,
                stretch: 0,
                loop_mode: LoopMode::Normal.value().unwrap(),
                gain: 0,
                quantization: 0,
                trim_start: 0,
                trim_end: 0,
                loop_start: 0,
                slices: [Slice {
                    trim_start: 0,
                    trim_end: 0,
                    loop_start: 0,
                }; 64],
                slices_len: 0,
                checksum: 0,
            };

            assert_eq!(
                LoopConfig::from_decoded(&decoded).unwrap(),
                LoopConfig {
                    start: 0,
                    length: 10,
                    mode: LoopMode::Normal
                }
            );
        }

        #[test]
        fn test_loop_pingpong() {
            let decoded = SampleSettingsFile {
                header: SAMPLES_HEADER,
                datatype_version: SAMPLES_FILE_VERSION,
                unknown: 0,
                tempo: 128000,
                trim_len: 0,
                loop_len: 10,
                stretch: 0,
                loop_mode: LoopMode::PingPong.value().unwrap(),
                gain: 0,
                quantization: 0,
                trim_start: 0,
                trim_end: 0,
                loop_start: 0,
                slices: [Slice {
                    trim_start: 0,
                    trim_end: 0,
                    loop_start: 0,
                }; 64],
                slices_len: 0,
                checksum: 0,
            };

            assert_eq!(
                LoopConfig::from_decoded(&decoded).unwrap(),
                LoopConfig {
                    start: 0,
                    length: 10,
                    mode: LoopMode::PingPong
                }
            );
        }
    }
}
