package de.renew.plugin.command;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import de.renew.plugin.IPlugin;
import de.renew.plugin.PluginManager;
import de.renew.plugin.PluginProperties;


/**
 * This command loads a plugin from the given URL. If the argument "-v" is
 * given, verbose output is printed.
 *
 * @author Jörn Schumacher
 * @author Michael Simon
 */
public class LoadCommand implements InteractiveCLCommand {
    /**
     * Logger for logging purposes as specified by Apache Log4j
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(LoadCommand.class);

    @Override
    public void execute(String[] args, PrintStream response) {
        execute(args, response, null);
    }

    @Override
    public void execute(
        String[] args, PrintStream response, InputSupplier additionalInputSupplier)
    {
        if (args.length != 1 || isHelpArgument(args[0])) {
            printUsage(response);
            return;
        }

        URL url = null;
        File parameterFile = null;
        final String parameter = args[0];

        response.println("\n\n------- LOAD -------\n\n");

        if (!parameter.contains("*")) {
            try {
                // Try to interpret the parameter as an URL:
                URL tmpURL = new URL(parameter);
                parameterFile = getFile(tmpURL);
                if (parameterFile == null) {
                    // The URL does not point to a local file.
                    // Use it directly:
                    url = tmpURL;
                }
            } catch (MalformedURLException e) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(LoadCommand.class.getSimpleName() + ": Ignoring exception: " + e);
                }
                // The parameter could not be interpreted as an URL.
                // Treat it as a file path instead:
                parameterFile = new File(parameter);
            }
        }

        // Is the URL already determined?
        if (url != null) {
            // Yes, because it does not point to a local file.
            // Is it already loaded?
            if (isLoaded(url, response)) {
                // Abort.
                url = null;
            }
        } else {
            // No. Determine the right URL...
            try {
                if (parameterFile != null && parameterFile.isAbsolute() && parameterFile.exists()) {
                    // The file exists and is a file (not a directory).
                    // Is it already loaded?
                    if (!isLoaded(parameterFile, response)) {
                        // Not loaded.
                        url = createURL(parameterFile);
                        if (LOGGER.isDebugEnabled()) {
                            LOGGER.debug(
                                LoadCommand.class.getSimpleName() + ": directly found URL: " + url);
                        }
                    }

                    // Otherwise it is already loaded: abort.
                } else {
                    // The file does not exist directly.
                    // Search for it in the plug-in locations...
                    List<File> pluginLocations = getPluginLocations();
                    List<File> matches =
                        getMatchingPlugins(pluginLocations, parameterFile, parameter);

                    if (matches.size() > 1) {
                        // Multiple matches.
                        printMatches(response, matches, parameter);

                        if (additionalInputSupplier != null) {
                            int choice =
                                selectFile(response, matches, parameter, additionalInputSupplier);
                            if (choice >= 0) {
                                url = createURL(matches.get(choice));
                            } else {
                                // Aborted by user.
                                return;
                            }
                        } else {
                            response.println(
                                "Multiple matches found for " + parameter
                                    + ". Could not load plugin.");
                            return;
                        }
                        // Let the user select a file:
                    } else if (matches.size() == 1) {
                        // Single match.
                        final File file = matches.get(0);

                        // Is the plug-in already loaded?
                        if (!isLoaded(file, response)) {
                            // Not loaded.
                            url = createURL(file);
                        }

                        // Otherwise it is already loaded: abort.
                    } else {
                        // No match.
                        response.println("No match found for " + parameter + ".");
                    }
                }
            } catch (Exception e) {
                response.println("load failed: " + e);
                LOGGER.error(e.getMessage(), e);
                url = null;
            }
        }

        if (url == null) {
            response.println("Stop command 'load " + args[0] + "'.");
            return;
        }

        LOGGER
            .debug(LoadCommand.class.getSimpleName() + ": Trying to load plugin from URL: " + url);

        IPlugin plugin = PluginManager.getInstance().loadPlugin(url);
        if (plugin != null) {
            response.println("\nPlug-in: \t" + plugin.getName() + "\t successfully loaded.\n");
        } else {
            response.println("Plug-in (" + url + ") not loaded. See log messages for details.");
        }
    }

    private static boolean isHelpArgument(String arg) {
        return Arrays.asList("h", "-h", "--help", "--h", "-help").contains(arg);
    }

    private static void printUsage(PrintStream response) {
        response.println("""
            usage: load <url>
            examples:\s
            load file:/path/to/location/name/plugin.cfg
            load file:/path/to/location/pluginname.jar
            load pluginname.jar
            load pluginname*
            load *pluginname*""");
    }

    private static List<File> getPluginLocations() throws IOException, URISyntaxException {
        String pluginLocationProp = PluginProperties.getUserProperties()
            .getProperty(PluginManager.PLUGIN_LOCATIONS_PROPERTY);
        List<File> pluginLocations = new ArrayList<>();

        if (pluginLocationProp != null) {
            String[] pluginLocationNames = pluginLocationProp.split(File.pathSeparator);
            for (String string : pluginLocationNames) {
                pluginLocations.add(new File(string).getCanonicalFile());
            }
        }
        File loaderLocation =
            new File(new File(PluginManager.getLoaderLocation().toURI()), "plugins")
                .getCanonicalFile();
        if (!pluginLocations.contains(loaderLocation)) {
            pluginLocations.add(loaderLocation);
        }
        return pluginLocations;
    }

    private static List<File> getMatchingPlugins(
        List<File> pluginLocations, File parameterFile, String parameter)
    {
        List<File> matches = new ArrayList<>();
        for (File pluginLocation : pluginLocations) {
            LOGGER.debug(
                LoadCommand.class.getSimpleName() + ": Plugin Location found: "
                    + pluginLocation.getAbsolutePath());
            if (!pluginLocation.exists()) {
                continue;
            }

            // Was the argument given without a trailing '*'?
            if (parameterFile != null) {
                // Yes. Search for it directly in the plug-in locations.
                File file = new File(pluginLocation, parameterFile.getPath());
                if (file.exists()) {
                    // Compile the results from all plug-in locations.
                    matches.add(file);
                }
            } else {
                // No. Compile possible candidates...
                File[] files = pluginLocation.listFiles();
                if (files == null) {
                    continue;
                }
                for (File file : files) {
                    String filenameLower = file.getName().toLowerCase();
                    String parameterLower = parameter.toLowerCase();

                    // create regex pattern from parameter with wildcards
                    String pattern = Arrays.stream(parameterLower.split("\\*")).map(Pattern::quote)
                        .collect(Collectors.joining(".*"));
                    if (parameterLower.endsWith("*")) {
                        pattern += ".*";
                    }

                    if (filenameLower.matches(pattern)) {
                        LOGGER.debug(
                            LoadCommand.class.getSimpleName() + ": Adding to matched plugin list: "
                                + file.getAbsolutePath());
                        if (file.isDirectory()) {
                            File pluginFile = new File(file, "plugin.cfg");
                            if (pluginFile.exists() && !matches.contains(pluginFile)) {
                                matches.add(pluginFile);
                            }
                        } else {
                            if (!matches.contains(file)) {
                                matches.add(file);
                            }
                        }
                    }
                }
            }
        }
        return matches;
    }

    /**
     * Creates URL path for given File
     * @param file The file for which a path is to be created
     * @return A URL to the given file
     * @throws MalformedURLException If an error occurs during URL creation
     */
    private static URL createURL(File file) throws MalformedURLException {
        return file.toURI().toURL();
    }

    /**
     * If the {@link URL} points to a local file, return it.
     *  @param url The URL at which a file is expected to be found
     * @return the local file or {@code null}, if there is none
     */
    private static File getFile(URL url) {
        if ("file".equals(url.getProtocol())) {
            // The URL points to a local file.
            return new File(url.getFile());
        } else {
            return null;
        }
    }

    /**
     * Helper Method used in {@link #execute(String[], PrintStream, InputSupplier)} to allow user to select plugins/files to load
     * in CL that match given criteria and return them to calling CL.
     * @param out Calling CL PrintStream
     * @param matches Plugins/Files to choose from
     * @param prompt String representation of first argument used in execute
     * @param additionalInputSupplier  a function that can be called to ask for additional user input
     * @return Selection made by user or -1 if an error occurs or the user aborts
     */
    private static int selectFile(
        PrintStream out, List<File> matches, String prompt, InputSupplier additionalInputSupplier)
    {
        out.print("Press enter to stop loading plug-in, or type selection number: ");
        String selection;
        String stop = "Stop  command 'load " + prompt + "'.";
        try {
            selection = additionalInputSupplier.get();
        } catch (IOException e) {
            out.println("Could not load selected plug-in due to exception : " + e + ".");
            out.println(stop);
            return -1;
        }
        if (selection != null && !selection.trim().isEmpty()) {
            selection = selection.trim();
            try {
                int choice = Integer.parseInt(selection) - 1;
                if (choice < 0 || choice >= matches.size()) {
                    out.println("Selection " + selection + " not known.");
                    out.println(stop);
                    return -1;
                } else {
                    final File file = matches.get(choice);
                    if (file == null) {
                        out.println("Selection " + choice + " not known");
                        out.println(stop);
                        return -1;
                    } else if (isLoaded(file, out)) {
                        out.println(stop);
                        return -1;
                    } else {
                        return choice;
                    }
                }
            } catch (Exception e) {
                out.println("Selection " + selection + " not known.");
                out.println(stop);
                return -1;
            }
        }
        out.println("No selection made.");
        out.println(stop);
        return -1;
    }

    private static void printMatches(PrintStream out, List<File> matches, String prompt) {
        int maxLength = calculateMaxLength(matches);
        String columnGap = " ".repeat(3);

        out.println("The following plug-ins were found for " + prompt + " : \n");
        String title = " Selection" + columnGap + String.format("%-" + maxLength + "s", "Path")
            + columnGap + "Status ";
        out.println(title);
        out.println("-".repeat(title.length()));

        for (int i = 0; i < matches.size(); i++) {
            File file = matches.get(i);
            String absolutePath = file.getAbsolutePath();
            String numberFormat = String.format("%9d", i + 1);
            String status = isLoaded(file) ? "loaded" : "  --";
            out.println(
                " " + numberFormat + columnGap + String.format("%-" + maxLength + "s", absolutePath)
                    + columnGap + status);

        }
        out.println();
    }

    private static int calculateMaxLength(List<File> files) {
        return files.stream().map(File::getAbsolutePath).mapToInt(String::length).max().orElse(0);
    }

    /**
     * Whether the plug-in at {@code address} is already loaded.
     *
     * @param address must be a {@link File} or {@link URL} instance
     * @return whether the plug-in is already loaded
     *
     * @author Michael Simon
     */
    private static boolean isLoaded(Object address) {
        return getPlugin(address) != null;
    }

    /**
     * Returns the same as {@link #isLoaded(Object)}, but writes a notification to {@code ps},
     * if the plug-in is already loaded.
     *
     * @param address must be a {@link File} or {@link URL} instance
     * @param ps the stream to write the notification to
     * @return whether the plug-in is already loaded
     *
     * @author Michael Simon
     */
    private static boolean isLoaded(Object address, PrintStream ps) {
        IPlugin plugin = getPlugin(address);

        String addressName;
        if (address instanceof File file) {
            addressName = file.getName();
            if ("plugin.cfg".equals(addressName)) {
                addressName = file.getParentFile().getName();
            }
        } else {
            addressName = address.toString();
        }

        if (plugin != null) {
            ps.println(
                "\n\tPlug-in \t" + plugin.getName() + " [" + addressName + "]\t already loaded.\n");
            return true;
        } else {
            return false;
        }
    }

    /**
     * Find a plug-in with the same path or URL as the given file.
     *
     * @param address must be a {@link File} or {@link URL} instance
     * @author Michael Simon
     */
    private static IPlugin getPlugin(Object address) {
        if (address instanceof File) {
            return getPlugin((File) address);
        } else if (address instanceof URL) {
            return getPlugin((URL) address);
        } else {
            throw new IllegalArgumentException(
                address + " is neither a " + File.class.getSimpleName() + " nor a "
                    + URL.class.getSimpleName() + " instance.");
        }
    }

    /**
     * Find a plug-in with the same path as the given file.
     * @param file file used for search of plugin
     * @return Returns either a plugin matching given file or nothing if no matching plugin can be found
     *
     * @author Eva Mueller
     * @author Michael Simon
     */
    private static IPlugin getPlugin(File file) {
        try {
            final File cf = file.getCanonicalFile();
            for (IPlugin plugin : PluginManager.getInstance().getPlugins()) {
                // Test if the plug-in's file equals the given file.
                try {
                    final File pf = getFile(plugin.getProperties().getURL());
                    if (pf != null) {
                        File cpf = pf.getCanonicalFile();
                        if ( // Compare the given path with the plug-in URL's path:
                        cf.equals(cpf)
                            // Also compare the given path with the plug-in's parent directory:
                            || (cf.equals(cpf.getParentFile()) && cpf.isFile())
                            // And the plug-in's path with the given path's parent directory:
                            || (cpf.equals(cf.getParentFile()) && cf.isFile())) {
                            // This covers all cases, because the plug-in's URL is either a file,
                            // or its parent directory and the same it true for the given path.
                            return plugin;
                        }
                    }
                } catch (IOException e) {
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug(
                            LoadCommand.class.getSimpleName()
                                + ": Ignoring exception while comparing the given file with "
                                + plugin.getProperties().getURL().getFile() + ": " + e);
                    }
                }
            }
        } catch (IOException e) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(
                    LoadCommand.class.getSimpleName() + ": Aborting the comparison of " + file
                        + " with the plug-in URLs because of: " + e);
            }
        }

        // If no plug-in for the given file was found.
        return null;
    }

    /**
     * Find a plug-in with the same URL as the given URL.
     * @param url URL used for search of plugin
     * @return Returns either a plugin matching file found at given url or nothing if no matching plugin can be found
     * @author Michael Simon
     */
    private static IPlugin getPlugin(URL url) {
        for (IPlugin plugin : PluginManager.getInstance().getPlugins()) {
            if (url.sameFile(plugin.getProperties().getURL())) {
                return plugin;
            }
        }

        // If no plug-in for the given file was found.
        return null;
    }

    @Override
    public String getDescription() {
        return "Load a new plug-in. Type 'load -help' to get examples of usage.";
    }

    @Override
    public String getArguments() {
        return "locationNames";
    }
}