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

//! Types and ser/de of `markers.*` binary data files.
//!
//! A "Markers" file contains data related to the playback settings of loaded
//! sample slots
//! - Trim Start
//! - Trim End
//! - Loop Point
//! - Slices (Start / End / Loop Point)
//!
//! The Markers file is dependent on a Project's Sample Slots. Switching out a
//! sample slot updates playback settings in the markers file.
//!
//! TODO: Figure out exactly WHEN update occurs (when adding a file to the slot?)

// TODO
use crate::slices::{Slice, SLICE_LOOP_POINT_DEFAULT};
use crate::{
    CalculateChecksum, CheckChecksum, CheckFileVersion, CheckHeader, Decode, Encode, IsDefault,
    OtToolsIoErrors, RBoxErr, SwapBytes,
};
use ot_tools_io_derive::{Decodeable, IntegrityChecks, OctatrackFile};
use serde::{Deserialize, Serialize};
use serde_big_array::{Array, BigArray};
use std::array::from_fn;
/*
# ===== DEFAULT DATA FILE ===== #
00000000  46 4f 52 4d 00 00 00 00  44 50 53 31 53 41 4d 50  |FORM....DPS1SAMP|
00000010  00 00 00 00 00 04 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
# ***** REPEATS *****
00032890  00 00 00 00 00 00 00 04                           |........|
00032898
*/

/// Header data for the markers file
pub const MARKERS_HEADER: [u8; 21] = [
    0x46, 0x4f, 0x52, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x41, 0x4d, 0x50,
    0x00, 0x00, 0x00, 0x00, 0x00,
];

/// Current/supported version of markers files.
pub const MARKERS_FILE_VERSION: u8 = 4;

/// Slot playback trigger points/markers. Generally, data related to trim, looping and slices for each slot.
/// Global slot settings like quantization/timestrech are stored in the [`crate::projects::slots::SlotAttributes`] type
///
/// 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 ... these are the Slot MARKERS (trim/loop/slice markers) which are saved
/// to a settings file.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Decodeable, Hash)]
pub struct SlotMarkers {
    /// main sample trim offset (start)
    pub trim_offset: u32,

    /// main sample trim end
    pub trim_end: u32,

    /// main sample loop point
    pub loop_point: u32,

    /// slices array for slice position editing
    #[serde(with = "BigArray")]
    pub slices: [Slice; 64],

    /// number of slices
    pub slice_count: u32,
}

impl SlotMarkers {
    fn validate(&self) -> RBoxErr<bool> {
        for slice in self.slices.iter() {
            slice.validate()?;
        }
        let trim_ok = self.trim_offset < self.trim_end;
        let slice_count_ok = self.slice_count == self.slices.len() as u32;
        let loop_start_ok = self.loop_point >= self.trim_offset;
        let loop_end_ok =
            self.loop_point <= self.trim_end || self.loop_point == SLICE_LOOP_POINT_DEFAULT;

        if !(trim_ok && slice_count_ok && loop_start_ok && loop_end_ok) {
            return Err(OtToolsIoErrors::TodoError.into());
        }

        Ok(true)
    }
}

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

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

        let bswapped = Self {
            trim_offset: self.trim_offset.swap_bytes(),
            trim_end: self.trim_end.swap_bytes(),
            loop_point: self.loop_point.swap_bytes(),
            slices,
            slice_count: self.slice_count.swap_bytes(),
        };

        Ok(bswapped)
    }
}

impl Default for SlotMarkers {
    fn default() -> Self {
        Self {
            trim_offset: 0,
            trim_end: 0,
            loop_point: 0,
            slices: from_fn(|_| Slice::default()),
            slice_count: 0,
        }
    }
}

/// The 'markers' file. Contains sample editor data for all slots in a project.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, OctatrackFile, IntegrityChecks)]
pub struct MarkersFile {
    #[serde(with = "BigArray")]
    pub header: [u8; 21],

    /// version of data file. used in OS upgrades for patching files (and checks
    /// performed on files during loading of a project).
    ///
    /// ### background / context
    ///
    /// Got this error on the device when i messed up the default markers file
    /// when i didn't include the `4` (current value for 1.40B).
    /// ```text
    /// >>> 2025-04-24 22:06:00 ERROR Couldn't read from markers file ...
    /// >>> '/test-set-bankcopy/PROJECT-BLANK/markers.work' ('INVALID FILESIZE')
    /// ```
    ///
    /// Additionally, Banks for projects created with 1.40X have a 'version'
    /// number of 23, while the LESSSELF/V1 project started with 1.25E has a
    /// version of 15.
    ///
    /// So yep. These weird numbers are version numbers for the data types /
    /// structures / files.
    pub datatype_version: u8,

    /// flex slots playback data
    pub flex_slots: Box<Array<SlotMarkers, 136>>,

    /// static slots playback data
    pub static_slots: Box<Array<SlotMarkers, 128>>,

    // i'm assuming a u16 checksum here as well?
    pub checksum: u16,
}

impl MarkersFile {
    #[allow(dead_code)]
    fn validate(&self) -> RBoxErr<bool> {
        for slot in self.flex_slots.iter() {
            slot.validate()?;
        }
        for slot in self.static_slots.iter() {
            slot.validate()?;
        }

        Ok(true)
    }
}

impl SwapBytes for MarkersFile {
    fn swap_bytes(self) -> RBoxErr<Self> {
        let mut flex_slots = self.flex_slots.clone();
        for (i, slot) in self.flex_slots.iter().enumerate() {
            flex_slots[i] = slot.clone().swap_bytes()?;
        }

        let mut static_slots = self.static_slots.clone();
        for (i, slot) in self.static_slots.iter().enumerate() {
            static_slots[i] = slot.clone().swap_bytes()?;
        }

        let bswapped = Self {
            header: self.header,
            datatype_version: self.datatype_version,
            flex_slots,
            static_slots,
            checksum: self.checksum.swap_bytes(),
        };

        Ok(bswapped)
    }
}

impl Decode for MarkersFile {
    fn decode(bytes: &[u8]) -> RBoxErr<Self>
    where
        Self: Sized,
        Self: for<'a> Deserialize<'a>,
    {
        let mut x: Self = bincode::deserialize(bytes)?;
        #[cfg(target_endian = "little")]
        {
            x = x.swap_bytes()?;
        }

        Ok(x)
    }
}

impl Encode for MarkersFile {
    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 MarkersFile {
    fn new(flex_slots: [SlotMarkers; 136], static_slots: [SlotMarkers; 128]) -> RBoxErr<Self> {
        let mut init = Self {
            header: MARKERS_HEADER,
            datatype_version: MARKERS_FILE_VERSION,
            flex_slots: Array(flex_slots).into(),
            static_slots: Array(static_slots).into(),
            checksum: 0,
        };

        init.checksum = init.calculate_checksum()?;
        Ok(init)
    }
}

impl Default for MarkersFile {
    fn default() -> Self {
        Self::new(
            from_fn(|_| SlotMarkers::default()),
            from_fn(|_| SlotMarkers::default()),
        )
        .unwrap()
    }
}

impl IsDefault for MarkersFile {
    fn is_default(&self) -> bool {
        let default = &mut MarkersFile::default();
        default == self
    }
}

impl CalculateChecksum for MarkersFile {
    fn calculate_checksum(&self) -> RBoxErr<u16> {
        let bytes = bincode::serialize(self)?;
        let mut chk: u16 = 0;
        for byte in &bytes[16..bytes.len() - 2] {
            chk = chk.wrapping_add(*byte as u16);
        }
        Ok(chk)
    }
}

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

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

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