use crate::{
    ebi_framework::{
        ebi_command::{EBI_COMMANDS, EbiCommand},
        ebi_file_handler::EBI_FILE_HANDLERS,
        ebi_input::EbiInputType,
        ebi_output::EbiOutput,
    },
    prom::java_object_handler::{JavaInputTypes, JavaObjectHandler, get_possible_inputs_with_java},
    text::JavaEscaper,
};
use anyhow::Result;
use itertools::Itertools;
use std::io::Write;
use strum::IntoEnumIterator;

/**
 * Print the classes that are necessary to call Ebi from Java/ProM.
 */
pub fn print_java_plugins() -> Result<EbiOutput> {
    let mut f = vec![];

    //header with imports
    writeln!(f, "package org.processmining.ebi.plugins;\n")?;
    writeln!(
        f,
        "import org.deckfour.uitopia.api.event.TaskListener.InteractionResult;"
    )?;
    writeln!(
        f,
        "import org.processmining.contexts.uitopia.UIPluginContext;"
    )?;
    writeln!(
        f,
        "import org.processmining.contexts.uitopia.annotations.UITopiaVariant;"
    )?;
    writeln!(f, "import org.processmining.ebi.CallEbi;")?;
    writeln!(
        f,
        "import org.processmining.framework.plugin.PluginContext;"
    )?;
    writeln!(
        f,
        "import org.processmining.framework.plugin.annotations.Plugin;"
    )?;
    writeln!(
        f,
        "import org.processmining.framework.plugin.annotations.PluginCategory;"
    )?;
    writeln!(
        f,
        "import org.processmining.framework.plugin.annotations.PluginLevel;"
    )?;
    writeln!(
        f,
        "import org.processmining.framework.plugin.annotations.PluginVariant;"
    )?;
    writeln!(
        f,
        "import org.processmining.plugins.InductiveMiner.plugins.dialogs.IMMiningDialog;\n"
    )?;
    writeln!(
        f,
        "\n/**\n * This file is automatically generated by Ebi. Do not edit it manually.\n * @author sander\n *\n */"
    )?;
    writeln!(f, "public class EbiPlugins {{\n")?;

    for path in EBI_COMMANDS.get_command_paths() {
        if let Some(EbiCommand::Command {
            explanation_short,
            cli_command,
            input_types: input_typess,
            output_type,
            input_helps,
            ..
        }) = path.last()
        {
            writeln!(
                f,
                "\n\n// == command {} == \n",
                EbiCommand::path_to_string(&path)
            )?;

            if cli_command.is_some() {
                writeln!(
                    f,
                    "\t//command cannot be called from Java as it takes non-standard input from the command line\n"
                )?;
                continue;
            }

            for exporter in output_type.get_exporters() {
                //function name
                let ebi_function_name = EbiCommand::path_to_string(&path).escape_java_code();
                let java_exporter_name = exporter.get_name().to_string().escape_java_code();

                //output
                for output_java_object_handler in exporter.get_java_object_handlers() {
                    if let Some(output_translator) =
                        output_java_object_handler.translator_ebi_to_java
                    {
                        //inputs (create one function for each combination of inputs in the cartesian product)
                        let input_typesss = input_typess
                            .iter()
                            .map(|arr| get_possible_inputs_with_java(arr))
                            .collect::<Vec<_>>();
                        for inputs_java_object_handler in
                            input_typesss.iter().multi_cartesian_product()
                        {
                            let inputs_java_object_handler_without_gui = inputs_java_object_handler
                                .iter()
                                .filter(|java_object_handler| {
                                    java_object_handler.input_gui.is_none()
                                })
                                .collect::<Vec<_>>();
                            let inputs_java_object_handler_with_gui = inputs_java_object_handler
                                .iter()
                                .enumerate()
                                .filter_map(|(i, java_object_handler)| {
                                    if java_object_handler.input_gui.is_some() {
                                        Some((i, java_object_handler))
                                    } else {
                                        None
                                    }
                                })
                                .collect::<Vec<_>>();
                            // let java_plugin_inputs = inputs_java_object_handler.iter().enumerate().map(|(i, input)| format!(", {} input_{}", input.java_class, i)).join("");
                            let java_plugin_inputs_without_gui =
                                inputs_java_object_handler_without_gui
                                    .iter()
                                    .enumerate()
                                    .map(|(i, input)| format!(", {} input_{}", input.java_class, i))
                                    .join("");
                            let command = EbiCommand::path_to_string(&path);
                            let output_extension = exporter.get_extension();
                            let has_gui = inputs_java_object_handler
                                .iter()
                                .any(|java_object_handler| java_object_handler.input_gui.is_some());
                            let mut parameterlabels = inputs_java_object_handler_without_gui
                                .iter()
                                .map(|java_object_handler| {
                                    java_object_handler.name.escape_java_string()
                                });

                            //create function
                            let java_function_name = format!(
                                "{}__as__{}__to__{}",
                                ebi_function_name,
                                java_exporter_name,
                                output_java_object_handler.name
                            );
                            let java_function_inputs = inputs_java_object_handler
                                .iter()
                                .enumerate()
                                .map(|(i, input)| format!(", {} input_{}", input.java_class, i))
                                .join("");
                            writeln!(
                                f,
                                "\tpublic static {} {}(PluginContext context{}) throws Exception {{",
                                output_java_object_handler.java_class,
                                java_function_name,
                                java_function_inputs
                            )?;

                            //function body
                            let inputs_in_function = inputs_java_object_handler
                                .iter()
                                .enumerate()
                                .map(|(i, input)| {
                                    format!(
                                        "{}(context, input_{})",
                                        input.translator_java_to_ebi.unwrap(),
                                        i
                                    )
                                })
                                .join(", ");
                            writeln!(
                                f,
                                "\t\tString result = CallEbi.call_ebi(\"{}\", \".{}\", new String[] {{{}}});",
                                command, output_extension, inputs_in_function
                            )?;
                            writeln!(f, "\t\treturn {}(context, result);", output_translator)?;
                            writeln!(f, "\t}}\n")?;

                            //create ProM plug-in
                            writeln!(f, "\t@Plugin(")?;
                            let java_plugin_name = format!("prom_{}", java_function_name);
                            let abbreviated_inputs_without_gui =
                                inputs_java_object_handler_without_gui
                                    .iter()
                                    .map(|java_object_handler| java_object_handler.name)
                                    .collect::<Vec<_>>();
                            writeln!(
                                f,
                                "\t\tname = \"{} (input: {}; output: {})\",",
                                explanation_short.escape_java_string(),
                                abbreviated_inputs_without_gui
                                    .join(", ")
                                    .escape_java_string(),
                                output_java_object_handler.name.escape_java_string()
                            )?;
                            writeln!(f, "\t\tlevel = PluginLevel.PeerReviewed, ")?;
                            writeln!(
                                f,
                                "\t\treturnLabels = {{ \"{}\" }}, ",
                                output_java_object_handler.name.escape_java_string()
                            )?;
                            writeln!(
                                f,
                                "\t\treturnTypes = {{ {}.class }},",
                                output_java_object_handler.java_class
                            )?;
                            writeln!(
                                f,
                                "\t\tparameterLabels = {{ \"{}\" }},",
                                parameterlabels.join("\", \"")
                            )?;
                            writeln!(f, "\t\tuserAccessible = true,")?;
                            writeln!(
                                f,
                                "\t\tcategories = {{ PluginCategory.Discovery, PluginCategory.Analytics, PluginCategory.ConformanceChecking }},"
                            )?;
                            writeln!(
                                f,
                                "\t\thelp = \"{} (calls Ebi)\"",
                                path.last().unwrap().explanation_long().escape_java_string()
                            )?;
                            writeln!(f, "\t)")?;
                            writeln!(
                                f,
                                "\t@UITopiaVariant(affiliation = IMMiningDialog.affiliation, author = IMMiningDialog.author, email = IMMiningDialog.email)"
                            )?;
                            writeln!(
                                f,
                                "\t@PluginVariant(variantLabel = \"Call Ebi\", requiredParameterLabels = {{ {} }})",
                                (0..inputs_java_object_handler_without_gui.len()).join(", ")
                            )?;
                            if !has_gui {
                                //non-gui plug-in
                                writeln!(
                                    f,
                                    "\tpublic {} {}(PluginContext context{}) throws Exception {{",
                                    output_java_object_handler.java_class,
                                    java_plugin_name,
                                    java_plugin_inputs_without_gui
                                )?;
                            } else {
                                //gui plug-in
                                writeln!(
                                    f,
                                    "\tpublic {} {}(UIPluginContext context{}) throws Exception {{",
                                    output_java_object_handler.java_class,
                                    java_plugin_name,
                                    java_plugin_inputs_without_gui
                                )?;
                                writeln!(f, "\t\tEbiDialog dialog = new EbiDialog();")?;
                                for (i, java_object_handler) in &inputs_java_object_handler_with_gui
                                {
                                    writeln!(
                                        f,
                                        "\t\tdialog.add_input({}(\"{}\"));",
                                        java_object_handler.input_gui.unwrap(),
                                        input_helps[*i].escape_java_string()
                                    )?;
                                }
                                writeln!(
                                    f,
                                    "\t\tInteractionResult result = context.showWizard(\"{}\", true, true, dialog);\n",
                                    explanation_short.escape_java_string()
                                )?;
                                writeln!(f, "\t\tif (result != InteractionResult.FINISHED) {{")?;
                                writeln!(f, "\t\t\tcontext.getFutureResult(0).cancel(false);")?;
                                writeln!(f, "\t\t\treturn null;")?;
                                writeln!(f, "}}")?;

                                for (j, (i, java_object_handler)) in
                                    inputs_java_object_handler_with_gui.iter().enumerate()
                                {
                                    writeln!(
                                        f,
                                        "\t\t{} input_{} = dialog.get_parameter_{}({});",
                                        java_object_handler.java_class,
                                        i,
                                        java_object_handler
                                            .java_class
                                            .to_string()
                                            .escape_java_code(),
                                        j
                                    )?;
                                }
                            }

                            let inputs_to_call_function = (0..inputs_java_object_handler.len())
                                .map(|i| format!(", input_{}", i))
                                .join("");
                            writeln!(
                                f,
                                "\t\treturn {}(context{});",
                                java_function_name, inputs_to_call_function
                            )?;
                            writeln!(f, "\t}}\n")?;
                        }
                    }
                }
            }
        }
    }

    //It is important that every function declared in Rust is implemented in Java, even if it is not actually used (yet).
    //Therefore, we add a function that is never called but nevertheless has every java_object_handler interface in it.
    //Then, the Java compiler will complain, rather than the error showing during testing or at runtime.
    writeln!(f, "@SuppressWarnings(\"unused\")")?;
    writeln!(
        f,
        "\n\tprivate static void call_every_ebi_to_java_translator(PluginContext context) throws Exception {{"
    )?;
    for file_handler in EBI_FILE_HANDLERS {
        for java_object_handler in file_handler.java_object_handlers {
            java_compiler_trigger(&mut f, java_object_handler)?;
        }
    }
    for input_type in EbiInputType::iter() {
        for java_object_handler in input_type.get_java_object_handlers() {
            java_compiler_trigger(&mut f, java_object_handler)?;
        }
    }
    writeln!(f, "\t}}\n")?;

    writeln!(f, "}}")?;

    Ok(EbiOutput::String(String::from_utf8(f).unwrap()))
}

fn java_compiler_trigger(f: &mut Vec<u8>, java_object_handler: &JavaObjectHandler) -> Result<()> {
    writeln!(f, "\t\t{{")?;

    if let Some(translator_ebi_to_java) = java_object_handler.translator_ebi_to_java {
        writeln!(
            f,
            "\t\t\t{} input = {}(context, \"\");",
            java_object_handler.java_class, translator_ebi_to_java
        )?;
    } else {
        writeln!(f, "\t\t\t{} input = null;", java_object_handler.java_class)?;
    }

    if let Some(translator_java_to_ebi) = java_object_handler.translator_java_to_ebi {
        writeln!(f, "\t\t\t{}(context, input);", translator_java_to_ebi)?;
    }

    Ok(writeln!(f, "\t\t}}")?)
}
