// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright © 2024 Mike Robeson [dijksterhuis]
//! Provides two interfaces for loading and interacting with Octatrack projects,
//! either the whole directory via [`OctatrackProject`] or with a specific state
//! of project directory files via [`OctatrackProjectState`].
//!
//! # [`OctatrackProject`]
//! ```rust
//! # use ot_tools_interfaces::projects::OctatrackProject;
//! # use ot_tools_interfaces::OtToolsInterfacesError;
//! # fn main() -> Result<(), OtToolsInterfacesError> {
//! # use std::fs::{create_dir_all, remove_dir_all};
//! # let some_new_path = "/tmp/ot-tools-interfaces/docs/interfaces/demo";
//! let project = OctatrackProject::load("./test-data/blank-project")?;
//! project.validate()?;
//!
//! let mut cloned = project.to_new(some_new_path)?;
//! # if std::path::PathBuf::from(some_new_path).exists() { remove_dir_all(some_new_path)? };
//! create_dir_all(some_new_path)?;
//! cloned.save(true)?;
//! # Ok(()) }
//! ```
//! # [`OctatrackProjectState`]
//! ```rust
//! # use ot_tools_interfaces::projects::OctatrackProjectState;
//! use ot_tools_io::identifiers::SavedState;
//! # use ot_tools_interfaces::OtToolsInterfacesError;
//! # fn main() -> Result<(), OtToolsInterfacesError> {
//! # use std::fs::{create_dir_all, remove_dir_all};
//! # let some_new_path = "/tmp/ot-tools-interfaces/docs/interfaces/demo";
//!
//! let project_state = OctatrackProjectState::load("./test-data/blank-project", &SavedState::Working)?;
//! project_state.validate()?;
//!
//! let mut cloned_state = project_state.to_new(some_new_path, None)?;
//! # if std::path::PathBuf::from(some_new_path).exists() { remove_dir_all(some_new_path)? };
//! create_dir_all(some_new_path)?;
//! cloned_state.save(true)?;
//! # Ok(()) }
//! ```

mod arrangements;
mod banks;
mod errors;
mod markers;
mod project_dir;
mod project_state;
#[allow(clippy::module_inception)]
mod projects;
mod slots;

pub use errors::ProjectsErrors;

use std::array::from_fn;
use std::path::PathBuf;

use ot_tools_io::{Defaults, OctatrackFileIO};

#[doc(inline)]
pub use crate::projects::slots::Slots;
#[doc(inline)]
pub use arrangements::ArrDataFile;
#[doc(inline)]
pub use banks::BankDataFile;
#[doc(inline)]
pub use markers::MarkersDataFile;
#[doc(inline)]
pub use project_dir::OctatrackProject;
#[doc(inline)]
pub use project_state::OctatrackProjectState;
#[doc(inline)]
pub use projects::ProjDataFile;

use crate::{OtToolsInterfacesError, R};

/// Generic type for storing a data from an Octatrack binary data file, with a
/// reference to the file's source file path.
/// You shouldn't need to use this
/// -- the types for specific Octatrack binary data files build on top of this
/// as via aliased types to avoid having to import a trait all the time.
#[doc(hidden)]
#[derive(Default, Clone, PartialEq, Debug, Hash, PartialOrd, Ord, Eq)]
pub struct DataItem<T> {
    /// path to the data file
    source: PathBuf,
    /// Data from reading the data file
    data: T,
}

impl<T: Default, const N: usize> Defaults<[DataItem<T>; N]> for DataItem<T> {
    fn defaults() -> [DataItem<T>; N]
    where
        Self: Default,
    {
        from_fn(|_| Self::default())
    }
}

impl<T: Default, const N: usize> Defaults<Box<[DataItem<T>; N]>> for DataItem<T> {
    fn defaults() -> Box<[DataItem<T>; N]>
    where
        Self: Default,
    {
        Box::new(from_fn(|_| Self::default()))
    }
}

impl<T: OctatrackFileIO + PartialEq + Default> DataItem<T> {
    pub fn source(&self) -> &PathBuf {
        &self.source
    }

    pub fn data(self) -> T {
        self.data
    }

    pub fn data_ref(&self) -> &T {
        &self.data
    }

    pub fn data_mut(&mut self) -> &mut T {
        &mut self.data
    }

    pub fn save(&self) -> R<()> {
        self.data
            .to_data_file(&self.source)
            .map_err(OtToolsInterfacesError::from)
    }

    fn new(source: PathBuf, data: T) -> Self {
        DataItem::<T> { source, data }
    }

    fn read(source: PathBuf) -> R<DataItem<T>> {
        let path = source.to_path_buf();

        Ok(DataItem::<T> {
            source,
            data: T::from_data_file(&path)?,
        })
    }

    /// Write the file data only if
    /// * the file does not already exist
    /// * file does exist and data to write has changed
    pub fn write(&self) -> R<()> {
        let file_path = self.source.as_path();

        if file_path.exists() {
            let existing = T::from_data_file(file_path)?;
            // only write if file has changed
            if self.data != existing {
                self.data.to_data_file(file_path)?;
            }
        } else {
            // always write if file does not exist
            self.data.to_data_file(file_path)?;
        }
        Ok(())
    }

    /// Always write the file data, regardless of whether the data for the file
    /// has been modified or not.
    pub fn write_force(&self) -> R<()> {
        self.data.to_data_file(self.source.as_path())?;
        Ok(())
    }
}

// TODO: For handling validation errors
// #[derive(Default)]
// pub struct ErrorsContext(Option<Vec<Box<dyn std::error::Error>>>);
//
// impl ErrorsContext {
//     pub fn new() -> Self {
//         ErrorsContext(Some(Vec::new()))
//     }
//
//     pub fn push(&mut self, error: Box<dyn std::error::Error>) {
//         if let Some(vec) = &mut self.0 {
//             vec.push(error)
//         }
//     }
//
//     // originally sourced from the serde crate -- (MIT/Apache 2.0 license)
//     pub fn check(self) -> R<()> {
//         if let Some(errs) = self.0 {
//             let mut errors = errs.into_iter();
//
//             let mut combined = match errors.next() {
//                 Some(first) => first.to_string(),
//                 None => return Ok(()),
//             };
//             combined.push_str("\n");
//
//             for rest in errors {
//                 combined.push_str(rest.to_string().as_str());
//                 combined.push_str("\n");
//             }
//         }
//
//         Ok(())
//     }
// }
