/*
 * Copyright (c) 2008-2012, Rickard Öberg.
 * Copyright (c) 2008-2012, Niclas Hedhman.
 * Copyright (c) 2012, Paul Merlin.
 *
 * 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.structure;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.qi4j.api.composite.AmbiguousTypeException;
import org.qi4j.api.composite.ModelDescriptor;
import org.qi4j.api.service.NoSuchServiceException;
import org.qi4j.api.service.ServiceReference;
import org.qi4j.functional.Function;
import org.qi4j.functional.Specification;
import org.qi4j.functional.Specifications;
import org.qi4j.runtime.composite.TransientModel;
import org.qi4j.runtime.entity.EntityModel;
import org.qi4j.runtime.object.ObjectModel;
import org.qi4j.runtime.value.ValueModel;
import org.qi4j.spi.module.ModelModule;

import static org.qi4j.api.common.Visibility.application;
import static org.qi4j.api.common.Visibility.layer;
import static org.qi4j.api.common.Visibility.module;
import static org.qi4j.api.util.Classes.RAW_CLASS;
import static org.qi4j.api.util.Classes.interfacesOf;
import static org.qi4j.functional.Iterables.cast;
import static org.qi4j.functional.Iterables.filter;
import static org.qi4j.functional.Iterables.first;
import static org.qi4j.functional.Iterables.flatten;
import static org.qi4j.functional.Iterables.flattenIterables;
import static org.qi4j.functional.Iterables.iterable;
import static org.qi4j.functional.Iterables.toList;
import static org.qi4j.functional.Iterables.unique;

/**
 * Central place for Composite Type lookups.
 */
public class TypeLookup
{

    // Constructor parameters
    private final ModuleInstance moduleInstance;
    // Eager instance objects
    private final Map<Class<?>, ModelModule<ObjectModel>> objectModels;
    private final Map<Class<?>, ModelModule<TransientModel>> transientModels;
    private final Map<Class<?>, ModelModule<ValueModel>> valueModels;
    private final Map<Class<?>, Iterable<ModelModule<EntityModel>>> allEntityModels;
    private final Map<Class<?>, ModelModule<EntityModel>> unambiguousEntityModels;
    private final Map<Type, ServiceReference<?>> serviceReferences;
    private final Map<Type, Iterable<ServiceReference<?>>> servicesReferences;

    /**
     * Create a new TypeLookup bound to the given ModuleInstance.
     *
     * @param moduleInstance ModuleInstance bound to this TypeLookup
     */
    /* package */ TypeLookup( ModuleInstance moduleInstance )
    {
        // Constructor parameters
        this.moduleInstance = moduleInstance;

        // Eager instance objects
        objectModels = new ConcurrentHashMap<>();
        transientModels = new ConcurrentHashMap<>();
        valueModels = new ConcurrentHashMap<>();
        allEntityModels = new ConcurrentHashMap<>();
        unambiguousEntityModels = new ConcurrentHashMap<>();
        serviceReferences = new ConcurrentHashMap<>();
        servicesReferences = new ConcurrentHashMap<>();
    }

    /**
     * Lookup first Object Model matching the given Type.
     *
     * <p>First, if Object Models exactly match the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>exact</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Second, if Object Models match a type assignable to the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>assignable</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Type lookup is done lazily and cached.</p>
     *
     * @param type Looked up Type
     *
     * @return First matching Object Model
     */
    @SuppressWarnings( { "raw", "unchecked" } )
    /* package */ ModelModule<ObjectModel> lookupObjectModel( final Class type )
    {
        ModelModule<ObjectModel> model = objectModels.get( type );

        if( model == null )
        {
            // Unambiguously and lazily resolve ObjectModel
            Iterable<ModelModule<ObjectModel>> flatten = flatten(
                ambiguousTypeCheck( type,
                                    findModels( new ExactTypeLookupSpecification( type ),
                                                moduleInstance.visibleObjects( module ),
                                                moduleInstance.layerInstance().visibleObjects( layer ),
                                                moduleInstance.layerInstance().visibleObjects( application ),
                                                moduleInstance.layerInstance()
                                                    .usedLayersInstance()
                                                    .visibleObjects() ) ),
                ambiguousTypeCheck( type,
                                    findModels( new AssignableTypeLookupSpecification( type ),
                                                moduleInstance.visibleObjects( module ),
                                                moduleInstance.layerInstance().visibleObjects( layer ),
                                                moduleInstance.layerInstance().visibleObjects( application ),
                                                moduleInstance.layerInstance()
                                                    .usedLayersInstance()
                                                    .visibleObjects() ) ) );

            model = first( flatten );

            if( model != null )
            {
                objectModels.put( type, model );
            }
        }

        return model;
    }

    /**
     * Lookup first Transient Model matching the given Type.
     *
     * <p>First, if Transient Models exactly match the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>exact</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Second, if Transient Models match a type assignable to the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>assignable</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Type lookup is done lazily and cached.</p>
     *
     * @param type Looked up Type
     *
     * @return First matching Transient Model
     */
    @SuppressWarnings( { "raw", "unchecked" } )
    /* package */ ModelModule<TransientModel> lookupTransientModel( final Class type )
    {
        ModelModule<TransientModel> model = transientModels.get( type );

        if( model == null )
        {
            // Unambiguously and lazily resolve TransientModel
            Iterable<ModelModule<TransientModel>> allModels = flatten(
                ambiguousTypeCheck( type,
                                    findModels( new ExactTypeLookupSpecification( type ),
                                                moduleInstance.visibleTransients( module ),
                                                moduleInstance.layerInstance().visibleTransients( layer ),
                                                moduleInstance.layerInstance().visibleTransients( application ),
                                                moduleInstance.layerInstance().usedLayersInstance().visibleTransients()
                                    )
                ),

                ambiguousTypeCheck( type,
                                    findModels( new AssignableTypeLookupSpecification( type ),
                                                moduleInstance.visibleTransients( module ),
                                                moduleInstance.layerInstance().visibleTransients( layer ),
                                                moduleInstance.layerInstance().visibleTransients( application ),
                                                moduleInstance.layerInstance().usedLayersInstance().visibleTransients()
                                    )
                )
            );
            model = first( allModels );

            if( model != null )
            {
                transientModels.put( type, model );
            }
        }

        return model;
    }

    /**
     * Lookup first Value Model matching the given Type.
     *
     * <p>First, if Value Models exactly match the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>exact</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Second, if Value Models match a type assignable to the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>assignable</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Type lookup is done lazily and cached.</p>
     *
     * @param type Looked up Type
     *
     * @return First matching Value Model
     */
    @SuppressWarnings( { "raw", "unchecked" } )
    public ModelModule<ValueModel> lookupValueModel( final Class type )
    {
        ModelModule<ValueModel> model = valueModels.get( type );

        if( model == null )
        {
            // Unambiguously and lazily resolve ValueModel
            Iterable<ModelModule<ValueModel>> flatten = flatten(
                ambiguousTypeCheck( type,
                                    findModels( new ExactTypeLookupSpecification( type ),
                                                moduleInstance.visibleValues( module ),
                                                moduleInstance.layerInstance().visibleValues( layer ),
                                                moduleInstance.layerInstance().visibleValues( application ),
                                                moduleInstance.layerInstance().usedLayersInstance().visibleValues() ) ),
                ambiguousTypeCheck( type,
                                    findModels( new AssignableTypeLookupSpecification( type ),
                                                moduleInstance.visibleValues( module ),
                                                moduleInstance.layerInstance().visibleValues( layer ),
                                                moduleInstance.layerInstance().visibleValues( application ),
                                                moduleInstance.layerInstance().usedLayersInstance().visibleValues()
                                    )
                )
            );

            model = first( flatten );

            if( model != null )
            {
                valueModels.put( type, model );
            }
        }

        return model;
    }

    /**
     * Lookup first Entity Model matching the given Type.
     *
     * <p>First, if Entity Models exactly match the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>exact</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Second, if Entity Models match a type assignable to the given type, the closest one (Visibility then Assembly order) is returned.
     * Multiple <b>assignable</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     *
     * <p>Type lookup is done lazily and cached.</p>
     *
     * <p><b>Should be used for creational use cases only.</b> For non-creational use cases see
     * {@link #lookupEntityModels(java.lang.Class)}.</p>
     *
     * @param type Looked up Type
     *
     * @return First matching Entity Model
     */
    @SuppressWarnings( { "raw", "unchecked" } )
    /* package */ ModelModule<EntityModel> lookupEntityModel( final Class type )
    {
        ModelModule<EntityModel> model = unambiguousEntityModels.get( type );

        if( model == null )
        {
            // Unambiguously and lazily resolve EntityModels
            Iterable<ModelModule<EntityModel>> allModels = flatten(
                ambiguousTypeCheck( type,
                                    findModels( new ExactTypeLookupSpecification( type ),
                                                moduleInstance.visibleEntities( module ),
                                                moduleInstance.layerInstance().visibleEntities( layer ),
                                                moduleInstance.layerInstance().visibleEntities( application ),
                                                moduleInstance.layerInstance()
                                                    .usedLayersInstance()
                                                    .visibleEntities() ) ),
                ambiguousTypeCheck( type,
                                    findModels( new AssignableTypeLookupSpecification( type ),
                                                moduleInstance.visibleEntities( module ),
                                                moduleInstance.layerInstance().visibleEntities( layer ),
                                                moduleInstance.layerInstance().visibleEntities( application ),
                                                moduleInstance.layerInstance().usedLayersInstance().visibleEntities()
                                    )
                )
            );

            model = first( allModels );

            if( model != null )
            {
                unambiguousEntityModels.put( type, model );
            }
        }

        return model;
    }

    /**
     * Lookup all Entity Models matching the given Type.
     *
     * <p>Returned Iterable contains, in order, Entity Models that: </p>
     *
     * <ul>
     * <li>exactly match the given type, in Visibility then Assembly order ;</li>
     * <li>match a type assignable to the given type, in Visibility then Assembly order.</li>
     * </ul>
     *
     * <p>Multiple <b>exact</b> matches with the same Visibility are <b>forbidden</b> and result in an AmbiguousTypeException.</p>
     * <p>Multiple <b>assignable</b> matches are <b>allowed</b> to enable polymorphic fetches and queries.</p>
     *
     * <p>Type lookup is done lazily and cached.</p>
     *
     * <p><b>Should be used for non-creational use cases only.</b> For creational use cases see
     * {@link #lookupEntityModel(java.lang.Class)}.</p>
     *
     * @param type Looked up Type
     *
     * @return All matching Entity Models
     */
    @SuppressWarnings( { "raw", "unchecked" } )
    /* package */ Iterable<ModelModule<EntityModel>> lookupEntityModels( final Class type )
    {
        Iterable<ModelModule<EntityModel>> models = allEntityModels.get( type );
        if( models == null )
        {
            // Ambiguously and lasily resolve EntityModels
            Iterable<ModelModule<EntityModel>> matchingEntityModels = flatten(
                ambiguousTypeCheck( type,
                                    findModels( new ExactTypeLookupSpecification( type ),
                                                moduleInstance.visibleEntities( module ),
                                                moduleInstance.layerInstance().visibleEntities( layer ),
                                                moduleInstance.layerInstance().visibleEntities( application ),
                                                moduleInstance.layerInstance().usedLayersInstance().visibleEntities()
                                    )
                ),
                findModels( new AssignableTypeLookupSpecification( type ),
                            moduleInstance.visibleEntities( module ),
                            moduleInstance.layerInstance().visibleEntities( layer ),
                            moduleInstance.layerInstance().visibleEntities( application ),
                            moduleInstance.layerInstance().usedLayersInstance().visibleEntities() ) );

            // Don't return the same EntityModel multiple times
            matchingEntityModels = unique( matchingEntityModels );

            models = toList( matchingEntityModels );
            allEntityModels.put( type, models );
        }
        return models;
    }

    /**
     * Lookup first ServiceReference matching the given Type.
     *
     * <p>Type lookup is done lazily and cached.</p>
     *
     * <p>See {@link #lookupServiceReferences(java.lang.reflect.Type)}.</p>
     *
     * @param <T>         Service Type
     * @param serviceType Looked up Type
     *
     * @return First matching ServiceReference
     */
    /* package */
    @SuppressWarnings( "unchecked" )
    <T> ServiceReference<T> lookupServiceReference( Type serviceType )
    {
        ServiceReference<?> serviceReference = serviceReferences.get( serviceType );
        if( serviceReference == null )
        {
            // Lazily resolve ServiceReference
            serviceReference = first( lookupServiceReferences( serviceType ) );
            if( serviceReference != null )
            {
                serviceReferences.put( serviceType, serviceReference );
            }
        }

        if( serviceReference == null )
        {
            throw new NoSuchServiceException( RAW_CLASS.map( serviceType ).getName(), moduleInstance.name() );
        }

        return (ServiceReference<T>) serviceReference;
    }

    /**
     * Lookup all ServiceReferences matching the given Type.
     *
     * <p>Returned Iterable contains, in order, ServiceReferences that: </p>
     *
     * <ul>
     * <li>exactly match the given type, in Visibility then Assembly order ;</li>
     * <li>match a type assignable to the given type, in Visibility then Assembly order.</li>
     * </ul>
     *
     * <p>Multiple <b>exact</b> matches with the same Visibility are <b>allowed</b> to enable polymorphic lookup/injection.</p>
     * <p>Multiple <b>assignable</b> matches with the same Visibility are <b>allowed</b> for the very same reason.</p>
     *
     * <p>Type lookup is done lazily and cached.</p>
     *
     * @param <T>         Service Type
     * @param serviceType Looked up Type
     *
     * @return All matching ServiceReferences
     */
    @SuppressWarnings( "unchecked" )
    /* package */ <T> Iterable<ServiceReference<T>> lookupServiceReferences( final Type serviceType )
    {
        Iterable<ServiceReference<?>> serviceRefs = servicesReferences.get( serviceType );
        if( serviceRefs == null )
        {
            // Lazily resolve ServicesReferences
            Iterable<ServiceReference<?>> matchingServices = flatten(
                findServiceReferences( new ExactTypeLookupSpecification( serviceType ),
                                       moduleInstance.visibleServices( module ),
                                       moduleInstance.layerInstance().visibleServices( layer ),
                                       moduleInstance.layerInstance().visibleServices( application ),
                                       moduleInstance.layerInstance().usedLayersInstance().visibleServices() ),
                findServiceReferences( new AssignableTypeLookupSpecification( serviceType ),
                                       moduleInstance.visibleServices( module ),
                                       moduleInstance.layerInstance().visibleServices( layer ),
                                       moduleInstance.layerInstance().visibleServices( application ),
                                       moduleInstance.layerInstance().usedLayersInstance().visibleServices() ) );

            // Don't return the same ServiceReference multiple times
            matchingServices = unique( matchingServices );

            serviceRefs = toList( matchingServices );
            servicesReferences.put( serviceType, serviceRefs );
        }

        return cast( serviceRefs );
    }

    @SuppressWarnings( { "raw", "unchecked" } )
    private static <T extends ModelDescriptor> Iterable<ModelModule<T>> findModels( Specification<Iterable<Class<?>>> specification,
                                                                                    Iterable<ModelModule<T>>... models
    )
    {
        Specification<ModelModule<T>> spec = Specifications.translate( new ModelModuleTypesFunction(), specification );
        Iterable<ModelModule<T>> flattened = flattenIterables( iterable( models ) );
        return filter( spec, flattened );
    }

    @SuppressWarnings( { "raw", "unchecked" } )
    private static Iterable<ServiceReference<?>> findServiceReferences( Specification<Iterable<Class<?>>> specification,
                                                                        Iterable<ServiceReference<?>>... references
    )
    {
        Specification<ServiceReference<?>> spec = Specifications.translate( new ServiceReferenceTypesFunction(), specification );
        Iterable<ServiceReference<?>> flattened = flattenIterables( iterable( references ) );
        return filter( spec, flattened );
    }

    /**
     * Check if the list of models contains several ones with the same visibility. If yes, then
     * throw an AmbiguousTypeException
     */
    @SuppressWarnings( "raw" )
    private static <T extends ModelDescriptor> Iterable<ModelModule<T>> ambiguousTypeCheck( final Class type,
                                                                                            final Iterable<ModelModule<T>> models
    )
    {
        return new Iterable<ModelModule<T>>()
        {

            @Override
            public Iterator<ModelModule<T>> iterator()
            {
                ModelModule<T> current = null;
                List<ModelModule<T>> ambiguous = null;
                List<ModelModule<T>> results = new ArrayList<>();
                for( ModelModule<T> model : models )
                {
                    if( current != null && !model.equals( current ) )
                    {
                        if( model.model().visibility() == current.model().visibility() )
                        {
                            if( ambiguous == null )
                            {
                                ambiguous = new ArrayList<>();
                            }
                            ambiguous.add( model );
                        }
                    }
                    else
                    {
                        current = model;
                    }
                    results.add( model );
                }
                if( ambiguous != null )
                {
                    // Check if we had any ambiguities
                    ambiguous.add( current );
                    throw new AmbiguousTypeException( "More than one type matches " + type.getName() + ":" + ambiguous );
                }
                // Ambiguity check done, and no ambiguities found. Return results
                return results.iterator();
            }
        };
    }

    private static class ModelModuleTypesFunction<T extends ModelDescriptor>
        implements Function<ModelModule<T>, Iterable<Class<?>>>
    {

        @Override
        public Iterable<Class<?>> map( ModelModule<T> modelModule )
        {
            return modelModule.model().types();
        }
    }

    private static class ServiceReferenceTypesFunction
        implements Function<ServiceReference<?>, Iterable<Class<?>>>
    {

        @Override
        public Iterable<Class<?>> map( ServiceReference<?> serviceReference )
        {
            return serviceReference.types();
        }
    }

    private static abstract class AbstractTypeLookupSpecification
        implements Specification<Iterable<Class<?>>>
    {

        protected final Type lookedUpType;

        private AbstractTypeLookupSpecification( Type lookedUpType )
        {
            this.lookedUpType = lookedUpType;
        }

        @Override
        public final boolean satisfiedBy( Iterable<Class<?>> types )
        {
            if( lookedUpType instanceof Class )
            {
                // Straight class assignability check
                return checkClassMatch( types, (Class) lookedUpType );
            }
            else
            {
                if( lookedUpType instanceof ParameterizedType )
                {
                    // Foo<Bar> check
                    // First check Foo
                    ParameterizedType parameterizedType = (ParameterizedType) lookedUpType;
                    if( !checkClassMatch( types, (Class) parameterizedType.getRawType() ) )
                    {
                        return false;
                    }
                    // Then check Bar
                    for( Type intf : interfacesOf( types ) )
                    {
                        if( intf.equals( lookedUpType ) )
                        {
                            // All parameters are the same - ok!
                            return true;
                        }
                    }
                    return false;
                }
                else if( lookedUpType instanceof WildcardType )
                {
                    return true;
                }
                return false;
            }
        }

        private boolean checkClassMatch( Iterable<Class<?>> candidates, Class<?> lookedUpType )
        {
            for( Class<?> candidate : candidates )
            {
                if( checkClassMatch( candidate, lookedUpType ) )
                {
                    return true;
                }
            }
            return false;
        }

        protected abstract boolean checkClassMatch( Class<?> candidate, Class<?> lookedUpType );
    }

    private static final class ExactTypeLookupSpecification
        extends AbstractTypeLookupSpecification
    {

        private ExactTypeLookupSpecification( Type lookedupType )
        {
            super( lookedupType );
        }

        @Override
        protected boolean checkClassMatch( Class<?> candidate, Class<?> lookedUpType )
        {
            return candidate.equals( lookedUpType );
        }
    }

    private static final class AssignableTypeLookupSpecification
        extends AbstractTypeLookupSpecification
    {

        private AssignableTypeLookupSpecification( Type lookedupType )
        {
            super( lookedupType );
        }

        @Override
        protected boolean checkClassMatch( Class<?> candidate, Class<?> lookedUpType )
        {
            return !candidate.equals( lookedUpType ) && lookedUpType.isAssignableFrom( candidate );
        }
    }
}
