use cdtypes::types::time::Time;
use std::path::PathBuf;
use tool_helper::{format_if_error, path_with_env_from, print_warning, string_with_env_from};
use crate::{config_reader::Directory, types::LicenseFile};

use super::{CDAudioFile, CommonProperties, Configuration, Error, File, FileKind, LZ4State};

mod license_tag {
    pub const NAME: &'static str = "License";
    pub mod attribute {
        pub const ALT_LOGO: &'static str = "alt-logo";
        pub const ALT_TEXT: &'static str = "alt-text";
    }
}

mod root_tag {
    pub const NAME: &'static str = "PSXCD";
    pub mod attribute {}
}

mod filesystem_tag {
    pub const NAME: &'static str = "Filesystem";
    pub mod attribute {
        pub const LEAD_OUT:&'static str = "lead-out";
    }
}

mod file_and_directory_attribute {
    pub const NAME:            &'static str = "name";
    pub const HIDDEN:          &'static str = "hidden";
    pub const PADDED_SIZE:     &'static str = "padded_size";
    pub const LBA_SOURCE:      &'static str = "lba_source";
    pub const LZ4_STATE:       &'static str = "lz4";    
}

mod interleaved_file_tag {
    pub const NAME: &'static str = "InterleavedFile";
    pub mod attribute {}
}

mod cdda_tag {
    pub const NAME: &'static str = "AudioTrack";
    pub mod attribute {}
}

mod alt_license_tag {
    pub const NAME: &'static str = "AltLicense";
    pub mod attribute {
        pub const TEXT: &'static str = "text";
    }
}

pub fn parse(xml: String) -> Result<Configuration, Error> {
    let mut config = Configuration::new();
    let parser     = format_if_error!(roxmltree::Document::parse(xml.as_str()), "Parsing XML file failed with: {error_text}")?;
    let children   = parser.root().children();

    for node in children {
        if node.is_element() && node.tag_name().name() == root_tag::NAME {
            parse_root(node, &mut config)?;
        }
    }

    Ok(config)
}

fn parse_root(root: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
    let mut parsed_track = false;

    for node in root.children() {
        if node.is_element() {
            match node.tag_name().name() {
                "Description"        => parse_description(node, config),
                filesystem_tag::NAME => {
                    if !parsed_track {
                        parsed_track = true;
                        parse_track(node, config)
                    }

                    else {
                        Err(Error::from_text(format!("Found more than 1 filesystem. Multi filesystem discs are not supported")))
                    }                    
                },
                cdda_tag::NAME => parse_cd_audio(node, config),
                _ => Ok(())
            }?;
        }
    }
    Ok(())
}

fn parse_description(description: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
    fn parse_license_path(license: roxmltree::Node) -> (LicenseFile, LicenseFile) {
        let required_lic_file = LicenseFile::from_authentic_option(path_from_node(&license, license_tag::NAME).ok());
        let alt_logo          = {
            if let Some(path) = license.attribute(license_tag::attribute::ALT_LOGO) {
                Some(path_with_env_from(path))
            }

            else {
                None
            }
        };
        let alt_text = license.attribute(license_tag::attribute::ALT_TEXT);
        (required_lic_file, LicenseFile::from_tmd_option(alt_logo, alt_text))
    }

    fn parse_altlicense_path(license: roxmltree::Node) -> LicenseFile {
        LicenseFile::from_tmd_option(path_from_node(&license, alt_license_tag::NAME).ok(), license.attribute(alt_license_tag::attribute::TEXT))
    }

    for node in description.descendants() {
        if node.is_element() {
            match node.tag_name().name() {
                "Publisher"           => config.publisher                                  = Some(String::from(node.text().unwrap_or_default())),
                license_tag::NAME     => (config.license_file, config.backup_license_file) = parse_license_path(node),
                alt_license_tag::NAME => {
                    if matches!(config.license_file, LicenseFile::None) {
                        config.license_file = parse_altlicense_path(node)
                    }

                    else {
                        print_warning("Alternate license ignored because a (alternate) license was already provided".to_owned())
                    }
                },
                _ => ()
            }
        }
    }
    Ok(())
}

fn parse_track(track: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
    fn parse_regular_file(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
        let common = read_common_properties(&file, is_hidden, None)?;
        let path   = path_from_node(&file, &common.name)?;

        Ok(File{common, path, kind: FileKind::Regular})
    }

    fn parse_main_file(file: roxmltree::Node) -> Result<File, Error> {
        if let Some(lba_path) = file.attribute(file_and_directory_attribute::LBA_SOURCE) {
            let common = read_common_properties(&file, false, Some(LZ4State::None))?;
            let path   = path_from_node(&file, &common.name)?;

            Ok(File{common, path, kind: FileKind::Main(PathBuf::from(lba_path))})
        }

        else {
            tool_helper::print_warning(format!("The main file should always contain the \"{}\" attribute, even when just empty", file_and_directory_attribute::LBA_SOURCE));
            parse_regular_file(file, false)
        }
    }

    fn parse_overlay_file(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
        //                                                        v They will be compressed automatically
        let common     = read_common_properties(&file, is_hidden, Some(LZ4State::AlreadyCompressed))?;
        let path       = path_from_node(&file, &common.name)?;
        let lba_source = {
            if let Some(lba_source) = file.attribute(file_and_directory_attribute::LBA_SOURCE) {
                lba_source
            }

            else {
                tool_helper::print_warning(format!("Overlays should always contain the \"{}\" attribute, even when just empty", file_and_directory_attribute::LBA_SOURCE));
                ""
            }
        };
        Ok(File{common, path, kind: FileKind::Overlay(PathBuf::from(lba_source))})
    }

    fn parse_interleaved(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
        //                                                     v Never compress interleaved files
        let common  = read_common_properties(&file, is_hidden, Some(LZ4State::None))?;
        let channel = {
            let mut channel = Vec::new();
            for node in file.children() {
                if node.is_element() && node.tag_name().name() == "Channel" {
                    channel.push(path_from_node(&node, "Channel")?);
                }
            }

            channel
        };

        Ok(File{common, path: PathBuf::new(), kind: FileKind::XAAudio(channel)})
    }

    fn parse_file_system(cur_node: roxmltree::Node, root: &mut Directory, mut is_hidden: bool) -> Result<(), Error> {
        for node in cur_node.children() {
            if node.is_element() {
                match node.tag_name().name() {
                    "File"                     => root.add_file(parse_regular_file(node, is_hidden)?),
                    "Main"                     => root.add_file(parse_main_file(node)?),
                    "Overlay"                  => root.add_file(parse_overlay_file(node, is_hidden)?),
                    interleaved_file_tag::NAME => root.add_file(parse_interleaved(node, is_hidden)?),
                    "Directory"                => {
                        is_hidden |= parse_boolean_attribute(&node, file_and_directory_attribute::HIDDEN)?;
                        let mut new_dir = Directory::new(node.attribute(file_and_directory_attribute::NAME).unwrap_or_default(), is_hidden);

                        parse_file_system(node, &mut new_dir, is_hidden)?;
                        root.add_dir(new_dir);
                    },
                    _ => (),
                }
            }
        }

        Ok(())
    }

    fn get_lead_out_sectors(track: roxmltree::Node) -> Result<Option<usize>, Error> {
        const MIN_OPTION_NAME:&'static str    = "minutes";
        const SECOND_OPTION_NAME:&'static str = "seconds";
        const SECTOR_OPTION_NAME:&'static str = "sectors";

        fn parse_split<'a>(who: &'a str, str_opt: Option<&'a str>) -> Result<u8, Error> {
            match str_opt.unwrap_or("0").parse::<u8>() {
                Ok(num)    => Ok(num),
                Err(error) => Err(Error::from_text(format!("Failed converting \"{}\" option for \"{}\" attribute \"{}\": {}", who, filesystem_tag::NAME, filesystem_tag::attribute::LEAD_OUT, error)))
            }
        }

        //---------------------------------------------------------------------        

        if let Some(length_str) = track.attribute(filesystem_tag::attribute::LEAD_OUT) {
            let mut pieces = length_str.split(':');
            let (mins, seconds, sectors) = (
                parse_split(MIN_OPTION_NAME,    pieces.next())?,
                parse_split(SECOND_OPTION_NAME, pieces.next())?,
                parse_split(SECTOR_OPTION_NAME, pieces.next())?
            );

            Ok(Some(Time::from_mss(mins, seconds, sectors).as_sectors()))
        }

        else {
            Ok(None)
        }
    }

    config.lead_out_sectors = get_lead_out_sectors(track)?;
    parse_file_system(track, &mut config.root, false)
}

fn parse_cd_audio(cdda: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
    config.cd_audio_files.push(CDAudioFile::new(path_from_node(&cdda, "CD Audio file")?, parse_boolean_attribute(&cdda, "align")?));
    Ok(())
}

fn read_common_properties(xml: &roxmltree::Node, is_hidden: bool, force_lz4_state: Option<LZ4State>) -> Result<CommonProperties, Error> {
    let lz4_state = {
        if let Some(forced_lz4_state) = force_lz4_state {
            forced_lz4_state
        }
        
        else {
            if let Some(state_str) = xml.attribute(file_and_directory_attribute::LZ4_STATE) {
                match state_str.to_lowercase().as_str() {
                    "yes"     => LZ4State::Compress,
                    "already" => LZ4State::AlreadyCompressed,
                    _         => LZ4State::None,
                }
            }

            else {
                LZ4State::None
            }
        }
    };

    Ok(CommonProperties{
        name:        string_with_env_from(xml.attribute(file_and_directory_attribute::NAME).unwrap_or_default()), 
        is_hidden:   is_hidden | parse_boolean_attribute(&xml, file_and_directory_attribute::HIDDEN)?,
        lz4_state,
        padded_size: read_padded_size(&xml)? 
    })
}

fn read_padded_size(xml: &roxmltree::Node) -> Result<Option<usize>, Error> {
    if let Some(padded_attr) = xml.attribute(file_and_directory_attribute::PADDED_SIZE) {
        let padded_size = format_if_error!(padded_attr.parse::<usize>(), "Failed reading \"{}\" as padded size: {error_text}", padded_attr)?;
        Ok(Some(padded_size))
    }

    else {
        Ok(None)
    }
}

fn parse_boolean_attribute(xml: &roxmltree::Node, attribute_name: &str) -> Result<bool, Error> {
    if let Some(bool_str) = xml.attribute(attribute_name) {
        match bool_str {
            "true"  | "1" => Ok(true),
            "false" | "0" => Ok(false),
            _ => {
                return Err(Error::from_text(format!("Attribute \"{}\" expects either true,false or 1,0 as valid attributes - found {}", attribute_name, bool_str)));
            }
        }
    }
    
    else {
        Ok(false)
    }
}

fn path_from_node(node: &roxmltree::Node, name: &str) -> Result<PathBuf, Error> {
    if let Some(path) = node.text() {
        Ok(path_with_env_from(path.trim()))
    }

    else {
        Err(Error::from_text(format!("No path specified for {}", name)))
    } 
}