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

//! todo

use std::ops::Index;
use std::path::PathBuf;

use crate::projects::errors::ProjectsErrors;
use crate::projects::markers::MarkersDataFile;
use crate::projects::projects::ProjDataFile;
use crate::projects::R;
use crate::OtToolsInterfacesError;
use ot_tools_io::identifiers::PlaybackSlotId;
use ot_tools_io::types::SlotAttributes;
use ot_tools_io::types::SlotMarkers;
use ot_tools_io::types::SlotType;
use ot_tools_io::types::{ActiveSlot, PlaybackSlots};

/// Get the path of a slot sample file, or throw an error
#[allow(dead_code)]
pub fn safe_get_slot_path(attrs: &SlotAttributes) -> R<PathBuf> {
    attrs
        .path
        .clone()
        .ok_or(ProjectsErrors::EmptySlotPath)
        .map_err(OtToolsInterfacesError::from)
}

/// A type to work around having to deal with sample slot data stored in two
/// disjointed files.
///
/// Slot data is stored in two separate file locations:
/// - `projects.*` files store the slot attributes
/// - `markers.*` files store the playback markers
///
/// The causes an absolute nightmare if you want to be able to update
/// both types of data in one go, which we often do!
/// Slots are one of the most **annoying** parts of the Octatrack data model to
/// interact with because of rust borrow checker limitations.
///
/// I can't remember the exact details off-hand, but i was having an
/// absolute nightmare of a time with mutable references.
/// I think the "only one mutable reference at a time" issue was cropping
/// up bunch because i had one mutably referenced [`OctatrackProjectState`][ps]
/// which i then needed to take 2x independent mutable references from
/// to update the two disjointed slot file data locations
/// (`projects.*` and `markers.*`).
///
/// I spent a lot of time duplicating code around updating both types of
/// data for a single slot.
/// It was not fun.
///
/// So, instead, this **massive hack** reads all the slot data and
/// re-organises it all in a more sane way for humans to interact with.
///
/// There is no `markers_mut` method in a [`OctatrackProjectState`][ps]
/// type, nor are there `markers_ref` / `markers` methods.
/// All the markers data should be easily accessible via this type in a
/// [`OctatrackProjectState`][ps].
///
/// # DANGER
/// this data will write over any changes made via the
/// [`OctatrackProjectState::project_mut`][ps] method.
/// Unfortunately hiding the access to slots data there is non-trivial.
///
///
/// [ps]: crate::projects::OctatrackProjectState
#[derive(Clone, Default, PartialEq, Debug, Hash, PartialOrd, Ord, Eq)]
pub struct Slots {
    static_slots: PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>>,
    flex_slots: PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>>,
}

impl Slots {
    /// Create a new instance.
    /// Does not validate provided data.
    ///
    /// # Example
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// use ot_tools_io::types::PlaybackSlots;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// Slots::new(PlaybackSlots::default(), PlaybackSlots::default());
    /// # Ok(()) }
    /// ```
    pub fn new(
        flex_slots: PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>>,
        static_slots: PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>>,
    ) -> Self {
        Self {
            flex_slots,
            static_slots,
        }
    }

    /// Wraps the `clone` method.
    pub fn to_new(&self) -> Self {
        self.clone()
    }

    /// Returns an owned instance of the static slots array.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let s_slots = Slots::default().static_slots();
    /// assert_eq!(s_slots.len(), 128);
    /// # Ok(()) }
    /// ```
    pub fn static_slots(self) -> PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        self.static_slots
    }

    /// Returns a reference to the static slots array.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let slots = Slots::default();
    /// let s_slots = slots.static_slots_ref();
    /// assert_eq!(s_slots.len(), 128);
    /// # Ok(()) }
    /// ```
    pub fn static_slots_ref(
        &self,
    ) -> &PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        &self.static_slots
    }

    /// Returns a mutable reference to the static slots array.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut slots = Slots::default();
    /// let s_slots = slots.static_slots_mut();
    /// assert_eq!(s_slots.len(), 128);
    /// s_slots[0] = None;
    /// # Ok(()) }
    /// ```
    pub fn static_slots_mut(
        &mut self,
    ) -> &mut PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        &mut self.static_slots
    }

    /// Returns an owned instance of the flex slots array.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let f_slots = Slots::default().flex_slots();
    /// assert_eq!(f_slots.len(), 128);
    /// # Ok(()) }
    /// ```
    pub fn flex_slots(self) -> PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        self.flex_slots
    }

    /// Returns a reference to the flex slots array.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let slots = Slots::default();
    /// let f_slots = slots.flex_slots_ref();
    /// assert_eq!(f_slots.len(), 128);
    /// # Ok(()) }
    /// ```
    pub fn flex_slots_ref(
        &self,
    ) -> &PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        &self.flex_slots
    }

    /// Returns a mutable reference to the flex slots array.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut slots = Slots::default();
    /// let f_slots = slots.flex_slots_mut();
    /// assert_eq!(f_slots.len(), 128);
    /// f_slots[0] = None;
    /// # Ok(()) }
    /// ```
    pub fn flex_slots_mut(
        &mut self,
    ) -> &mut PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        &mut self.flex_slots
    }

    /// Convienence method to avoid having to `match` on `SlotType` throughout
    /// downstream code.
    ///
    /// Returns an owned instance of the slots array for the desired slot type.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// use ot_tools_io::types::SlotType;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let slots = Slots::default().by_type(SlotType::Static);
    /// assert_eq!(slots.len(), 128);
    /// # Ok(()) }
    /// ```
    pub fn by_type<T: AsRef<SlotType>>(
        self,
        slot_type: T,
    ) -> PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        match slot_type.as_ref() {
            SlotType::Static => self.static_slots(),
            SlotType::Flex => self.flex_slots(),
        }
    }

    /// Convienence method to avoid having to `match` on `SlotType` throughout
    /// downstream code.
    ///
    /// Returns a reference to the slots array for the desired slot type.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// use ot_tools_io::types::SlotType;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let slots = Slots::default();
    /// let s_slots = slots.by_type(SlotType::Static);
    /// assert_eq!(s_slots.len(), 128);
    /// # Ok(()) }
    /// ```
    pub fn by_type_ref<T: AsRef<SlotType>>(
        &self,
        slot_type: T,
    ) -> &PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        match slot_type.as_ref() {
            SlotType::Static => self.static_slots_ref(),
            SlotType::Flex => self.flex_slots_ref(),
        }
    }

    /// Convienence method to avoid having to `match` on `SlotType` throughout
    /// downstream code.
    ///
    /// Returns a mutable reference to the slots array for the desired slot type.
    /// ```
    /// use ot_tools_interfaces::projects::Slots;
    /// use ot_tools_io::types::SlotType;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let mut slots = Slots::default();
    /// let s_slots = slots.by_type_mut(SlotType::Static);
    /// assert_eq!(s_slots.len(), 128);
    /// s_slots[0] = None;
    /// # Ok(()) }
    /// ```
    pub fn by_type_mut<T: AsRef<SlotType>>(
        &mut self,
        slot_type: T,
    ) -> &mut PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>> {
        match slot_type.as_ref() {
            SlotType::Static => self.static_slots_mut(),
            SlotType::Flex => self.flex_slots_mut(),
        }
    }

    /// Reads [`SlotAttributes`] and [`SlotMarkers`] data into a single unified array
    /// for each [`SlotType`].
    ///
    /// Takes ownership of the slot data read from the [`ProjDataFile`]
    /// and [`MarkersDataFile`] instances
    /// -- meaning we can have one (mutable) reference to slot data at a time,
    /// rather than always tracking two (mutable) references due to the two
    /// underlying arrays of data.
    ///
    /// Taking ownership basically gets stops downstream code being heavily duplicative.
    pub fn load(project_data: &ProjDataFile, markers_data: &MarkersDataFile) -> R<Self> {
        let def: [Option<ActiveSlot<SlotAttributes, SlotMarkers>>; 128] =
            std::array::from_fn(|_| None);
        let slots = PlaybackSlots::new(def);

        let static_attrs = &project_data.data.slots.static_slots;
        let static_marks = &markers_data.data.static_slots;

        let mut static_slots = slots.clone();

        for slot_id in PlaybackSlotId::iter_all() {
            if let Some(attr) = static_attrs.index(slot_id.as_index()) {
                let mark = *static_marks.index(slot_id.as_index());
                let active = ActiveSlot::new(attr.clone(), mark);
                static_slots[slot_id.as_index()] = Some(active);
            } else {
                static_slots[slot_id.as_index()] = None;
            }
        }

        let flex_attrs = &project_data.data.slots.flex_slots;
        let flex_marks = &markers_data.data.flex_slots;

        let mut flex_slots = slots.clone();

        for slot_id in PlaybackSlotId::iter_all() {
            if let Some(attr) = flex_attrs.index(slot_id.as_index()) {
                let mark = *flex_marks.index(slot_id.as_index());
                let active = ActiveSlot::new(attr.clone(), mark);
                flex_slots[slot_id.as_index()] = Some(active);
            } else {
                flex_slots[slot_id.as_index()] = None;
            }
        }

        Ok(Self {
            static_slots,
            flex_slots,
        })
    }

    /// Write [`SlotAttributes`] data from the [`Slots`] type into to a [`ProjDataFile`].
    /// Essentially "loads" the [`SlotAttributes`] back into the right type so it can be written
    /// to the appropriate files on the filesystem.
    /// ```
    /// # use ot_tools_io::types::{SlotType, SlotAttributes, SlotMarkers, ActiveSlot};
    /// # use ot_tools_interfaces::projects::{ProjDataFile, Slots};
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// # let mut proj = ProjDataFile::default();
    /// # let mut slots = Slots::default();
    /// let attrs = SlotAttributes::new_no_opts(SlotType::Static, 1, None)?;
    /// let new_slot = ActiveSlot::new(attrs, SlotMarkers::default());
    ///
    /// slots.static_slots_mut()[0] = Some(new_slot);
    /// slots.write_to_project(&mut proj);
    ///
    /// assert!(proj.data_ref().slots.static_slots[0].is_some());
    /// assert_eq!(proj.data_ref().slots.static_slots.iter().flatten().count(), 1);
    ///
    /// let loaded_slot = proj.data_ref().slots.static_slots[0].clone().unwrap();
    /// assert_eq!(loaded_slot.slot_type, SlotType::Static);
    /// assert_eq!(loaded_slot.slot_id, 1);
    /// # Ok(()) }
    /// ```
    pub fn write_to_project(&self, project_data: &mut ProjDataFile) {
        write_to_project(&SlotType::Static, self.static_slots_ref(), project_data);
        write_to_project(&SlotType::Flex, self.flex_slots_ref(), project_data);
    }

    /// Write [`SlotMarkers`] data from the [`Slots`] type into to a [`MarkersDataFile`].
    /// Essentially "loads" the [`SlotMarkers`] back into the right type so it can be written
    /// to the appropriate files on the filesystem.
    /// ```
    /// # use ot_tools_io::types::{SlotType, SlotAttributes, SlotMarkers, ActiveSlot};
    /// # use ot_tools_interfaces::projects::{MarkersDataFile, Slots};
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// # let mut markers = MarkersDataFile::default();
    /// # let mut slots = Slots::default();
    /// let attrs = SlotAttributes::new_no_opts(SlotType::Static, 1, None)?;
    /// let marks = SlotMarkers::new(10, 20, None, None)?;
    /// let new_slot = ActiveSlot::new(attrs, marks);
    ///
    /// slots.static_slots_mut()[0] = Some(new_slot);
    /// slots.write_to_markers(&mut markers);
    ///
    /// assert_eq!(markers.data_ref().static_slots[0].trim_offset, 10);
    /// assert_eq!(markers.data_ref().static_slots[0].trim_end, 20);
    /// # Ok(()) }
    /// ```
    pub fn write_to_markers(&self, markers_data: &mut MarkersDataFile) {
        write_to_markers(&SlotType::Static, self.static_slots_ref(), markers_data);
        write_to_markers(&SlotType::Flex, self.flex_slots_ref(), markers_data);
    }
}

// original kept for moment
// pub(crate) fn load(
//     project_data: &ProjDataFile,
//     markers_data: &MarkersDataFile,
//     slot_type: &SlotType,
// ) -> R<Self> {
//     let mut slots = Vec::with_capacity(PlaybackSlotId::MAX_ID);
//
//     let (attrs, marks) = match slot_type {
//         SlotType::Static => (
//             project_data.data.slots.static_slots.iter().collect_vec(),
//             markers_data.data.static_slots.iter().collect_vec(),
//         ),
//         SlotType::Flex => (
//             project_data.data.slots.flex_slots.iter().collect_vec(),
//             markers_data.data.flex_slots.iter().collect_vec(),
//         ),
//     };
//
//     for slot_id in PlaybackSlotId::iter_all() {
//         if let Some(attr) = attrs.index(slot_id.as_index()) {
//             let mark = *marks.index(slot_id.as_index());
//             let active = ActiveSlot::new(attr.clone(), *mark);
//             slots.push(Some(active));
//         } else {
//             slots.push(None);
//         }
//     }
//
//     Ok(Self {
//         slot_type: *slot_type,
//         slots,
//     })
// }

// ******* MASSIVE ******* HACK AROUND THE BORROW CHECKER
//
// WARN: This needs to be written to project and markers data files before
// they write out to files!
fn write_to_project(
    slot_type: &SlotType,
    slots: &PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>>,
    project_data: &mut ProjDataFile,
) {
    let attrs_mut = project_data.data_mut();

    // first, wipe all existing slot data
    for slot_id in PlaybackSlotId::iter_all() {
        match slot_type {
            SlotType::Static => attrs_mut.slots.static_slots[slot_id.as_index()] = None,
            SlotType::Flex => attrs_mut.slots.flex_slots[slot_id.as_index()] = None,
        };
    }

    // then write our data
    for active in slots.iter().flatten() {
        let slot_id_u8 = active.attrs_ref().slot_id;

        match slot_type {
            SlotType::Static => {
                attrs_mut.slots.static_slots[(slot_id_u8 - 1) as usize] =
                    Some(active.attrs_ref().clone())
            }
            SlotType::Flex => {
                attrs_mut.slots.flex_slots[(slot_id_u8 - 1) as usize] =
                    Some(active.attrs_ref().clone())
            }
        };
    }
}

// ******* MASSIVE ******* HACK AROUND THE BORROW CHECKER
//
// WARN: This needs to be written to project and markers data files before
// they write out to files!
fn write_to_markers(
    slot_type: &SlotType,
    slots: &PlaybackSlots<Option<ActiveSlot<SlotAttributes, SlotMarkers>>>,
    markers_data: &mut MarkersDataFile,
) {
    let marks_mut = markers_data.data_mut();

    // first, wipe all existing slot data
    for slot_id in PlaybackSlotId::iter_all() {
        match slot_type {
            SlotType::Static => marks_mut.static_slots[slot_id.as_index()] = SlotMarkers::default(),
            SlotType::Flex => marks_mut.flex_slots[slot_id.as_index()] = SlotMarkers::default(),
        };
    }

    // then write our data
    for active in slots.iter().flatten() {
        let slot_id_u8 = active.attrs_ref().slot_id;

        match slot_type {
            SlotType::Static => {
                marks_mut.static_slots[(slot_id_u8 - 1) as usize] = *active.marks_ref()
            }
            SlotType::Flex => marks_mut.flex_slots[(slot_id_u8 - 1) as usize] = *active.marks_ref(),
        };
    }
}
