// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright © 2024 Mike Robeson [dijksterhuis]
//! An interfaces for loading and interacting with an Octatrack project (whole
//! directory -- not just the `project.*` file!
//! ```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 = std::env::temp_dir()
//!     .join("ot-tools-interfaces")
//!     .join("docs")
//!     .join("projects")
//!     .join("dir")
//!     .join("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(()) }
//! ```

use std::path::{Path, PathBuf};

use crate::projects::project_state::OctatrackProjectState;

use ot_tools_io::identifiers::SavedState;

use crate::{OtToolsInterfacesError, R};

/// Interface for loading and modifying all data files for a specific Octatrack
/// Project directory.
///
/// ```rust
/// use std::path::PathBuf;
/// use ot_tools_io::{HasChecksumField, IsDefault};
/// use ot_tools_interfaces::projects::OctatrackProject;
/// use ot_tools_io::identifiers::{SavedState, BankId};
/// # use ot_tools_interfaces::OtToolsInterfacesError;
/// # fn main() -> Result<(), OtToolsInterfacesError> {
/// # use std::fs::{create_dir_all, remove_dir_all};
/// #
/// # let project_path = std::env::temp_dir()
/// #     .join("ot-tools-interfaces")
/// #     .join("docs")
/// #     .join("interface")
/// #     .join("project");
/// #
/// # if project_path.exists() { remove_dir_all(&project_path)? };
/// # if !project_path.parent().unwrap().exists() { create_dir_all(project_path.parent().unwrap())? };
/// # copy_dir::copy_dir(
/// #     PathBuf::from("test-data").join("blank-project"),
/// #     &project_path,
/// # )?;
///
/// // -----------------------------------------------------------------------
/// // read only inspection
/// // -----------------------------------------------------------------------
///
/// let project_readonly = OctatrackProject::load(&project_path)?;
/// project_readonly.working_ref().validate()?;
///
/// assert!(
///     project_readonly
///         .working_ref()
///         .banks_ref()
///         .id_ref(&BankId::A)
///         .is_default()
/// );
///
/// println!(
///     "bank header: {:?}",
///     project_readonly
///         .working_ref()
///         .banks_ref()
///         .id_ref(&BankId::A)
///         .header
/// );
///
/// // -----------------------------------------------------------------------
/// // read and write
/// // -----------------------------------------------------------------------
///
/// let mut project_readwrite = OctatrackProject::load(&project_path).unwrap();
/// project_readwrite.working_ref().validate()?;
///
/// // change the first bank's checksum to an incorrect value
/// let bank_data_mut = project_readwrite.working_mut().banks_mut().id_mut(&BankId::A);
/// bank_data_mut.checksum = 0;
///
/// // should not pass validation
/// assert!(project_readwrite.working_mut().validate().is_err());
///
/// // but we can still write the data
/// // -- checksums are updated on low-level file writes
/// // -- note: file headers/patch versions etc. are NOT updated on low-level file writes!
/// project_readwrite.save(false)?;
///
/// // change another value in the first bank, calculate and reset the checksum value
/// let bank_data_mut = project_readwrite.working_mut().banks_mut().id_mut(&BankId::A);
/// bank_data_mut.parts_edited_bitmask = 15;
/// let new_checksum = bank_data_mut.calculate_checksum()?;
/// bank_data_mut.checksum = new_checksum;
///
/// // should pass validation
/// assert!(project_readwrite.working_mut().validate().is_ok());
///
/// // write the data
/// project_readwrite.save(false)?;
/// # Ok(())}
/// ```
#[derive(Clone, PartialEq, Debug, Hash, PartialOrd, Ord, Eq)]
pub struct OctatrackProject {
    working: OctatrackProjectState,
    stored: Option<OctatrackProjectState>,
    dirpath: PathBuf,
}

impl OctatrackProject {
    /// Owned copy of the directory path of the project
    pub fn dirpathbuf(&self) -> PathBuf {
        self.dirpath.to_path_buf()
    }

    /// Reference to the directory path of the directory
    pub fn dirpath(&self) -> &Path {
        &self.dirpath
    }

    /// Name of the project (dir name)
    ///
    /// # panics!
    ///
    /// Will panic on empty directory name! (which shouldn't happen?)
    pub fn name(&self) -> String {
        self.dirpath()
            .file_name()
            .unwrap()
            .to_string_lossy()
            .to_string()
    }

    /// Path of the directory of the set the project is a member of.
    ///
    /// DOES NOT "set" the dirpath of the project.
    /// Use [`Self::to_new`] if you want to clone this into a new instance with
    /// a different project name / location.
    pub fn set_dirpath(&self) -> PathBuf {
        self.dirpath().join("..")
    }

    /// Name of the set the project is a member of.
    ///
    /// DOES NOT "set" the name of the project.
    /// Use [`Self::to_new`] if you want to clone this into a new instance with
    /// a different project name / location.
    ///
    /// # panics!
    ///
    /// Will panic on empty set directory name! (which shouldn't happen?)
    pub fn set_name(&self) -> String {
        self.set_dirpath()
            .file_name()
            .unwrap()
            .to_string_lossy()
            .to_string()
    }

    /// Path to the AUDIO pool directory for the set the project is a member of.
    pub fn audio_pool_dirpath(self) -> PathBuf {
        self.set_dirpath().join("AUDIO")
    }

    /// Owned access to the [`SavedState::Working`] data of the project.
    pub fn working(self) -> OctatrackProjectState {
        self.working
    }

    /// Reference access to the [`SavedState::Working`] data of the project.
    pub fn working_ref(&self) -> &OctatrackProjectState {
        &self.working
    }

    /// Mutable reference access to the [`SavedState::Working`] data of the project.
    pub fn working_mut(&mut self) -> &mut OctatrackProjectState {
        &mut self.working
    }

    /// Owned access to the [`SavedState::Stored`] data of the project.
    pub fn stored(self) -> Option<OctatrackProjectState> {
        self.stored
    }

    /// Reference access to the [`SavedState::Stored`] data of the project.
    pub fn stored_ref(&self) -> Option<&OctatrackProjectState> {
        if let Some(s) = &self.stored {
            Some(s)
        } else {
            None
        }
    }

    /// Mutable reference access to the [`SavedState::Stored`] data of the project.
    pub fn stored_mut(&mut self) -> Option<&mut OctatrackProjectState> {
        if let Some(s) = self.stored.as_mut() {
            Some(s)
        } else {
            None
        }
    }

    /// Load the project from the specified directory.
    /// ```
    /// # use ot_tools_interfaces::projects::OctatrackProject;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// OctatrackProject::load("test-data/blank-project")?;
    /// # Ok(()) }
    /// ```
    pub fn load<P: AsRef<Path>>(dirpath: P) -> R<Self> {
        let working = OctatrackProjectState::load(dirpath.as_ref(), &SavedState::Working)?;
        let stored = OctatrackProjectState::load(dirpath.as_ref(), &SavedState::Stored).ok();

        Ok(Self {
            working,
            stored,
            dirpath: dirpath.as_ref().to_path_buf(),
        })
    }

    /// Write the current state to the relevant files.
    ///
    /// The `overwrite_unchanged` flag modifies the save behaviour:
    /// * `false`: only write data files when there have been changes
    /// * `true`: write all data file even if no changes made
    ///
    /// You will usually want to provide `false`.
    /// (There is currently a known bug with serialization / deserialization of
    /// arrangements data in the underlying `ot-tools-io` library crate which
    /// will break your arrangements if you currently set this to `true`).
    ///
    /// Warning: Checksum updates are applied to files when writing. To gain
    /// access to the updated checksum values you will need to read the project
    /// data from the filesystem again.
    ///
    /// ```
    /// # use ot_tools_interfaces::projects::OctatrackProject;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let read = OctatrackProject::load("test-data/blank-project")?;
    ///
    /// let new_dirpath = std::env::temp_dir()
    ///     .join("ot-tools-interfaces")
    ///     .join("docs")
    ///     .join("projects")
    ///     .join("project_dir")
    ///     .join("save");
    /// # std::fs::create_dir_all(&new_dirpath)?;
    ///
    /// let mut new = read.to_new(&new_dirpath)?;
    /// new.save(true)?;
    /// # Ok(()) }
    /// ```
    pub fn save(&mut self, overwrite_unchanged: bool) -> R<()> {
        // save working state first, prioritize protecting stored changes.
        self.working.save(overwrite_unchanged)?;
        if let Some(s) = self.stored.as_mut() {
            s.save(overwrite_unchanged)?;
        }
        Ok(())
    }

    /// Inspect and validate that both project states and their files are loaded
    /// correctly and can be used by the library
    /// ```
    /// # use ot_tools_interfaces::projects::OctatrackProject;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let read = OctatrackProject::load("test-data/blank-project")?;
    /// read.validate()?;
    /// # Ok(()) }
    /// ```
    pub fn validate(&self) -> R<()> {
        self.working_ref().validate()?;
        if let Some(x) = self.stored_ref() {
            x.validate()?;
        }
        Ok(())
    }

    /// Create a new instance, mostly with default data, which will write files
    /// to the specified directory path on calling [`Self::save`].
    ///
    /// Note that [`SavedState::Stored`] data is not created by default, as per
    /// creating a new project on the Octatrack.
    pub fn new<P: AsRef<Path>>(dirpath: P) -> R<Self> {
        if !dirpath.as_ref().exists() {
            std::fs::create_dir_all(dirpath.as_ref()).map_err(OtToolsInterfacesError::from)?;
        }
        let working = OctatrackProjectState::new(dirpath.as_ref(), None)?;
        Ok(Self {
            working,
            stored: None,
            dirpath: dirpath.as_ref().to_path_buf(),
        })
    }

    /// Create a new project in memory (to eventually write as a new project on
    /// filesystem).
    ///
    /// This basically copies the project states into a new instance, allowing
    /// you to copy the project to a different location.
    ///
    /// # Write a new copy
    /// ```
    /// # use ot_tools_interfaces::projects::OctatrackProject;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let read = OctatrackProject::load("test-data/blank-project")?;
    /// // doesn't write, would need to call save() to do that
    /// let new = read.to_new("some/new/path")?;
    /// assert_eq!(
    ///     new.working_ref().project_ref().source(),
    ///     &std::path::PathBuf::from("some/new/path/project.work"),
    /// );
    /// # Ok(()) }
    /// ```
    pub fn to_new<P: AsRef<Path>>(&self, new_dirpath: P) -> R<Self> {
        let working = self.working.to_new(&new_dirpath, None)?;
        let stored = if let Some(x) = &self.stored {
            Some(x.to_new(&new_dirpath, None)?)
        } else {
            None
        };

        Ok(Self {
            working,
            stored,
            dirpath: new_dirpath.as_ref().to_path_buf(),
        })
    }
}
