use super::{
    ebi_file_handler::EbiFileHandler,
    ebi_input::{self, EbiInput, EbiInputType},
    ebi_output::{EbiExporter, EbiOutput, EbiOutputType},
};
use crate::{
    ebi_commands::{
        ebi_command_analyse, ebi_command_analyse_non_stochastic, ebi_command_association,
        ebi_command_conformance, ebi_command_conformance_non_stochastic, ebi_command_convert,
        ebi_command_discover, ebi_command_discover_non_stochastic, ebi_command_filter,
        ebi_command_info,
        ebi_command_itself::{self},
        ebi_command_probability, ebi_command_reduce, ebi_command_sample, ebi_command_test,
        ebi_command_validate, ebi_command_visualise,
    },
    ebi_framework::{ebi_importer_parameters, ebi_output},
    prom::java_object_handler::get_possible_inputs_with_java,
    text::Joiner,
};
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
use ebi_objects::{
    EbiObjectType,
    anyhow::{Context, Result, anyhow},
    ebi_arithmetic::{Fraction, exact::set_exact_globally, parsing::FractionNotParsedYet},
};
use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools;
use log::Level;
use logging_timer::timer;
use std::{
    collections::BTreeSet,
    fmt::{Debug, Display},
    hash::Hash,
    io::Write,
    path::PathBuf,
    time::Duration,
};

pub const EBI_COMMANDS: EbiCommand = EbiCommand::Group {
    name_short: "Ebi",
    name_long: None,
    explanation_short: "Ebi: a tool for stochastic process mining.",
    explanation_long: None,
    children: &[
        &ebi_command_analyse::EBI_ANALYSE,
        &ebi_command_analyse_non_stochastic::EBI_ANALYSE_NON_STOCHASTIC,
        &ebi_command_association::EBI_ASSOCIATION,
        &ebi_command_conformance::EBI_CONFORMANCE,
        &ebi_command_conformance_non_stochastic::EBI_CONFORMANCE_NON_STOCHASTIC,
        &ebi_command_convert::EBI_CONVERT,
        &ebi_command_discover::EBI_DISCOVER,
        &ebi_command_discover_non_stochastic::EBI_DISCOVER_NON_STOCHASTIC,
        &ebi_command_filter::EBI_FILTER,
        &ebi_command_itself::EBI_ITSELF,
        &ebi_command_info::EBI_INFO,
        &ebi_command_probability::EBI_PROBABILITY,
        &ebi_command_reduce::EBI_REDUCE,
        &ebi_command_sample::EBI_SAMPLE,
        &ebi_command_test::EBI_TEST,
        &ebi_command_validate::EBI_VALIDATE,
        &ebi_command_visualise::EBI_VISUALISE,
    ],
};

pub const ARG_SHORT_OUTPUT: char = 'o';
pub const ARG_SHORT_OUTPUT_TYPE: char = 't';
pub const ARG_SHORT_APPROX: char = 'a';
pub const ARG_ID_OUTPUT: &str = "output";
pub const ARG_ID_OUTPUT_TYPE: &str = "output_type";

pub enum EbiCommand {
    Group {
        name_short: &'static str,
        name_long: Option<&'static str>,
        explanation_short: &'static str,
        explanation_long: Option<&'static str>,
        children: &'static [&'static EbiCommand],
    },
    Command {
        /// The short name of the command. Do not use macros in this field, due to Python command-hunting.
        name_short: &'static str,
        name_long: Option<&'static str>,
        explanation_short: &'static str,
        explanation_long: Option<&'static str>,

        /// Latex code to be inserted in the manual.
        latex_link: Option<&'static str>,

        /// Provides access to adapt the cli command. If this is not set to None, the command will be unavailable in other environments than the CLI.
        cli_command: Option<fn(command: Command) -> Command>,

        /// Ff set to true, then Ebi will use exact arithmetic by default, but allow the user to disable it. If false, the command will only be available in approximate mode.
        exact_arithmetic: bool,

        /// for each fixed-position input parameter, the ebi traits that are accepted. If accept_source_file is true, that is the first parameter
        input_types: &'static [&'static [&'static EbiInputType]],
        input_names: &'static [&'static str],
        input_helps: &'static [&'static str],

        execute: fn(inputs: Vec<EbiInput>, cli_matches: Option<&ArgMatches>) -> Result<EbiOutput>, //the cli_matches are provided only when cli_command is set to Some(_).
        output_type: &'static EbiOutputType,
    },
}

impl EbiCommand {
    pub fn build_cli(&self) -> Command {
        let mut command;
        match self {
            EbiCommand::Group {
                name_short,
                name_long,
                explanation_short,
                explanation_long,
                children,
            } => {
                let name = if let Some(x) = name_long {
                    x
                } else {
                    name_short
                };
                command = Command::new(name)
                    .about(explanation_short)
                    .subcommand_required(true)
                    .allow_external_subcommands(false);

                if name_long.is_some() {
                    command = command.alias(name_short);
                }

                if let Some(l) = explanation_long {
                    command = command.long_about(l);
                }

                for child in children.iter() {
                    let subcommand = child.build_cli();
                    command = command.subcommand(subcommand);
                }
            }
            EbiCommand::Command {
                name_short,
                name_long,
                explanation_short,
                explanation_long,
                cli_command,
                exact_arithmetic,
                input_types,
                input_helps: input_help,
                input_names,
                output_type,
                ..
            } => {
                let name = if let Some(x) = name_long {
                    x
                } else {
                    name_short
                };
                command = Command::new(name).about(explanation_short);

                if name_long.is_some() {
                    command = command.alias(name_short);
                }

                if let Some(l) = explanation_long {
                    command = command.long_about(l);
                }

                for (i, (input_name, (input_type, input_help))) in input_names
                    .iter()
                    .zip(input_types.iter().zip(input_help.iter()))
                    .enumerate()
                {
                    let mut arg = Arg::new(format!("{}x{}", input_name, i))
                        .action(ArgAction::Set)
                        .value_name(input_name)
                        .help(input_help)
                        .value_parser(EbiInputType::get_parser_of_list(input_type))
                        .long_help(EbiInputType::possible_inputs_as_strings_with_articles(
                            input_type, " and ",
                        ));

                    if let Some(default) = ebi_input::default(input_type) {
                        arg = arg.required(false).default_value(default);
                    } else {
                        arg = arg.required(true);
                    }

                    command = command.arg(arg);
                }

                if let Some(f) = cli_command {
                    command = (f)(command);
                }

                //output type flag
                if output_type.get_exporters().len() > 1 {
                    let output_extensions = output_type
                        .get_exporters()
                        .iter()
                        .map(|exporter| exporter.get_extension())
                        .collect::<Vec<_>>()
                        .join_with(", ", " and ");
                    command = command.arg(
                        Arg::new(ARG_ID_OUTPUT_TYPE)
                            .short(ARG_SHORT_OUTPUT_TYPE)
                            .long(ARG_ID_OUTPUT_TYPE)
                            .action(ArgAction::Set)
                            .value_name("OUTPUT_TYPE")
                            .help("Specify the output type.")
                            .long_help(format!("Specify the output file extension (without period). The default is {}. Possible values are {}.", output_type.get_default_exporter().get_extension(), output_extensions))
                            .value_parser(value_parser!(String)),
                        );
                };

                //output file flag
                command = command.arg(
                    { let mut arg = Arg::new(ARG_ID_OUTPUT)
                        .short(ARG_SHORT_OUTPUT)
                        .long(ARG_ID_OUTPUT)
                        .action(ArgAction::Set)
                        .value_name("FILE")
                        .help("Saves the result to a file.")
                        .required(false)
                        .value_parser(value_parser!(PathBuf));

                        if output_type.get_exporters().len() > 1 {
                            arg = arg.long_help(format!("Saves the results to a file. The file type is determined by its extension, unless the -{} parameter is also given.", ARG_SHORT_OUTPUT_TYPE))
                        }

                        arg
                    }
                );

                //exact arithmetic flag
                if *exact_arithmetic {
                    command = command.arg(
                        Arg::new("approx")
                            .short(ARG_SHORT_APPROX)
                            .long("approximate")
                            .action(ArgAction::SetTrue)
                            .num_args(0)
                            .help("Use approximate arithmetic instead of exact arithmetic.")
                            .required(false)
                            .value_parser(value_parser!(bool)),
                    )
                }

                //importer parameters
                for (input_index, inputs) in input_types.iter().enumerate() {
                    let merged_importer_parameters =
                        ebi_importer_parameters::merge_importer_parameters(inputs);
                    command = ebi_importer_parameters::build_cli(
                        command,
                        merged_importer_parameters,
                        input_index,
                    );
                }
            }
        };
        return command;
    }

    pub fn short_name(&self) -> &str {
        match self {
            EbiCommand::Group { name_short, .. } => name_short,
            EbiCommand::Command { name_short, .. } => name_short,
        }
    }

    pub fn long_name(&self) -> &str {
        match self {
            EbiCommand::Group {
                name_short,
                name_long,
                ..
            } => match name_long {
                Some(x) => x,
                None => &name_short,
            },
            EbiCommand::Command {
                name_short,
                name_long,
                ..
            } => match name_long {
                Some(x) => x,
                None => &name_short,
            },
        }
    }

    pub fn explanation_short(&self) -> &str {
        match self {
            EbiCommand::Group {
                explanation_short, ..
            } => &explanation_short,

            EbiCommand::Command {
                explanation_short, ..
            } => &explanation_short,
        }
    }

    pub fn explanation_long(&self) -> &str {
        match self {
            EbiCommand::Group {
                explanation_short,
                explanation_long,
                ..
            } => match explanation_long {
                Some(x) => x,
                None => &explanation_short,
            },
            EbiCommand::Command {
                explanation_short,
                explanation_long,
                ..
            } => match explanation_long {
                Some(x) => x,
                None => &explanation_short,
            },
        }
    }

    pub fn get_progress_bar_ticks(total_ticks: usize) -> ProgressBar {
        let pb = ProgressBar::new(total_ticks.try_into().unwrap());
        pb.set_style(
            ProgressStyle::with_template(&("[{wide_bar:.cyan/blue}] {pos:>7}/{len:7}".to_owned()))
                .unwrap()
                .progress_chars("#>-"),
        );
        pb.set_position(0);
        pb
    }

    pub fn get_progress_bar_message(message: String) -> ProgressBar {
        let pb = ProgressBar::new_spinner();
        pb.set_style(ProgressStyle::with_template(&("{spinner} {wide_msg}")).unwrap());
        pb.enable_steady_tick(Duration::from_millis(100));
        pb.set_message(message);
        pb.tick();
        pb
    }

    pub fn execute(&self, cli_matches: &ArgMatches) -> Result<()> {
        match self {
            EbiCommand::Group { children, .. } => {
                for child in children.iter() {
                    if let Some(sub_matches) = cli_matches.subcommand_matches(child.long_name()) {
                        return child.execute(sub_matches);
                    }
                }
            }
            EbiCommand::Command {
                exact_arithmetic,
                input_types: input_typess,
                execute,
                output_type,
                input_names,
                ..
            } => {
                //set exact arithmetic
                if !exact_arithmetic || cli_matches.get_flag("approx") {
                    log::info!("Use approximate arithmetic");
                    set_exact_globally(false);
                }

                //read the inputs
                let mut inputs = vec![];
                for (i, (input_types, input_name)) in
                    input_typess.iter().zip(input_names.iter()).enumerate()
                {
                    let cli_id = format!("{}x{}", input_name, i);

                    //read input
                    log::info!("Reading {}", input_name);
                    let input = Self::attempt_parse(input_types, cli_matches, &cli_id, i)
                        .with_context(|| format!("Reading parameter {}.", input_name))?;
                    log::debug!(
                        "Read {input_name} as {} ({})",
                        input.get_type(),
                        input
                            .importing_file_handler()
                            .map(|x| format!("file handler: {}", x.name))
                            .unwrap_or_else(|| "no file handler".to_string())
                    );
                    inputs.push(input);
                }

                log::info!("Starting {} command", self.long_name());
                let result = {
                    let _tmr = timer!(Level::Info; self.long_name());

                    (execute)(inputs, Some(cli_matches))?
                };

                if &&result.get_type() != output_type {
                    return Err(anyhow!(
                        "Output type {} does not match the declared output of {}.",
                        result.get_type(),
                        output_type
                    ));
                }

                if let Some(to_file) = cli_matches.get_one::<PathBuf>(ARG_ID_OUTPUT) {
                    //write result to file
                    let exporter =
                        Self::select_exporter(output_type, Some(to_file), Some(cli_matches))?;
                    log::info!(
                        "Writing result to {:?} as {} {}",
                        to_file,
                        exporter.get_article(),
                        exporter
                    );
                    ebi_output::export_object(to_file, result, exporter)?;
                } else {
                    //write result to STDOUT
                    let exporter = Self::select_exporter(output_type, None, Some(cli_matches))?;
                    log::info!("Writing result as {} {}", exporter.get_article(), exporter);
                    if exporter.is_binary() {
                        let mut out = std::io::stdout();
                        out.write_all(&ebi_output::export_to_bytes(result, &exporter)?)?;
                        out.flush()?;
                    } else {
                        println!("{}", ebi_output::export_to_string(result, &exporter)?);
                    }
                }

                return Ok(());
            }
        }
        Err(anyhow!("command not recognised"))
    }

    pub fn select_exporter(
        output_type: &EbiOutputType,
        to_file: Option<&PathBuf>,
        cli_matches: Option<&ArgMatches>,
    ) -> Result<EbiExporter> {
        let exporters = output_type.get_exporters();

        if let Some(cli_matches) = cli_matches
            && exporters.len() > 1
        {
            //attempt to read the cli parameter
            if let Some(extension) = cli_matches.get_one::<String>(ARG_ID_OUTPUT_TYPE) {
                for exporter in exporters {
                    if exporter.get_extension() == extension {
                        //found the requested exporter
                        return Ok(exporter);
                    }
                }
                return Err(anyhow!(
                    "the requested output file type {} is not available for this command",
                    extension
                ));
            }
        }

        match to_file {
            Some(file) => {
                //strategy: take the exporter with the longest extension first (to export .xes.gz before .xes)
                {
                    let mut exporters = exporters.clone();
                    exporters.sort_by(|a, b| a.get_extension().len().cmp(&b.get_extension().len()));

                    //see whether an output extension matches the requested file
                    for exporter in exporters {
                        if let EbiExporter::Object(_, file_handler) = exporter {
                            if file
                                .display()
                                .to_string()
                                .ends_with(&(".".to_string() + file_handler.file_extension))
                            {
                                return Ok(exporter);
                            }
                        }
                    }
                }

                //otherwise, take the default one
                return Ok(output_type.get_default_exporter());
            }
            None => {
                return Ok(output_type.get_default_exporter());
            }
        }
    }

    /**
     * Attempt to parse an input as any of the given input types. Returns the last error if unsuccessful.
     */
    pub fn attempt_parse(
        input_types: &[&'static EbiInputType],
        cli_matches: &ArgMatches,
        cli_id: &str,
        input_index: usize,
    ) -> Result<EbiInput> {
        //an input may be of several types; go through each of them
        let mut error = None;
        let mut reader = match ebi_input::get_reader(cli_matches, cli_id).context("Getting reader.")
        {
            Ok(x) => Some(x),
            Err(e) => {
                error = Some(e);
                None
            }
        };

        for input_type in input_types.iter() {
            //try to parse the input as this type
            match input_type {
                EbiInputType::Trait(etrait) => {
                    //try to parse a trait
                    if let Some(ref mut reader) = reader {
                        match ebi_input::read_as_trait(
                            etrait,
                            reader,
                            Some(cli_matches),
                            input_index,
                        )
                        .with_context(|| format!("Parsing as the trait `{}`.", etrait))
                        {
                            Ok((object, file_handler)) => {
                                return Ok(EbiInput::Trait(object, file_handler));
                            }
                            Err(e) => error = Some(e),
                        }
                    }
                }
                EbiInputType::Object(etype) => {
                    //try to parse a specific object
                    if let Some(ref mut reader) = reader {
                        match ebi_input::read_as_object(
                            etype,
                            reader,
                            Some(cli_matches),
                            input_index,
                        )
                        .with_context(|| format!("Parsing as the object type `{}`.", etype))
                        {
                            Ok((object, file_handler)) => {
                                return Ok(EbiInput::Object(object, file_handler));
                            }
                            Err(e) => error = Some(e),
                        }
                    }
                }
                EbiInputType::AnyObject => {
                    if let Some(ref mut reader) = reader {
                        match ebi_input::read_as_any_object(reader, Some(cli_matches), input_index)
                            .context("Parsing as any object.")
                        {
                            Ok((object, file_handler)) => {
                                return Ok(EbiInput::Object(object, file_handler));
                            }
                            Err(e) => error = Some(e),
                        }
                    }
                }
                EbiInputType::FileHandler => {
                    if let Some(value) = cli_matches.get_one::<EbiFileHandler>(&cli_id) {
                        return Ok(EbiInput::FileHandler(value.clone()));
                    }
                }
                EbiInputType::String(None, _) => {
                    if let Some(value) = cli_matches.get_one::<String>(&cli_id) {
                        return Ok(EbiInput::String(value.clone(), &input_type));
                    }
                }
                EbiInputType::String(Some(allowed_values), _) => {
                    if let Some(value) = cli_matches.get_one::<String>(&cli_id) {
                        if allowed_values.contains(&value.as_str()) {
                            return Ok(EbiInput::String(value.clone(), &input_type));
                        } else {
                            error = Some(anyhow!("value should be one of {:?}", allowed_values));
                        }
                    }
                }
                EbiInputType::Usize(min, max, _) => {
                    if let Some(value) = cli_matches.get_one::<usize>(&cli_id) {
                        match (min, max) {
                            (Some(min), Some(max)) => {
                                if value < min || value > max {
                                    error =
                                        Some(anyhow!("Value must be between {} and {}.", min, max))
                                } else {
                                    return Ok(EbiInput::Usize(value.clone(), input_type));
                                }
                            }
                            (Some(min), None) => {
                                if value < min {
                                    error = Some(anyhow!("Value must be below {}.", min))
                                } else {
                                    return Ok(EbiInput::Usize(value.clone(), input_type));
                                }
                            }
                            (None, Some(max)) => {
                                if value > max {
                                    error = Some(anyhow!("Value must be above {}.", max))
                                } else {
                                    return Ok(EbiInput::Usize(value.clone(), input_type));
                                }
                            }
                            (None, None) => {
                                return Ok(EbiInput::Usize(value.clone(), input_type));
                            }
                        }
                    }
                }
                EbiInputType::Fraction(min, max, _) => {
                    if let Some(value) = cli_matches.get_one::<FractionNotParsedYet>(&cli_id) {
                        let value: Fraction = value.try_into()?;

                        match (min, max) {
                            (Some(min), Some(max)) => {
                                if min > &value || max < &value {
                                    error =
                                        Some(anyhow!("Value must be between {} and {}.", min, max))
                                } else {
                                    return Ok(EbiInput::Fraction(value, input_type));
                                }
                            }
                            (Some(min), None) => {
                                if min > &value {
                                    error = Some(anyhow!("Value must be below {}.", min))
                                } else {
                                    return Ok(EbiInput::Fraction(value, input_type));
                                }
                            }
                            (None, Some(max)) => {
                                if max > &value {
                                    error = Some(anyhow!("Value must be above {}.", max))
                                } else {
                                    return Ok(EbiInput::Fraction(value, input_type));
                                }
                            }
                            (None, None) => {
                                return Ok(EbiInput::Fraction(value, input_type));
                            }
                        }
                    }
                }
            }
        }

        match error {
            Some(e) => Err(e),
            None => Err(anyhow!("argument was not given")),
        }
    }

    pub fn path_to_string(path: &[&EbiCommand]) -> String {
        let result: Vec<&str> = path.iter().map(|command| command.long_name()).collect();
        result.join(" ")
    }

    pub fn path_to_short_string(path: &Vec<&EbiCommand>) -> String {
        let result: Vec<&str> = path.iter().map(|command| command.short_name()).collect();
        result.join(" ")
    }

    pub fn find_command_with_string(&self, name: &String) -> Option<Vec<&EbiCommand>> {
        for path in self.get_command_paths() {
            if Self::path_to_string(&path) == *name {
                return Some(path);
            }
        }
        None
    }

    pub fn get_command_paths(&self) -> BTreeSet<Vec<&'static EbiCommand>> {
        let mut result = BTreeSet::new();
        self.get_paths_recursive(&EBI_COMMANDS, &mut result, vec![]);
        result
    }

    fn get_paths_recursive(
        &self,
        command: &'static EbiCommand,
        result: &mut BTreeSet<Vec<&'static EbiCommand>>,
        prefix: Vec<&'static EbiCommand>,
    ) {
        match command {
            EbiCommand::Group { children, .. } => {
                for child in children.iter() {
                    let mut prefix = prefix.clone();
                    prefix.push(command);
                    self.get_paths_recursive(child, result, prefix);
                }
            }
            EbiCommand::Command { .. } => {
                let mut prefix = prefix.clone();
                prefix.push(command);
                result.insert(prefix);
            }
        }
    }

    /// Executes the command using the provided inputs without reading from CLI.
    /// Returns the resulting object.
    pub fn execute_with_inputs(&self, inputs: Vec<EbiInput>) -> Result<EbiOutput> {
        match self {
            EbiCommand::Command {
                execute,
                output_type,
                ..
            } => {
                // Call the command’s execute closure directly.
                // Passing None for ArgMatches since we don’t need any CLI options.
                let result = (execute)(inputs, None)?;
                if &&result.get_type() != output_type {
                    return Err(anyhow!(
                        "Output type {} does not match the declared output of {}.",
                        result.get_type(),
                        output_type
                    ));
                }
                Ok(result)
            }
            _ => Err(anyhow!("Not a command variant.")),
        }
    }

    pub fn is_in_java(&self) -> bool {
        if let EbiCommand::Command {
            cli_command,
            output_type,
            input_types: input_typess,
            ..
        } = &self
        {
            if cli_command.is_some() {
                return false;
            }
            for exporter in output_type.get_exporters() {
                for output_java_object_handler in exporter.get_java_object_handlers() {
                    if let Some(_) = output_java_object_handler.translator_ebi_to_java {
                        let input_typesss = input_typess
                            .iter()
                            .map(|arr| get_possible_inputs_with_java(arr))
                            .collect::<Vec<_>>();
                        for _ in input_typesss.iter().multi_cartesian_product() {
                            return true;
                        }
                    }
                }
            }
            return false;
        } else {
            false
        }
    }

    pub fn is_in_python(&self) -> bool {
        if let EbiCommand::Command {
            cli_command,
            input_types,
            ..
        } = &self
        {
            return cli_command.is_none() && !input_types.is_empty();
        } else {
            false
        }
    }

    pub fn is_in_javascript(&self) -> bool {
        if let EbiCommand::Command {
            cli_command,
            input_types,
            ..
        } = &self
        {
            return cli_command.is_none() && !input_types.is_empty();
        } else {
            false
        }
    }
}

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

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

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

impl Debug for EbiCommand {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Group {
                name_short,
                name_long,
                ..
            } => f
                .debug_struct("Group")
                .field("name_short", name_short)
                .field("name_long", name_long)
                .finish(),
            Self::Command {
                name_short,
                name_long,
                ..
            } => f
                .debug_struct("Command")
                .field("name_short", name_short)
                .field("name_long", name_long)
                .finish(),
        }
    }
}

impl Eq for EbiCommand {}

impl PartialEq for EbiCommand {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (
                Self::Group {
                    name_short: l_name_short,
                    name_long: l_name_long,
                    explanation_short: l_explanation_short,
                    explanation_long: l_explanation_long,
                    children: l_children,
                },
                Self::Group {
                    name_short: r_name_short,
                    name_long: r_name_long,
                    explanation_short: r_explanation_short,
                    explanation_long: r_explanation_long,
                    children: r_children,
                },
            ) => {
                l_name_short == r_name_short
                    && l_name_long == r_name_long
                    && l_explanation_short == r_explanation_short
                    && l_explanation_long == r_explanation_long
                    && l_children == r_children
            }
            (
                Self::Command {
                    name_short: l_name_short,
                    name_long: l_name_long,
                    explanation_short: l_explanation_short,
                    explanation_long: l_explanation_long,
                    latex_link: l_latex_link,
                    cli_command: _,
                    exact_arithmetic: l_exact_arithmetic,
                    input_types: l_input_types,
                    input_names: l_input_names,
                    input_helps: l_input_helps,
                    execute: _,
                    output_type: l_output,
                },
                Self::Command {
                    name_short: r_name_short,
                    name_long: r_name_long,
                    explanation_short: r_explanation_short,
                    explanation_long: r_explanation_long,
                    latex_link: r_latex_link,
                    cli_command: _,
                    exact_arithmetic: r_exact_arithmetic,
                    input_types: r_input_types,
                    input_names: r_input_names,
                    input_helps: r_input_helps,
                    execute: _,
                    output_type: r_output,
                },
            ) => {
                l_name_short == r_name_short
                    && l_name_long == r_name_long
                    && l_explanation_short == r_explanation_short
                    && l_explanation_long == r_explanation_long
                    && l_latex_link == r_latex_link
                    && l_exact_arithmetic == r_exact_arithmetic
                    && l_input_types == r_input_types
                    && l_input_names == r_input_names
                    && l_input_helps == r_input_helps
                    && l_output == r_output
            }
            _ => false,
        }
    }
}

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

pub fn get_applicable_commands(object_type: &EbiObjectType) -> BTreeSet<Vec<&'static EbiCommand>> {
    let mut result = EBI_COMMANDS.get_command_paths();
    result.retain(|path| {
        if let EbiCommand::Command { input_types, .. } = path[path.len() - 1] {
            for input_typess in input_types.iter() {
                for input_typesss in input_typess.iter() {
                    if input_typesss == &&EbiInputType::AnyObject
                        || input_typesss == &&EbiInputType::Object(object_type.clone())
                    {
                        return true;
                    }
                }
            }
        }
        false
    });
    result
}

#[cfg(any(feature = "python", feature = "test_generation"))]
pub(crate) fn search_command_in_source_files(path: &Vec<&EbiCommand>) -> Result<String> {
    //start the search from the EBI_COMMANDS const
    let mut path = path.clone();
    path.remove(0);
    let (mut last_file, mut command_declaration) = root_command()?;

    //walk over the path of commands to find the file and const name that belongs to the lowest/actual command
    for child_command in path {
        (last_file, command_declaration) = search_child(&command_declaration, child_command)?;
    }

    let mut file_name = last_file.file_name().unwrap().to_str().unwrap().to_owned();
    file_name.pop();
    file_name.pop();
    file_name.pop();
    Ok(format!(
        "crate::ebi_commands::{}::{}",
        file_name, command_declaration.ident
    ))
}

#[cfg(any(feature = "python", feature = "test_generation"))]
fn root_command() -> Result<(PathBuf, syn::ItemConst)> {
    let file = "src/ebi_framework/ebi_command.rs";
    let contents = std::fs::read_to_string(file)?;
    let syn_file = syn::parse_file(&contents)?;
    for item in &syn_file.items {
        if let syn::Item::Const(const_item) = item {
            if let syn::Type::Path(x) = &*const_item.ty {
                if let Some(j) = x.path.get_ident() {
                    if j.to_string() == "EbiCommand"
                        && const_item.ident.to_string() == "EBI_COMMANDS"
                    {
                        return Ok((PathBuf::from(file), const_item.clone()));
                    }
                }
            }
        }
    }
    return Err(anyhow!("source file not found"));
}

#[cfg(any(feature = "python", feature = "test_generation"))]
fn search_child(
    parent_declaration: &syn::ItemConst,
    child_command: &EbiCommand,
) -> Result<(PathBuf, syn::ItemConst)> {
    let children_const_names = extract_children_names(parent_declaration)?;

    let child_short_name = child_command.short_name();

    let paths = std::fs::read_dir("src/ebi_commands")?;
    for entry in paths {
        let entry = entry?;
        let meta = entry.metadata()?;

        if meta.is_file() {
            let contents = std::fs::read_to_string(entry.path())?;
            let file = syn::parse_file(&contents)?;
            for item in &file.items {
                if let syn::Item::Const(const_item) = item {
                    if children_const_names.contains(&const_item.ident.to_string()) {
                        if let syn::Type::Path(x) = &*const_item.ty {
                            if let Some(j) = x.path.get_ident() {
                                if j.to_string() == "EbiCommand" {
                                    //find the short name
                                    if let syn::Expr::Struct(x) = &*const_item.expr {
                                        for field in &x.fields {
                                            if let syn::Member::Named(field_name) = &field.member {
                                                if field_name.to_string() == "name_short" {
                                                    if let syn::Expr::Lit(lit) = &field.expr {
                                                        if let syn::Lit::Str(str) = &lit.lit {
                                                            if str.value() == child_short_name {
                                                                // println!(
                                                                //     "\tfound EbiCommand {} with short name {}",
                                                                //     const_item.ident,
                                                                //     child_short_name
                                                                // );
                                                                return Ok((
                                                                    entry.path(),
                                                                    const_item.clone(),
                                                                ));
                                                            }
                                                        }
                                                    }
                                                }
                                            } else {
                                                return Err(anyhow!("unexpected field"));
                                            }
                                        }
                                    } else {
                                        return Err(anyhow!("unexpected expr"));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return Err(anyhow!("todo file not found {}", child_command));
}

#[cfg(any(feature = "python", feature = "test_generation"))]
fn extract_children_names(const_item: &syn::ItemConst) -> Result<Vec<String>> {
    if let syn::Expr::Struct(x) = &*const_item.expr {
        for field in &x.fields {
            if let syn::Member::Named(field_name) = &field.member {
                if field_name.to_string() == "children" {
                    if let syn::Expr::Reference(refe) = &field.expr {
                        if let syn::Expr::Array(arr) = &*refe.expr {
                            let mut result = vec![];
                            for child in &arr.elems {
                                if let syn::Expr::Reference(child_ref) = child {
                                    if let syn::Expr::Path(command_name) = &*child_ref.expr {
                                        result.push(
                                            command_name
                                                .path
                                                .segments
                                                .last()
                                                .unwrap()
                                                .ident
                                                .to_string(),
                                        );
                                    } else {
                                        return Err(anyhow!("unexpected child found"));
                                    }
                                } else {
                                    return Err(anyhow!("unexpected child found"));
                                }
                            }
                            return Ok(result);
                        } else {
                            return Err(anyhow!("unexpected child found"));
                        }
                    } else {
                        return Err(anyhow!("unexpected child found"));
                    }
                }
            }
        }
    }
    Err(anyhow!("cannot extract children"))
}

#[cfg(test)]
pub(crate) mod tests {
    use super::{EBI_COMMANDS, EbiCommand};
    use std::collections::HashSet;

    #[test]
    fn build_cli() {
        EBI_COMMANDS.build_cli();
    }

    #[test]
    fn basic_calls() {
        for command in EBI_COMMANDS.get_command_paths() {
            command.first().unwrap().short_name();
            command.first().unwrap().explanation_long();
            command.last().unwrap().short_name();
            command.last().unwrap().explanation_long();
            EbiCommand::path_to_short_string(&command);
            command.last().unwrap().is_in_java();
            command.first().unwrap().is_in_java();
            command.last().unwrap().to_string();
            let _ = format!("{:?}", command.first().unwrap());
            let _ = format!("{:?}", command.last().unwrap());
            let _ = command
                .first()
                .unwrap()
                .partial_cmp(&command.last().unwrap());
            let _ = command.first().unwrap().eq(command.last().unwrap());
            let _ = command.first().unwrap().eq(command.first().unwrap());
            let _ = command.last().unwrap().eq(command.last().unwrap());
            let mut hash = HashSet::new();
            hash.insert(command.last().unwrap());
        }
    }
}
