package org.typemeta.funcj.codec.utils;

import java.lang.reflect.*;
import java.util.*;

import static java.util.stream.Collectors.toList;

/**
 * Utility methods relating to Reflection.
 */
@SuppressWarnings("unchecked")
public abstract class ReflectionUtils {
    private static final int ENUM      = 0x00004000;

    /**
     * Determine whether a class is an enum type.
     * @param clazz     the class to check
     * @return          true if the class is an enum type.
     */
    public static boolean isEnumSubType(Class<?> clazz) {
        return (clazz.getModifiers() & ENUM) != 0 &&
                (clazz.getSuperclass() != null &&
                        clazz.getSuperclass().getSuperclass() == java.lang.Enum.class
                );
    }

    /**
     * Return the {@link Class} for the given name.
     * Alternative to {@link Class#forName(String)}, that throws a {@link RuntimeException}.
     * @param className the class name
     * @return          the class
     */
    public static Class<?> classForName(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Data structure representing the type arguments of a generic type.
     */
    public static class TypeArgs {
        public final List<Class<?>> typeArgs;

        public TypeArgs(List<Class<?>> typeArgs) {
            this.typeArgs = typeArgs;
        }

        public TypeArgs() {
            this.typeArgs = Collections.emptyList();
        }

        public int size() {
            return typeArgs.size();
        }

        public Class<?> get(int index) {
            if (index < typeArgs.size()) {
                return typeArgs.get(index);
            } else {
                return Object.class;
            }
        }
    }

    /**
     * Inspect the given field. If it implements the given generic interface
     * then extract and return the type arguments.
     * @param field     the field to inspect
     * @param iface     the generic interface
     * @return          the type arguments
     */
    public static TypeArgs getTypeArgs(Field field, Class<?> iface) {
        final Type type = field.getGenericType();
        if (type instanceof ParameterizedType) {
            final ParameterizedType pt = (ParameterizedType) type;
            if (pt.getRawType() instanceof Class &&
                    iface.isAssignableFrom((Class)pt.getRawType())) {
                final Type[] typeArgs = pt.getActualTypeArguments();
                final List<Class<?>> results = new ArrayList<>(typeArgs.length);
                for (Type typeArg : pt.getActualTypeArguments()) {
                    if (typeArg instanceof Class) {
                        results.add((Class<?>) typeArg);
                    } else if (typeArg instanceof TypeVariable) {
                        final TypeVariable<?> tv = (TypeVariable<?>) typeArg;
                        final Type[] bounds = tv.getBounds();
                        if (bounds.length == 1 && bounds[0] instanceof Class) {
                            results.add((Class<?>) bounds[0]);
                        } else {
                            results.add(Object.class);
                        }
                    } else {
                        results.add(Object.class);
                    }
                }

                return new TypeArgs(results);
            }
        }

        return new TypeArgs();
    }

    /**
     * Inspect the given type. If it implements the given generic interface
     * then extract and return the type arguments.
     * @param implClass the type to inspect
     * @param iface     the generic interface
     * @return          the type arguments
     */
    public static TypeArgs getTypeArgs(Class<?> implClass, Class<?> iface) {
        final List<ParameterizedType> genIfaces =
                Arrays.stream(implClass.getGenericInterfaces())
                        .filter(t -> t instanceof ParameterizedType)
                        .map(t -> (ParameterizedType) t)
                        .collect(toList());
        return genIfaces.stream()
                .filter(pt -> pt.getRawType() instanceof Class)
                .filter(pt -> iface.isAssignableFrom((Class)pt.getRawType()))
                .findFirst()
                .map(ReflectionUtils::getGenTypeArgs)
                .orElseGet(TypeArgs::new);
    }

    private static TypeArgs getGenTypeArgs(ParameterizedType type) {
        final List<Class<?>> typeArgs =
                Arrays.stream(type.getActualTypeArguments())
                        .filter(t -> t instanceof Class)
                        .map(t -> (Class<?>)t)
                        .collect(toList());
        return new TypeArgs(typeArgs);
    }
}

@@ -2522,945 +2522,29 @@
     
-final Type%5B%5D typeArgs = pt.getActualTypeArguments();%0A                final List%3CClass%3C?%3E%3E results = new ArrayList%3C%3E(typeArgs.length);%0A                for (Type typeArg : pt.getActualTypeArguments()) %7B%0A                    if (typeArg instanceof Class) %7B%0A                        results.add((Class%3C?%3E) typeArg);%0A                    %7D else if (typeArg instanceof TypeVariable) %7B%0A                        final TypeVariable%3C?%3E tv = (TypeVariable%3C?%3E) typeArg;%0A                        final Type%5B%5D bounds = tv.getBounds();%0A                        if (bounds.length == 1 && bounds%5B0%5D instanceof Class) %7B%0A                            results.add((Class%3C?%3E) bounds%5B0%5D);%0A                        %7D else %7B%0A                            results.add(Object.class);%0A                        %7D%0A                    %7D else %7B%0A                        results.add(Object.class);%0A                    %7D%0A                %7D%0A%0A                return new TypeArgs(results
+return getTypeArgs(pt
 );%0A 
@@ -3488,19 +3488,16 @@
 ils::get
-Gen
 TypeArgs
@@ -3579,19 +3579,16 @@
 Args get
-Gen
 TypeArgs
@@ -3610,15 +3610,74 @@
 ype 
-type) %7B
+pt) %7B%0A        final Type%5B%5D typeArgs = pt.getActualTypeArguments();
 %0A   
@@ -3714,175 +3714,614 @@
 Args
+2
  =
-%0A                Arrays.stream(type.getActualTypeArguments())%0A                        .filter(t -%3E t instanceof Class)%0A                        .map(t -%3E (Class%3C?%3E)t)
+ new ArrayList%3C%3E(typeArgs.length);%0A        for (Type typeArg : typeArgs) %7B%0A            typeArgs2.add(determineConcreteType(typeArg));%0A        %7D%0A        return new TypeArgs(typeArgs2);%0A    %7D%0A%0A    private static Class%3C?%3E determineConcreteType(Type type) %7B%0A        if (type instanceof Class) %7B%0A            return (Class%3C?%3E) type;%0A        %7D else if (type instanceof TypeVariable) %7B%0A            final TypeVariable%3C?%3E tv = (TypeVariable%3C?%3E) type;%0A            final Type%5B%5D bounds = tv.getBounds();%0A            if (bounds.length == 1 && bounds%5B0%5D instanceof Class) %7B%0A                return (Class%3C?%3E) bounds%5B0%5D;
 %0A   
@@ -4321,33 +4321,41 @@
 0%5D;%0A            
- 
+%7D else %7B%0A
            .coll
@@ -4353,71 +4353,293 @@
     
-.collect(toList());%0A        return new TypeArgs(typeArgs);%0A    %7D
+     return Object.class;%0A            %7D%0A        %7D else if (type instanceof ParameterizedType) %7B%0A            final ParameterizedType typeArgPt = (ParameterizedType) type;%0A            return (Class)typeArgPt.getRawType();%0A        %7D else %7B%0A            return Object.class;%0A        %7D%0A    %7D%0A
 %0A%7D%0A

Fix type parameter determination logic.