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

use crate::RBoxErr;
use serde::{Deserialize, Serialize};
use serde_big_array::Array;
use std::array::from_fn;
use std::fmt::Debug;
use std::path::Path;

/// Convenience trait for types which directly correspond to Elektron Octatrack binary data files.
/// Associated functions and methods etc. for File I/O, plus a `repr` method for debugging.
pub trait OctatrackFileIO: Decode + Encode + Serialize + for<'a> Deserialize<'a> {
    /// Read type from an Octatrack data file at path
    /// ```rust
    /// # use std::path::PathBuf;
    /// # let path = PathBuf::from("test-data").join("blank-project").join("bank01.work");
    /// use ot_tools_io::{BankFile, OctatrackFileIO};
    /// assert!(BankFile::from_data_file(&path).unwrap().repr(None).is_ok());
    /// assert!(BankFile::from_data_file(&path).unwrap().repr(Some(false)).is_ok());
    /// assert!(BankFile::from_data_file(&path).unwrap().repr(Some(true)).is_ok());
    /// ```
    fn repr(&self, newlines: Option<bool>) -> RBoxErr<()>
    where
        Self: Debug,
    {
        if newlines.unwrap_or(true) {
            println!("{self:#?}")
        } else {
            println!("{self:?}")
        };
        Ok(())
    }
    // BYTES
    /// Read type from an Octatrack data file at path
    /// ```rust
    /// # use std::path::PathBuf;
    /// # let path = PathBuf::from("test-data").join("blank-project").join("bank01.work");
    /// use ot_tools_io::{BankFile, OctatrackFileIO};
    /// let bank = BankFile::from_data_file(&path).unwrap();
    /// assert_eq!(bank.datatype_version, 23);
    /// ```
    fn from_data_file(path: &Path) -> RBoxErr<Self> {
        let bytes = crate::read_bin_file(path)?;
        let data = Self::from_bytes(&bytes)?;
        Ok(data)
    }
    /// Read type from bytes
    /// ```rust
    /// // need to create everything from scratch in this example as many bytes otherwise
    /// use serde::{Deserialize, Serialize};
    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
    ///
    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
    /// struct Something {
    ///     value: u8,
    /// }
    ///
    /// impl Decode for Something {}
    /// impl Encode for Something {}
    /// impl OctatrackFileIO for Something {}
    ///
    /// let x = [10];
    ///
    /// let r = Something::from_bytes(&x);
    /// assert!(r.is_ok());
    /// assert_eq!(r.unwrap(), Something { value: 10});
    /// ```
    fn from_bytes(bytes: &[u8]) -> RBoxErr<Self> {
        let x = Self::decode(bytes)?;
        Ok(x)
    }
    /// Write type to an Octatrack data file at path
    /// ```rust
    /// # use std::{path::PathBuf, fs::create_dir_all, env::temp_dir};
    /// # let path = temp_dir()
    /// #    .join("ot-tools-io")
    /// #    .join("doctest")
    /// #    .join("to_bytes_file.bank.work");
    /// # create_dir_all(&path.parent().unwrap()).unwrap();
    /// #
    /// use ot_tools_io::{OctatrackFileIO, BankFile};
    /// let r = BankFile::default().to_data_file(&path);
    /// assert!(r.is_ok());
    /// assert!(path.exists());
    /// ```
    fn to_data_file(&self, path: &Path) -> RBoxErr<()> {
        let bytes = Self::to_bytes(self)?;
        crate::write_bin_file(&bytes, path)?;
        Ok(())
    }
    /// Create bytes from type
    /// ```rust
    /// // need to create everything from scratch in this example as many bytes otherwise
    /// use serde::{Deserialize, Serialize};
    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
    ///
    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
    /// struct Something {
    ///     value: u8,
    /// }
    ///
    /// impl Decode for Something {}
    /// impl Encode for Something {}
    /// impl OctatrackFileIO for Something {}
    ///
    /// let x = Something { value: 10 };
    /// let r = x.to_bytes();
    /// assert!(r.is_ok());
    /// assert_eq!(r.unwrap(), [10]);
    /// ```
    fn to_bytes(&self) -> RBoxErr<Vec<u8>> {
        self.encode()
    }
    // YAML
    /// Read type from a YAML file at path
    /// ```rust
    /// # use std::path::PathBuf;
    /// # let path = PathBuf::from("test-data").join("projects").join("default.yaml");
    /// use ot_tools_io::{ProjectFile, OctatrackFileIO};
    /// let project = ProjectFile::from_yaml_file(&path).unwrap();
    /// assert_eq!(project.metadata.project_version, 19);
    /// ```
    fn from_yaml_file(path: &Path) -> RBoxErr<Self> {
        let string = crate::read_str_file(path)?;
        let data = Self::from_yaml_str(&string)?;
        Ok(data)
    }
    /// Read type from YAML string
    /// ```rust
    /// // need to create everything from scratch in this example
    /// use serde::{Deserialize, Serialize};
    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
    ///
    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
    /// struct Something {
    ///     value: u8,
    /// }
    ///
    /// impl Decode for Something {}
    /// impl Encode for Something {}
    /// impl OctatrackFileIO for Something {}
    ///
    /// let r = Something::from_yaml_str("value: 10\n");
    /// assert!(r.is_ok());
    /// assert_eq!(r.unwrap(), Something { value: 10 });
    /// ```
    fn from_yaml_str(yaml: &str) -> RBoxErr<Self> {
        let x = serde_yml::from_str(yaml)?;
        Ok(x)
    }
    /// Write type to a YAML file at path
    /// ```rust
    /// # use std::{path::PathBuf, fs::create_dir_all, env::temp_dir};
    /// # let path = temp_dir()
    /// #    .join("ot-tools-io")
    /// #    .join("doctest")
    /// #    .join("to_bytes_file.bank.yaml");
    /// # create_dir_all(&path.parent().unwrap()).unwrap();
    /// #
    /// use ot_tools_io::{OctatrackFileIO, BankFile};
    /// let r = BankFile::default().to_yaml_file(&path);
    /// assert!(r.is_ok());
    /// assert!(path.exists());
    /// ```
    fn to_yaml_file(&self, path: &Path) -> RBoxErr<()> {
        let yaml = Self::to_yaml_string(self)?;
        crate::write_str_file(&yaml, path)?;
        Ok(())
    }
    /// Create YAML string from type
    /// ```rust
    /// use ot_tools_io::{BankFile, OctatrackFileIO};
    /// let bank = BankFile::default().to_yaml_string().unwrap();
    /// assert_eq!(bank.len(), 12578424);
    /// assert_eq!(bank[0..15], "header:\n- 70\n- ".to_string());
    /// ```
    fn to_yaml_string(&self) -> RBoxErr<String> {
        Ok(serde_yml::to_string(self)?)
    }
    // JSON
    /// Read type from a JSON file at path
    fn from_json_file(path: &Path) -> RBoxErr<Self> {
        let string = crate::read_str_file(path)?;
        let data = Self::from_yaml_str(&string)?;
        Ok(data)
    }
    /// Create type from JSON string
    /// ```rust
    /// // need to create everything from scratch in this example
    /// use serde::{Deserialize, Serialize};
    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
    ///
    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
    /// struct Something {
    ///     value: u8,
    /// }
    ///
    /// impl Decode for Something {}
    /// impl Encode for Something {}
    /// impl OctatrackFileIO for Something {}
    ///
    /// let r = Something::from_json_str("{\"value\":10}");
    /// assert!(r.is_ok());
    /// assert_eq!(r.unwrap(), Something { value: 10 });
    /// ```
    fn from_json_str(json: &str) -> RBoxErr<Self> {
        Ok(serde_json::from_str::<Self>(json)?)
    }
    /// Write type to a JSON file at path
    /// ```rust
    /// # use std::{path::PathBuf, fs::create_dir_all, env::temp_dir};
    /// # let path = temp_dir()
    /// #    .join("ot-tools-io")
    /// #    .join("doctest")
    /// #    .join("to_bytes_file.bank.json");
    /// # create_dir_all(&path.parent().unwrap()).unwrap();
    /// #
    /// use ot_tools_io::{OctatrackFileIO, BankFile};
    /// let r = BankFile::default().to_json_file(&path);
    /// assert!(r.is_ok());
    /// assert!(path.exists());
    /// ```
    fn to_json_file(&self, path: &Path) -> RBoxErr<()> {
        let yaml = Self::to_json_string(self)?;
        crate::write_str_file(&yaml, path)?;
        Ok(())
    }
    /// Create JSON string from type
    /// ```rust
    /// use ot_tools_io::{BankFile, OctatrackFileIO};
    /// let json = BankFile::default().to_json_string().unwrap();
    /// assert_eq!(json.len(), 7807763);
    /// assert_eq!(json[0..15], "{\"header\":[70,7".to_string());
    /// ```
    fn to_json_string(&self) -> RBoxErr<String> {
        Ok(serde_json::to_string(&self)?)
    }
}

/// Trait to convert between Enum option instances and their corresponding value.
/// ```
/// use std::error::Error;
/// use ot_tools_io::OptionEnumValueConvert;
///
/// #[derive(std::fmt::Debug, PartialEq, Copy, Clone)]
/// enum SomeThing {
///     X = 0,
///     Y = 19473295,
/// }
///
/// impl OptionEnumValueConvert<u32> for SomeThing {
///
///     fn from_value(v: &u32) -> Result<Self, Box<dyn Error>> {
///         match v {
///             0 => Ok(Self::X),
///             19473295 => Ok(Self::Y),
///             _ => Err(ot_tools_io::OtToolsIoErrors::NoMatchingOptionEnumValue.into())
///         }
///     }
///     fn value(&self) -> Result<u32, Box<dyn Error>> {
///         Ok(*self as u32)
///     }
/// }
///
/// assert_eq!(SomeThing::X.value().unwrap(), 0);
/// assert_eq!(SomeThing::Y.value().unwrap(), 19473295);
/// assert_eq!(SomeThing::from_value(&0).unwrap(), SomeThing::X);
/// assert_eq!(SomeThing::from_value(&19473295).unwrap(), SomeThing::Y);
/// assert!(SomeThing::from_value(&100).is_err());
/// assert!(SomeThing::from_value(&200).is_err());
/// ```
pub trait OptionEnumValueConvert<V> {
    /// Get an Enum instance from some value.
    fn from_value(v: &V) -> RBoxErr<Self>
    where
        Self: Sized;

    /// Get a numeric value for an Enum instance.
    fn value(&self) -> RBoxErr<V>;
}

/// Adds deserialisation via [`bincode`] and [`serde`] to a type.
/// Must be present on all major file types.
/// ```
/// use std::error::Error;
/// use ot_tools_io::Decode;
///
/// #[derive(serde::Deserialize, std::fmt::Debug, PartialEq)]
/// struct SomeType {
///     x: u8,
///     y: u32,
/// }
///
/// // default implementation
/// impl Decode for SomeType {}
///
/// let bytes: Vec<u8> = vec![8, 127, 95, 245, 1];
/// let decoded = SomeType::decode(&bytes).unwrap();
///
/// assert_eq!(
///     SomeType { x : 8, y: 32857983 },
///     decoded,
/// );
/// ```
pub trait Decode {
    fn decode(bytes: &[u8]) -> RBoxErr<Self>
    where
        Self: Sized,
        Self: for<'a> Deserialize<'a>,
    {
        let x: Self = bincode::deserialize(bytes)?;
        Ok(x)
    }
}

/// Adds serialisation via [`bincode`] and [`serde`] to a type.
/// Must be present on all major file types.
///
/// ```
/// use std::error::Error;
/// use ot_tools_io::Encode;
///
/// #[derive(serde::Serialize, std::fmt::Debug)]
/// struct SomeType {
///     x: u8,
///     y: u32,
/// }
///
/// // default implementation
/// impl Encode for SomeType {}
///
/// let x = SomeType { x : 8, y: 32857983 };
/// let encoded = x.encode().unwrap();
/// assert_eq!(
///     vec![8, 127, 95, 245, 1],
///     encoded,
/// );
/// ```
pub trait Encode {
    fn encode(&self) -> RBoxErr<Vec<u8>>
    where
        Self: Serialize,
    {
        Ok(bincode::serialize(&self)?)
    }
}

/// Trait for adding a method which swaps the bytes on all fields of a struct.
///
/// Useful for handling file writes to a different endianness.
/// ```
/// use std::error::Error;
/// use ot_tools_io::SwapBytes;
///
/// #[derive(std::fmt::Debug, PartialEq)]
/// struct SomeType {
///     x: u8,
///     y: u32,
/// }
///
/// impl SwapBytes for SomeType {
///     fn swap_bytes(self) -> Result<Self, Box<dyn Error>> {
///         Ok(Self {
///             x: self.x.swap_bytes(),
///             y: self.y.swap_bytes(),
///         })
///     }
/// }
///
/// let x = SomeType { x : 8, y: 32857983 };
/// let swapped = x.swap_bytes().unwrap();
/// assert_eq!(
///     SomeType { x: 8_u8.swap_bytes(), y: 32857983_u32.swap_bytes()},
///     swapped,
/// );
/// ```
pub trait SwapBytes {
    fn swap_bytes(self) -> RBoxErr<Self>
    where
        Self: Sized;
}

/// Used when we need a collection of default type instances
/// e.g. when creating a default bank we need 16 default patterns.
/// ```
/// use ot_tools_io::DefaultsArray;
///
/// struct SomeType {
///     x: u8,
/// }
///
/// impl Default for SomeType {
///     fn default() -> Self {
///         Self { x: 0 }
///     }
/// }
///
/// impl DefaultsArray for SomeType {}
///
/// let xs: [SomeType; 20] = SomeType::defaults();
/// assert_eq!(xs.len(), 20);
///
/// let xs = SomeType::defaults::<25>();
/// assert_eq!(xs.len(), 25);
/// ```
pub trait DefaultsArray {
    /// Create an Array containing `N` default instances of `Self`.
    fn defaults<const N: usize>() -> [Self; N]
    where
        Self: Default,
    {
        from_fn(|_| Self::default())
    }
}

/// Same as [`DefaultsArray`], but using a boxed [`serde_big_array::Array`]
/// container type
/// ```
/// use serde_big_array::Array;
/// use ot_tools_io::DefaultsArrayBoxed;
///
/// struct SomeType {
///     x: u8,
/// }
///
/// impl Default for SomeType {
///     fn default() -> Self {
///         Self { x: 0 }
///     }
/// }
///
/// impl DefaultsArrayBoxed for SomeType {}
///
/// let xs: Box<Array<SomeType, 20>> = SomeType::defaults();
/// assert_eq!(xs.len(), 20);
///
/// let xs = SomeType::defaults::<25>();
/// assert_eq!(xs.len(), 25);
/// ```
pub trait DefaultsArrayBoxed {
    /// Create a Boxed [serde_big_array::Array] containing `N` default instances
    /// of `Self`.
    fn defaults<const N: usize>() -> Box<Array<Self, N>>
    where
        Self: Default,
    {
        Box::new(Array(from_fn(|_| Self::default())))
    }
}

/// Adds a method to check the current data structure matches the default for the type
/// ```rust
/// use ot_tools_io::{IsDefault, BankFile};
///
/// let mut bank = BankFile::default();
/// assert_eq!(bank.is_default(), true);
///
/// bank.datatype_version = 190;
/// assert_eq!(bank.is_default(), false);
/// ```
pub trait IsDefault {
    fn is_default(&self) -> bool;
}

/// Method for calculating the checksum value for types that have a checksum field
/// ```rust
/// # use std::path::PathBuf;
/// # let path = PathBuf::from("test-data")
/// #     .join("blank-project")
/// #     .join("bank01.work");
/// use ot_tools_io::{CalculateChecksum, OctatrackFileIO, BankFile};
/// let bank = BankFile::from_data_file(&path).unwrap();
/// assert_eq!(bank.checksum, bank.calculate_checksum().unwrap())
/// ```
pub trait CalculateChecksum {
    fn calculate_checksum(&self) -> RBoxErr<u16>;
}

/// Adds a method to verify if header(s) are valid in some data.
/// [See this thread](https://www.elektronauts.com/t/bank-unavailable-octatrack/190647/27).
/// ```rust
/// # use std::path::PathBuf;
/// # let path = PathBuf::from("test-data")
/// #     .join("blank-project")
/// #     .join("bank01.work");
/// use ot_tools_io::{CheckHeader, OctatrackFileIO, BankFile};
/// assert!(BankFile::from_data_file(&path).unwrap().check_header()) // true for valid header values
/// ```
// NOTE: ot-tools-io does not validate headers on file read, which means it is
// possible to perform checks like this when a data file has been read.
// otherwise we'd have to do a complicated check to verify headers on every
// file we read, then throw out an error and probably do some complicated
// error handling to explain to the end user exactly why we couldn't load the
// file (bad header, which patterns, which track within patterns etc.).
pub trait CheckHeader {
    fn check_header(&self) -> bool;
}

/// Adds a method to verify if checksum is valid in some data type.
/// [See this thread](https://www.elektronauts.com/t/bank-unavailable-octatrack/190647/27).
/// ```rust
/// # use std::path::PathBuf;
/// # let path = PathBuf::from("test-data")
/// #     .join("blank-project")
/// #     .join("bank01.work");
/// use ot_tools_io::{CheckChecksum, OctatrackFileIO, BankFile};
/// // true for valid checksum values
/// assert!(BankFile::from_data_file(&path).unwrap().check_checksum().unwrap())
/// ```
pub trait CheckChecksum {
    fn check_checksum(&self) -> RBoxErr<bool>;
}

/// Adds a method to verify if the data file version field is valid for the given type.
/// ```rust
/// # use std::path::PathBuf;
/// # let path = PathBuf::from("test-data")
/// #     .join("blank-project")
/// #     .join("bank01.work");
/// use ot_tools_io::{CheckFileVersion, OctatrackFileIO, BankFile};
/// // true for valid version values
/// assert!(BankFile::from_data_file(&path).unwrap().check_file_version().unwrap())
/// ```
// NOTE: ot-tools-io does not validate headers on file read, which means it is
// possible to perform checks like this when a data file has been read.
// otherwise we'd have to do a complicated check to verify headers on every
// file we read, then throw out an error and probably do some complicated
// error handling to explain to the end user exactly why we couldn't load the
// file (bad header, which patterns, which track within patterns etc.).
pub trait CheckFileVersion {
    fn check_file_version(&self) -> RBoxErr<bool>;
}

/// Adds a single method using the [`CheckHeader::check_header`],
/// [`CheckChecksum::check_checksum`] and [`CheckFileVersion::check_file_version`]
/// methods to run a full integrity check.
/// ```rust
/// # use std::path::PathBuf;
/// # let path = PathBuf::from("test-data")
/// #     .join("blank-project")
/// #     .join("bank01.work");
/// use ot_tools_io::{CheckIntegrity, OctatrackFileIO, BankFile};
/// // true for valid checksum+header values
/// assert!(BankFile::from_data_file(&path).unwrap().check_integrity().unwrap())
/// ```
pub trait CheckIntegrity: CheckHeader + CheckChecksum + CheckFileVersion {
    fn check_integrity(&self) -> RBoxErr<bool> {
        Ok(self.check_header() && self.check_checksum()? && self.check_file_version()?)
    }
}
