/*
 * Copyright (c) 2011-2013, Niclas Hedhman. All Rights Reserved.
 * Copyright (c) 2014, Paul Merlin. 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.association;

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.qi4j.api.association.AssociationDescriptor;
import org.qi4j.api.association.ManyAssociation;
import org.qi4j.api.association.ManyAssociationWrapper;
import org.qi4j.api.association.NamedAssociation;
import org.qi4j.api.association.NamedAssociationWrapper;
import org.qi4j.api.entity.EntityReference;
import org.qi4j.api.entity.Identity;
import org.qi4j.api.util.NullArgumentException;
import org.qi4j.functional.Function;
import org.qi4j.functional.Function2;
import org.qi4j.functional.Iterables;
import org.qi4j.spi.entity.NamedAssociationState;

import static org.qi4j.functional.Iterables.map;

public class NamedAssociationInstance<T>
    extends AbstractAssociationInstance<T>
    implements NamedAssociation<T>
{

    private final NamedAssociationState namedAssociationState;

    public NamedAssociationInstance( AssociationInfo associationInfo,
                                     Function2<EntityReference, Type, Object> associationFunction,
                                     NamedAssociationState namedAssociationState
    )
    {
        super( associationInfo, associationFunction );
        this.namedAssociationState = namedAssociationState;
    }

    @Override
    public Iterator<String> iterator()
    {
        return namedAssociationState.iterator();
    }

    @Override
    public int count()
    {
        return namedAssociationState.count();
    }

    @Override
    public boolean containsName( String name )
    {
        return namedAssociationState.containsName( name );
    }

    @Override
    public boolean put( String name, T entity )
    {
        NullArgumentException.validateNotNull( "entity", entity );
        checkImmutable();
        checkType( entity );
        associationInfo.checkConstraints( entity );
        return namedAssociationState.put( name, new EntityReference( ( (Identity) entity ).identity().get() ) );
    }

    @Override
    public boolean remove( String name )
    {
        checkImmutable();
        return namedAssociationState.remove( name );
    }

    @Override
    public T get( String name )
    {
        return getEntity( namedAssociationState.get( name ) );
    }

    @Override
    public String nameOf( T entity )
    {
        return namedAssociationState.nameOf( getEntityReference( entity ) );
    }

    @Override
    public Map<String, T> toMap()
    {
        Map<String, T> map = new HashMap<>();
        for( String name : namedAssociationState )
        {
            map.put( name, getEntity( namedAssociationState.get( name ) ) );
        }
        return map;
    }

    @Override
    public Iterable<EntityReference> references()
    {
        return map( new Function<String, EntityReference>()
        {
            @Override
            public EntityReference map( String name )
            {
                return namedAssociationState.get( name );
            }
        }, namedAssociationState );
    }

    @Override
    public EntityReference referenceOf( String name )
    {
        return namedAssociationState.get( name );
    }

    public Iterable<Map.Entry<String, EntityReference>> getEntityReferences()
    {
        return map( new Function<String, Map.Entry<String, EntityReference>>()
        {
            @Override
            public Map.Entry<String, EntityReference> map( final String key )
            {
                final EntityReference value = namedAssociationState.get( key );
                return new Map.Entry<String, EntityReference>()
                {
                    @Override
                    public String getKey()
                    {
                        return key;
                    }

                    @Override
                    public EntityReference getValue()
                    {
                        return value;
                    }

                    @Override
                    public EntityReference setValue( EntityReference value )
                    {
                        throw new UnsupportedOperationException( "Immutable Map" );
                    }

                    @Override
                    public boolean equals( Object o )
                    {
                        if( o instanceof Map.Entry )
                        {
                            Map.Entry other = (Map.Entry) o;
                            return key.equals( other.getKey() );
                        }
                        return false;
                    }

                    @Override
                    public int hashCode()
                    {
                        return 997 * key.hashCode() + 981813497;
                    }
                };
            }
        }, namedAssociationState );
    }


    @Override
    public boolean equals( Object o )
    {
        if( this == o )
        {
            return true;
        }
        if( o == null || getClass() != o.getClass() )
        {
            return false;
        }
        NamedAssociation<?> that = (NamedAssociation) o;
        // Unwrap if needed
        while( that instanceof NamedAssociationWrapper )
        {
            that = ( (NamedAssociationWrapper) that ).next();
        }
        // Descriptor equality
        NamedAssociationInstance<?> thatInstance = (NamedAssociationInstance) that;
        AssociationDescriptor thatDescriptor = (AssociationDescriptor) thatInstance.associationInfo();
        if( !associationInfo.equals( thatDescriptor ) )
        {
            return false;
        }
        // State equality
        if( namedAssociationState.count() != thatInstance.namedAssociationState.count() )
        {
            return false;
        }
        for( String name : namedAssociationState )
        {
            if( !thatInstance.namedAssociationState.containsName( name ) )
            {
                return false;
            }
            EntityReference thisReference = namedAssociationState.get( name );
            EntityReference thatReference = thatInstance.namedAssociationState.get( name );
            if( !thisReference.equals( thatReference ) )
            {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode()
    {
        int hash = associationInfo.hashCode() * 31; // Descriptor
        for( String name : namedAssociationState )
        {
            hash += name.hashCode();
            hash += namedAssociationState.get( name ).hashCode() * 7; // State
        }
        return hash;
    }

}
