use super::{
    ebi_command::{EBI_COMMANDS, EbiCommand},
    ebi_input::{EbiInput, EbiObjectImporter, EbiTraitImporter},
    ebi_output::{EbiObjectExporter, EbiOutputType},
    ebi_trait::FromEbiTraitObject,
};
use crate::{
    ebi_commands::ebi_command_validate::EBI_VALIDATE,
    ebi_file_handlers::{
        business_process_model_and_notation::EBI_BUSINESS_PROCESS_MODEL_AND_NOTATION, compressed_event_log::EBI_COMPRESSED_EVENT_LOG, deterministic_finite_automaton::EBI_DETERMINISTIC_FINITE_AUTOMATON, directly_follows_graph::EBI_DIRECTLY_FOLLOWS_GRAPH, directly_follows_model::EBI_DIRECTLY_FOLLOWS_MODEL, event_log_csv::EBI_EVENT_LOG_CSV, event_log_ocel::EBI_EVENT_LOG_OCEL, event_log_python::EBI_EVENT_LOG_PYTHON, event_log_xes::EBI_EVENT_LOG_XES, executions::EBI_EXECUTIONS, finite_language::EBI_FINITE_LANGUAGE, finite_stochastic_language::EBI_FINITE_STOCHASTIC_LANGUAGE, finite_stochastic_partially_ordered_language::EBI_FINITE_STOCHASTIC_PARTIALLY_ORDERED_LANGUAGE, labelled_petri_net::EBI_LABELLED_PETRI_NET, language_of_alignments::EBI_LANGUAGE_OF_ALIGNMENTS, lola_net::EBI_LOLA_NET, petri_net_markup_language::EBI_PETRI_NET_MARKUP_LANGUAGE, portable_document_format::EBI_PORTABLE_DOCUMENT_FORMAT, portable_network_graphics::EBI_PORTABLE_NETWORK_GRAPHCIS, process_tree::EBI_PROCESS_TREE, process_tree_markup_language::EBI_PROCESS_TREE_MARKUP_LANGUAGE, scalable_vector_graphics::EBI_SCALABLE_VECTOR_GRAPHICS, stochastic_business_process_model_and_notation::EBI_STOCHASTIC_BUSINESS_PROCESS_MODEL_AND_NOTATION, stochastic_deterministic_finite_automaton::EBI_STOCHASTIC_DETERMINISTIC_FINITE_AUTOMATON, stochastic_directly_follows_model::EBI_STOCHASTIC_DIRECTLY_FOLLOWS_MODEL, stochastic_labelled_petri_net::EBI_STOCHASTIC_LABELLED_PETRI_NET, stochastic_language_of_alignments::EBI_STOCHASTIC_LANGUAGE_OF_ALIGNMENTS, stochastic_nondeterministic_finite_automaton::EBI_STOCHASTIC_NONDETERMINISTIC_FINITE_AUTOMATON, stochastic_process_tree::EBI_STOCHASTIC_PROCESS_TREE
    },
    ebi_framework::{
        ebi_command::get_applicable_commands, ebi_input::EbiObjectImporterFallible,
        ebi_trait::EbiTrait,
    },
    prom::java_object_handler::JavaObjectHandler,
};
use ebi_objects::{
    EbiObjectType,
    anyhow::{Error, Result, anyhow},
};
use std::{collections::BTreeSet, fmt::Display, hash::Hash, io::BufRead, str::FromStr};

/**
 * The order of this list is important: for the "any object" input type and for trait importers,
 * they are attempted in order. Thus, the more restrictive formats should come first.
 */
pub const EBI_FILE_HANDLERS: &'static [EbiFileHandler] = &[
    EBI_COMPRESSED_EVENT_LOG,
    EBI_DIRECTLY_FOLLOWS_GRAPH,
    EBI_DETERMINISTIC_FINITE_AUTOMATON,
    EBI_DIRECTLY_FOLLOWS_MODEL,
    EBI_STOCHASTIC_DIRECTLY_FOLLOWS_MODEL,
    EBI_EXECUTIONS,
    EBI_STOCHASTIC_BUSINESS_PROCESS_MODEL_AND_NOTATION,
    EBI_BUSINESS_PROCESS_MODEL_AND_NOTATION,
    EBI_EVENT_LOG_OCEL,
    EBI_EVENT_LOG_XES,
    EBI_FINITE_LANGUAGE,
    EBI_FINITE_STOCHASTIC_LANGUAGE,
    EBI_FINITE_STOCHASTIC_PARTIALLY_ORDERED_LANGUAGE,
    EBI_LABELLED_PETRI_NET,
    EBI_LANGUAGE_OF_ALIGNMENTS,
    EBI_LOLA_NET,
    EBI_PETRI_NET_MARKUP_LANGUAGE,
    EBI_STOCHASTIC_DETERMINISTIC_FINITE_AUTOMATON,
    EBI_STOCHASTIC_LABELLED_PETRI_NET,
    EBI_PROCESS_TREE,
    EBI_STOCHASTIC_LANGUAGE_OF_ALIGNMENTS,
    EBI_STOCHASTIC_PROCESS_TREE,
    EBI_PROCESS_TREE_MARKUP_LANGUAGE,
    EBI_EVENT_LOG_CSV,
    EBI_PORTABLE_DOCUMENT_FORMAT,
    EBI_SCALABLE_VECTOR_GRAPHICS,
    EBI_PORTABLE_DOCUMENT_FORMAT,
    EBI_PORTABLE_NETWORK_GRAPHCIS,
    EBI_STOCHASTIC_NONDETERMINISTIC_FINITE_AUTOMATON,
    EBI_EVENT_LOG_PYTHON,
];

#[derive(Clone, Debug)]
pub struct EbiFileHandler {
    pub name: &'static str,
    pub article: &'static str, //a or an
    pub file_extension: &'static str,
    
    /// Indicates whether the file format is binary.
    pub is_binary: bool,
    
    pub format_specification: &'static str,
    pub validator: Option<fn(&mut dyn BufRead) -> Result<()>>,

    /// This file format can be imported as the given traits.
    /// The order matters: importers are attempted in order.
    pub trait_importers: &'static [EbiTraitImporter],

    /// This file format can be imported as the given objects.
    /// The order matters: importers are attempted in order, fallible importers last.
    /// These importers should only fail if the parser failed or the underlying reader fails. If there is a conversion involved that may fail, list it under `trait_importers_fallible`.
    pub object_importers: &'static [EbiObjectImporter],

    /// This file format can be imported as the given objects.
    /// The order matters: importers are attempted in order, fallible importers last.
    /// These importers perform a conversion that may fail.
    pub object_importers_fallible: &'static [EbiObjectImporterFallible],

    /// This file format can be exported to from the given objects.
    /// The order matters, because if multiple file handlers can export an object, the one that mentions the object earliest is preferred. Should not fail unless the underlying writer yields an error.
    pub object_exporters: &'static [EbiObjectExporter],

    /// This file format can be exported to from the given objects.
    /// The order matters, because if multiple file handlers can export an object, the one that mentions the object earliest is preferred.
    pub object_exporters_fallible: &'static [EbiObjectExporter],
    pub java_object_handlers: &'static [JavaObjectHandler],
}

impl EbiFileHandler {
    pub fn get_applicable_commands(&self) -> BTreeSet<Vec<&'static EbiCommand>> {
        let mut result = BTreeSet::new();

        for importer in self.trait_importers {
            result.extend(importer.get_trait().get_applicable_commands());
        }
        for importer in self.object_importers {
            result.extend(get_applicable_commands(&importer.get_type()));
        }
        if self.validator.is_some() {
            result.insert(vec![&EBI_COMMANDS, &EBI_VALIDATE]);
        }

        result
    }

    pub fn get_producing_commands(&self) -> BTreeSet<Vec<&'static EbiCommand>> {
        //get objects that can export to this file handler
        let mut objects = vec![];
        for exporter in self.object_exporters {
            objects.push(exporter.get_type());
        }
        for exporter in self.object_exporters_fallible {
            objects.push(exporter.get_type());
        }

        //get commands that can output any of the objects
        let mut result = BTreeSet::new();
        for command_path in EBI_COMMANDS.get_command_paths() {
            if let Some(EbiCommand::Command { output_type, .. }) = command_path.last() {
                if let EbiOutputType::ObjectType(x) = output_type {
                    if objects.contains(x) {
                        result.insert(command_path);
                    }
                }
            }
        }

        result
    }

    pub fn can_import_as_object(&self, object_type: &EbiObjectType) -> Tri {
        for importer in self.object_importers {
            if importer.get_type() == *object_type {
                return Tri::Yes;
            }
        }
        for importer in self.object_importers_fallible {
            if importer.get_type() == *object_type {
                return Tri::Fallible;
            }
        }
        return Tri::No;
    }

    pub fn can_import_as_trait(&self, trait_type: &EbiTrait) -> bool {
        for importer in self.trait_importers {
            if importer.get_trait() == *trait_type {
                return true;
            }
        }
        return false;
    }

    pub fn can_export_as_object(&self, object_type: &EbiObjectType) -> Tri {
        for exporter in self.object_exporters {
            if exporter.get_type() == *object_type {
                return Tri::Yes;
            }
        }
        for exporter in self.object_exporters_fallible {
            if exporter.get_type() == *object_type {
                return Tri::Fallible;
            }
        }
        return Tri::No;
    }
}

impl FromStr for EbiFileHandler {
    type Err = Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        for file_handler in EBI_FILE_HANDLERS {
            if file_handler.validator.is_some()
                && (file_handler.name == s || file_handler.file_extension == s)
            {
                return Ok(file_handler.clone());
            }
        }
        return Err(anyhow!("{} is not an Ebi file handler.", s));
    }
}

impl FromStr for &'static EbiFileHandler {
    type Err = Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        for file_handler in EBI_FILE_HANDLERS {
            if file_handler.validator.is_some()
                && (file_handler.name == s || file_handler.file_extension == s)
            {
                return Ok(file_handler);
            }
        }
        return Err(anyhow!("{} is not an Ebi file handler.", s));
    }
}

impl FromEbiTraitObject for EbiFileHandler {
    fn from_trait_object(object: EbiInput) -> Result<Box<Self>> {
        match object {
            EbiInput::FileHandler(e) => Ok(Box::new(e)),
            _ => Err(anyhow!(
                "cannot read {} {} as an file handler",
                object.get_type().get_article(),
                object.get_type()
            )),
        }
    }
}

impl Eq for EbiFileHandler {}

impl PartialEq for EbiFileHandler {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl PartialOrd for EbiFileHandler {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.name
            .to_lowercase()
            .partial_cmp(&other.name.to_lowercase())
    }
}

impl Ord for EbiFileHandler {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.name.to_lowercase().cmp(&other.name.to_lowercase())
    }
}

impl Hash for EbiFileHandler {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state);
    }
}

impl Display for EbiFileHandler {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} (.{})", self.name, self.file_extension)
    }
}

pub fn get_file_handlers(object_type: &EbiObjectType) -> Vec<&'static EbiFileHandler> {
    let mut result = vec![];
    for file_handler in EBI_FILE_HANDLERS.iter() {
        for importer in file_handler.object_importers {
            if &importer.get_type() == object_type {
                result.push(file_handler);
                break;
            }
        }
    }
    result
}

pub fn get_file_handlers_fallible(object_type: &EbiObjectType) -> Vec<&'static EbiFileHandler> {
    let mut result = vec![];
    for file_handler in EBI_FILE_HANDLERS.iter() {
        for importer in file_handler.object_importers_fallible {
            if &importer.get_type() == object_type {
                result.push(file_handler);
                break;
            }
        }
    }
    result
}

pub enum Tri {
    Yes,
    No,
    Fallible,
}

impl Tri {
    pub fn to_latex_circle(&self) -> &str {
        match self {
            Tri::Yes => "\\CIRCLE",
            Tri::Fallible => "\\LEFTcircle",
            Tri::No => "\\Circle",
        }
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use crate::{
        ebi_file_handlers::{
            executions::EBI_EXECUTIONS, finite_stochastic_language::EBI_FINITE_STOCHASTIC_LANGUAGE,
            process_tree::EBI_PROCESS_TREE,
            stochastic_labelled_petri_net::EBI_STOCHASTIC_LABELLED_PETRI_NET,
        },
        ebi_framework::{
            ebi_file_handler::EbiFileHandler, ebi_input::EbiInput, ebi_trait::FromEbiTraitObject,
        },
    };

    #[test]
    fn file_handlers() {
        assert_eq!(
            EbiFileHandler::from_str("slang").unwrap(),
            EBI_FINITE_STOCHASTIC_LANGUAGE
        );
        assert!(EbiFileHandler::from_str("blablabla44252435").is_err());

        EbiFileHandler::get_producing_commands(&EBI_PROCESS_TREE);
        EbiFileHandler::get_producing_commands(&EBI_STOCHASTIC_LABELLED_PETRI_NET);

        assert!(
            EBI_PROCESS_TREE
                .cmp(&EBI_STOCHASTIC_LABELLED_PETRI_NET)
                .is_lt()
        );

        EbiFileHandler::from_trait_object(EbiInput::FileHandler(EBI_EXECUTIONS)).unwrap();
    }
}
