package de.renew.plugin;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipException;

/**
 * All knowing ClassLoader that creates a new plugin manager when instantiated
 * @author Sven Offermann
 *
 */
public class Loader {
    private static final String JAVA_PATH =
        System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";

    //Directory that contains external library need by renew
    private static final String DIR_COMMONLIBS = "libs";

    //Filename of the jar file that contains all necessary files to load renew
    private static final String JARFILE_LOADER = "de.renew.loader.jar";

    private static List<String> renewArgs;

    /**
     * Create new ClassLoader that knows all lib classes from the jars in the libs directory and create a
     * new PluginManager instance with this new ClassLoader.
     * @param args Given to Loader as String for renew
     */
    public static void main(String[] args) {
        if ("Mac OS X".equals(System.getProperty("os.name"))) {
            System.setProperty("com.apple.macos.useScreenMenubar", "true");
            System.setProperty("apple.laf.useScreenMenuBar", "true");
            System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Renew");
            System.setProperty("apple.awt.application.name", "Renew");
        }

        renewArgs = Arrays.asList(args);
        try {
            // Get the location of the libs directory
            URL url = Loader.class.getProtectionDomain().getCodeSource().getLocation();

            String base = url.toExternalForm();
            base = base.substring(0, base.lastIndexOf("/"));
            url = new URL(base + "/");
            loadRenew(url, args);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * The class loader is only returned if Renew is in test mode.
     * @param baseURL to the directory that contains loader.jar, the plugin folder and the external libraries folder
     * @param args to be passed to renew
     * @return No return value for test mode
     * @throws MalformedURLException when calling of URL fails
     */
    public static ClassLoader loadRenewAndReturnClassLoader(URL baseURL, String[] args)
        throws MalformedURLException
    {
        return null;
    }

    /**
     * Read the class path property and convert the elements into URLs. This is a
     * replacement for the getURLs() method, which is not available anymore in Java
     * 9 because the default ClassLoader is no URLClassLoader anymore.
     *
     * @return The classpath converted into a Collection of URLs
     */
    private static Collection<URL> classPathURLs() {
        List<URL> cpURLs = new ArrayList<URL>();
        String cp = System.getProperty("java.class.path");
        if (cp != null) {
            String[] cpSplit = cp.split(File.pathSeparator);
            for (String urlString : cpSplit) {
                try {
                    URL url = Paths.get(urlString).toRealPath().toFile().toURI().toURL();
                    if (url != null) {
                        cpURLs.add(url);
                    }
                } catch (InvalidPathException e) {
                    // FIXME: Describe why we're ignoring this exception
                } catch (IOException e) {
                    // FIXME: Describe why we're ignoring this exception
                }
            }
        }
        return cpURLs;
    }

    /**
     * Tries to find all jars in url to create a new classpath for renew. Looks inside
     * all found jars for nested jars. Invokes the PluginManagers main method with args
     * after the new cp has been created.
     * @param baseURL to the directory that contains loader.jar, the plugin folder and the external libraries folder
     * @param args the arguments that should be passed to renew
     * @throws MalformedURLException when calling of URL fails
     */
    public static void loadRenew(URL baseURL, String[] args) throws MalformedURLException {
        //URL baseURL = url;
        String base = baseURL.toExternalForm();
        URL libURL = new URL(base + DIR_COMMONLIBS);


        // read class path from java.class.path property
        Vector<URL> newCP = new Vector<URL>();
        newCP.addAll(classPathURLs());

        // add all jars in the libs directory and its subdirectories
        // to the new classpath
        File libsDir = null;
        try {
            libsDir = new File(new URI(libURL.toExternalForm()));
            newCP.addAll(getJars(libsDir));
        } catch (RuntimeException e) {
            System.out.println(
                "Loader: Libraries not found. Check your classpath! Classpath "
                    + "should point to loader.jar only.\n" + "Was looking in:" + libsDir);
            e.printStackTrace();
        } catch (URISyntaxException e) {
            System.out.println("Could not parse URL as URI. Could not load jars" + libURL);
            e.printStackTrace();
        } catch (MalformedURLException e) {
            System.out.println("Malformed URL. Could not load jars" + libURL);
            e.printStackTrace();
        }


        // Try to include possible internal libs of the loader.jar
        // look into jar file if there are additional libs jars 
        // includes
        String jarURL = "";
        try {
            jarURL = "jar:" + baseURL.toExternalForm() + JARFILE_LOADER + "!/";
            JarURLConnection jarConnection = (JarURLConnection) new URL(jarURL).openConnection();
            JarFile jar = jarConnection.getJarFile();
            Enumeration<JarEntry> e = jar.entries();
            while (e.hasMoreElements()) {
                JarEntry entry = e.nextElement();
                if ((entry.getName().startsWith("libs/")) && (entry.getName().endsWith(".jar"))) {
                    newCP.add(new URL(jarURL + entry.getName()));
                }
            }
        } catch (ZipException e2) {
            System.out.println("IO excption while unpacking jar" + jarURL);
            e2.printStackTrace();
        } catch (IOException e) {
            System.out.println("IO excption while creating classpath");
            e.printStackTrace();
        }

        URL[] urls = newCP.toArray(new URL[] { });
        ClassLoader parent = ClassLoader.getSystemClassLoader().getParent();
        ClassLoader cl = new URLClassLoader(urls, parent);


        // Load PluginManager with new ClassLoader and invoke
        // PluginManagers static main method with the given 
        // arguments.
        Class<?> pmClass;
        try {
            pmClass = cl.loadClass("de.renew.plugin.PluginManager");
            Method mainMethod =
                pmClass.getDeclaredMethod("main", new Class<?>[] { args.getClass(), URL.class });
            mainMethod.invoke(null, new Object[] { args, baseURL });
        } catch (ClassNotFoundException e) {
            System.out.println("Class PluginManager not found check your classpath");
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            System.out.println("PluginManager has no main method. Wrong version ?");
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            System.out.println("IllegalArgument error while invoking PluginManagers main class");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            System.out.println("IllegalAccess error while invoking PluginManagers main class");
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            System.out.println("InvocationTarget error while invoking PluginManagers main class");
            e.printStackTrace();
        }
    }

    private static Vector<URL> getJars(File dir) throws MalformedURLException {
        Vector<URL> v = new Vector<URL>();

        File[] files = dir.listFiles();
        for (int x = 0; x < files.length; x++) {
            if (files[x].isFile()) {
                if (files[x].getName().endsWith(".jar")) {
                    v.add(files[x].toURI().toURL());
                }
            } else if (files[x].isDirectory()) {
                v.addAll(getJars(files[x]));
            }
        }

        return v;
    }

    /**
     * Starts a new Renew instance by spawning a new operating system process.
     *
     * @throws IOException If the (system-dependent) starting of the new os process fails.
     */
    public static void startNewRenewInstance() throws IOException {
        List<String> arguments = ManagementFactory.getRuntimeMXBean().getInputArguments();

        final ArrayList<String> command = new ArrayList<>();
        command.add(JAVA_PATH);
        command.addAll(arguments);
        if (Loader.class.getModule().isNamed()) {
            command.add("--module");
            command.add(Loader.class.getModule().getName());
        } else {
            command.add("--class-path");
            command.add(System.getProperty("java.class.path"));
            command.add(Loader.class.getName());
        }
        command.addAll(renewArgs);

        final ProcessBuilder builder = new ProcessBuilder(command);
        builder.start();
    }
}