/*******************************************************************************
 * Copyright (c) 2010, 2013 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.inject;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;

import javax.inject.Qualifier;

import com.google.inject.ImplementedBy;
import com.google.inject.Key;
import com.google.inject.ProvidedBy;
import com.google.inject.TypeLiteral;

/**
 * Utility methods for dealing with generic type arguments.
 */
public final class TypeArguments
{
    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    private static final TypeLiteral<?>[] NO_TYPE_LITERALS = {};

    private static final TypeLiteral<?> OBJECT_TYPE_LITERAL = TypeLiteral.get( Object.class );

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    private TypeArguments()
    {
        // static utility class, not allowed to create instances
    }

    // ----------------------------------------------------------------------
    // Utility methods
    // ----------------------------------------------------------------------

    /**
     * Get all type arguments from a generic type, for example {@code [Foo,Bar]} from {@code Map<Foo,Bar>}.
     * 
     * @param typeLiteral The generic type
     * @return Array of type arguments
     */
    public static TypeLiteral<?>[] get( final TypeLiteral<?> typeLiteral )
    {
        final Type type = typeLiteral.getType();
        if ( type instanceof ParameterizedType )
        {
            final Type[] argumentTypes = ( (ParameterizedType) type ).getActualTypeArguments();
            final TypeLiteral<?>[] argumentLiterals = new TypeLiteral[argumentTypes.length];
            for ( int i = 0; i < argumentTypes.length; i++ )
            {
                argumentLiterals[i] = expand( argumentTypes[i] );
            }
            return argumentLiterals;
        }
        if ( type instanceof GenericArrayType )
        {
            return new TypeLiteral[] { expand( ( (GenericArrayType) type ).getGenericComponentType() ) };
        }
        return NO_TYPE_LITERALS;
    }

    /**
     * Get an indexed type argument from a generic type, for example {@code Bar} from {@code Map<Foo,Bar>}.
     * 
     * @param typeLiteral The generic type
     * @param index The argument index
     * @return Indexed type argument; {@code TypeLiteral<Object>} if the given type is a raw class
     */
    public static TypeLiteral<?> get( final TypeLiteral<?> typeLiteral, final int index )
    {
        final Type type = typeLiteral.getType();
        if ( type instanceof ParameterizedType )
        {
            return expand( ( (ParameterizedType) type ).getActualTypeArguments()[index] );
        }
        if ( type instanceof GenericArrayType )
        {
            if ( 0 == index )
            {
                return expand( ( (GenericArrayType) type ).getGenericComponentType() );
            }
            throw new ArrayIndexOutOfBoundsException( index );
        }
        return OBJECT_TYPE_LITERAL;
    }

    /**
     * Determines if the sub-type can be converted to the generic super-type via an identity or widening conversion.
     * 
     * @param superLiteral The generic super-type
     * @param subLiteral The generic sub-type
     * @return {@code true} if the sub-type can be converted to the generic super-type; otherwise {@code false}
     * @see Class#isAssignableFrom(Class)
     */
    public static boolean isAssignableFrom( final TypeLiteral<?> superLiteral, final TypeLiteral<?> subLiteral )
    {
        final Class<?> superClazz = superLiteral.getRawType();
        if ( !superClazz.isAssignableFrom( subLiteral.getRawType() ) )
        {
            return false;
        }
        final Type superType = superLiteral.getType();
        if ( superClazz == superType )
        {
            return true;
        }
        if ( superType instanceof ParameterizedType )
        {
            final Type resolvedType = subLiteral.getSupertype( superClazz ).getType();
            if ( resolvedType instanceof ParameterizedType )
            {
                final Type[] superArgs = ( (ParameterizedType) superType ).getActualTypeArguments();
                final Type[] subArgs = ( (ParameterizedType) resolvedType ).getActualTypeArguments();
                return isAssignableFrom( superArgs, subArgs );
            }
        }
        else if ( superType instanceof GenericArrayType )
        {
            final Type resolvedType = subLiteral.getSupertype( superClazz ).getType();
            if ( resolvedType instanceof GenericArrayType )
            {
                final Type superComponent = ( (GenericArrayType) superType ).getGenericComponentType();
                final Type subComponent = ( (GenericArrayType) resolvedType ).getGenericComponentType();
                return isAssignableFrom( new Type[] { superComponent }, new Type[] { subComponent } );
            }
        }
        return false;
    }

    /**
     * Determines if the given generic type represents a concrete type.
     * 
     * @param literal The generic type
     * @return {@code true} if the generic type is concrete; otherwise {@code false}
     */
    public static boolean isConcrete( final TypeLiteral<?> literal )
    {
        return isConcrete( literal.getRawType() );
    }

    /**
     * Determines if the given raw type represents a concrete type.
     * 
     * @param clazz The raw type
     * @return {@code true} if the raw type is concrete; otherwise {@code false}
     */
    public static boolean isConcrete( final Class<?> clazz )
    {
        return !clazz.isInterface() && !Modifier.isAbstract( clazz.getModifiers() );
    }

    /**
     * Determines if the given generic type represents an implicit binding.
     * 
     * @param literal The generic type
     * @return {@code true} if the generic type is implicit; otherwise {@code false}
     */
    public static boolean isImplicit( final TypeLiteral<?> literal )
    {
        return isImplicit( literal.getRawType() );
    }

    /**
     * Determines if the given raw type represents an implicit binding.
     * 
     * @param clazz The raw type
     * @return {@code true} if the raw type is implicit; otherwise {@code false}
     */
    public static boolean isImplicit( final Class<?> clazz )
    {
        return isConcrete( clazz ) || clazz.isAnnotationPresent( ImplementedBy.class )
            || clazz.isAnnotationPresent( ProvidedBy.class );
    }

    /**
     * Creates a special binding key for the given implicit type.
     * 
     * @param clazz The implicit type
     * @return Implicit binding key
     */
    public static <T> Key<T> implicitKey( final Class<T> clazz )
    {
        return Key.get( clazz, Implicit.class );
    }

    // ----------------------------------------------------------------------
    // Implementation types
    // ----------------------------------------------------------------------

    /**
     * {@link Qualifier} of bindings that should be treated as implicit.
     */
    @Qualifier
    @Retention( RetentionPolicy.RUNTIME )
    private static @interface Implicit
    {
    }

    // ----------------------------------------------------------------------
    // Implementation methods
    // ----------------------------------------------------------------------

    /**
     * Expands wild-card types where possible, for example {@code Bar} from {@code ? extends Bar}.
     * 
     * @param type The generic type
     * @return Widened type that is still assignment-compatible with the original.
     */
    private static TypeLiteral<?> expand( final Type type )
    {
        if ( type instanceof WildcardType )
        {
            return TypeLiteral.get( ( (WildcardType) type ).getUpperBounds()[0] );
        }
        if ( type instanceof TypeVariable<?> )
        {
            return TypeLiteral.get( ( (TypeVariable<?>) type ).getBounds()[0] );
        }
        return TypeLiteral.get( type );
    }

    /**
     * Determines whether the resolved sub-type arguments can be assigned to their generic super-type arguments.
     * 
     * @param superArgs The generic super-arguments
     * @param subArgs The resolved sub-arguments
     * @return {@code true} if all the super-arguments have assignable resolved arguments; otherwise {@code false}
     */
    private static boolean isAssignableFrom( final Type[] superArgs, final Type[] subArgs )
    {
        for ( int i = 0, len = Math.min( superArgs.length, subArgs.length ); i < len; i++ )
        {
            final Type superType = superArgs[i];
            final Type subType = subArgs[i];

            /*
             * Implementations could have unbound type variables, such as ArrayList<T>. We want to support injecting
             * MyList<T extends Number> into List<Double> which is why we reverse the following expanded arguments:
             */
            if ( subType instanceof TypeVariable<?> && isAssignableFrom( expand( subType ), expand( superType ) ) )
            {
                continue;
            }
            /*
             * Interfaces can have wild-card types, such as List<? extends Number>. Note: we only check the initial
             * upper-bound of the super-type against the resolved type (trading absolute accuracy for performance).
             */
            if ( superType instanceof WildcardType || superType instanceof TypeVariable<?> )
            {
                if ( !isAssignableFrom( expand( superType ), expand( subType ) ) )
                {
                    return false;
                }
            }
            /*
             * Non-wild-card arguments must be tested with equals: List<Number> is not assignable from List<Float>.
             */
            else if ( !superType.equals( subType ) )
            {
                return false;
            }
        }
        return true;
    }
}
