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

// https://en.wikipedia.org/wiki/Audio_Interchange_File_Format
// https://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/AIFF/AIFF.html

//! AIFF I/O handler (you should usually be using [`AudioFile`] by default).
//!
//! Only use this when [`AudioFile`] doesn't suit your needs, e.g. you
//! need to manually change the AIFF specification data.
//!
//! Uses the [`aifc`] crate.

use std::fs::File;
use std::io::BufReader;
use std::io::BufWriter;
use std::path::Path;

use aifc::AifcReadInfo;
use aifc::AifcReader;
use aifc::AifcWriter;
use aifc::SampleFormat;
use log::trace;
use thiserror::Error;

use super::f32_to_i24;
use super::AudioFile;
use super::{f32_to_i16, AudioErrors};
use crate::{OtToolsInterfacesError, R};

/// Errors for [`AiffFile`]s
#[derive(Debug, Error)]
pub enum AiffErrors {
    #[error("only 16 / 24 bit signed integer files are supported")]
    SampleFormat,
    #[error("unexpected error while writing the file")]
    Unexpected,
    #[error("could not create reader (aifc::AifcError)")]
    AifcReaderNew,
    #[error("could not read samples (aifc::AifcError)")]
    AifcReaderSamples,
    #[error("could not read sample (aifc::AifcError)")]
    AifcReaderSample,
    #[error("could not read specification (aifc::AifcError)")]
    AifcReaderSpec,
    #[error("could not create writer (aifc::AifcError)")]
    AifcWriterNew,
    #[error("could not finalise writer (aifc::AifcError)")]
    AifcWriterFinalize,
    #[error("could not write I24 samples (aifc::AifcError)")]
    AifcWriterSamplesI24,
    #[error("could not write I16 samples (aifc::AifcError)")]
    AifcWriterSamplesI16,
}

#[allow(dead_code)]
const DEFAULT_COMPRESSION_TYPE: [u8; 4] = [78, 79, 78, 69];

fn write_i24(writer: &mut AifcWriter<&mut BufWriter<File>>, samples: &[f32]) -> R<()> {
    let convert = samples.iter().map(|x| f32_to_i24(*x)).collect::<Vec<_>>();
    // TODO: aifc::AifcError type conversion
    writer
        .write_samples_i24(&convert)
        .map_err(|_| AiffErrors::AifcWriterSamplesI24)
        .map_err(AudioErrors::from)
        .map_err(OtToolsInterfacesError::from)
}

fn write_i16(writer: &mut AifcWriter<&mut BufWriter<File>>, samples: &[f32]) -> R<()> {
    let convert = samples.iter().map(|x| f32_to_i16(*x)).collect::<Vec<_>>();
    // TODO: aifc::AifcError type conversion
    writer
        .write_samples_i16(&convert)
        .map_err(|_| AiffErrors::AifcWriterSamplesI16)
        .map_err(AudioErrors::from)
        .map_err(OtToolsInterfacesError::from)
}

// simple conversion algorithm to convert Sample to f32
// source: https://github.com/karip/aifc/blob/main/examples/aifc-tinyaudio/src/main.rs
#[allow(dead_code)]
fn sample_to_f32(s: aifc::Sample) -> f32 {
    match s {
        aifc::Sample::U8(s) => s as f32 / (1u32 << 7) as f32 - 1.0,
        aifc::Sample::I8(s) => s as f32 / (1u32 << 7) as f32,
        aifc::Sample::I16(s) => s as f32 / (1u32 << 15) as f32,
        aifc::Sample::I24(s) => s as f32 / (1u32 << 23) as f32,
        aifc::Sample::I32(s) => s as f32 / (1u32 << 31) as f32,
        aifc::Sample::F32(s) => s,
        aifc::Sample::F64(s) => s as f32,
    }
}

/// Audio file data in a representation that can be written to an AIFF file.
#[derive(PartialEq, Debug, Clone)]
pub struct AiffFile {
    /// specification struct for the Aiff file.
    pub spec: AifcReadInfo,
    /// Number of audio samples in the Aiff file.
    pub len: u32,
    /// Audio samples
    pub samples: Vec<f32>,
}

impl From<AudioFile> for AiffFile {
    fn from(value: AudioFile) -> Self {
        Self {
            spec: AifcReadInfo {
                file_format: aifc::FileFormat::Aiff,
                channels: value.spec.channels as i16,
                sample_rate: value.spec.sample_rate as f64,
                sample_format: match value.spec.bit_depth {
                    16 => SampleFormat::I16,
                    24 => SampleFormat::I24,
                    _ => SampleFormat::I16,
                },
                sample_len: Some(value.len as u64),
                // TODO: No idea what this is -- is this x2 for 16 bit signed?
                sample_byte_len: value.len * 2,
                // TODO: No idea what this is -- post-non-interleaved length
                comm_num_sample_frames: value.len / value.spec.channels as u32,
                // TODO: No idea what this is -- bit depth?
                comm_sample_size: value.spec.bit_depth as i16,
                // TODO: No idea what this is
                comm_compression_type: DEFAULT_COMPRESSION_TYPE,
            },
            len: value.len,
            samples: value.samples,
        }
    }
}

impl AiffFile {
    /// Open a file read buffer for an AIFF file
    pub(crate) fn open_read_buf(path: &Path) -> R<AifcReader<BufReader<File>>> {
        trace!("Opening AIFF reader.");
        let bufreader = BufReader::new(File::open(path)?);
        // TODO: Error conversion
        let reader = aifc::AifcReader::new(bufreader)
            .map_err(|_| AiffErrors::AifcReaderNew)
            .map_err(AudioErrors::from)?;
        trace!("Opened AIFF reader.");
        Ok(reader)
    }

    /// Read the aifc specification data for an open AIFF file buffer
    pub(crate) fn get_spec(reader: &mut AifcReader<BufReader<File>>) -> AifcReadInfo {
        trace!("Reading AIFF spec from reader.");
        reader.info()
    }

    /// Read samples from an open AIFF file buffer.
    fn get_samples(reader: &mut AifcReader<BufReader<File>>) -> R<Vec<f32>> {
        trace!("Reading AIFF samples from reader.");
        let mut buf = Vec::<f32>::new();
        // TODO: Error conversion
        for sample in reader
            .samples()
            .map_err(|_| AiffErrors::AifcReaderSamples)
            .map_err(AudioErrors::from)?
        {
            buf.push(sample_to_f32(
                sample
                    .map_err(|_| AiffErrors::AifcReaderSample)
                    .map_err(AudioErrors::from)?,
            ));
        }
        trace!("Read AIFF samples from reader.");
        Ok(buf)
    }

    /// Read an AIFF file located at `path`.
    pub fn read(path: &Path) -> R<AiffFile> {
        // TODO: Validate file spec is ok

        trace!("Reading AIFF data from file: path={path:#?}");
        let mut reader = AiffFile::open_read_buf(path)?;

        trace!("Reading AIFF info spec: path={path:#?}");
        let info = AiffFile::get_spec(&mut reader);

        trace!("Reading AIFF samples: path={path:#?}");
        let samples = AiffFile::get_samples(&mut reader)?;

        trace!("Getting AIFF sample length: path={path:#?}");
        let length = info.sample_len.unwrap() as u32;

        trace!("Read AIFF file: path={path:#?}");
        Ok(AiffFile {
            spec: info,
            len: length,
            samples,
        })
    }

    /// Write data to a AIFF file at `path`.
    pub fn write(&self, path: &Path) -> R<()> {
        trace!("Writing to new AIFF file: path={path:#?}");
        let f = File::create(path)?;
        let mut stream = BufWriter::new(f);

        trace!("Creating new AIFF Write Spec: path={path:#?}");
        let write_format = aifc::AifcWriteInfo {
            file_format: self.spec.file_format,
            channels: self.spec.channels,
            sample_rate: self.spec.sample_rate,
            sample_format: self.spec.sample_format,
        };

        trace!("Creating new AIFF writer: path={path:#?}");
        // TODO: Error conversion
        let mut writer = AifcWriter::new(&mut stream, &write_format)
            .map_err(|_| AiffErrors::AifcWriterNew)
            .map_err(AudioErrors::from)?;

        trace!("Writing samples: path={path:#?}");
        match self.spec.sample_format {
            SampleFormat::I16 => write_i16(&mut writer, &self.samples),
            SampleFormat::I24 => write_i24(&mut writer, &self.samples),
            _ => Err(AudioErrors::Aiff(AiffErrors::SampleFormat).into()),
        }?;

        // TODO: Error conversion
        writer
            .finalize()
            .map_err(|_| AiffErrors::AifcWriterFinalize)
            .map_err(AudioErrors::from)?;

        trace!("Wrote AIFF file: path={path:#?}");

        Ok(())
    }
}
