/*
 * Copyright 2009 Niclas Hedhman.
 *
 * 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.types;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import org.qi4j.api.common.InvalidApplicationException;
import org.qi4j.api.common.MetaInfo;
import org.qi4j.api.common.Visibility;
import org.qi4j.api.type.CollectionType;
import org.qi4j.api.type.EnumType;
import org.qi4j.api.type.MapType;
import org.qi4j.api.type.Serialization;
import org.qi4j.api.type.ValueCompositeType;
import org.qi4j.api.type.ValueType;
import org.qi4j.api.util.Classes;
import org.qi4j.api.value.ValueComposite;
import org.qi4j.functional.HierarchicalVisitorAdapter;
import org.qi4j.functional.Iterables;
import org.qi4j.functional.Specifications;
import org.qi4j.runtime.association.AssociationsModel;
import org.qi4j.runtime.association.ManyAssociationsModel;
import org.qi4j.runtime.association.NamedAssociationsModel;
import org.qi4j.runtime.composite.CompositeMethodsModel;
import org.qi4j.runtime.composite.MixinsModel;
import org.qi4j.runtime.property.PropertiesModel;
import org.qi4j.runtime.structure.LayerModel;
import org.qi4j.runtime.structure.ModuleModel;
import org.qi4j.runtime.structure.UsedLayersModel;
import org.qi4j.runtime.value.ValueModel;
import org.qi4j.runtime.value.ValueStateModel;
import org.qi4j.runtime.value.ValuesModel;

public class ValueTypeFactory
{
    private static final ValueTypeFactory instance = new ValueTypeFactory();

    public static ValueTypeFactory instance()
    {
        return instance;
    }

    @SuppressWarnings( {"raw", "unchecked"} )
    public ValueType newValueType( Type type,
                                   Class declaringClass,
                                   Class compositeType,
                                   LayerModel layer,
                                   ModuleModel module,
                                   Serialization.Variant variant
    )
    {
        ValueType valueType = null;
        if( CollectionType.isCollection( type ) )
        {
            if( type instanceof ParameterizedType )
            {
                ParameterizedType pt = (ParameterizedType) type;
                Type collectionType = pt.getActualTypeArguments()[ 0 ];
                if( collectionType instanceof TypeVariable )
                {
                    TypeVariable collectionTypeVariable = (TypeVariable) collectionType;
                    collectionType = Classes.resolveTypeVariable( collectionTypeVariable, declaringClass, compositeType );
                }
                ValueType collectedType = newValueType( collectionType, declaringClass, compositeType, layer, module, variant );
                valueType = new CollectionType( Classes.RAW_CLASS.map( type ), collectedType );
            }
            else
            {
                ValueType collectedType = newValueType( Object.class, declaringClass, compositeType, layer, module, variant );
                valueType = new CollectionType( Classes.RAW_CLASS.map( type ), collectedType );
            }
        }
        else if( MapType.isMap( type ) )
        {
            if( type instanceof ParameterizedType )
            {
                ParameterizedType pt = (ParameterizedType) type;
                Type keyType = pt.getActualTypeArguments()[ 0 ];
                if( keyType instanceof TypeVariable )
                {
                    TypeVariable keyTypeVariable = (TypeVariable) keyType;
                    keyType = Classes.resolveTypeVariable( keyTypeVariable, declaringClass, compositeType );
                }
                ValueType keyedType = newValueType( keyType, declaringClass, compositeType, layer, module, variant );
                Type valType = pt.getActualTypeArguments()[ 1 ];
                if( valType instanceof TypeVariable )
                {
                    TypeVariable valueTypeVariable = (TypeVariable) valType;
                    valType = Classes.resolveTypeVariable( valueTypeVariable, declaringClass, compositeType );
                }
                ValueType valuedType = newValueType( valType, declaringClass, compositeType, layer, module, variant );
                valueType = new MapType( Classes.RAW_CLASS.map( type ), keyedType, valuedType, variant );
            }
            else
            {
                ValueType keyType = newValueType( Object.class, declaringClass, compositeType, layer, module, variant );
                ValueType valuesType = newValueType( Object.class, declaringClass, compositeType, layer, module, variant );
                valueType = new MapType( Classes.RAW_CLASS.map( type ), keyType, valuesType, variant );
            }
        }
        else if( ValueCompositeType.isValueComposite( type ) )
        {
            // Find ValueModel in module/layer/used layers
            ValueModel model = new ValueFinder( layer, module, Classes.RAW_CLASS.map( type ) ).getFoundModel();

            if( model == null )
            {
                if( type.equals( ValueComposite.class ) )
                {
                    // Create default model
                    MixinsModel mixinsModel = new MixinsModel();
                    Iterable valueComposite = (Iterable) Iterables.iterable( ValueComposite.class );
                    ValueStateModel valueStateModel = new ValueStateModel( new PropertiesModel(),
                                                                           new AssociationsModel(),
                                                                           new ManyAssociationsModel(),
                                                                           new NamedAssociationsModel() );
                    model = new ValueModel( valueComposite, Visibility.application, new MetaInfo(),
                                            mixinsModel, valueStateModel, new CompositeMethodsModel( mixinsModel ) );
                }
                else
                {
                    throw new InvalidApplicationException( "[" + module.name() + "] Could not find ValueComposite of type " + type );
                }
            }

            return model.valueType();
        }
        else if( EnumType.isEnum( type ) )
        {
            valueType = new EnumType( Classes.RAW_CLASS.map( type ) );
        }
        else
        {
            valueType = new ValueType( Classes.RAW_CLASS.map( type ) );
        }

        return valueType;
    }

    @SuppressWarnings( "raw" )
    private static class ValueFinder
        extends HierarchicalVisitorAdapter<Object, Object, RuntimeException>
    {
        private Class type;
        private ValueModel foundModel;
        private Visibility visibility;

        private ValueFinder( LayerModel layer, ModuleModel module, Class type )
        {
            this.type = type;

            visibility = Visibility.module;
            module.accept( this );

            if( foundModel == null )
            {
                visibility = Visibility.layer;
                layer.accept( this );

                if( foundModel == null )
                {
                    visibility = Visibility.application;
                    layer.usedLayers().accept( this );
                }
            }
        }

        public ValueModel getFoundModel()
        {
            return foundModel;
        }

        @Override
        public boolean visitEnter( Object visited )
            throws RuntimeException
        {
            if( visited instanceof ValuesModel )
            {
                return true;
            }
            else if( visited instanceof ModuleModel )
            {
                return true;
            }
            else if (visited instanceof LayerModel )
            {
                return true;
            }
            else if (visited instanceof UsedLayersModel )
            {
                return true;
            }
            else if( visited instanceof ValueModel )
            {
                ValueModel valueModel = (ValueModel) visited;
                boolean typeEquality = Specifications.in( valueModel.types() ).satisfiedBy( type );
                if( typeEquality && valueModel.visibility().ordinal() >= visibility.ordinal() )
                {
                    foundModel = valueModel;
                }
            }

            return false;
        }

        @Override
        public boolean visitLeave( Object visited )
            throws RuntimeException
        {
            return foundModel == null;
        }
    }
}
