/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.reflection.util;

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 net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.system.RuntimeUtils;
import net.ranides.assira.trace.ExceptionUtils;
import net.ranides.assira.trace.TraceUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class ClassUtils {
    
    private ClassUtils() {
        // utility class
    }
    
    public static IClass<?> box(IClass<?> ic) {
        return IAttribute.PRIMITIVE.matches(ic) ? IClass.typeinfo(box(ic.raw())) : ic;
    }
    
    @SuppressWarnings("PMD")
    public static Class<?> box(Class<?> clazz) {
        if (byte.class.equals(clazz)) {
            return Byte.class;
        } else if (short.class.equals(clazz)) {
            return Short.class;
        } else if (int.class.equals(clazz)) {
            return Integer.class;
        } else if (long.class.equals(clazz)) {
            return Long.class;
        } else if (char.class.equals(clazz)) {
            return Character.class;
        } else if (float.class.equals(clazz)) {
            return Float.class;
        } else if (double.class.equals(clazz)) {
            return Double.class;
        } else if (boolean.class.equals(clazz)) {
            return Boolean.class;
        } else if (void.class.equals(clazz)) {
            return Void.class;
        } else {
            return clazz;
        }
    }
    
    public static IClass<?> unbox(IClass<?> ic) {
        return IAttribute.BOXED.matches(ic) ? IClass.typeinfo(unbox(ic.raw())) : ic;
    }

    @SuppressWarnings("PMD")
    public static Class<?> unbox(Class<?> clazz) {
        if (Byte.class.equals(clazz)) {
            return byte.class;
        } else if (Short.class.equals(clazz)) {
            return short.class;
        } else if (Integer.class.equals(clazz)) {
            return int.class;
        } else if (Long.class.equals(clazz)) {
            return long.class;
        } else if (Character.class.equals(clazz)) {
            return char.class;
        } else if (Float.class.equals(clazz)) {
            return float.class;
        } else if (Double.class.equals(clazz)) {
            return double.class;
        } else if (Boolean.class.equals(clazz)) {
            return boolean.class;
        } else if (Void.class.equals(clazz)) {
            return void.class;
        } else {
            return clazz;
        }
    }

    /**
     * Sprawdza, czy podany obiekt {@code get} jest instancją klasy {@code ssuper}.
     * Uwzględnia autoboxing argumentów.
     * @param type
     * @param value
     * @return
     */
    public static boolean isInstance(Class<?> type, Object value) {
        return value==null || type.isInstance(value) || box(type).isInstance(value);
    }

    /**
     * Sprawdza, czy odpowiednie obiekty z tablicy {@code values} są istancjami
     * odpowiednich klas z tablicy {@code supers}. Uwzględnia autoboxing argumentów.
     * @param type
     * @param values
     * @return
     */
    public static boolean isInstance(Class<?>[] type, Object[] values) {
        if(type.length != values.length) {
            return false;
        }
        for(int i=0; i<type.length; i++) {
            if( !isInstance(type[i], values[i]) ) { return false; }
        }
        return true;
    }
    
    public static boolean isLoaded(String name) {
        for(ClassLoader c = loader(); c!=null; c=c.getParent()) {
            if(null != MethodUtils.invoke(Li.FIND_LOADED_CLASS, c, name) ) {
                return true;
            }
        }
        return false;
    }
    
    public static Class<?> forName(String type) throws ClassNotFoundException {
        return Class.forName(type, true, loader());
    }
    
    public static ClassLoader loader() {
		// just in case
//		ClassLoader caller = TraceUtils.getTypes().stream()
//			.skip(1)
//			.filter(c -> !c.isSynthetic())
//			.findFirst().get()
//			.getClassLoader();

		ClassLoader caller  = TraceUtils.getCallerType().getClassLoader();		
        ClassLoader context = Thread.currentThread ().getContextClassLoader();
        ClassLoader system  = ClassLoader.getSystemClassLoader();
        
        // If 'caller' and 'context' are in a parent-child relationship, choose the child
        ClassLoader result = isChild(context, caller) ? caller : context;
		
        // Precaution for when deployed as a bootstrap or extension class:
        if (isChild (result, system)) {
            result = system;
        }
        return result;
    }
    
    @SuppressWarnings("PMD.CompareObjectsWithEquals")
    static boolean isChild(ClassLoader parent, ClassLoader child) {
		for(ClassLoader c=child; c!=null; c=c.getParent()) {
			if(c == parent) {
				return true;
			}
		}
        return false;
    }
    
    public static ClassLoader loader(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));
	}
    
    public static ClassLoader loader(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));
	}
    
    private static final class Li { // NOPMD - lazy init idiom
        
        public static final java.lang.reflect.Method FIND_LOADED_CLASS;
        
        static {
            try {
                FIND_LOADED_CLASS = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class<?>[] { String.class });
                FIND_LOADED_CLASS.setAccessible(true);
            } catch(ReflectiveOperationException ex) {
                throw ExceptionUtils.rethrow(ex);
            }
        }
        
    }
    
}
