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

//! Serialization and Deserialization library for Elektron Octatrack data files.
//!
//! ## Important types
//!
//! | rust type    | octatrack filename pattern | description |
//! | ------------ | -------------------------- | ------------|
//! | [`ArrangementFile`] | `arr??.*` | data for arrangements |
//! | [`BankFile`] | `bank??.*` | data for parts and patterns |
//! | [`MarkersFile`] | `markers.*` | start trim/end trim/slices/loop points for sample slots |
//! | [`ProjectFile`] | `project.*` | project level settings; state; sample slots |
//! | [`SampleSettingsFile`] | `*.ot` | saved sample settings data, loops slices etc. |
//!
//!
//! Only the above types implement the [`OctatrackFileIO`] super trait, meaning
//! only these types can be read from / written to the filesystem using the
//! functions in this library.
//!
//! Read the relevant modules in this library for more detailed information on
//! the data contained in each file.
//!
//! ## How do different Octatrack data files relate to each other?
//! - Arrangements store a `u8` which references a Bank's Pattern, indicating it should play for that Arrange row.
//!   Zero indexed, where 0 (A01) -> 256 (P16).
//! - A Bank stores zero-indexed sample slot IDs to indicate which sample should be played when on a given track
//!   (part machine data and/or track p-lock trigs).
//! - Changing the sample loaded into a sample slot updates both the `project.*` file (change the trig quantization
//!   settings, file path etc) and the `markers.*` file (change the trim settings based on either initial load,
//!   or any data in a relevant `*.ot` sample settings file).
//! - Data from `project.*`and `markers.*` is written to an `*.ot` file when saving sample attributes data from the
//!   Octatrack's audio editing menu.
//! - Loading a sample into a project sample slot (`project.*` and `markers.*` files) reads any data in an `*.ot` files
//!   and configures the sample slot accordingly.
//!
//! ## When do `*.work` and `*.strd` files get modified by the Octatrack?
//!
//! - `*.work` files are created when creating a new project `PROJECT MENU -> CHANGE PROJECT -> CREATE NEW`.
//! - `*.work` files are updated by using the `PROJECT MENU -> SYNC TO CARD` operation.
//! - `*.work` files are updated when the user performs a `PROJECT MENU -> SAVE PROJECT` operation.
//! - `*.strd` files are created/updated when the user performs a `PROJECT MENU -> SAVE PROJECT` operation.
//! - `*.strd` files are not changed by the `PROJECT -> SYNC TO CARD` operation.
//! - `arr??.strd` files can also be saved via the `ARRANGER MENU -> SAVE ARRANGEMENT` operation.
//!
//! ## Notable 'gotcha's
//! - Project sample slots (`project.*` files) are one-indexed, but their references everywhere else are zero-indexed
//!   (`bank??.*` and `markers.*` files).
//! - A 'Disabled' loop point in either `markers.*` or `*.ot` files is a `0xFFFFFFFF` value, but the default
//!   loop point when creating new `markers.*` file is always `0_u32`. The 'Disabled' value setting is only set
//!   when a sample is loaded into a sample slot, and an `.ot` file is generated from that sample slot data.
//!
//! ## Example code
//! ```rust
//! /*
//! reading, mutating and writing a bank file
//! */
//!
//! use std::path::PathBuf;
//! use ot_tools_io::{OctatrackFileIO, BankFile};
//!
//! let path = PathBuf::from("test-data")
//!     .join("blank-project")
//!     .join("bank01.work");
//!
//! // read an editable version of the bank file
//! let mut bank = BankFile::from_data_file(&path).unwrap();
//!
//! // change active scenes on the working copy of Part 4
//! bank.parts.unsaved[3].active_scenes.scene_a = 2;
//! bank.parts.unsaved[3].active_scenes.scene_b = 6;
//!
//! // write to a new bank file
//! let outfpath = std::env::temp_dir()
//!     .join("ot-tools-io")
//!     .join("doctest")
//!     .join("main_example_1");
//!
//! # // when running in cicd env the /tmp/ot-tools-io directory doesn't exist yet
//! # let _ = std::fs::create_dir_all(outfpath.parent().unwrap());
//!
//! &bank.to_data_file(&outfpath).unwrap();
//! ```

pub mod arrangements;
pub mod banks;
mod common_options;
mod err;
pub mod markers;
pub mod parts;
pub mod patterns;
pub mod projects;
pub mod samples;
pub mod slices;
#[cfg(test)]
#[allow(dead_code)]
mod test_utils;
mod traits;

pub use crate::arrangements::ArrangementFile;
pub use crate::banks::BankFile;
pub use crate::common_options::*;
pub use crate::err::*;
pub use crate::markers::MarkersFile;
pub use crate::projects::ProjectFile;
pub use crate::samples::SampleSettingsFile;
pub use crate::traits::*;
use std::error::Error;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;

// todo: sized errors so not necessary to keep Boxing error enum varients
/// Shorthand type alias for a Result with a Boxed Error
type RBoxErr<T> = Result<T, Box<dyn Error>>;

fn u8_bytes_to_u16(bytes: &[u8; 2]) -> u16 {
    ((bytes[0] as u16) << 8) | bytes[1] as u16
}

#[doc(hidden)]
/// Read bytes from a file at `path`.
// ```rust
// let fpath = std::path::PathBuf::from("test-data")
//     .join("blank-project")
//     .join("bank01.work");
// let r = ot_tools_io::read_bin_file(&fpath);
// assert!(r.is_ok());
// assert_eq!(r.unwrap().len(), 636113);
//```
fn read_bin_file(path: &Path) -> RBoxErr<Vec<u8>> {
    let mut infile = File::open(path)?;
    let mut bytes: Vec<u8> = vec![];
    let _: usize = infile.read_to_end(&mut bytes)?;
    Ok(bytes)
}

#[doc(hidden)]
/// Write bytes to a file at `path`.
// ```rust
// use std::env::temp_dir;
// use std::array::from_fn;
//
// let arr: [u8; 27] = from_fn(|_| 0);
//
// let fpath = temp_dir()
//    .join("ot-tools-io")
//    .join("doctest")
//    .join("write_bin_file.example");
//
// # use std::fs::create_dir_all;
// # create_dir_all(&fpath.parent().unwrap()).unwrap();
// let r = ot_tools_io::write_bin_file(&arr, &fpath);
// assert!(r.is_ok());
// assert!(fpath.exists());
// ```
fn write_bin_file(bytes: &[u8], path: &Path) -> RBoxErr<()> {
    let mut file: File = File::create(path)?;
    file.write_all(bytes)?;
    Ok(())
}

#[doc(hidden)]
/// Read a file at `path` as a string.
// ```
// let fpath = std::path::PathBuf::from("test-data")
//     .join("blank-project")
//     .join("bank01.work");
// let r = ot_tools_io::read_bin_file(&fpath);
// assert!(r.is_ok());
// assert_eq!(r.unwrap().len(), 636113);
// ```
fn read_str_file(path: &Path) -> RBoxErr<String> {
    let mut file = File::open(path)?;
    let mut string = String::new();
    let _ = file.read_to_string(&mut string)?;
    Ok(string)
}

#[doc(hidden)]
/// Write a string to a file at `path`.
// ```rust
// use std::env::temp_dir;
//
// let data = "abcd".to_string();
//
// let fpath = temp_dir()
//    .join("ot-tools-io")
//    .join("doctest")
//    .join("write_str_file.example");
//
// # use std::fs::create_dir_all;
// # create_dir_all(&fpath.parent().unwrap()).unwrap();
// let r = ot_tools_io::write_str_file(&data, &fpath);
// assert!(r.is_ok());
// assert!(fpath.exists());
// ```
fn write_str_file(string: &str, path: &Path) -> RBoxErr<()> {
    let mut file: File = File::create(path)?;
    write!(file, "{string}")?;
    Ok(())
}
