package net.ranides.assira.reflection.util;

import lombok.experimental.UtilityClass;

import net.ranides.assira.system.RuntimeUtils;
import net.ranides.assira.trace.TraceUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Predicate;

/**
 * Helper methods for working with builtin ClassLoader instances.
 */
@UtilityClass
public class ClassLoaderUtils {


    /**
     * By default it returns ClassLoader used to load this library.
     *
     * Edge cases:
     * Returns thread context classloader if it is defined and it is child of just computed default.
     * Returns system classloader if executed in bootstrap mode.
     *
     * @return ClassLoader
     */
    public static ClassLoader getDefault() {
        return getContext(ClassLoaderUtils.class);
    }

    /**
     * It scans stacktrace to find a class used as entry point for classloader deduction.
     * If all classes from stacktrace don't match, it simply uses classloader used to load this library.
     *
     * Edge cases:
     * Returns thread context classloader if it is defined and it is child of just computed default.
     * Returns system classloader if executed in bootstrap mode.
     *
     * @param filter filter
     * @return ClassLoader
     */
    public static ClassLoader getCaller(Predicate<Class<?>> filter) {
        Class<?> caller = TraceUtils.getType(filter).orElse(ClassLoaderUtils.class);
        return getContext(caller);
    }

    /**
     * By default it returns ClassLoader used to load provided class.
     *
     * Edge cases:
     * Returns thread context classloader if it is defined and it is child of just computed default.
     * Returns system classloader if executed in bootstrap mode.
     *
     * @param scope scope
     * @return ClassLoader
     */
    public static ClassLoader getContext(Class<?> scope) {
        return getContext(scope.getClassLoader());
    }

    private static ClassLoader getContext(ClassLoader caller) {
        ClassLoader context = Thread.currentThread ().getContextClassLoader();
        ClassLoader system  = ClassLoader.getSystemClassLoader();

        // If 'caller' and 'context' are in a parent-child relationship, choose the child
        ClassLoader result = isParent(context, caller) ? caller : context;

        // Precaution for when deployed as a bootstrap or extension class:
        if (isParent(result, system)) {
            result = system;
        }
        return result;
    }


    /**
     * Returns true if "parent" is parent of "child" classloader.
     *
     * @param parent parent
     * @param child child
     * @return boolean
     */
    @SuppressWarnings("PMD.CompareObjectsWithEquals")
    public static boolean isParent(ClassLoader parent, ClassLoader child) {
        if(parent == null) {
            return false;
        }
        for (ClassLoader c = child; c != null; c = c.getParent()) {
            if (c == parent) {
                return true;
            }
        }
        return false;
    }

    /**
     * Creates new classloader which loads classes from provied jars (most probably URLClassLoader).
     * Classloader uses provided "parent" classloader as parent.
     *
     * @param parent parent
     * @param jars jars
     * @return ClassLoader
     * @throws IOException if any jar does not exist
     */
    public static ClassLoader newLoader(ClassLoader parent, Path... jars) throws IOException {
        URL[] urls = new URL[jars.length];
        for(int i=0; i<urls.length; i++) {
            if( !Files.exists(jars[i]) ) {
                throw new FileNotFoundException(jars[i].toString());
            }
            urls[i] = jars[i].toUri().toURL();
        }
        return RuntimeUtils.doPrivileged(()->new URLClassLoader(urls, parent));
    }

    /**
     * Creates new classloader which loads classes from provied jars (most probably URLClassLoader).
     * Classloader uses provided "parent" classloader as parent.
     *
     * @param parent parent
     * @param jars jars
     * @return ClassLoader
     * @throws IOException if any jar does not exist
     */
    public static ClassLoader newLoader(ClassLoader parent, File... jars) throws IOException {
        URL[] urls = new URL[jars.length];
        for(int i=0; i<urls.length; i++) {
            if( !jars[i].exists() ) {
                throw new FileNotFoundException(jars[i].toString());
            }
            urls[i] = jars[i].toURI().toURL();
        }
        return RuntimeUtils.doPrivileged(()->new URLClassLoader(urls, parent));
    }

    /**
     * Creates new classloader which loads classes from provied jars (most probably URLClassLoader).
     * Classloader uses provided "parent" classloader as parent.
     *
     * @param parent parent
     * @param jars jars
     * @return ClassLoader
     * @throws IOException if any jar URI is invalid
     */
    public static ClassLoader newLoader(ClassLoader parent, URI... jars) throws IOException {
        URL[] urls = new URL[jars.length];
        for(int i=0; i<urls.length; i++) {
            urls[i] = jars[i].toURL();
        }
        return RuntimeUtils.doPrivileged(()->new URLClassLoader(urls, parent));
    }

}
