pub mod types;

use clap::Args;
use std::{io::Write, str::FromStr};
use tool_helper::{Error, Input};
use types::{LPC, MonoADPCMIterator, VAGADPCM, VAGHeader};

#[derive(Args)]
pub struct Arguments {
    #[clap(long, help="Specify the file name to be embedded in the header", default_value = "File name without extensions up to 16 characters")]
    name:   Option<String>,
    #[clap(short='l', help="Set a loop at the specified time", value_name = "<min>:<sec>.<msec>[-<min>:<sec>.<msec>]", value_parser = clap::value_parser!(Loop))]
    r#loop: Option<Loop>
}

#[derive(Clone)]
pub struct Loop {
    start_time_sec: f64,
    end_time_sec:   Option<f64>,
}

impl Loop {
    const END_DELIMITER: char = '-';
    const MIN_DELIMITER: char = ':';

    fn parse(str: Option<&str>) -> Result<Option<f64>, String> {
        fn print_sec_conversion_error(str: &str, error: &dyn core::fmt::Display) -> String {
            format!("Converting specified seconds \"{}\" failed with: {}", str, error)
        }

        if let Some(str) = str {
            let time = {
                if let Some(min_delim_idx) = str.find(Self::MIN_DELIMITER) {
                    let (min, sec) = str.split_at(min_delim_idx);
                    let min        = min.parse::<u64>().map_err(|e| format!("Converting specified minutes \"{}\" failed with: {}", min, e))?;
                    let sec        = sec.trim_start_matches(Self::MIN_DELIMITER).parse::<f64>().map_err(|e| print_sec_conversion_error(sec, &e))?;

                    (min*60) as f64 + sec
                }

                else {
                    str.parse::<f64>().map_err(|e| print_sec_conversion_error(str, &e))?
                }
            };
            Ok(Some(time))
        }

        else {
            Ok(None)
        }
    }
}

impl FromStr for Loop {
    type Err = String;

    fn from_str(arg: &str) -> Result<Self, Self::Err> {
        let mut args           = arg.split(Self::END_DELIMITER);
        let     start_time_sec = Self::parse(args.next()).map_err(|e| format!("{}", e))?.ok_or_else(|| format!("A start time is required for a loop"))?;
        let     end_time_sec   = Self::parse(args.next()).map_err(|e| format!("{}", e))?;

        Ok(Loop{start_time_sec, end_time_sec})
    }
}

struct LoopInfo {
    start_sample: usize,
    end_sample:   usize,
}

impl LoopInfo {
    fn create(r#loop: &Loop, sample_frequency: u32, vagadpcm_count: usize) -> LoopInfo {
        fn calculate_sample(time_sec: f64, vag_frequency: f64) -> usize {
            (time_sec*vag_frequency) as usize
        }

        let sample_frequency = sample_frequency as f64 / VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM as f64;
        let start_sample     = calculate_sample(r#loop.start_time_sec, sample_frequency);
        let end_sample       = {
            if let Some(end_time_sec) = r#loop.end_time_sec {
                let end_sample = calculate_sample(end_time_sec, sample_frequency);
                if end_sample > vagadpcm_count {vagadpcm_count} else {end_sample}
            }

            else {
                vagadpcm_count
            }
        };

        LoopInfo{start_sample, end_sample}
    }

    fn is_loop_start_reached(&self, sample: usize) -> bool {
        self.start_sample == sample
    }

    fn is_loop_end_reached(&self, sample: usize) -> bool {
        self.end_sample == sample
    }
}

pub fn convert(args: Arguments, output_file_path: &Option<std::path::PathBuf>, input: Input, output: &mut dyn Write) -> Result<(), Error> {
    let mut wav_file   = hound::WavReader::new(input)?;
    let     wav_header = wav_file.spec();

    validate(&wav_header)?;

    let     vagadpcm_samples = VAGHeader::expected_vagadpcm_samples(wav_file.len()) + 1;
    let     sample_info      = args.r#loop.map(|v| LoopInfo::create(&v, wav_header.sample_rate, vagadpcm_samples as usize));
    let mut lpc              = LPC::empty();

    tool_helper::raw::write_raw(output, &VAGHeader::create(vagadpcm_samples, wav_header.sample_rate, &get_name_for_file_header(args.name, output_file_path))?)?;
    for (sample_id, adpcm_sample) in MonoADPCMIterator::create(wav_file.samples::<i16>()).enumerate() {
        let (mut vagadpcm, new_lpc) = VAGADPCM::create(adpcm_sample?, lpc);

        if let Some(sample_info) = &sample_info {
            if sample_info.is_loop_start_reached(sample_id) {
                vagadpcm = vagadpcm.set_loop_start();
            }

            if sample_info.is_loop_end_reached(sample_id + 1) {
                vagadpcm = vagadpcm.set_loop_end_repeat();
            }            
        }

        tool_helper::raw::write_raw(output, &vagadpcm)?;   
        lpc = new_lpc;
    }

    if sample_info.is_none() {
        tool_helper::raw::write_raw(output, &VAGADPCM::end())?;
    }
    Ok(())
}

fn validate(wav_header: &hound::WavSpec) -> Result<(), Error> {
    if wav_header.sample_format != hound::SampleFormat::Int {
        return Err(Error::from_str("VAG: Only integer samples are supported as input."));
    }

    if wav_header.bits_per_sample != 16 {
        return Err(Error::from_str("VAG: Only 16bits samples are currently supported as input."));
    }

    if wav_header.channels != 1 {
        return Err(Error::from_str("VAG: Only mono samples are currently supported"));
    }

    Ok(())
}

fn get_name_for_file_header(name: Option<String>, output_file_path: &Option<std::path::PathBuf>) -> String {
    if let Some(name) = name {
        return name;
    }

    if let Some(output_file_path) = output_file_path {
        if let Some(file_name) = output_file_path.file_name() {
            let mut string = file_name.to_string_lossy().to_string();

            if let Some(idx) = string.rfind('.') {
                string.replace_range(idx.., "");
            }
            return string;
        }
    }

    return "<out>".to_owned();
}