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

//! Interfaces for interacting with sample file pairs.
//!
//! A sample file pair constitutes at least one audio file (compatible `.wav` or
//! `.aiff` file) and optionally a sample settings file (an `.ot` file).
//!
//! There are two main implementations which implement the same methods to
//! standardise usage
//! - [`EagerSampleFilePair`]: a simple 'eager' interface, where file contents
//!   are read immediately via [`EagerSampleFilePair::load`]
//! - [`LazySampleFilePair`]: a 'lazy' interface, where file contents are only
//!   read when data is first accessed (rather than when calling
//!   [`LazySampleFilePair::load`]).
//!   data can be uncached to reduce memory usage, which is useful when
//!   operating on an entire CF card of audio files!
//!
//! But, you can just use the plain [`AudioFile`] and [`SampleSettingsFile`] types if
//! you'd like.

mod eager;
mod errors;
mod lazy;

pub use errors::SampleFilesErrors;

pub use eager::{
    EagerAudioFile, EagerAudioFiles, EagerFile, EagerSampleFilePair, EagerSampleFilePairs,
    EagerSampleSettingsFile, EagerSampleSettingsFiles,
};
pub use lazy::{
    LazyAudioFile, LazyFile, LazySampleFilePair, LazySampleFilePairs, LazySampleSettingsFile,
};
use log::trace;
use ot_tools_io::{OctatrackFileIO, SampleSettingsFile};
use std::path::{Path, PathBuf};
use std::slice::{Iter, IterMut};
use std::vec::IntoIter;
use walkdir::{DirEntry, WalkDir};

use crate::audio::{direntry_is_compat_aiff, direntry_is_compat_wav, AudioFile};
use crate::R;

/// Trait used with [`EagerFile`] and [`LazyFile`] to enable them
/// to read some type of file, given a path.
pub trait ReadFile<T> {
    fn read_file<P: AsRef<Path>>(path: P) -> R<T>;
}

/// Trait used with [`EagerFile`] and [`LazyFile`] to enable them
/// to write some type of file, given a path.
pub trait WriteFile<T> {
    fn write_file<P: AsRef<Path>>(data: T, path: P) -> R<()>;
}

/// Trait used with [`Files<T>`] to enable reading a bunch of some type of files
/// from a given directory path.
pub trait ReadFiles<T> {
    fn load<P: AsRef<Path>>(dirpath: P) -> R<Files<T>>;
}

/// Trait used with [`Files<T>`] to enable writing files to the appropriate
/// locations on the filesystem.
pub trait CommitFiles<T> {
    fn commit(&mut self) -> R<()>;
}

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

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

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

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

/// The [`DirEntry`] is a directory. Kind of obvious.
fn direntry_is_dir(entry: &DirEntry) -> bool {
    entry.file_type().is_dir()
}

/// The [`DirEntry`] file name isn't prefixed with `.` indicating it's a hidden
/// file on POSIX compliant systems.
fn direntry_is_not_posix_hidden(entry: &DirEntry) -> bool {
    !entry.file_name().to_string_lossy().starts_with('.')
}

/// The [`DirEntry`] has a file extension of some description.
/// Pretty obvious tbh.
#[allow(dead_code)]
fn direntry_has_file_extension(entry: &DirEntry, v: &str) -> bool {
    entry
        .file_name()
        .to_str()
        .map(|s| s.ends_with(v))
        .unwrap_or(false)
}

/// Recursively search for compatible audio files within a given directory tree.
pub fn scan_dir_path_for_audio_files<P: AsRef<Path> + std::fmt::Debug>(
    dir_path: P,
) -> R<Vec<PathBuf>> {
    trace!("Recursively searching for Octatrack compatible audio files: dirpath={dir_path:?}",);

    let fpaths: Vec<PathBuf> = WalkDir::new(dir_path.as_ref())
        .sort_by_file_name()
        .min_depth(1)
        .into_iter()
        .filter_entry(|e| {
            // ignore hidden files on *nix
            direntry_is_not_posix_hidden(e) && {
                // need to ensure we recurse through dirs in the tree
                // need to filter them out below (only want audio files)
                direntry_is_dir(e) || direntry_is_compat_wav(e) || direntry_is_compat_aiff(e)
            }
        })
        .map(|x| x.unwrap())
        .filter(|x| !x.file_type().is_dir())
        .map(|x| x.path().to_path_buf())
        .collect();

    trace!(
        "Found Octatrack compatible audio files: dirpath={:1?} found={:2?}",
        dir_path,
        fpaths.len(),
    );

    Ok(fpaths)
}

/// Generic new type wrapper around a [`Vec<T>`] specifically for collections of
/// [`EagerFile<T>`] or [`LazyFile<T>`] types.
///
/// Has all stable [`Vec<T>`] traits implemented and deref-coercion to the inner
/// [`Vec<T>`].
///
/// I'd really like to have trait bounds which limit the usage of types which
/// implement the [`ReadFile<T>`] and [`WriteFile<T>`] traits ...
/// I'd want to use trait bounds in type aliases to achieve this,
/// but that language functionality is a WIP
/// [issue #112792](https://github.com/rust-lang/rust/issues/112792)
#[derive(Default, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct Files<T>(Vec<T>);

impl<T> Files<T> {
    pub fn new() -> Self {
        Self(vec![])
    }
}

impl<T> Extend<T> for Files<T> {
    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
        self.0.extend(iter)
    }
}

impl<T> From<Vec<T>> for Files<T> {
    fn from(value: Vec<T>) -> Self {
        Self(value)
    }
}

impl<T: Clone> From<&[T]> for Files<T> {
    fn from(value: &[T]) -> Self {
        Self(value.to_vec())
    }
}

impl<T: Clone> From<&mut [T]> for Files<T> {
    fn from(value: &mut [T]) -> Self {
        Self(value.to_vec())
    }
}

impl<T, const N: usize> From<[T; N]> for Files<T> {
    fn from(value: [T; N]) -> Self {
        Self(Vec::from(value))
    }
}

impl<T: Clone, const N: usize> From<&[T; N]> for Files<T> {
    fn from(value: &[T; N]) -> Self {
        Self(Vec::from(value))
    }
}

impl<T: Clone, const N: usize> From<&mut [T; N]> for Files<T> {
    fn from(value: &mut [T; N]) -> Self {
        Self(Vec::from(value))
    }
}

impl<T> AsRef<Vec<T>> for Files<T> {
    fn as_ref(&self) -> &Vec<T> {
        self.0.as_ref()
    }
}

impl<T> AsRef<[T]> for Files<T> {
    fn as_ref(&self) -> &[T] {
        self.0.as_ref()
    }
}

impl<T> AsRef<Files<T>> for Files<T> {
    fn as_ref(&self) -> &Files<T> {
        self
    }
}

impl<T> std::ops::Deref for Files<T> {
    type Target = Vec<T>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> std::ops::DerefMut for Files<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl<'a, T> IntoIterator for &'a Files<T> {
    type Item = &'a T;
    type IntoIter = Iter<'a, T>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.iter()
    }
}

impl<'a, T> IntoIterator for &'a mut Files<T> {
    type Item = &'a mut T;
    type IntoIter = IterMut<'a, T>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.iter_mut()
    }
}

impl<T> IntoIterator for Files<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl<T> FromIterator<T> for Files<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        let mut new = Files::new();
        for x in iter {
            new.push(x);
        }
        new
    }
}

impl<'a, T: Clone> FromIterator<&'a T> for Files<T> {
    fn from_iter<I: IntoIterator<Item = &'a T>>(iter: I) -> Self {
        let mut new = Files::new();
        for x in iter {
            new.push(x.clone());
        }
        new
    }
}

impl<T> std::ops::Index<usize> for Files<T> {
    type Output = T;
    fn index(&self, index: usize) -> &Self::Output {
        self.0.index(index)
    }
}

impl<T> std::ops::IndexMut<usize> for Files<T> {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        self.0.index_mut(index)
    }
}
