/*******************************************************************************
 * Copyright (c) 2014 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 ******************************************************************************/
package org.eclipse.persistence.internal.jaxb;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map;


/**
 * Utility class for Generic class hierarchy.
 *
 */
public class GenericsClassHelper {

    /**
     * A tuple consisting of a concrete class, declaring class that declares a
     * generic interface type.
     */
    private static class DeclaringClassInterfacePair {
        public final Class concreteClass;

        public final Class declaringClass;

        public final Type genericInterface;

        private DeclaringClassInterfacePair(Class concreteClass, Class declaringClass, Type genericInteface) {
            this.concreteClass = concreteClass;
            this.declaringClass = declaringClass;
            this.genericInterface = genericInteface;
        }
    }

    /**
     * Get the parameterized type arguments for a declaring class that
     * declares a generic class or interface type.
     *
     * @param concrete the concrete class than directly or indirectly
     *                 implements or extends an interface class.
     * @param classOrIface    the interface or class.
     * @return the parameterized type arguments, or null if the generic
     * interface type is not a parameterized type.
     */
    public static Type[] getParameterizedTypeArguments(Class concrete, Class classOrIface) {
        DeclaringClassInterfacePair declaringClassInterfacePair = getClass(concrete, classOrIface);
        if (null != declaringClassInterfacePair) {
            return getParameterizedTypeArguments(declaringClassInterfacePair);
        }
        return null;
    }

    /**
     * Get the parameterized type arguments for a declaring class that
     * declares a generic interface type.
     *
     * @param p the declaring class
     * @return the parameterized type arguments, or null if the generic
     * interface type is not a parameterized type.
     */
    private static Type[] getParameterizedTypeArguments(DeclaringClassInterfacePair p) {
        if (p.genericInterface instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) p.genericInterface;
            Type[] as = pt.getActualTypeArguments();
            Type[] ras = new Type[as.length];

            for (int i = 0; i < as.length; i++) {
                Type a = as[i];
                if (a instanceof Class) {
                    ras[i] = a;
                } else if (a instanceof ParameterizedType) {
                    pt = (ParameterizedType) a;
                    ras[i] = a;
                } else if (a instanceof TypeVariable) {
                    ClassTypePair ctp = resolveTypeVariable(p.concreteClass, p.declaringClass, (TypeVariable) a);
                    if (null != ctp) {
                        ras[i] = ctp.t;
                    }
                }
            }
            return ras;
        } else {
            return null;
        }
    }

    /**
     * Find the declaring class that implements or extends an interface or class.
     *
     * @param concrete the concrete class than directly or indirectly
     *                 implements or extends an interface class.
     * @param classOrIface    the interface or class.
     * @return the tuple of the declaring class and the generic interface or class
     * type.
     */
    private static DeclaringClassInterfacePair getClass(Class concrete, Class classOrIface) {
        return getClass(concrete, classOrIface, concrete);
    }

    private static DeclaringClassInterfacePair getClass(Class concrete, Class classOrIface, Class c) {
        Type[] gis = null;

        if (null != c.getGenericSuperclass()) {
            gis = new Type[] {c.getGenericSuperclass()};
        }

        if (null == gis) {
            gis = c.getGenericInterfaces();
        }

        DeclaringClassInterfacePair p = getType(concrete, classOrIface, c, gis);
        if (p != null)
            return p;

        c = c.getSuperclass();
        if (c == null || c == Object.class)
            return null;

        return getClass(concrete, classOrIface, c);
    }

    private static DeclaringClassInterfacePair getType(Class concrete, Class classOrIface, Class c, Type[] ts) {
        for (Type t : ts) {
            DeclaringClassInterfacePair p = getType(concrete, classOrIface, c, t);
            if (p != null)
                return p;
        }
        return null;
    }

    private static DeclaringClassInterfacePair getType(Class concrete, Class classOrIface, Class c, Type t) {
        if (t instanceof Class) {
            if (t == classOrIface) {
                return new DeclaringClassInterfacePair(concrete, c, t);
            } else {
                return getClass(concrete, classOrIface, (Class) t);
            }
        } else if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            if (pt.getRawType() == classOrIface) {
                return new DeclaringClassInterfacePair(concrete, c, t);
            } else {
                return getClass(concrete, classOrIface, (Class) pt.getRawType());
            }
        }
        return null;
    }

    /**
     * A tuple consisting of a class and type of the class.
     */
    private static class ClassTypePair {

        /**
         * The type of the class.
         */
        public final Type t;

        public ClassTypePair(Class c) {
            this(c, c);
        }

        public ClassTypePair(Class c, Type t) {
            this.t = t;
        }
    }

    /**
     * Given a type variable resolve the Java class of that variable.
     *
     * @param c  the concrete class from which all type variables are resolved
     * @param dc the declaring class where the type variable was defined
     * @param tv the type variable
     * @return the resolved Java class and type, otherwise null if the type variable
     * could not be resolved
     */
    private static ClassTypePair resolveTypeVariable(Class c, Class dc, TypeVariable tv) {
        return resolveTypeVariable(c, dc, tv, new HashMap<TypeVariable, Type>());
    }

    private static ClassTypePair resolveTypeVariable(Class c, Class dc, TypeVariable tv,
                                                     Map<TypeVariable, Type> map) {
        Type[] gis = c.getGenericInterfaces();
        for (Type gi : gis) {
            if (gi instanceof ParameterizedType) {
                // process pt of interface
                ParameterizedType pt = (ParameterizedType) gi;
                ClassTypePair ctp = resolveTypeVariable(pt, (Class) pt.getRawType(), dc, tv, map);
                if (ctp != null)
                    return ctp;
            }
        }

        Type gsc = c.getGenericSuperclass();
        if (gsc instanceof ParameterizedType) {
            // process pt of class
            ParameterizedType pt = (ParameterizedType) gsc;
            return resolveTypeVariable(pt, c.getSuperclass(), dc, tv, map);
        } else if (gsc instanceof Class) {
            return resolveTypeVariable(c.getSuperclass(), dc, tv, map);
        }
        return null;
    }

    private static ClassTypePair resolveTypeVariable(ParameterizedType pt, Class c, Class dc, TypeVariable tv,
                                                     Map<TypeVariable, Type> map) {
        Type[] typeArguments = pt.getActualTypeArguments();

        TypeVariable[] typeParameters = c.getTypeParameters();

        Map<TypeVariable, Type> submap = new HashMap<TypeVariable, Type>();
        for (int i = 0; i < typeArguments.length; i++) {
            // Substitute a type variable with the Java class
            if (typeArguments[i] instanceof TypeVariable) {
                Type t = map.get(typeArguments[i]);
                submap.put(typeParameters[i], t);
            } else {
                submap.put(typeParameters[i], typeArguments[i]);
            }
        }

        if (c == dc) {
            Type t = submap.get(tv);
            if (t instanceof Class) {
                return new ClassTypePair((Class) t);
            } else if (t instanceof GenericArrayType) {
                t = ((GenericArrayType) t).getGenericComponentType();
                if (t instanceof Class) {
                    c = (Class) t;
                    try {
                        return new ClassTypePair(getArrayClass(c));
                    } catch (Exception e) {
                    }
                    return null;
                } else if (t instanceof ParameterizedType) {
                    Type rt = ((ParameterizedType) t).getRawType();
                    if (rt instanceof Class) {
                        c = (Class) rt;
                    } else {
                        return null;
                    }
                    try {
                        return new ClassTypePair(getArrayClass(c), t);
                    } catch (Exception e) {
                        return null;
                    }
                } else {
                    return null;
                }
            } else if (t instanceof ParameterizedType) {
                pt = (ParameterizedType) t;
                if (pt.getRawType() instanceof Class) {
                    return new ClassTypePair((Class) pt.getRawType(), pt);
                } else
                    return null;
            } else {
                return null;
            }
        } else {
            return resolveTypeVariable(c, dc, tv, submap);
        }
    }

    protected static Class getClassOfType(Type type) {
        if (type instanceof Class) {
            return (Class) type;
        } else if (type instanceof GenericArrayType) {
            GenericArrayType arrayType = (GenericArrayType) type;
            Type t = arrayType.getGenericComponentType();
            if (t instanceof Class) {
                return getArrayClass((Class) t);
            }
        } else if (type instanceof ParameterizedType) {
            ParameterizedType subType = (ParameterizedType) type;
            Type t = subType.getRawType();
            if (t instanceof Class) {
                return (Class) t;
            }
        }
        return null;
    }

    /**
     * Get Array class of component class.
     *
     * @param c the component class of the array
     * @return the array class.
     */
    private static Class getArrayClass(Class c) {
        try {
            Object o = Array.newInstance(c, 0);
            return o.getClass();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

}
