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

import java.lang.annotation.Annotation;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import net.ranides.assira.collection.ArrayUtils;
import net.ranides.assira.reflection.util.ReflectFormat;

/**
 *
 * @author ranides
 */
public final class TypeFactory {
    
    // @todo (prev # 2) merge current raw type with parameterized type
    
    public static final GenericClass[] EMPTY_CLASSES = new GenericClass[0];
    
    public static final GenericClass UNKNOWN_CLASS = new GCUnknown();
    
    public static final Type[] EMPTY_TYPES = new Type[0];
    
    public static final Type UNKNOWN_TYPE = new GTUnknown();
    
    public static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
    
    private TypeFactory() {
        // utility class
    }
    
    public static GenericClass construct(Class<?> type, GenericClass... params) {
        return new GCRaw(type, params);
    }
    
    public static GenericClass[] construct(Type[] types) {
        if(types==null || types.length==0) {
            return EMPTY_CLASSES; // NOPMD
        }
        int n = types.length;
        GenericClass[] result = new GenericClass[n];
        for(int i=0; i<n; i++) {
            result[i] = construct(types[i]);
        }
        return result;
    }
    
    public static GenericClass construct(Type type) {
        if(type instanceof Class) {
            return new GCRaw((Class<?>)type);
        } 
        if(type instanceof ParameterizedType) {
            return new GCReflect(type);
        } 
        if(type instanceof WildcardType) {
            Type[] lower = ((WildcardType)type).getLowerBounds();
            Type[] upper = ((WildcardType)type).getUpperBounds();
            if(lower!=null && lower.length > 0) {
                return construct(lower[0]);
            } else {
                return construct(upper[0]);
            }
        } 
        if(type instanceof TypeVariable) {
            return new GCVariable( ((TypeVariable<?>)type).getName() );
        }
        return UNKNOWN_CLASS;
    }
    
    public static Type reflect(GenericClass gclass) {
        return new GTParameterized(gclass);
    }
    
    /**
     * To przejdzie tylko wtedy, gdy wywołamy na rzecz klasy, która jest subklasą genericsa
     * @param clazz
     * @return
     */
    public static GenericClass[] getParams(Class<?> clazz) {
        Type[] types = ((ParameterizedType)clazz.getGenericSuperclass()).getActualTypeArguments();
        GenericClass[] result = new GenericClass[types.length];
        for(int i=0; i<types.length; i++) { 
            result[i] = TypeFactory.construct(types[i]); 
        }
        return result;
    }
    
    private static class GCUnknown implements GenericClass {
        @Override
        public String name() {
            return "?";
        }
        @Override
        public Class<?> rawType() {
            return Object.class;
        }
        @Override
        public GenericClass[] params() {
            return EMPTY_CLASSES; // NOPMD
        }
        @Override
        public boolean isParametrised() {
            return false;
        }
        @Override
        public boolean isResolved() {
            return false;
        }
        @Override
        public String toString() {
            return ReflectFormat.signature(this);
        }
        @Override
        public Type type() {
            return UNKNOWN_TYPE;
        }

        @Override
        public boolean equals(Object obj) {
            return (obj instanceof GenericClass) && obj.toString().equals(toString());
        }

        @Override
        public int hashCode() {
            return toString().hashCode();
        }
        
    }
    
    private static class GCRaw implements GenericClass {
        
        private final Class<?> type;
        private final GenericClass[] params;
        
        public GCRaw(Class<?> type, GenericClass[] params) {
            if( type.getTypeParameters().length != params.length ) {
                throw new IllegalArgumentException("Expected "+type.getTypeParameters().length+" type arguments, but passed " + params.length);
            }
            this.type = type;
            this.params = (params.length==0) ? EMPTY_CLASSES : ArrayUtils.arraycopy(params);
        }

        public GCRaw(Class<?> type) {
            this.type = type;
            if(type.getTypeParameters().length == 0) {
                this.params = EMPTY_CLASSES;
            } else {
                this.params = ArrayUtils.make(type.getTypeParameters().length, UNKNOWN_CLASS);
            }
        }
        
        @Override
        public String name() {
            return type.getSimpleName();
        }
        
        @Override
        public Class<?> rawType() {
            return type;
        }
        
        @Override
        public GenericClass[] params() {
            return params;
        }
        
        @Override
        public boolean isParametrised() {
            return params != EMPTY_CLASSES;
        }
        
        @Override
        public boolean isResolved() {
            return true;
        }
        
        @Override
        public String toString() {
            return ReflectFormat.signature(this);
        }
        
        @Override
        public Type type() {
            if(!isParametrised()) {
                return rawType();
            } else {
                return new GTParameterized(this);
            }
        }
        
        @Override
        public boolean equals(Object obj) {
            return (obj instanceof GenericClass) && obj.toString().equals(toString());
        }

        @Override
        public int hashCode() {
            return toString().hashCode();
        }
    }
    
    private static class GCVariable implements GenericClass {
        
        private final String name;

        public GCVariable(String name) {
            this.name = name;
        }

        @Override
        public String name() {
            return name;
        }

        @Override
        public Class<?> rawType() {
            return Object.class;
        }

        @Override
        public GenericClass[] params() {
            return EMPTY_CLASSES; // NOPMD
        }

        @Override
        public boolean isParametrised() {
            return false;
        }

        @Override
        public boolean isResolved() {
            return false;
        }

        @Override
        public Type type() {
            return new GTVariable(name);
        }

        @Override
        public String toString() {
            return name;
        }
        
        @Override
        public boolean equals(Object obj) {
            return (obj instanceof GenericClass) && obj.toString().equals(toString());
        }

        @Override
        public int hashCode() {
            return toString().hashCode();
        }
        
    }

    private static class GCReflect implements GenericClass {

        private final Type rtype;

        public GCReflect(Type type) {
            this.rtype = type;
        }

        @Override
        public Type type() { 
            return rtype; 
        }

        @Override
        public String name() {
            return rawType().getSimpleName();
        }

        @Override
        public Class<?> rawType() {
            if(rtype instanceof ParameterizedType) {
                return (Class)((ParameterizedType)rtype).getRawType();
            } else {
                return (Class)rtype;
            }
        }

        @Override
        public GenericClass[] params() {
            if( !(rtype instanceof ParameterizedType) ) {
                return EMPTY_CLASSES; // NOPMD
            }
            Type[] source = ((ParameterizedType)rtype).getActualTypeArguments();
            int n = source.length;
            GenericClass[] result = new GenericClass[n];
            for(int i=0; i<n; i++) {
                result[i] = construct(source[i]);
            }
            return result;
        }

        @Override
        public boolean isParametrised() {
            return rtype instanceof ParameterizedType;
        }

        @Override
        public boolean isResolved() {
            return true;
        }

        @Override
        public String toString() {
            return ReflectFormat.signature(this);
        }
        
        @Override
        public boolean equals(Object obj) {
            return (obj instanceof GenericClass) && obj.toString().equals(toString());
        }

        @Override
        public int hashCode() {
            return toString().hashCode();
        }

    }
    
    private static class GTUnknown implements WildcardType {

        @Override
        public Type[] getUpperBounds() {
            return new Type[]{ Object.class };
        }

        @Override
        public Type[] getLowerBounds() {
            return EMPTY_TYPES; //NOPMD
        }
        
    }
    
    private static class GTParameterized implements ParameterizedType {
        
        private final GenericClass gclass;

        public GTParameterized(GenericClass gclass) {
            this.gclass = gclass;
        }

        @Override
        public Type[] getActualTypeArguments() {
            GenericClass[] source = gclass.params();
            Type[] target = new Type[source.length];
            for(int i=0, n=source.length; i<n; i++) {
                target[i] = source[i].type();
            }
            return target;
        }

        @Override
        public Type getRawType() {
            return gclass.rawType();
        }

        @Override
        public Type getOwnerType() {
            return gclass.rawType().getDeclaringClass();
        }
        
    }
    
    private static class GTVariable implements TypeVariable<GenericDeclaration> {
        
        private final String name;

        public GTVariable(String name) {
            this.name = name;
        }

        @Override
        public Type[] getBounds() {
            return new Type[]{ Object.class };
        }

        @Override
        public GenericDeclaration getGenericDeclaration() {
            return new GenericDeclaration() {
                @Override
                public TypeVariable<?>[] getTypeParameters() {
                    return new TypeVariable<?>[]{ GTVariable.this };
                }
            };
        }

        @Override
        public String getName() {
            return name;
        }
    }
    
    
}
