/*
 * 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.api.common;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import org.qi4j.api.concern.Concerns;
import org.qi4j.api.mixin.Mixins;
import org.qi4j.api.sideeffect.SideEffects;
import org.qi4j.api.util.Classes;

import static java.util.Arrays.asList;
import static org.qi4j.api.util.Classes.typesOf;

/**
 * Used to declare and access meta-info.
 * <p>
 * <strong>This is effectively an internal class and should not be used directly.</strong>
 * </p>
 * <p>
 * MetaInfo can be set on composites during the assembly phase, a.k.a the bootstrap
 * process. MetaInfo is any additional data that one wishes to associate at the 'class level' instead of instance
 * level of a composite declaration.
 * </p>
 * <p>
 * To set the MetaInfo on a Composite, call the {@code setMetaInfo()} methods on the various composite declaration
 * types, such as;
 * </p>
 * <pre><code>
 * public void assemble( ModuleAssembly module )
 *     throws AssemblyException
 * {
 *     Map&lt;String,String&gt; properties = ...;
 *     module.services( MyService.class ).setMetaInfo( properties );
 * }
 * </code></pre>
 * <p>
 * which can later be retrieved by calling the {@code metaInfo()} method on the composite itself. For the example
 * above that would be;
 * </p>
 * <pre><code>
 * &#64;Mixins(MyServiceMixin.class)
 * public interface MyService extends ServiceComposite
 * {
 *
 * }
 *
 * public abstract class MyServiceMixin
 *     implements MyService
 * {
 *     private Properties props;
 *
 *     public MyServiceMixin()
 *     {
 *         props = metaInfo( Map.class );
 *     }
 * }
 * </code></pre>
 */
public final class MetaInfo
{
    private final static Collection<Class> ignored;

    static
    {
        ignored = new HashSet<Class>( 4, 0.8f ); // Optimize size used.
        ignored.addAll( asList( Mixins.class, Concerns.class, SideEffects.class ) );
    }

    private final Map<Class<?>, Object> metaInfoMap;

    public MetaInfo()
    {
        metaInfoMap = new LinkedHashMap<Class<?>, Object>();
    }

    public MetaInfo( MetaInfo metaInfo )
    {
        metaInfoMap = new LinkedHashMap<Class<?>, Object>();
        metaInfoMap.putAll( metaInfo.metaInfoMap );
    }

    public void set( Object metaInfo )
    {
        if( metaInfo instanceof Annotation )
        {
            Annotation annotation = (Annotation) metaInfo;
            metaInfoMap.put( annotation.annotationType(), metaInfo );
        }
        else
        {
            Class<?> metaInfoclass = metaInfo.getClass();
            Iterable<Type> types = typesOf( metaInfoclass );
            for( Type type : types )
            {
                metaInfoMap.put( Classes.RAW_CLASS.map( type ), metaInfo );
            }
        }
    }

    public <T> T get( Class<T> metaInfoType )
    {
        return metaInfoType.cast( metaInfoMap.get( metaInfoType ) );
    }

    public <T> void add( Class<T> infoType, T info )
    {
        metaInfoMap.put( infoType, info );
    }

    public MetaInfo withAnnotations( AnnotatedElement annotatedElement )
    {
        for( Annotation annotation : annotatedElement.getAnnotations() )
        {
            if( !ignored.contains( annotation.annotationType() )
                && get( annotation.annotationType() ) == null )
            {
                set( annotation );
            }
        }
        return this;
    }

    @Override
    public String toString()
    {
        return metaInfoMap.toString();
    }

    public void remove( Class serviceFinderClass )
    {
        metaInfoMap.remove( serviceFinderClass );
    }
}
