use psxcdread::{*};
use cdtypes::{cd::{reader::UnknownSectorBehavior, CD}, types::{cue, sector::SECTOR_SIZE}, Error};
use std::{fs::OpenOptions, io::{stdin, stdout, Write}, path::{Path, PathBuf}};

fn open_file(path: &Path, write_access: bool) -> Result<std::fs::File, Error> {
    match OpenOptions::new().read(true).write(write_access).create(write_access).open(path) {
        Ok(file)   => Ok(file),
        Err(error) => {
            let path = {
                if let Some(path) = path.to_str() {
                    path.to_owned()
                }

                else {
                    "<Invalid encoded file path>".to_owned()
                }
            };
            Err(Error::IOError(Some(path), error))
        }
    }
}

fn open_cue(file: std::fs::File, cue_path: &Path) -> Result<CD, Error> {
    fn get_bin_path(specifier: &Vec<cue::Specifier>, cue_path: &Path) -> Result<Option<PathBuf>, Error> {
        for specifier in specifier {
            match specifier {
                cue::Specifier::File{path, format} => {
                    if matches!(format, cue::Format::Binary) {
                        let mut bin_path = cue_path.to_path_buf();

                        if let Some(_) = bin_path.file_name() {
                            bin_path.pop();
                        }

                        bin_path.push(path);
                        return Ok(Some(bin_path));                        
                    }

                    else {
                        return Err(Error::GenericError("Only binary CUE files are supported".to_owned()));
                    }
                },
                _ => ()
            }
        }

        Ok(None)
    }
    let specifier = cue::read::read(file)?;
    if let Some(bin_path) = get_bin_path(&specifier, cue_path)? {
        Ok(CD::from_file(open_file(bin_path.as_path(), false)?, Some(specifier), UnknownSectorBehavior::CauseError)?)
    }

    else {
        Err(Error::GenericError("No binary file specified".to_owned()))
    }
}

fn open_cd(path: &str) -> Result<CD, Error> {
    fn is_cue(path: &Path) -> bool {
        if let Some(extension) = path.extension() {
            extension == "cue"
        }

        else {
            false
        }
    }
    let path = Path::new(path);
    let file = open_file(path, false)?;

    if is_cue(&path) {
        open_cue(file, &path)
    }

    else {
        Ok(CD::from_file(file, None, UnknownSectorBehavior::TreatAsAudio)?)
    }
}

fn read_input(cd: &CD, cur_sec: usize) -> Result<String, std::io::Error> {
    println!("Current Sector: [{}/{}]", cur_sec, cd.last_sector_idx());
    stdout().write("> ".as_bytes())?;
    stdout().flush()?;

    let mut input = String::new();
    stdin().read_line(&mut input)?;
    Ok(input)
}

enum ChangeSectorNumMode {
    Increment,
    Decrement,
    Absolute
}

fn get_number_from_str(number_str: &str) -> Option<usize> {
    let (number, base) = {
        if number_str.starts_with("0x") {
            (number_str.trim_start_matches("0x"), 16)
        }

        else {
            (number_str, 10)
        }
    };

    if let Ok(number) = usize::from_str_radix(number, base) {
        Some(number)
    }

    else {
        None
    }
}

fn change_sector_number(mode: ChangeSectorNumMode, cd: &CD, mut cur_sec: usize, mut iter: std::str::Split<&str>) -> usize {
    if let Some(number) = iter.next() {
        let last_sec = cd.last_sector_idx();        

        if let Some(number) = get_number_from_str(number) {
            let new_sec = {
                match mode {
                    ChangeSectorNumMode::Increment => cur_sec + number,
                    ChangeSectorNumMode::Decrement => cur_sec - number,
                    ChangeSectorNumMode::Absolute  => number
                }
            };

            if new_sec <= last_sec {
                cur_sec = new_sec;
            }

            else {
                println!("{} is higher then last valid index which is {}", new_sec, last_sec);
            }
        }

        else {
            println!("{} is no valid number", number);
        }
    }
    
    cur_sec
}

fn write_data(cd: &CD, cur_sec: usize, mut iter: std::str::Split<&str>) -> Result<(), Error> {
    if let Some(number_str) = iter.next() {
        let file_name = {
            let mut file_name = String::new();
            for str_part in iter {
                file_name.push_str(str_part);
                file_name.push(' ');
            }
            file_name
        };

        if !file_name.is_empty() {
            if let Some(number) = get_number_from_str(number_str) {
                let mut file = open_file(Path::new(file_name.as_str()), true)?;
                for i in 0..number {
                    if let Some(sector) = cd.get_sector(cur_sec + i) {
                        file.write(sector.get_data())?;
                    }

                    else {
                        return Err(Error::GenericError(format!("Failed writing sector {} - data incomplete", (cur_sec + i))));
                    }
                }

                Ok(())
            }

            else {
                Err(Error::GenericError(format!("{} is not a valid number", number_str)))
            }
        }

        else {
            Err(Error::GenericError(format!("No output file specified")))
        }
    }

    else {
        Err(Error::GenericError(format!("Sector count not specified")))
    }
}

fn dump(cd: &CD, cur_sec: usize, dump_ecc: bool) {
    if let Some(sector) = cd.get_sector(cur_sec) {
        let header: &dyn std::fmt::Display = {
            if let Some(header) = sector.get_header() {
                header
            }

            else {
                &"<No Header>"
            }            
        };
        let sub_header = sector.get_sub_header();

        println!("{}:", sector.get_friendly_name());
        println!("Header:    {}", header);
        match sub_header {
            Some(sub_header) => println!("SubHeader: {}", sub_header),
            None             => println!("SubHeader: <None>")
        }
        
        let (data_offset, data_size) = sector.get_data_offset_with_size();
        let data_start = (cur_sec*SECTOR_SIZE) + data_offset;

        println!("Data: at 0x{:X} - 0x{:x} ({} Bytes)", data_start, (data_start + data_size - 1), data_size);
        let (edc, ecc) = {
            if let Some(error_cor) = sector.get_error_correction() {
                (Some(error_cor.0), error_cor.1)
            }
            
            else {
                (None, None)
            }
        };
        println!("EDC: {}\nECC:\n{}", {
            if let Some(edc) = &edc {
                edc
            }

            else {
                &"<none>" as &dyn std::fmt::Display
            }
        }, {
            if dump_ecc {
                if let Some(ecc) = &ecc {
                    ecc
                }

                else {
                    &"<none>" as &dyn std::fmt::Display
                }
            }

            else {
                &"<run \"d ecc\">" as &dyn std::fmt::Display
            }
        });
    }
}

fn handle_complex(cd: &CD, cur_sec: usize, mut iter: std::str::Split<&str>) -> Result<(), Error> {
    let handle_dir_parms = |iter| {
        let mut dump_root   = false;
        let mut dump_detail = false;

        for cmd in iter {
            match cmd {
                "root" => dump_root   = true,
                "-d"   => dump_detail = true,
                _      => return Err(Error::GenericError(format!("Unkown option {}", cmd)))
            }
        }

        Ok((dump_root, dump_detail))
    };

    if let Some(cmd) = iter.next() {
        match cmd {
            "pvd"    => complex_dump::dump(&cd),
            "dir"    => {
                let (dump_root, dump_detail) = handle_dir_parms(iter)?;
                
                if dump_root {
                    complex_dump::dump_root_dir_rec(&cd, dump_detail)
                }

                else {
                    complex_dump::dump_dir_rec(&cd, cur_sec, dump_detail)
                }
            },
            "path"   => {
                complex_dump::dump_path_table(&cd)
            },
            "edc"    => complex_dump::check_edc(&cd, cur_sec),
            "ecc"    => complex_dump::check_ecc(&cd, cur_sec),
            "stats"  => complex_dump::dump_stats(&cd),
            "tracks" => complex_dump::dump_tracks(&cd),
            "?"      => {
                println!("pvd Dump Primary Volume Descriptor");
                println!("dir [root] [-d] Dumps current sector as Directory Record or the Root Directory Record");
                println!("path Dumps any PathTable");
                println!("edc ");
                println!("ecc ");
                println!("stats Lists statistics about all sectors");
                println!("tracks Lists all the tracks of this CD");
                return Ok(());
            },
            _   => {
                println!("Unkown command {} - try cplx ?", cmd);
                return Ok(());
            }
        }
    }

    else {
        Err(Error::GenericError("No command found".to_string()))
    }
}

fn main() {
    let file = {
        match std::env::args().skip(1).next()
        {
            Some(file) => file,
            None => {
                println!("psxcdread <path fo file>");
                return;   
            }
        }
    };

    let mut sec_idx = 0;
    let cd = {
        match open_cd(file.as_str()) {
            Ok(cd) => cd,
            Err(error) => {
                println!("Readin CD failed with: {}", error);
                return;
            }
        }
    };
    
    loop {
        match read_input(&cd, sec_idx) {
            Ok(input) => {
                let mut iter = input.trim().split(" ");
                if let Some(cmd) = iter.next() {
                    match cmd {
                        ""     => (),
                        ">"    => sec_idx = change_sector_number(ChangeSectorNumMode::Increment, &cd, sec_idx, iter),
                        "<"    => sec_idx = change_sector_number(ChangeSectorNumMode::Decrement, &cd, sec_idx, iter),
                        "!"    => sec_idx = change_sector_number(ChangeSectorNumMode::Absolute,  &cd, sec_idx, iter),
                        "d"    => dump(&cd, sec_idx, {
                                    if let Some(cmd) = iter.next() {
                                        cmd == "ecc"
                                    }

                                    else {
                                        false
                                    }
                                }),
                        "w"    => {
                            if let Err(txt) = write_data(&cd, sec_idx, iter) {
                                println!("{}", txt);
                            }
                        },
                        "cplx" => {
                            if let Err(txt) = handle_complex(&cd, sec_idx, iter) {
                                println!("{}", txt);
                            }
                        }
                        "q"    => return,
                        "?"    => {
                            println!(">    <number> move number sectors forward");
                            println!("<    <number> move number sectors backward");
                            println!("!    <number> move to sector number");
                            println!("d    dump current sector");
                            println!("cplx <sub command> dumps extended infos");
                            println!("w    <sector count> <file path> writes the data of the sectos to a file");
                            println!("q    exit programm");
                        },
                        _   => println!("Unkown CMD: \"{}\"", cmd)
                    }
                }
            },
            Err(error) => {
                println!("Reading input failed with: {}", error);
                return;
            }
        }
    }
}
