pub use tool_helper::Error;

pub mod config_reader;
pub mod encoder;
pub mod file_writer;
pub mod types;

use crate::types::RawData;
use cdtypes::types::sector::AudioSample;
use config_reader::{CDAudioFile, LZ4State};
use encoder::{cd::{self, calculate_lbas, calculate_length_for}, SizeInfo};
use tool_helper::{format_if_error, Output, read_file};
use types::{layout::Layout, CDDATrack, CDDesc, Directory, File, FileType, FileSystemMap, Properties, SharedPtr};
use std::path::PathBuf;

pub type LBAEmbeddedFiles     = Vec<SharedPtr<File>>;

struct ContentDumpAlignment {
    name:     usize,
    lba_pair: usize,
    size:     usize,
    ex_size:  usize,
}

const DEFAULT_CONTENT_ALIGNMENT:ContentDumpAlignment = ContentDumpAlignment{name: 24, lba_pair: 16, size: 16, ex_size: 16};

pub fn process(config: config_reader::Configuration) -> Result<(CDDesc, LBAEmbeddedFiles), Error> {
    let (mut cd_desc, lba_embedded_files) = parse_configuration(config)?;

    calculate_lbas(&mut cd_desc);
    Ok((cd_desc, lba_embedded_files))
}

pub fn process_files(file_map: FileSystemMap, lba_embedded_files: LBAEmbeddedFiles) -> Result<(), Error> {
    for lba_embedded_file_raw in lba_embedded_files {
        let new_content_info = {
            let mut lba_embedded_file = lba_embedded_file_raw.borrow_mut();

            match &mut lba_embedded_file.content {
                FileType::Overlay(content, lba_names) => Some(types::overlay::update_content(content, lba_names, &file_map)?),
                FileType::Main(content, lba_names)    => Some(types::overlay::update_content_for_main(content, lba_names, &file_map)?),
                _ => None
            }
        };

        if let Some(new_content) = new_content_info {
            let old_size_info = calculate_length_for(&Layout::File(lba_embedded_file_raw.clone()));
            lba_embedded_file_raw.borrow_mut().make_regular(new_content);
            let new_size_info = calculate_length_for(&Layout::File(lba_embedded_file_raw.clone()));

            if new_size_info.full_size.sectors != old_size_info.full_size.sectors {
                let lba_embedded_file      = lba_embedded_file_raw.borrow();
                let lba_embedded_file_name = lba_embedded_file.name.to_string();

                if new_size_info.full_size.sectors < old_size_info.full_size.sectors {
                    return Err(Error::from_text(format!("Failed converting Overlay \"{}\" because new size ({} sectors) is small then {} sectors! (This might be allowed in the future)", lba_embedded_file_name, new_size_info.full_size.sectors, old_size_info.full_size.sectors)));    
                }
                
                else {
                    return Err(Error::from_text(format!("Failed converting Overlay \"{}\" because new size ({} sectors) is bigger then {} sectors!", lba_embedded_file_name, new_size_info.full_size.sectors, old_size_info.full_size.sectors)));
                }
            }
        }
    }
    
    Ok(())
}

pub fn dump_content(cd_desc: &CDDesc, mut out: Output) -> Result<(), Error> {
    const NO_NAME:&'static str = "<No Name>";
    const ARROW:&'static str   = "|==>";
    const INDENT_STEP:usize    = 4;

    fn get_visible_indicator(is_hidden: bool) -> char {
        if is_hidden {
            '-'
        }

        else {
            'o'
        }
    }

    fn create_pair_display_string<T: std::fmt::Display>(rel_lba: T, abs_lba: T) -> String {
        format!("{}({})", rel_lba, abs_lba)
    }

    fn write_file(out: &mut Output, indent: usize, file_name: String, properties: &Properties, file_rel_lba: usize, file_abs_lba: usize, file_size: SizeInfo) -> Result<(), Error> {
        Ok(writeln!(out, "{:>indent$}File: ({}) {:<name_align$} @{:<lba_pair_align$} ={:<size_align$} >{:<ex_size_align$}", ARROW, get_visible_indicator(properties.is_hidden), file_name, create_pair_display_string(file_rel_lba, file_abs_lba), create_pair_display_string(file_size.real_size.sectors, file_size.real_size.bytes.unwrap()), create_pair_display_string(file_size.full_size.sectors, file_size.full_size.bytes.unwrap()), 
        indent=indent + ARROW.len(), name_align=DEFAULT_CONTENT_ALIGNMENT.name, lba_pair_align=DEFAULT_CONTENT_ALIGNMENT.lba_pair, size_align=DEFAULT_CONTENT_ALIGNMENT.size, ex_size_align=DEFAULT_CONTENT_ALIGNMENT.ex_size)?)
    }

    fn write_dir(out: &mut Output, indent: usize, dir_name: &str, properties: &Properties, dir_rel_lba: usize, dir_abs_lba: usize) -> Result<(), Error> {
        macro_rules! LBA_OUT {
            () => {
                "{:<lba_pair_align$}"
            };
        }
        let is_hidden = properties.is_hidden;

        write!(out, "{:>indent$}Dir:  ({}) {:<name_align$} @", ARROW, get_visible_indicator(is_hidden), dir_name, 
        indent=indent + ARROW.len(), name_align=DEFAULT_CONTENT_ALIGNMENT.name)?;

        if is_hidden {
            Ok(writeln!(out, LBA_OUT!(), create_pair_display_string("<None>", "<None>"), lba_pair_align=DEFAULT_CONTENT_ALIGNMENT.lba_pair)?)
        }

        else {
            Ok(writeln!(out, LBA_OUT!(), create_pair_display_string(dir_rel_lba, dir_abs_lba), lba_pair_align=DEFAULT_CONTENT_ALIGNMENT.lba_pair)?)
        }
    }

    fn write_intro(out: &mut Output) -> Result<(), Error> {
        writeln!(out, "{:>indent$}Type: ( ) {:<name_align$} @{:<lba_pair_align$} ={:<size_align$} >{:<ex_size_align$}", "", "Name", create_pair_display_string("LBA", "abs LBA"), "Sectors(bytes)", "Padded sectors(padded bytes)",
        indent=ARROW.len(), name_align=DEFAULT_CONTENT_ALIGNMENT.name, lba_pair_align=DEFAULT_CONTENT_ALIGNMENT.lba_pair, size_align=DEFAULT_CONTENT_ALIGNMENT.size, ex_size_align=DEFAULT_CONTENT_ALIGNMENT.ex_size)?;
        Ok(writeln!(out, "")?)
    }

    fn dump_dir(dir: &Directory, out: &mut Output, indent: usize) -> Result<(), Error> {
        for file in dir.file_iter() {
            let file         = file.borrow();
            let file_name    = file.name.as_string().unwrap_or(NO_NAME.to_owned());
            let properties   = &file.properties;
            let file_rel_lba = file.get_track_rel_lba();
            let file_abs_lba = file.get_absolute_lba();
            let file_size    = cd::size_of::file(&file);
            
            write_file(out, indent, file_name, properties, file_rel_lba, file_abs_lba, file_size)?;
        }

        for dir in dir.dir_iter() {
            let dir         = dir.borrow();
            let dir_name    = dir.name.as_str().unwrap_or(NO_NAME);
            let properties  = dir.properties.borrow();
            let dir_rel_lba = dir.get_track_rel_lba();
            let dir_abs_lba = dir.get_absolute_lba();

            write_dir(out, indent, dir_name, &properties, dir_rel_lba, dir_abs_lba)?;
            dump_dir(&dir, out, indent + INDENT_STEP)?;
        }

        Ok(())
    }

    format_if_error!(write_intro(&mut out),                         "Writing content dump intro failed with: {error_text}")?;
    format_if_error!(dump_dir(&cd_desc.root.borrow(), &mut out, 0), "Creating content dump failed with: {error_text}")
}

fn parse_configuration(config: config_reader::Configuration) -> Result<(CDDesc, LBAEmbeddedFiles), Error> {
    fn parse_dir(dst_dir: &mut types::Directory, src_dir: config_reader::Directory, lba_embedded_files: &mut LBAEmbeddedFiles) -> Result<(), Error> {
        for member in src_dir.into_iter() {
            match member {
                config_reader::DirMember::Directory(dir) => {
                    let mut new_dir = types::Directory::new(dir.name.as_str())?;

                    new_dir.properties.borrow_mut().is_hidden = dir.is_hidden;
                    parse_dir(&mut new_dir, dir, lba_embedded_files)?;
                    dst_dir.add_dir(new_dir);
                },

                config_reader::DirMember::File(file)     => {
                    let lz4_state = file.common.lz4_state;
                    let (mut desc_file, needs_treatment) = {
                        fn handle_file_load(file_path: &PathBuf, lz4_state: &LZ4State) -> Result<RawData, Error> {
                            let file_content = read_file(file_path)?;

                            if matches!(lz4_state, LZ4State::Compress) {
                                tool_helper::compress::psx_default::lz4(&file_content)
                            }

                            else {
                                Ok(file_content)
                            }
                        }

                        fn handle_multiple_file_load(file_paths: Vec<PathBuf>, lz4_state: &LZ4State) -> Result<Vec<RawData>, Error> {
                            let mut files = Vec::new();

                            for file_path in file_paths {
                                files.push(handle_file_load(&file_path, &lz4_state)?);
                            }
                            Ok(files)
                        }

                        match file.kind {
                            config_reader::FileKind::Regular             => (types::File::new_regular(file.common.name.as_str(),      handle_file_load(&file.path, &lz4_state)?)?,               false),
                            config_reader::FileKind::Main(lba_source)    => (types::overlay::load_for_main(file.common.name.as_str(), handle_file_load(&file.path, &lz4_state)?,   lba_source)?, true),
                            config_reader::FileKind::Overlay(lba_source) => (types::overlay::load_from(file.common.name.as_str(),     &file.path,                                  lba_source)?, true),
                            config_reader::FileKind::XAAudio(channels)   => (types::File::new_xa_audio(file.common.name.as_str(),     handle_multiple_file_load(channels, &lz4_state)?)?,        false),
                        }
                    };

                    desc_file.properties.padded_size_bytes = file.common.padded_size;
                    desc_file.properties.is_hidden         = file.common.is_hidden;
                    desc_file.properties.is_lz4            = !matches!(lz4_state, LZ4State::None);
                    
                    let new_file = dst_dir.add_file(desc_file);
                    if needs_treatment {
                        lba_embedded_files.push(new_file);
                    }
                },
            }
        }
        Ok(())
    }

    fn parse_cd_da(cd_da_tracks: &mut Vec<CDDATrack>, cd_da_files: Vec<CDAudioFile>) -> Result<(), Error> {
        fn convert_into_16<R:std::io::Read, S:hound::Sample>(samples: hound::WavSamples<R,S>, file_path: &PathBuf) -> Result<Vec<AudioSample>, Error> {
            let mut sample_buffer = 0;
            let mut sample_id     = 0;
            let mut cd_samples    = Vec::new();

            for sample in samples {
                match sample {
                    Ok(sample) => {
                        if sample_id == 0 {
                            sample_buffer = sample.as_i16();
                            sample_id     = 1;
                        }

                        else {
                            cd_samples.push(AudioSample::new(sample_buffer, sample.as_i16()));
                            sample_id = 0;
                        }
                    },
                    Err(err)   => {
                        return Err(Error::from_text(format!("{}: {}", file_path.to_string_lossy(), err)));
                    }
                }
            }

            Ok(cd_samples)
        }

        for cd_da_file in cd_da_files {
            let mut audio_io  = {
                    match hound::WavReader::open(&cd_da_file.file_path) {
                        Ok(audio_io) => Ok(audio_io),
                        Err(error)   => Err(Error::from_text(format!("Failed to open CD Audio Track {} with error: {}", cd_da_file.file_path.to_string_lossy(), error)))
                    }
                }?;
            let header = audio_io.spec();

            if header.sample_format != hound::SampleFormat::Int {
                return Err(Error::from_text(format!("{}: Only integer WAV format (PCM) is supported for Audio tracks", cd_da_file.file_path.display())));
            }
        
            if header.sample_rate != 44_100 {
                return Err(Error::from_text(format!("{}: Only a sampling rate of 44.1kHz is supported for Audio tracks", cd_da_file.file_path.display())));
            }

            cd_da_tracks.push(CDDATrack::new(convert_into_16(audio_io.samples::<i16>(), &cd_da_file.file_path)?, cd_da_file.align));
        }
        Ok(())
    }

    let mut cd_desc            = CDDesc::new();
    let mut lba_embedded_files = Vec::new();

    if let Some(publisher) = config.publisher {
        cd_desc.pvd.borrow_mut().set_publisher(publisher);
    }

    cd_desc.system_area.borrow_mut().license_file        = config.license_file;
    cd_desc.system_area.borrow_mut().backup_license_file = config.backup_license_file;

    parse_dir(&mut cd_desc.root.borrow_mut(), config.root, &mut lba_embedded_files)?;
    parse_cd_da(&mut cd_desc.cd_da_tracks, config.cd_audio_files)?;

    cd_desc.lead_out_sectors = config.lead_out_sectors.unwrap_or(0);
    Ok((cd_desc, lba_embedded_files))
}
