/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.polygene.runtime.bootstrap;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.polygene.api.association.Association;
import org.apache.polygene.api.association.GenericAssociationInfo;
import org.apache.polygene.api.association.ManyAssociation;
import org.apache.polygene.api.association.NamedAssociation;
import org.apache.polygene.api.common.MetaInfo;
import org.apache.polygene.api.common.Optional;
import org.apache.polygene.api.common.QualifiedName;
import org.apache.polygene.api.common.UseDefaults;
import org.apache.polygene.api.common.Visibility;
import org.apache.polygene.api.concern.Concerns;
import org.apache.polygene.api.constraint.Constraint;
import org.apache.polygene.api.constraint.ConstraintDeclaration;
import org.apache.polygene.api.constraint.Constraints;
import org.apache.polygene.api.constraint.Name;
import org.apache.polygene.api.entity.Lifecycle;
import org.apache.polygene.api.injection.scope.State;
import org.apache.polygene.api.injection.scope.This;
import org.apache.polygene.api.mixin.Initializable;
import org.apache.polygene.api.mixin.Mixins;
import org.apache.polygene.api.property.GenericPropertyInfo;
import org.apache.polygene.api.property.Immutable;
import org.apache.polygene.api.property.Property;
import org.apache.polygene.api.sideeffect.SideEffects;
import org.apache.polygene.api.type.HasTypes;
import org.apache.polygene.api.util.Annotations;
import org.apache.polygene.api.util.Classes;
import org.apache.polygene.api.util.Fields;
import org.apache.polygene.api.util.HierarchicalVisitorAdapter;
import org.apache.polygene.bootstrap.AssemblyReportException;
import org.apache.polygene.bootstrap.StateDeclarations;
import org.apache.polygene.runtime.association.AssociationModel;
import org.apache.polygene.runtime.association.AssociationsModel;
import org.apache.polygene.runtime.association.ManyAssociationModel;
import org.apache.polygene.runtime.association.ManyAssociationsModel;
import org.apache.polygene.runtime.association.NamedAssociationModel;
import org.apache.polygene.runtime.association.NamedAssociationsModel;
import org.apache.polygene.runtime.composite.AbstractConstraintModel;
import org.apache.polygene.runtime.composite.CompositeConstraintModel;
import org.apache.polygene.runtime.composite.CompositeMethodModel;
import org.apache.polygene.runtime.composite.CompositeMethodsModel;
import org.apache.polygene.runtime.composite.ConcernModel;
import org.apache.polygene.runtime.composite.ConcernsModel;
import org.apache.polygene.runtime.composite.ConstraintModel;
import org.apache.polygene.runtime.composite.ConstraintsModel;
import org.apache.polygene.runtime.composite.GenericPredicate;
import org.apache.polygene.runtime.composite.InterfaceDefaultMethodsMixin;
import org.apache.polygene.runtime.composite.MixinModel;
import org.apache.polygene.runtime.composite.MixinsModel;
import org.apache.polygene.runtime.composite.SideEffectModel;
import org.apache.polygene.runtime.composite.SideEffectsModel;
import org.apache.polygene.runtime.composite.StateModel;
import org.apache.polygene.runtime.composite.ValueConstraintsInstance;
import org.apache.polygene.runtime.composite.ValueConstraintsModel;
import org.apache.polygene.runtime.injection.Dependencies;
import org.apache.polygene.runtime.injection.DependencyModel;
import org.apache.polygene.runtime.property.PropertiesModel;
import org.apache.polygene.runtime.property.PropertyModel;

import static java.util.stream.Stream.concat;
import static java.util.stream.Stream.of;
import static org.apache.polygene.api.composite.InvalidCompositeException.handleInvalidCompositeType;
import static org.apache.polygene.api.util.Annotations.isType;
import static org.apache.polygene.api.util.Annotations.typeHasAnnotation;
import static org.apache.polygene.api.util.Classes.classHierarchy;
import static org.apache.polygene.api.util.Classes.interfacesOf;
import static org.apache.polygene.api.util.Classes.isAssignableFrom;
import static org.apache.polygene.api.util.Classes.typeOf;
import static org.apache.polygene.api.util.Classes.typesOf;
import static org.apache.polygene.api.util.Classes.wrapperClass;

/**
 * Declaration of a Composite.
 */
public abstract class CompositeAssemblyImpl
    implements HasTypes
{
    List<Class<?>> concerns = new ArrayList<>();
    List<Class<?>> sideEffects = new ArrayList<>();
    List<Class<?>> mixins = new ArrayList<>();
    List<Class<?>> types = new ArrayList<>();
    MetaInfo metaInfo = new MetaInfo();
    Visibility visibility = Visibility.module;

    private boolean immutable;
    PropertiesModel propertiesModel;
    StateModel stateModel;
    MixinsModel mixinsModel;
    CompositeMethodsModel compositeMethodsModel;
    private AssemblyHelper helper;
    private StateDeclarations stateDeclarations;

    private Set<String> registeredStateNames = new HashSet<>();

    CompositeAssemblyImpl( Class<?> mainType )
    {
        types.add( mainType );
    }

    @Override
    public Stream<Class<?>> types()
    {
        return types.stream();
    }

    protected StateModel createStateModel()
    {
        return new StateModel( propertiesModel );
    }

    protected MixinsModel createMixinsModel()
    {
        return new MixinsModel();
    }

    void buildComposite( AssemblyHelper helper,
                         StateDeclarations stateDeclarations
                       )
    {
        this.stateDeclarations = stateDeclarations;
        this.helper = helper;
        for( Class<?> compositeType : types )
        {
            metaInfo = new MetaInfo( metaInfo ).withAnnotations( compositeType );
            addAnnotationsMetaInfo( compositeType, metaInfo );
        }

        immutable = metaInfo.get( Immutable.class ) != null;
        propertiesModel = new PropertiesModel();
        stateModel = createStateModel();
        mixinsModel = createMixinsModel();
        compositeMethodsModel = new CompositeMethodsModel( mixinsModel );

        // Implement composite methods
        List<Class<?>> constraintClasses = toList( constraintDeclarations( getAllTypes() ) );
        List<Class<?>> concernClasses = toList( concat( concerns.stream(), concernDeclarations( getAllTypes() ) ) );
        List<Class<?>> sideEffectClasses = toList( concat( sideEffects.stream(), sideEffectDeclarations( getAllTypes() ) ) );
        List<Class<?>> mixinClasses = toList( concat( mixins.stream(), mixinDeclarations( getAllTypes() ) ) );
        //noinspection unchecked
        implementMixinType( types,
                            constraintClasses,
                            concernClasses,
                            sideEffectClasses,
                            mixinClasses
                          );

        // Add state from methods and fields
        //noinspection unchecked
        addState( constraintClasses );
    }

    private List<Class<?>> toList( Stream<Class<?>> stream )
    {
        return stream.collect( Collectors.toList() );
    }

    private void addAnnotationsMetaInfo( Class<?> type, MetaInfo compositeMetaInfo )
    {
        Class[] declaredInterfaces = type.getInterfaces();
        for( int i = declaredInterfaces.length - 1; i >= 0; i-- )
        {
            addAnnotationsMetaInfo( declaredInterfaces[ i ], compositeMetaInfo );
        }
        compositeMetaInfo.withAnnotations( type );
    }

    private void implementMixinType( List<? extends Class<?>> types,
                                     List<Class<?>> constraintClasses,
                                     List<Class<?>> concernClasses,
                                     List<Class<?>> sideEffectClasses,
                                     List<Class<?>> mixinClasses
                                   )
    {
        Set<Throwable> exceptions = new HashSet<>();
        Set<Class<?>> thisDependencies = new HashSet<>();
        types.stream()
             .peek( mixinType -> mixinsModel.addMixinType( mixinType ) )
             .flatMap( mixinType -> Arrays.stream( mixinType.getMethods() ) )
             .forEach( method -> implementMixinMethod( method, mixinClasses, constraintClasses,
                                                       concernClasses,
                                                       sideEffectClasses,
                                                       exceptions,
                                                       thisDependencies
                                                     )
                     );

        // Implement all @This dependencies that were found
        thisDependencies.forEach(
            thisDependency ->
            {
                // Add additional declarations from the @This type
                Stream<Class<?>> typeConstraintClasses = concat(
                    constraintClasses.stream(),
                    constraintDeclarations( thisDependency ) );
                Stream<Class<?>> typeConcernClasses = concat(
                    concernClasses.stream(),
                    concernDeclarations( thisDependency ) );
                Stream<Class<?>> typeSideEffectClasses = concat(
                    sideEffectClasses.stream(),
                    sideEffectDeclarations( thisDependency ) );
                Stream<Class<?>> typeMixinClasses = concat(
                    mixinClasses.stream(),
                    mixinDeclarations( thisDependency ) );
                List<? extends Class<?>> singleton = Collections.singletonList( thisDependency );
                implementMixinType( singleton,
                                    toList( typeConstraintClasses ),
                                    toList( typeConcernClasses ),
                                    toList( typeSideEffectClasses ),
                                    toList( typeMixinClasses )
                                  );
            } );
        if( exceptions.size() > 0 )
        {
            throw new AssemblyReportException( exceptions );
        }
    }

    private void implementMixinMethod( Method method,
                                       List<Class<?>> mixinClasses,
                                       List<Class<?>> constraintClasses,
                                       List<Class<?>> concernClasses,
                                       List<Class<?>> sideEffectClasses,
                                       Set<Throwable> exceptions,
                                       Set<Class<?>> thisDependencies )
    {
        try
        {
            if( !compositeMethodsModel.isImplemented( method )
                && !Proxy.class.equals( method.getDeclaringClass().getSuperclass() )
                && !Proxy.class.equals( method.getDeclaringClass() )
                && !Modifier.isStatic( method.getModifiers() ) )
            {
                MixinModel mixinModel = implementMethod( method, mixinClasses );
                if( mixinModel != null )
                {
                    ConcernsModel concernsModel = concernsFor(
                        method,
                        mixinModel.mixinClass(),
                        concat( concernDeclarations( mixinModel.mixinClass() ),
                                concernClasses.stream() )
                                                             );
                    SideEffectsModel sideEffectsModel = sideEffectsFor(
                        method,
                        mixinModel.mixinClass(),
                        concat( sideEffectDeclarations( mixinModel.mixinClass() ),
                                sideEffectClasses.stream() )
                                                                      );
                    Stream<Class<?>> concat = concat( constraintDeclarations( mixinModel.mixinClass() ),
                                                      constraintClasses.stream() );
                    ConstraintsModel constraints = constraintsFor( method, toList( concat ) );
                    CompositeMethodModel methodComposite = new CompositeMethodModel(
                        method,
                        constraints,
                        concernsModel,
                        sideEffectsModel,
                        mixinsModel
                    );

                    Stream<? extends Dependencies> source = of( methodComposite, mixinModel );
                    source.flatMap( Dependencies::dependencies )
                          .filter( new DependencyModel.ScopeSpecification( This.class ) )
                          .map( DependencyModel::rawInjectionType )
                          .forEach( thisDependencies::add );

                    interfacesOf( mixinModel.mixinClass() )
                        .map( Classes.RAW_CLASS )
                        .filter( clazz -> of( Initializable.class, Lifecycle.class, InvocationHandler.class )
                            .noneMatch( c -> c.equals( clazz ) ) )
                        .forEach( thisDependencies::add );

                    compositeMethodsModel.addMethod( methodComposite );
                }
            }
        }
        catch( Exception e )
        {
            exceptions.add( e );
        }
    }

    @SuppressWarnings( "raw" )
    private MixinModel implementMethod( Method method, List<Class<?>> mixinDeclarations )
    {
        MixinModel implementationModel = mixinsModel.mixinFor( method );
        if( implementationModel != null )
        {
            return implementationModel;
        }
        Class mixinClass = findTypedImplementation( method, mixinDeclarations.stream() );
        if( mixinClass != null )
        {
            return implementMethodWithClass( method, mixinClass );
        }

        // Check generic implementations
        mixinClass = findGenericImplementation( method, concat( mixinDeclarations.stream(), of( InterfaceDefaultMethodsMixin.class ) ) );
        if( mixinClass != null )
        {
            return implementMethodWithClass( method, mixinClass );
        }
        handleInvalidCompositeType( "No implementation found for method", null, null, null, null, method, types );
        return null;
    }

    private Class<?> findTypedImplementation( final Method method, Stream<Class<?>> mixins )
    {
        // Check if mixinClass implements the method. If so, check if the mixinClass is generic or if the filter passes.
        // If a mixinClass is both generic AND non-generic at the same time, then the filter applies to the non-generic
        // side only.
        Predicate<Class<?>> appliesToSpec = item -> helper.appliesTo( item, method, types, item );
        return mixins.filter( isAssignableFrom( method.getDeclaringClass() )
                                  .and( GenericPredicate.INSTANCE.or( appliesToSpec ) ) )
                     .findFirst().orElse( null );
    }

    private Class<?> findGenericImplementation( final Method method, Stream<Class<?>> mixins )
    {
        // Check if mixinClass is generic and the applies-to filter passes
        Predicate<Class<?>> appliesToSpec = item -> helper.appliesTo( item, method, types, item );
        return mixins.filter( GenericPredicate.INSTANCE.and( appliesToSpec ) ).findFirst().orElse( null );
    }

    private MixinModel implementMethodWithClass( Method method, Class mixinClass )
    {
        MixinModel mixinModel = mixinsModel.getMixinModel( mixinClass );
        if( mixinModel == null )
        {
            mixinModel = helper.getMixinModel( mixinClass );
            mixinsModel.addMixinModel( mixinModel );
        }
        mixinsModel.addMethodMixin( method, mixinModel );
        return mixinModel;
    }

    private void addState( final List<Class<?>> constraintClasses )
    {
        // Add method state
        compositeMethodsModel.accept( new HierarchicalVisitorAdapter<Object, Object, RuntimeException>()
        {
            @Override
            public boolean visitEnter( Object visited )
                throws RuntimeException
            {
                if( visited instanceof CompositeMethodModel )
                {
                    CompositeMethodModel methodModel = (CompositeMethodModel) visited;
                    if( methodModel.method().getParameterTypes().length == 0 )
                    {
                        addStateFor( methodModel.method(), constraintClasses );
                    }

                    return false;
                }

                return super.visitEnter( visited );
            }
        } );

        // Add field state
        mixinsModel.accept( new HierarchicalVisitorAdapter<Object, Object, RuntimeException>()
        {
            @Override
            public boolean visitEnter( Object visited )
                throws RuntimeException
            {
                if( visited instanceof MixinModel )
                {
                    MixinModel model = (MixinModel) visited;
                    Consumer<Field> addState = field -> addStateFor( field, constraintClasses );
                    Fields.FIELDS_OF.apply( model.mixinClass() )
                                    .filter( Annotations.hasAnnotation( State.class ) )
                                    .forEach( addState );
                    return false;
                }
                return super.visitEnter( visited );
            }
        } );
    }

    private void addStateFor( AccessibleObject accessor, List<Class<?>> constraintClasses )
    {
        String stateName = QualifiedName.fromAccessor( accessor ).name();

        if( registeredStateNames.contains( stateName ) )
        {
            return; // Skip already registered names
        }

        Class<?> accessorType = Classes.RAW_CLASS.apply( typeOf( accessor ) );
        if( Property.class.isAssignableFrom( accessorType ) )
        {
            propertiesModel.addProperty( newPropertyModel( accessor, constraintClasses ) );
            registeredStateNames.add( stateName );
        }
        else if( Association.class.isAssignableFrom( accessorType ) )
        {
            associationsModel().addAssociation( newAssociationModel( accessor, constraintClasses ) );
            registeredStateNames.add( stateName );
        }
        else if( ManyAssociation.class.isAssignableFrom( accessorType ) )
        {
            manyAssociationsModel().addManyAssociation( newManyAssociationModel( accessor, constraintClasses ) );
            registeredStateNames.add( stateName );
        }
        else if( NamedAssociation.class.isAssignableFrom( accessorType ) )
        {
            namedAssociationsModel().addNamedAssociation( newNamedAssociationModel( accessor, constraintClasses ) );
            registeredStateNames.add( stateName );
        }
    }

    protected AssociationsModel associationsModel()
    {
        return null;
    }

    protected ManyAssociationsModel manyAssociationsModel()
    {
        return null;
    }

    protected NamedAssociationsModel namedAssociationsModel()
    {
        return null;
    }

    private PropertyModel newPropertyModel( AccessibleObject accessor,
                                            List<Class<?>> constraintClasses
                                          )
    {
        List<Annotation> annotations = Annotations.findAccessorAndTypeAnnotationsIn( accessor );
        boolean optional = annotations.stream().anyMatch( isType( Optional.class ) );
        ValueConstraintsModel valueConstraintsModel = constraintsFor(
            annotations.stream(),
            GenericPropertyInfo.propertyTypeOf( accessor ),
            ( (Member) accessor ).getName(),
            optional,
            constraintClasses,
            accessor );
        ValueConstraintsInstance valueConstraintsInstance = valueConstraintsModel.newInstance();
        MetaInfo metaInfo = stateDeclarations.metaInfoFor( accessor );
        UseDefaults useDefaultsDeclaration = metaInfo.get( UseDefaults.class );
        Object initialValue = stateDeclarations.initialValueOf( accessor );
        if( initialValue == null && useDefaultsDeclaration != null )
        {
            initialValue = useDefaultsDeclaration.value();
        }
        boolean useDefaults = useDefaultsDeclaration != null || stateDeclarations.useDefaults( accessor );
        boolean immutable = this.immutable || metaInfo.get( Immutable.class ) != null;
        return new PropertyModel(
            accessor,
            immutable,
            useDefaults,
            valueConstraintsInstance,
            metaInfo,
            initialValue
        );
    }

    // Model
    private ConstraintsModel constraintsFor( Method method,
                                             List<Class<?>> constraintClasses
                                           )
    {
        List<ValueConstraintsModel> parameterConstraintModels = Collections.emptyList();

        Parameter[] parameters = method.getParameters();
        Type[] parameterTypes = method.getGenericParameterTypes();
        boolean constrained = false;
        for( int i = 0; i < parameters.length; i++ )
        {
            Parameter param = parameters[i];

            Annotation[] parameterAnnotation = param.getAnnotations();

            Name nameAnnotation = (Name) of( parameterAnnotation ).filter( isType( Name.class ) )
                                                                  .findFirst().orElse( null );
            String name = nameAnnotation == null ? param.getName() : nameAnnotation.value();
            boolean optional = of( parameterAnnotation )
                .anyMatch( isType( Optional.class ) );
            ValueConstraintsModel parameterConstraintsModel = constraintsFor(
                Arrays.stream( parameterAnnotation ),
                parameterTypes[ i ],
                name,
                optional,
                constraintClasses,
                method );
            if( parameterConstraintsModel.isConstrained() )
            {
                constrained = true;
            }

            if( parameterConstraintModels.isEmpty() )
            {
                parameterConstraintModels = new ArrayList<>();
            }
            parameterConstraintModels.add( parameterConstraintsModel );
        }

        if( !constrained )
        {
            return new ConstraintsModel( Collections.emptyList() );
        }
        else
        {
            return new ConstraintsModel( parameterConstraintModels );
        }
    }

    private ValueConstraintsModel constraintsFor(
        Stream<Annotation> constraintAnnotations,
        Type valueType,
        String name,
        boolean optional,
        Iterable<Class<?>> constraintClasses,
        AccessibleObject accessor
                                                )
    {
        valueType = wrapperClass( valueType );

        List<AbstractConstraintModel> constraintModels = new ArrayList<>();
        List<Annotation> filtered = constraintAnnotations
            .filter( typeHasAnnotation( ConstraintDeclaration.class ) )
            .collect( Collectors.toList() );

        // TODO: This massive block below should be cleaned up.
        nextConstraint:
        for( Annotation constraintAnnotation : filtered )
        {
            // Check composite declarations first
            Class<? extends Annotation> annotationType = constraintAnnotation.annotationType();
            for( Class<?> constraint : constraintClasses )
            {
                @SuppressWarnings( "unchecked" )
                Class<? extends Constraint<?, ?>> constraintType = (Class<? extends Constraint<?, ?>>) constraint;
                if( helper.appliesTo( constraintType, annotationType, valueType ) )
                {
                    constraintModels.add( new ConstraintModel( constraintAnnotation, constraintType ) );
                    continue nextConstraint;
                }
            }

            // Check the annotation itself
            Constraints constraints = annotationType.getAnnotation( Constraints.class );
            if( constraints != null )
            {
                for( Class<? extends Constraint<?, ?>> constraintClass : constraints.value() )
                {
                    if( helper.appliesTo( constraintClass, annotationType, valueType ) )
                    {
                        constraintModels.add( new ConstraintModel( constraintAnnotation, constraintClass ) );
                        continue nextConstraint;
                    }
                }
            }

            // No implementation found!
            // Check if if it's a composite constraints
            if( typeHasAnnotation( ConstraintDeclaration.class ).test( constraintAnnotation ) )
            {
                ValueConstraintsModel valueConstraintsModel = constraintsFor(
                    Arrays.stream( annotationType.getAnnotations() ),
                    valueType,
                    name,
                    optional,
                    constraintClasses,
                    accessor );
                CompositeConstraintModel compositeConstraintModel = new CompositeConstraintModel(
                    constraintAnnotation,
                    valueConstraintsModel );
                constraintModels.add( compositeConstraintModel );
            }
            else
            {
                handleInvalidCompositeType( "Cannot find implementation of constraint @", null, annotationType, null, valueType, (Member) accessor, types );
            }
        }
        return new ValueConstraintsModel( constraintModels, name, optional );
    }

    private ConcernsModel concernsFor( Method method,
                                       Class<?> mixinClass,
                                       Stream<Class<?>> concernClasses
                                     )
    {
        List<ConcernModel> concernsFor = new ArrayList<>();
        concernClasses.forEach( concern ->
                                {
                                    if( helper.appliesTo( concern, method, types, mixinClass ) )
                                    {
                                        addConcernOrRepositionIfExists( concernsFor, helper.getConcernModel( concern ) );
                                    }
                                    else
                                    {
                                        // Lookup method in mixin
                                        if( !InvocationHandler.class.isAssignableFrom( mixinClass ) )
                                        {
                                            try
                                            {
                                                Method mixinMethod = mixinClass.getMethod( method.getName(), method.getParameterTypes() );
                                                if( helper.appliesTo( concern, mixinMethod, types, mixinClass ) )
                                                {
                                                    addConcernOrRepositionIfExists( concernsFor, helper.getConcernModel( concern ) );
                                                }
                                            }
                                            catch( NoSuchMethodException e )
                                            {
                                                // Ignore
                                            }
                                        }
                                    }
                                } );

        for( Annotation annotation : method.getAnnotations() )
        {
            if( annotation instanceof Concerns )
            {
                // Check @Concerns annotations directly on methods
                Concerns concerns = (Concerns) annotation;
                addConcerns( method, mixinClass, concernsFor, concerns );
            }
            else
            {
                // Check annotations on method that have @Concerns annotations themselves
                Concerns concerns = annotation.annotationType().getAnnotation( Concerns.class );
                addConcerns( method, mixinClass, concernsFor, concerns );
            }
        }

        if( concernsFor.isEmpty() )
        {
            return ConcernsModel.EMPTY_CONCERNS;
        }
        else
        {
            return new ConcernsModel( concernsFor );
        }
    }

    private void addConcerns( Method method, Class<?> mixinClass, List<ConcernModel> concernsFor, Concerns concerns )
    {
        if( concerns != null )
        {
            for( Class<?> concern : concerns.value() )
            {
                if( helper.appliesTo( concern, method, types, mixinClass ) )
                {
                    ConcernModel concernModel = helper.getConcernModel( concern );
                    addConcernOrRepositionIfExists( concernsFor, concernModel );
                }
            }
        }
    }

    private void addConcernOrRepositionIfExists( List<ConcernModel> concernsFor, ConcernModel concernModel )
    {
        // This remove/add is to allow re-ordering of the concerns
        concernsFor.remove( concernModel );
        concernsFor.add( concernModel );
    }

    private SideEffectsModel sideEffectsFor( Method method,
                                             Class<?> mixinClass,
                                             Stream<Class<?>> sideEffectClasses
                                           )
    {
        List<SideEffectModel> sideEffectsFor = new ArrayList<>();
        sideEffectClasses.forEach( sideEffect ->
                                   {
                                       SideEffectModel sideEffectModel = helper.getSideEffectModel( sideEffect );
                                       if( helper.appliesTo( sideEffect, method, types, mixinClass ) )
                                       {
                                           addSideEffectOrRepositionIfExists( sideEffectsFor, sideEffectModel );
                                       }
                                       else
                                       {
                                           // Lookup method in mixin
                                           if( !InvocationHandler.class.isAssignableFrom( mixinClass ) )
                                           {
                                               try
                                               {
                                                   Method mixinMethod = mixinClass.getMethod( method.getName(), method.getParameterTypes() );
                                                   if( helper.appliesTo( sideEffect, mixinMethod, types, mixinClass ) )
                                                   {
                                                       addSideEffectOrRepositionIfExists( sideEffectsFor, sideEffectModel );
                                                   }
                                               }
                                               catch( NoSuchMethodException e )
                                               {
                                                   // Ignore
                                               }
                                           }
                                       }
                                   } );

        for( Annotation annotation : method.getAnnotations() )
        {
            if( annotation instanceof SideEffects )
            {
                // Check SideEffects annotation on method
                SideEffects sideEffects = (SideEffects) annotation;
                addSideEffects( method, mixinClass, sideEffectsFor, sideEffects );
            }
            else
            {
                // Check annotations on method that have @SideEffects annotations themselves
                SideEffects sideEffects = annotation.annotationType().getAnnotation( SideEffects.class );
                addSideEffects( method, mixinClass, sideEffectsFor, sideEffects );
            }
        }

        if( sideEffectsFor.isEmpty() )
        {
            return SideEffectsModel.EMPTY_SIDEEFFECTS;
        }
        else
        {
            return new SideEffectsModel( sideEffectsFor );
        }
    }

    private void addSideEffects( Method method, Class<?> mixinClass, List<SideEffectModel> sideEffectsFor, SideEffects sideEffects )
    {
        if( sideEffects != null )
        {
            for( Class<?> sideEffect : sideEffects.value() )
            {
                if( helper.appliesTo( sideEffect, method, types, mixinClass ) )
                {
                    SideEffectModel sideEffectModel = helper.getSideEffectModel( sideEffect );
                    addSideEffectOrRepositionIfExists( sideEffectsFor, sideEffectModel );
                }
            }
        }
    }

    private void addSideEffectOrRepositionIfExists( List<SideEffectModel> sideEffectsFor,
                                                    SideEffectModel sideEffectModel
                                                  )
    {
        // This add/remove is to allow reording of SideEffects.
        sideEffectsFor.remove( sideEffectModel );
        sideEffectsFor.add( sideEffectModel );
    }

    @SuppressWarnings( "unchecked" )
    private Stream<Class<?>> constraintDeclarations( Class<?> type )
    {
        Stream<? extends Type> types = getTypes( type );
        return constraintDeclarations( types );
    }

    private Stream<Class<?>> constraintDeclarations( Stream<? extends Type> types )
    {
        return types
            .filter( mixinType -> Annotations.annotationOn( mixinType, Constraints.class ) != null )
            .flatMap( mixinType -> Arrays.stream( Annotations.annotationOn( mixinType, Constraints.class ).value() ) );
    }

    @SuppressWarnings( "unchecked" )
    private Stream<Class<?>> concernDeclarations( Class<?> type )
    {
        Stream<? extends Type> types = getTypes( type );
        return concernDeclarations( types );
    }

    private Stream<Class<?>> concernDeclarations( Stream<? extends Type> types )
    {
        return types
            .filter( mixinType -> Annotations.annotationOn( mixinType, Concerns.class ) != null )
            .flatMap( mixinType -> Arrays.stream( Annotations.annotationOn( mixinType, Concerns.class ).value() ) );
    }

    @SuppressWarnings( "unchecked" )
    private Stream<Class<?>> sideEffectDeclarations( Class<?> type )
    {
        Stream<? extends Type> types = getTypes( type );
        return sideEffectDeclarations( types );
    }

    private Stream<Class<?>> sideEffectDeclarations( Stream<? extends Type> types )
    {
        return types
            .filter( mixinType -> Annotations.annotationOn( mixinType, SideEffects.class ) != null )
            .flatMap( mixinType -> Arrays.stream( Annotations.annotationOn( mixinType, SideEffects.class ).value() ) );
    }

    private Stream<Class<?>> mixinDeclarations( Class<?> type )
    {
        //Stream<? extends Type> types = typesOf( type );
        return mixinDeclarations( of( type ) );
    }

    private Stream<Class<?>> mixinDeclarations( Stream<? extends Class> types )
    {
        return types.flatMap( this::getTypes ).flatMap( Classes::typesOf )
                    .filter( mixinType -> Annotations.annotationOn( mixinType, Mixins.class ) != null )
                    .flatMap( mixinType -> Arrays.stream( Annotations.annotationOn( mixinType, Mixins.class ).value() ) );
    }

    private Stream<Class> getAllTypes()
    {
        return this.types.stream().flatMap( this::getTypes );
    }

    private Stream<Class> getTypes( Class<?> clazz )
    {
        if( clazz.isInterface() )
        {
            return typesOf( clazz ).map( Classes.RAW_CLASS );
        }
        else
        {
            return classHierarchy( clazz ).map( Classes.RAW_CLASS );
        }
    }

    private AssociationModel newAssociationModel( AccessibleObject accessor,
                                                  List<Class<?>> constraintClasses
                                                )
    {
        List<Annotation> annotations = Annotations.findAccessorAndTypeAnnotationsIn( accessor );
        boolean optional = annotations.stream().anyMatch( isType( Optional.class ) );

        // Constraints for Association references
        ValueConstraintsModel constraintsModel =
            constraintsFor( annotations.stream(),
                            GenericAssociationInfo.associationTypeOf( accessor ),
                            ( (Member) accessor ).getName(),
                            optional,
                            constraintClasses,
                            accessor );
        ValueConstraintsInstance valueConstraintsInstance = constraintsModel.newInstance();

        // Constraints for the Association itself
        constraintsModel = constraintsFor( annotations.stream(),
                                           Association.class,
                                           ( (Member) accessor ).getName(),
                                           optional,
                                           constraintClasses,
                                           accessor );
        ValueConstraintsInstance associationValueConstraintsInstance = constraintsModel.newInstance();

        MetaInfo metaInfo = stateDeclarations.metaInfoFor( accessor );
        return new AssociationModel( accessor, valueConstraintsInstance, associationValueConstraintsInstance, metaInfo );
    }

    private ManyAssociationModel newManyAssociationModel( AccessibleObject accessor,
                                                          List<Class<?>> constraintClasses
                                                        )
    {
        List<Annotation> annotations = Annotations.findAccessorAndTypeAnnotationsIn( accessor );
        boolean optional = annotations.stream().anyMatch( isType( Optional.class ) );

        // Constraints for entities in ManyAssociation
        ValueConstraintsModel valueConstraintsModel =
            constraintsFor( annotations.stream(),
                            GenericAssociationInfo.associationTypeOf( accessor ),
                            ( (Member) accessor ).getName(),
                            optional,
                            constraintClasses,
                            accessor );
        ValueConstraintsInstance valueConstraintsInstance = valueConstraintsModel.newInstance();

        // Constraints for the ManyAssociation itself
        valueConstraintsModel = constraintsFor( annotations.stream(),
                                                ManyAssociation.class,
                                                ( (Member) accessor ).getName(),
                                                optional,
                                                constraintClasses,
                                                accessor );
        ValueConstraintsInstance manyValueConstraintsInstance = valueConstraintsModel.newInstance();

        MetaInfo metaInfo = stateDeclarations.metaInfoFor( accessor );
        return new ManyAssociationModel( accessor, valueConstraintsInstance, manyValueConstraintsInstance, metaInfo );
    }

    private NamedAssociationModel newNamedAssociationModel( AccessibleObject accessor,
                                                            List<Class<?>> constraintClasses
                                                          )
    {
        List<Annotation> annotations = Annotations.findAccessorAndTypeAnnotationsIn( accessor );
        boolean optional = annotations.stream().anyMatch( isType( Optional.class ) );

        // Constraints for entities in NamedAssociation
        ValueConstraintsModel valueConstraintsModel =
            constraintsFor( annotations.stream(),
                            GenericAssociationInfo.associationTypeOf( accessor ),
                            ( (Member) accessor ).getName(),
                            optional,
                            constraintClasses,
                            accessor );
        ValueConstraintsInstance valueConstraintsInstance = valueConstraintsModel.newInstance();

        // Constraints for the NamedAssociation itself
        valueConstraintsModel = constraintsFor( annotations.stream(),
                                                NamedAssociation.class,
                                                ( (Member) accessor ).getName(),
                                                optional,
                                                constraintClasses,
                                                accessor );
        ValueConstraintsInstance namedValueConstraintsInstance = valueConstraintsModel.newInstance();

        MetaInfo metaInfo = stateDeclarations.metaInfoFor( accessor );
        return new NamedAssociationModel( accessor, valueConstraintsInstance, namedValueConstraintsInstance, metaInfo );
    }
}
