/*
 * Copyright (c) 2008, Rickard Öberg. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.qi4j.runtime.injection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collections;
import org.qi4j.api.common.ConstructionException;
import org.qi4j.api.common.Optional;
import org.qi4j.api.composite.DependencyDescriptor;
import org.qi4j.bootstrap.BindingException;
import org.qi4j.bootstrap.InvalidInjectionException;
import org.qi4j.functional.Function;
import org.qi4j.functional.Iterables;
import org.qi4j.functional.Specification;
import org.qi4j.functional.Visitable;
import org.qi4j.functional.Visitor;
import org.qi4j.runtime.injection.provider.InjectionProviderException;
import org.qi4j.runtime.model.Binder;
import org.qi4j.runtime.model.Resolution;

import static org.qi4j.api.util.Annotations.isType;
import static org.qi4j.functional.Iterables.iterable;

/**
 * JAVADOC
 * move all the extraction code to a TypeUtils class
 */
public final class DependencyModel
    implements Binder, DependencyDescriptor, Visitable<DependencyModel>
{
    public static boolean isOptional( Annotation injectionAnnotation, Annotation[] annotations )
    {
        if( Iterables.matchesAny( isType( Optional.class ), iterable( annotations ) ) )
        {
            return true;
        }

        Method[] methods = injectionAnnotation.annotationType().getMethods();
        for( Method method : methods )
        {
            if( method.getName().equals( "optional" ) )
            {
                try
                {
                    return (Boolean) method.invoke( injectionAnnotation );
                }
                catch( Throwable e )
                {
                    return false;
                }
            }
        }

        return false;
    }

    // Model
    private final Annotation injectionAnnotation;
    private final Type injectionType;
    private final Class<?> injectedClass;
    private final Class<?> rawInjectionClass;
    private final boolean optional;
    private final Annotation[] annotations;

    // Binding
    private InjectionProvider injectionProvider;

    public DependencyModel( Annotation injectionAnnotation,
                            Type genericType,
                            Class<?> injectedClass,
                            boolean optional,
                            Annotation[] annotations
    )
    {
        this.injectionAnnotation = injectionAnnotation;
        this.injectedClass = injectedClass;

        this.injectionType = genericType;
        this.optional = optional;
        this.annotations = annotations;
        this.rawInjectionClass = mapPrimitiveTypes( extractRawInjectionClass( injectedClass, injectionType ) );
    }

    @Override
    public <ThrowableType extends Throwable> boolean accept( Visitor<? super DependencyModel, ThrowableType> visitor )
        throws ThrowableType
    {
        return visitor.visit( this );
    }

    private Class<?> extractRawInjectionClass( Class<?> injectedClass, final Type injectionType )
    {
        // Calculate raw injection type
        if( injectionType instanceof Class )
        {
            return (Class<?>) injectionType;
        }
        else if( injectionType instanceof ParameterizedType )
        {
            return (Class<?>) ( (ParameterizedType) injectionType ).getRawType();
        }
        else if( injectionType instanceof TypeVariable )
        {
            return extractRawInjectionClass( injectedClass, (TypeVariable<?>) injectionType );
        }
        throw new IllegalArgumentException(
            "Could not extract the rawInjectionClass of " + injectedClass + " and " + injectionType );
    }

    private Class<?> extractRawInjectionClass( Class<?> injectedClass, TypeVariable<?> injectionTypeVariable )
    {
        int index = 0;
        for( TypeVariable<?> typeVariable : injectionTypeVariable.getGenericDeclaration().getTypeParameters() )
        {
            if( injectionTypeVariable.getName().equals( typeVariable.getName() ) )
            {
                return (Class<?>) getActualType( injectedClass, index );
            }
            index++;
        }
        throw new IllegalArgumentException(
            "Could not extract the rawInjectionClass of " + injectedClass + " and " + injectionTypeVariable );
    }

    // todo continue refactoring

    private Type getActualType( Class<?> injectedClass, int index )
    {
        // Type index found - map it to actual type
        Type genericType = injectedClass;
        Type type = null;

        while( !Object.class.equals( genericType ) && type == null )
        {
            genericType = ( (Class<?>) genericType ).getGenericSuperclass();
            if( genericType instanceof ParameterizedType )
            {
                type = ( (ParameterizedType) genericType ).getActualTypeArguments()[ index ];
            }
            else
            {
                Type[] genericInterfaces = ( (Class<?>) genericType ).getGenericInterfaces();
                if( genericInterfaces.length > index )
                {
                    type = genericInterfaces[ index ];
                    if( type instanceof ParameterizedType )
                    {
                        type = ( (ParameterizedType) type ).getActualTypeArguments()[ index ];
                    }
                    // TODO type may still be one of the generic interfaces???
                }
            }
        }

        if( type == null )
        {
            type = Object.class; // Generic type with no constraints so Object is fine
        }

        return type;
    }

    // FIXME This method is unused, remove it.
    private Type extractDependencyType( Type injectionType )
    {
        if( injectionType instanceof ParameterizedType )
        {
            return ( (ParameterizedType) injectionType ).getActualTypeArguments()[ 0 ];
        }
        else if( injectionType instanceof TypeVariable )
        {
            return ( (TypeVariable) injectionType ).getBounds()[ 0 ];
        }
        return injectionType;
    }

    // Model
    @Override
    public Annotation injectionAnnotation()
    {
        return injectionAnnotation;
    }

    @Override
    public Type injectionType()
    {
        return injectionType;
    }

    @Override
    public Class<?> injectedClass()
    {
        return injectedClass;
    }

    /**
     * Get the raw dependency type.
     * <p>
     * If the dependency uses generics this is the raw type,
     * and otherwise it is the type of the field.
     * <p>
     * Examples:
     * <p>
     * {@code @Service MyService service} -&gt; MyService
     * <p>
     * {@code @Entity Iterable<Foo> fooList} -&gt; Iterable
     * <p>
     * {@code @Entity Query<Foo> fooQuery} -&gt; Query
     *
     * @return raw injection type.
     */
    @Override
    public Class<?> rawInjectionType()
    {
        return rawInjectionClass;
    }

    @Override
    public boolean optional()
    {
        return optional;
    }

    @Override
    public Annotation[] annotations()
    {
        return annotations;
    }

    @Override
    public void bind( Resolution resolution )
        throws BindingException
    {
        InjectionProviderFactory providerFactory = resolution.application().injectionProviderFactory();

        try
        {
            injectionProvider = providerFactory.newInjectionProvider( resolution, this );

            if( injectionProvider == null && !optional )
            {
                String message =
                    "[Module " + resolution.module()
                        .name() + "] Non-optional @" + rawInjectionClass.getName() + " was not bound in " + injectedClass
                        .getName();
                throw new ConstructionException( message );
            }
        }
        catch( InvalidInjectionException e )
        {
            throw new BindingException( "Could not bind dependency injection", e );
        }
    }

    // Context
    public Object inject( InjectionContext context )
    {
        if( injectionProvider == null )
        {
            return null;
        }
        Object injectedValue;
        try
        {
            injectedValue = injectionProvider.provideInjection( context );
        }
        catch( InjectionProviderException e )
        {
            Throwable ex = e;
            if( ex.getCause() != null )
            {
                ex = ex.getCause();
            }

            String message = "[Module " + context.module().name() + "] InjectionProvider unable to resolve @" +
                             injectionAnnotation.annotationType().getSimpleName() + " " + injectionType.toString();
            throw new ConstructionException( message, ex );
        }
        if( injectedValue == null && !optional )
        {
            String simpleName = injectionAnnotation.annotationType().getSimpleName();
            String message = "[Module " + context.module().name() + "] Non-optional @" +
                             simpleName + " " + injectionType.toString() +
                             " was null in " + injectedClass.getName();
            if( simpleName.toLowerCase().contains( "service" ) )
            {
                message = message + ". Did you mean the @Service injection scope?";
            }
            throw new ConstructionException( message );
        }
        return getInjectedValue( injectedValue );
    }

    @SuppressWarnings( "unchecked" )
    private Object getInjectedValue( Object injectionResult )
    {
        if( injectionResult == null )
        {
            return null;
        }

        if( injectionResult instanceof Iterable )
        {
            if( Iterable.class.isAssignableFrom( rawInjectionClass ) || rawInjectionClass.isInstance(
                injectionResult ) )
            {
                return injectionResult;
            }
            else
            {
                return Iterables.first( (Iterable) injectionResult );
            }
        }
        else
        {
            if( Iterable.class.equals( injectionType ) )
            {
                return Collections.singleton( injectionResult );
            }
        }
        return injectionResult;
    }

    private final static Class<?>[] primitiveTypeMapping = {
        boolean.class, Boolean.class,
        byte.class, Byte.class,
        short.class, Short.class,
        char.class, Character.class,
        long.class, Long.class,
        double.class, Double.class,
        float.class, Float.class,
        int.class, Integer.class,
    };

    private Class<?> mapPrimitiveTypes( Class<?> rawInjectionType )
    {
        if( rawInjectionType == null || !rawInjectionType.isPrimitive() )
        {
            return rawInjectionType;
        }
        for( int i = 0; i < primitiveTypeMapping.length; i += 2 )
        {
            if( primitiveTypeMapping[ i ].equals( rawInjectionType ) )
            {
                return primitiveTypeMapping[ i + 1 ];
            }
        }
        return rawInjectionType;
    }

    public boolean hasScope( final Class<? extends Annotation> scope )
    {
        return scope == null || scope.equals( injectionAnnotation().annotationType() );
    }

    public Class<? extends Annotation> injectionAnnotationType()
    {
        if( injectionAnnotation == null )
        {
            return null;
        }
        return injectionAnnotation.annotationType();
    }

    @Override
    public String toString()
    {
        return injectionAnnotation + " for " + injectionType + " in " + injectedClass.getName();
    }

    public static class ScopeSpecification
        implements Specification<DependencyModel>
    {
        private final Class<? extends Annotation> scope;

        public ScopeSpecification( Class<? extends Annotation> scope )
        {
            this.scope = scope;
        }

        @Override
        public boolean satisfiedBy( DependencyModel model )
        {
            return model.hasScope( scope );
        }
    }

    public static class InjectionTypeFunction
        implements Function<DependencyModel, Class<?>>
    {
        @Override
        public Class<?> map( DependencyModel dependencyModel )
        {
            return dependencyModel.rawInjectionType();
        }
    }
}
