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

use super::Files;
use std::fs::remove_file;
use std::path::Path;
use std::path::PathBuf;

use ot_tools_io::samples::SampleSettingsFile;
use ot_tools_io::OctatrackFileIO;
use ot_tools_io_derive::{AsMutDerive, AsRefDerive};

use crate::audio::AudioFile;
use crate::sample_files::{ReadFile, WriteFile};
use crate::{sample_files, R};

/// A generic type to eagerly read some type of file.
/// The inner type must implement the [`ReadFile`] and [`WriteFile`] traits.
///
/// Data is read eagerly, i.e. when the [`Self::load`] method is called.
/// Write your changes to a file with the [`Self::commit`] method.
///
/// For lazy reading, refer to the [`crate::sample_files::LazyFile`]
/// type and other and related "Lazy" types.
///
/// # Example: Write new file
/// ```
/// # use ot_tools_interfaces::sample_files::EagerAudioFile;
/// # use ot_tools_interfaces::OtToolsInterfacesError;
/// # use std::path::PathBuf;
/// # fn main() -> Result<(), OtToolsInterfacesError> {
/// let audio_file = EagerAudioFile::load("./test-data/sample-files/sample.wav")?;
///
/// let new_dirpath = std::env::temp_dir()
///     .join("ot-tools-interfaces")
///     .join("sample-files")
///     .join("eager")
///     .join("eager_file")
///     .join("commit-new");
///
/// let new = audio_file.to_new(&new_dirpath.join("sample.wav"));
/// std::fs::create_dir_all(&new_dirpath)?;
/// new.commit()?;
///
/// assert!(new_dirpath.join("sample.wav").exists());
/// # Ok(())}
/// ```
///
/// # Example: Skip writing over same data
/// ```
/// use std::fs::metadata;
///
/// # use ot_tools_interfaces::sample_files::EagerAudioFile;
/// # use ot_tools_interfaces::OtToolsInterfacesError;
/// # fn main() -> Result<(), OtToolsInterfacesError> {
/// let original = metadata("test-data/sample-files/sample.wav")?
///     .modified()
///     .unwrap();
///
/// let audio = EagerAudioFile::load("test-data/sample-files/sample.wav")?;
/// audio.commit()?;
///
/// // file has not changed
/// assert_eq!(
///    metadata("test-data/sample-files/sample.wav")?
///        .modified()
///        .unwrap(),
///    original,
/// );
/// # Ok(()) }
/// ```
///
/// # Example: Write over existing file with changed data
/// ```
/// use std::fs::metadata;
///
/// # use ot_tools_interfaces::sample_files::EagerAudioFile;
/// # use ot_tools_interfaces::OtToolsInterfacesError;
/// # fn main() -> Result<(), OtToolsInterfacesError> {
/// let dirpath = std::env::temp_dir()
///     .join("ot-tools-interfaces")
///     .join("sample-files")
///     .join("eager")
///     .join("eager_file")
///     .join("commit-writeover");
///
/// # if dirpath.exists() { std::fs::remove_dir_all(&dirpath)?; };
/// # std::fs::create_dir_all(&dirpath.parent().unwrap())?;
/// # copy_dir::copy_dir("test-data/sample-files", &dirpath)?;
/// let original = metadata(&dirpath.join("sample.wav"))?
///     .modified()
///     .unwrap();
///
/// // load, modify, write
/// let mut x = EagerAudioFile::load(&dirpath.join("sample.wav"))?;
/// x.data_mut().linear_fade_in(0.2);
/// x.commit()?;
///
/// // file has changed
/// assert_ne!(
///     metadata(&dirpath.join("sample.wav"))?
///         .modified()
///         .unwrap(),
///     original,
/// );
///
/// # Ok(()) }
/// ```
///
/// # Example: Deleting a file
/// ```
/// # use ot_tools_interfaces::sample_files::EagerAudioFile;
/// # use ot_tools_interfaces::OtToolsInterfacesError;
/// # fn main() -> Result<(), OtToolsInterfacesError> {
/// let dirpath = std::env::temp_dir()
///     .join("ot-tools-interfaces")
///     .join("sample-files")
///     .join("eager")
///     .join("eager_file")
///     .join("commit-delete");
///
/// # if dirpath.exists() { std::fs::remove_dir_all(&dirpath)?; };
/// # std::fs::create_dir_all(&dirpath.parent().unwrap())?;
/// # copy_dir::copy_dir("test-data/sample-files", &dirpath)?;
/// let mut x = EagerAudioFile::load(dirpath.join("sample.wav"))?;
/// x.set_delete_marker(true);
/// x.commit()?;
///
/// // file was deleted
/// assert!(!dirpath.join("sample.wav").exists());
///
/// // calling `commit()` **does not** reset the delete marker
/// assert!(x.delete_marker());
///
/// // the file doesn't exist anymore, calling `commit` again does nothing
/// // todo: should this be an error?
/// assert!(x.commit().is_ok());
/// # Ok(()) }
/// ```
#[derive(Clone, PartialEq, Debug, PartialOrd)]
pub struct EagerFile<T: Clone + ReadFile<T> + WriteFile<T>> {
    data: T,
    source: PathBuf,
    delete_marker: bool,
}

impl<T: Clone + ReadFile<T> + WriteFile<T> + PartialEq> EagerFile<T> {
    pub fn new<P: AsRef<Path>>(path: P, data: T) -> Self {
        Self {
            source: path.as_ref().to_path_buf(),
            data,
            delete_marker: false,
        }
    }

    /// Eagerly read file data from the given path.
    /// Calls the relevant [`ReadFile<T>::read_file`] trait implementation under the hood.
    ///
    /// - [`EagerAudioFile::load`] calls [`AudioFile::read_by_ext`].
    /// - [`EagerSampleSettingsFile::load`] calls [`SampleSettingsFile::from_data_file`]
    /// ```
    /// # use ot_tools_interfaces::sample_files::EagerAudioFile;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let audio = EagerAudioFile::load("test-data/sample-files/sample.wav")?;
    /// assert_eq!(audio.data_ref().len, 55897);
    /// assert_eq!(audio.data_ref().spec.channels, 2);
    /// assert_eq!(audio.pathbuf(), PathBuf::from("test-data/sample-files/sample.wav"));
    /// # Ok(()) }
    /// ```
    pub fn load<P: AsRef<Path>>(path: P) -> R<Self> {
        Ok(Self {
            source: path.as_ref().to_path_buf(),
            data: T::read_file(path.as_ref())?,
            delete_marker: false,
        })
    }

    /// Owned access to the registered path of the source file
    pub fn pathbuf(&self) -> PathBuf {
        self.source.to_path_buf()
    }

    /// Reference access to the registered path of the source file
    pub fn path(&self) -> &Path {
        &self.source
    }

    /// Owned access to the eagerly read data
    pub fn data(self) -> T {
        self.data
    }

    /// Reference access to the eagerly read data
    pub fn data_ref(&self) -> &T {
        &self.data
    }

    /// Mutable reference access to the eagerly read data
    pub fn data_mut(&mut self) -> &mut T {
        &mut self.data
    }

    /// Setter for delete marker.
    /// If this is set to `true`, the file will be deleted from the filesystem
    /// when the [`Self::commit`] method is called.
    ///
    /// See the relevant example in [`Self::commit`].
    pub fn set_delete_marker(&mut self, delete_marker: bool) {
        self.delete_marker = delete_marker;
    }

    /// Getter for current delete marker state.
    pub fn delete_marker(&self) -> bool {
        self.delete_marker
    }

    /// Creates a new copy targeted at a different path on the filesystem.
    /// Use this to create copies of a file.
    ///
    /// See the relevant example in [`Self::commit`].
    pub fn to_new<P: AsRef<Path>>(&self, path: P) -> Self {
        let mut new = self.clone();
        new.source = path.as_ref().to_path_buf();
        new
    }

    /// Makes changes to the file at the currently registered path.
    ///
    /// Calling this method will
    /// - delete the file from the filesystem if the file exists and the delete marker is set to `true`.
    /// - write a new file if the file does not currently exist.
    /// - write over the existing file if the current stored data differs to the data on the filesystem
    pub fn commit(&self) -> R<()> {
        // delete file if marked for deleting
        if self.delete_marker {
            if self.path().exists() {
                remove_file(self.path())?;
            }
        } else {
            // new file, write it
            if !self.path().exists() {
                T::write_file(self.data.clone(), self.path())?;
            } else {
                // only write if file differs to existing
                let existing = Self::load(self.path())?;
                if existing != *self {
                    T::write_file(self.data.clone(), self.path())?;
                };
            }
        }

        Ok(())
    }
}

impl ReadFile<AudioFile> for EagerSampleFilePair {
    fn read_file<P: AsRef<Path>>(path: P) -> R<AudioFile> {
        AudioFile::read_by_ext(path.as_ref())
    }
}

impl ReadFile<SampleSettingsFile> for EagerSampleFilePair {
    fn read_file<P: AsRef<Path>>(path: P) -> R<SampleSettingsFile> {
        Ok(SampleSettingsFile::from_data_file(path.as_ref())?)
    }
}

impl WriteFile<AudioFile> for EagerSampleFilePair {
    fn write_file<P: AsRef<Path>>(data: AudioFile, path: P) -> R<()> {
        data.write_by_ext(path.as_ref())?;
        Ok(())
    }
}

impl WriteFile<SampleSettingsFile> for EagerSampleFilePair {
    fn write_file<P: AsRef<Path>>(data: SampleSettingsFile, path: P) -> R<()> {
        data.to_data_file(path.as_ref())?;
        Ok(())
    }
}

/// Eagerly read a pair of audio and sample settings files as one entitity.
/// No fancy tricks. Nice 'n' simple.
///
/// Data is read eagerly, i.e. when the [`Self::load`] method is called.
/// Write your changes to a file with the [`Self::commit`] method.
///
/// For lazy reading, refer to the [`crate::sample_files::LazySampleFilePair`]
/// type and other and related "Lazy" types.
///
/// # Danger
/// There is no validation to stop you from deleting the audio file and not
/// deleting the settings file.
/// It is your responsibility to ensure you delete both.
#[derive(Clone, PartialEq, Debug, PartialOrd, AsMutDerive, AsRefDerive)]
pub struct EagerSampleFilePair {
    audio_file: EagerFile<AudioFile>,
    settings_file: Option<EagerFile<SampleSettingsFile>>,
}

impl EagerSampleFilePair {
    /// Create a new instance, given all the required data.
    /// No validation is performed by this method.
    pub fn new(
        audio_file: EagerFile<AudioFile>,
        settings_file: Option<EagerFile<SampleSettingsFile>>,
    ) -> Self {
        Self {
            audio_file,
            settings_file,
        }
    }

    fn audio_path_to_ot_path(path: &Path) -> PathBuf {
        let mut ot_path = path.to_path_buf();
        ot_path.set_extension("ot");
        ot_path
    }

    /// Owned access to the audio file data
    pub fn audio_file(self) -> EagerFile<AudioFile> {
        self.audio_file
    }

    /// Reference access to the audio file data
    pub fn audio_file_ref(&self) -> &EagerFile<AudioFile> {
        &self.audio_file
    }

    /// Mutable reference access to the audio file data
    pub fn audio_file_mut(&mut self) -> &mut EagerFile<AudioFile> {
        &mut self.audio_file
    }

    /// Owned access to the settings file data
    pub fn settings_file(self) -> Option<EagerFile<SampleSettingsFile>> {
        self.settings_file
    }

    /// Reference access to the settings file data
    pub fn settings_file_ref(&self) -> Option<&EagerFile<SampleSettingsFile>> {
        self.settings_file.as_ref()
    }

    /// Mutable reference access to the settings file data
    pub fn settings_file_mut(&mut self) -> Option<&mut EagerFile<SampleSettingsFile>> {
        self.settings_file.as_mut()
    }

    /// Read a sample file pair from the filesystem.
    ///
    /// A missing [`SampleSettingsFile`] (`*.ot` file) is fine,
    /// as it is not required for the Octatrack to load a sample into a slot.
    /// Although an audio file *must* exist at the location, for obvious reasons.
    /// ```
    /// # use ot_tools_interfaces::sample_files::EagerSampleFilePair;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let sample_file = EagerSampleFilePair::load("./test-data/sample-files/sample.wav")?;
    /// assert_eq!(sample_file.audio_file_ref().data_ref().len, 55897);
    /// assert_eq!(sample_file.settings_file_ref().unwrap().data_ref().slices_len, 1);
    /// # Ok(())}
    /// ```
    pub fn load<P: AsRef<Path>>(audio_file_path: P) -> R<Self> {
        let ot_path = Self::audio_path_to_ot_path(audio_file_path.as_ref());
        let s_f = if ot_path.exists() {
            EagerFile::<SampleSettingsFile>::load(ot_path).ok()
        } else {
            None
        };
        let a_f = EagerFile::<AudioFile>::load(audio_file_path.as_ref())?;
        Ok(Self::new(a_f, s_f))
    }

    /// Essentially `clone`, but changes the path for the sample file pair.
    /// Use this to create a new copy of a sample file pair.
    /// ```
    /// # use ot_tools_interfaces::sample_files::EagerSampleFilePair;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// let sample_file = EagerSampleFilePair::load("./test-data/sample-files/sample.wav")?;
    ///
    /// let new_path = "/tmp/ot-tools-interfaces/sample-files/eager/to_new/sample.wav";
    /// let new = sample_file.to_new(&new_path);
    /// # std::fs::create_dir_all(PathBuf::from(&new_path).parent().unwrap())?;
    /// new.commit()?;
    ///
    /// assert!(PathBuf::from(new_path).exists());
    /// assert!(PathBuf::from(new_path).with_extension("ot").exists());
    /// # Ok(())}
    /// ```
    pub fn to_new<P: AsRef<Path>>(&self, path: P) -> Self {
        let mut new = self.clone();

        if let Some(s_f) = new.settings_file.as_mut() {
            s_f.source = Self::audio_path_to_ot_path(path.as_ref());
        }

        new.audio_file.source = path.as_ref().to_path_buf();

        new
    }

    /// Setter for delete marker on **both** files in the file pair.
    /// Calls the [`EagerFile::set_delete_marker`] method on both files.
    ///
    /// This means you can delete the files by calling [`Self::commit`] or by
    /// calling the [`EagerFile::commit`] method for each file.
    ///
    /// Note that we don't provide a getter/accessor for `delete_marker` on this
    /// type -- we're manipulating the inner types with this method.
    pub fn set_delete_marker(&mut self, delete_marker: bool) {
        self.audio_file_mut().set_delete_marker(delete_marker);
        if let Some(x) = self.settings_file_mut() {
            x.set_delete_marker(delete_marker)
        };
    }

    /// Will write the data to the file(s) at the paths registered in the
    /// internal fields of the inner types.
    ///
    /// ```
    /// # use ot_tools_interfaces::sample_files::EagerSampleFilePair;
    /// # use ot_tools_interfaces::OtToolsInterfacesError;
    /// # use std::path::PathBuf;
    /// # fn main() -> Result<(), OtToolsInterfacesError> {
    /// # std::fs::create_dir_all(PathBuf::from("/tmp/ot-tools-interfaces/sample-files/eager/commit"))?;
    /// let sample_file = EagerSampleFilePair::load("./test-data/sample-files/sample.wav")?;
    ///
    /// // first copy
    /// let new_1 = sample_file.to_new("/tmp/ot-tools-interfaces/sample-files/eager/commit/1.wav");
    /// new_1.commit()?;
    ///
    /// assert!(new_1.audio_file_ref().path().exists());
    /// assert!(new_1.settings_file_ref().unwrap().path().exists());
    ///
    /// // second copy
    /// let mut new_2 = sample_file.to_new("/tmp/ot-tools-interfaces/sample-files/eager/commit/2.wav");
    /// new_2.commit()?;
    ///
    /// assert!(new_2.audio_file_ref().path().exists());
    /// assert!(new_2.settings_file_ref().unwrap().path().exists());
    ///
    /// // deleting second copy files
    /// new_2.set_delete_marker(true);
    /// new_2.commit()?;
    ///
    /// assert!(!new_2.audio_file_ref().path().exists());
    /// assert!(!new_2.settings_file_ref().unwrap().path().exists());
    ///
    /// // third copy
    /// let mut new_3 = sample_file.to_new("/tmp/ot-tools-interfaces/sample-files/eager/commit/3.wav");
    /// new_3.commit()?;
    ///
    /// assert!(new_3.audio_file_ref().path().exists());
    /// assert!(new_3.settings_file_ref().unwrap().path().exists());
    ///
    /// // deleting third copy settings_file only
    /// new_3.settings_file_mut().unwrap().set_delete_marker(true);
    /// new_3.commit()?;
    ///
    /// assert!(new_3.audio_file_ref().path().exists());
    /// assert!(!new_3.settings_file_ref().unwrap().path().exists());
    /// # Ok(())}
    /// ```
    pub fn commit(&self) -> R<()> {
        self.audio_file.commit()?;
        if let Some(s_f) = self.settings_file.as_ref() {
            s_f.commit()?;
        };

        Ok(())
    }
}

/// Collection of [`EagerSampleFilePair`] instances aliased via the [`Files<T>`] type
pub type EagerSampleFilePairs = Files<EagerSampleFilePair>;

impl EagerSampleFilePairs {
    pub fn load<P: AsRef<Path>>(dirpath: P) -> R<Self> {
        let files = sample_files::scan_dir_path_for_audio_files(dirpath.as_ref())?;

        let mut new = Self::new();
        for file in files {
            new.push(EagerSampleFilePair::load(file)?);
        }

        Ok(new)
    }

    /// Writes all files with cached changes to the filesystem
    pub fn commit(&mut self) -> R<()> {
        for f in self.0.iter_mut() {
            f.commit()?;
        }
        Ok(())
    }
}

pub type EagerAudioFile = EagerFile<AudioFile>;

impl ReadFile<AudioFile> for EagerAudioFile {
    fn read_file<P: AsRef<Path>>(path: P) -> R<AudioFile> {
        AudioFile::read_by_ext(path.as_ref())
    }
}

impl WriteFile<AudioFile> for EagerAudioFile {
    fn write_file<P: AsRef<Path>>(data: AudioFile, path: P) -> R<()> {
        data.write_by_ext(path.as_ref())?;
        Ok(())
    }
}

/// Collection of [`EagerAudioFile`] instances aliased via the [`Files<T>`] type
pub type EagerAudioFiles = Files<EagerAudioFile>;

impl Files<EagerAudioFile> {
    pub fn load<P: AsRef<Path>>(dirpath: P) -> R<Self> {
        let files = sample_files::scan_dir_path_for_audio_files(dirpath.as_ref())?;

        let mut new = Self::new();
        for file in files {
            new.push(EagerAudioFile::load(file)?);
        }

        Ok(new)
    }

    pub fn commit(&mut self) -> R<()> {
        for f in self.0.iter() {
            f.commit()?;
        }
        Ok(())
    }
}

pub type EagerSampleSettingsFile = EagerFile<SampleSettingsFile>;

impl ReadFile<SampleSettingsFile> for EagerSampleSettingsFile {
    fn read_file<P: AsRef<Path>>(path: P) -> R<SampleSettingsFile> {
        let x = SampleSettingsFile::from_data_file(path.as_ref())?;
        Ok(x)
    }
}

impl WriteFile<SampleSettingsFile> for EagerSampleSettingsFile {
    fn write_file<P: AsRef<Path>>(data: SampleSettingsFile, path: P) -> R<()> {
        data.to_data_file(path.as_ref())?;
        Ok(())
    }
}

/// Collection of [`EagerSampleSettingsFile`] instances aliased via the [`Files<T>`] type
pub type EagerSampleSettingsFiles = Files<EagerSampleSettingsFile>;

impl Files<EagerSampleSettingsFile> {
    pub fn load<P: AsRef<Path>>(dirpath: P) -> R<Self> {
        let files = sample_files::scan_dir_path_for_audio_files(dirpath.as_ref())?;
        let mut new = Self::new();
        for file in files.into_iter().map(|x| x.with_extension("ot")) {
            new.push(EagerSampleSettingsFile::load(file)?);
        }

        Ok(new)
    }

    pub fn commit(&mut self) -> R<()> {
        for f in self.0.iter() {
            f.commit()?;
        }
        Ok(())
    }
}
