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

import lombok.experimental.UtilityClass;

import net.ranides.assira.generic.Ref;
import net.ranides.assira.reflection.*;
import net.ranides.assira.trace.ExceptionUtils;

import java.util.function.Function;

/**
 * Utility methods for type transformation
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class ClassUtils {

    /**
     * Tries to cast provided object. Returns none if impossible.
     * Does not throw ClassCastException
     *
     * @param type type
     * @param value value
     * @param <T> T
     * @return optional
     */
    @SuppressWarnings("unchecked")
    public static <T> Ref<T> cast(IClass<T> type, Object value) {
        if(value != null && type.isInstance(value)) {
            return Ref.of((T)value);
        } else {
            return Ref.empty();
        }
    }

    /**
     * Tries to cast provided object. Returns none if impossible.
     * Does not throw ClassCastException
     *
     * @param type type
     * @param value value
     * @param <T> T
     * @return optional
     */
    @SuppressWarnings("unchecked")
    public static <T> Ref<T> cast(Class<T> type, Object value) {
        if(value != null && type.isInstance(value)) {
            return Ref.of((T)value);
        } else {
            return Ref.empty();
        }
    }

    /**
     * Returns function which casts object into type.
     * Function does not throw ClassCastException but returns empty optional if cast is impossible.
     *
     * @param type type
     * @param <T> T
     * @return Function
     */
    public static <T> Function<Object, Ref<T>> cast(IClass<T> type) {
        return value -> cast(type, value);
    }

    /**
     * Returns function which casts object into type.
     * Function does not throw ClassCastException but returns empty optional if cast is impossible.
     *
     * @param type type
     * @param <T> T
     * @return Function
     */
    public static <T> Function<Object, Ref<T>> cast(Class<T> type) {
        return value -> cast(type, value);
    }

    /**
     * If "source" is subclass of "target" returns target class.
     * Returns empty optional otherwise.
     *
     * @param target target
     * @param source source
     * @param <T> T
     * @return optional
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    public static <T> Ref<Class<? extends T>> asSubclass(Class<T> target, Class<?> source) {
        return (Ref) Ref.ofNullable(source).filter(target::isAssignableFrom);
    }

    /**
     * If "source" is subclass of "target" returns target class.
     * Returns empty optional otherwise.
     *
     * @param target target
     * @param source source
     * @param <T> T
     * @return optional
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    public static <T> Ref<IClass<? extends T>> asSubclass(IClass<T> target, IClass<?> source) {
        return (Ref) Ref.ofNullable(source).filter(target::isSuper);
    }

    /**
     * If this class is primitive type, returns boxed type.
     * Returns input parameter otherwise.
     *
     * @param ic ic
     * @param <S> S
     * @return class
     */
    public static <S> IClass<S> box(IClass<S> ic) {
        return ic.attributes().has(IAttribute.PRIMITIVE) ? IClass.typeinfo(box(ic.raw())) : ic;
    }

    /**
     * If this class is primitive type, returns boxed type.
     * Returns input parameter otherwise.
     *
     * @param clazz clazz
     * @param <S> S
     * @return class
     */
    @SuppressWarnings({"PMD", "unchecked"})
    public static <S> Class<S> box(Class<S> clazz) {
        if (byte.class.equals(clazz)) {
            return (Class<S>)Byte.class;
        } else if (short.class.equals(clazz)) {
            return (Class<S>)Short.class;
        } else if (int.class.equals(clazz)) {
            return (Class<S>)Integer.class;
        } else if (long.class.equals(clazz)) {
            return (Class<S>)Long.class;
        } else if (char.class.equals(clazz)) {
            return (Class<S>)Character.class;
        } else if (float.class.equals(clazz)) {
            return (Class<S>)Float.class;
        } else if (double.class.equals(clazz)) {
            return (Class<S>)Double.class;
        } else if (boolean.class.equals(clazz)) {
            return (Class<S>)Boolean.class;
        } else if (void.class.equals(clazz)) {
            return (Class<S>)Void.class;
        } else {
            return clazz;
        }
    }

    /**
     * If this class is boxed primitive type, returns unboxed type.
     * Returns input parameter otherwise.
     *
     * @param ic ic
     * @param <S> S
     * @return class
     */
    public static <S> IClass<S> unbox(IClass<S> ic) {
        return ic.attributes().has(IAttribute.BOXED) ? IClass.typeinfo(unbox(ic.raw())) : ic;
    }

    /**
     * If this class is boxed primitive type, returns unboxed type.
     * Returns input parameter otherwise.
     *
     * @param clazz clazz
     * @param <S> S
     * @return class
     */
    @SuppressWarnings({"PMD", "unchecked"})
    public static <S> Class<S> unbox(Class<S> clazz) {
        if (Byte.class.equals(clazz)) {
            return (Class<S>)byte.class;
        } else if (Short.class.equals(clazz)) {
            return (Class<S>)short.class;
        } else if (Integer.class.equals(clazz)) {
            return (Class<S>)int.class;
        } else if (Long.class.equals(clazz)) {
            return (Class<S>)long.class;
        } else if (Character.class.equals(clazz)) {
            return (Class<S>)char.class;
        } else if (Float.class.equals(clazz)) {
            return (Class<S>)float.class;
        } else if (Double.class.equals(clazz)) {
            return (Class<S>)double.class;
        } else if (Boolean.class.equals(clazz)) {
            return (Class<S>)boolean.class;
        } else if (Void.class.equals(clazz)) {
            return (Class<S>)void.class;
        } else {
            return clazz;
        }
    }

    /**
     * Checks if provided object is instance of "type"
     * It supports autoboxing.
     *
     * @param type type
     * @param value value
     * @return boolean
     */
    public static boolean isInstance(Class<?> type, Object value) {
        return value==null || type.isInstance(value) || box(type).isInstance(value);
    }

    /**
     * Checks if objects from provided arrays are instance of according type from "type" array.
     * It supports autoboxing.
     *
     * @param type type
     * @param values values
     * @return boolean
     */
    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;
    }

    /**
     * Returns true if default classloader already loaded class with provided fully qualified name.
     *
     * @param name name
     * @return boolean
     */
    public static boolean isLoaded(String name) {
        for(ClassLoader c = ClassLoaderUtils.getDefault(); c!=null; c=c.getParent()) {
            if(null != MethodUtils.invoke(Li.FIND_LOADED_CLASS, c, name) ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Uses default classloader to resolve provided fully qualified name.
     * Returns empty optional if class is not found.
     * Does not throw exceptions.
     *
     * @param type type
     * @return optional
     */
    public static Ref<Class<?>> forName(String type) {
        try {
            return Ref.of(Class.forName(type, true, ClassLoaderUtils.getDefault()));
        } catch (ClassNotFoundException cause) {
            return Ref.empty();
        }
    }

    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", String.class);
                FIND_LOADED_CLASS.setAccessible(true);
            } catch(ReflectiveOperationException ex) {
                throw ExceptionUtils.rethrow(ex);
            }
        }

    }

}
