/*
 * Copyright (c) 2008-2011, 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.library.rdf.entity;

import org.openrdf.model.BNode;
import org.openrdf.model.Graph;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.ValueFactory;
import org.openrdf.model.impl.GraphImpl;
import org.qi4j.api.Qi4j;
import org.qi4j.api.association.AssociationDescriptor;
import org.qi4j.api.composite.Composite;
import org.qi4j.api.entity.EntityDescriptor;
import org.qi4j.api.entity.EntityReference;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.property.PropertyDescriptor;
import org.qi4j.api.service.qualifier.Tagged;
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.api.value.ValueSerialization;
import org.qi4j.api.value.ValueSerializer;
import org.qi4j.api.value.ValueSerializer.Options;
import org.qi4j.library.rdf.Rdfs;
import org.qi4j.spi.entity.EntityState;
import org.qi4j.spi.entity.ManyAssociationState;

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

/**
 * JAVADOC
 */
public class EntityStateSerializer
{

    @Service
    @Tagged( ValueSerialization.Formats.JSON )
    private ValueSerializer valueSerializer;

    public URI createEntityURI( ValueFactory valueFactory, EntityReference identity )
    {
        return valueFactory.createURI( identity.toURI() );
    }

    public Iterable<Statement> serialize( final EntityState entityState )
    {
        return serialize( entityState, true );
    }

    public Iterable<Statement> serialize( final EntityState entityState,
                                          final boolean includeNonQueryable )
    {
        Graph graph = new GraphImpl();
        serialize( entityState, includeNonQueryable, graph );
        return graph;
    }

    public void serialize( final EntityState entityState,
                           final boolean includeNonQueryable,
                           final Graph graph )
    {
        ValueFactory values = graph.getValueFactory();
        EntityReference identity = entityState.identity();
        URI entityUri = createEntityURI( values, identity );

        graph.add( entityUri,
                   Rdfs.TYPE,
                   values.createURI( Classes.toURI( first( entityState.entityDescriptor().types() ) ) ) );

        serializeProperties( entityState,
                             graph,
                             entityUri,
                             entityState.entityDescriptor(),
                             includeNonQueryable );

        serializeAssociations( entityState,
                               graph,
                               entityUri,
                               entityState.entityDescriptor().state().associations(),
                               includeNonQueryable );

        serializeManyAssociations( entityState,
                                   graph,
                                   entityUri,
                                   entityState.entityDescriptor().state().manyAssociations(),
                                   includeNonQueryable );
    }

    private void serializeProperties( final EntityState entityState,
                                      final Graph graph,
                                      final Resource subject,
                                      final EntityDescriptor entityType,
                                      final boolean includeNonQueryable )
    {
        // Properties
        for( PropertyDescriptor persistentProperty : entityType.state().properties() )
        {
            Object property = entityState.propertyValueOf( persistentProperty.qualifiedName() );
            if( property != null )
            {
                serializeProperty( persistentProperty, property, subject, graph, includeNonQueryable );
            }
        }
    }

    private void serializeProperty( PropertyDescriptor persistentProperty,
                                    Object property,
                                    Resource subject,
                                    Graph graph,
                                    boolean includeNonQueryable )
    {
        if( !( includeNonQueryable || persistentProperty.queryable() ) )
        {
            return; // Skip non-queryable
        }

        ValueType valueType = persistentProperty.valueType();

        final ValueFactory valueFactory = graph.getValueFactory();

        String propertyURI = persistentProperty.qualifiedName().toURI();
        URI predicate = valueFactory.createURI( propertyURI );
        String baseURI = propertyURI.substring( 0, propertyURI.indexOf( '#' ) ) + "/";

        if( valueType instanceof ValueCompositeType )
        {
            serializeValueComposite( subject, predicate, (ValueComposite) property, valueType,
                                     graph, baseURI, includeNonQueryable );
        }
        else
        {
            String stringProperty = valueSerializer.serialize( new Options().withoutTypeInfo(), property );
            final Literal object = valueFactory.createLiteral( stringProperty );
            graph.add( subject, predicate, object );
        }
    }

    private void serializeValueComposite( Resource subject, URI predicate,
                                          ValueComposite value,
                                          ValueType valueType,
                                          Graph graph,
                                          String baseUri,
                                          boolean includeNonQueryable )
    {
        final ValueFactory valueFactory = graph.getValueFactory();
        BNode collection = valueFactory.createBNode();
        graph.add( subject, predicate, collection );

        for( PropertyDescriptor persistentProperty : ( (ValueCompositeType) valueType ).properties() )
        {
            Object propertyValue = Qi4j.FUNCTION_COMPOSITE_INSTANCE_OF
                .map( (Composite) value )
                .state()
                .propertyFor( persistentProperty.accessor() )
                .get();

            if( propertyValue == null )
            {
                continue; // Skip null values
            }

            ValueType type = persistentProperty.valueType();
            if( type instanceof ValueCompositeType )
            {
                URI pred = valueFactory.createURI( baseUri, persistentProperty.qualifiedName().name() );
                serializeValueComposite( collection, pred, (ValueComposite) propertyValue, type, graph,
                                         baseUri + persistentProperty.qualifiedName().name() + "/",
                                         includeNonQueryable );
            }
            else
            {
                serializeProperty( persistentProperty, propertyValue, collection, graph, includeNonQueryable );
            }
        }
    }

    private void serializeAssociations( final EntityState entityState,
                                        final Graph graph, URI entityUri,
                                        final Iterable<? extends AssociationDescriptor> associations,
                                        final boolean includeNonQueryable )
    {
        ValueFactory values = graph.getValueFactory();

        // Associations
        for( AssociationDescriptor associationType : associations )
        {
            if( !( includeNonQueryable || associationType.queryable() ) )
            {
                continue; // Skip non-queryable
            }

            EntityReference associatedId = entityState.associationValueOf( associationType.qualifiedName() );
            if( associatedId != null )
            {
                URI assocURI = values.createURI( associationType.qualifiedName().toURI() );
                URI assocEntityURI = values.createURI( associatedId.toURI() );
                graph.add( entityUri, assocURI, assocEntityURI );
            }
        }
    }

    private void serializeManyAssociations( final EntityState entityState,
                                            final Graph graph,
                                            final URI entityUri,
                                            final Iterable<? extends AssociationDescriptor> associations,
                                            final boolean includeNonQueryable )
    {
        ValueFactory values = graph.getValueFactory();

        // Many-Associations
        for( AssociationDescriptor associationType : associations )
        {
            if( !( includeNonQueryable || associationType.queryable() ) )
            {
                continue; // Skip non-queryable
            }

            BNode collection = values.createBNode();
            graph.add( entityUri, values.createURI( associationType.qualifiedName().toURI() ), collection );
            graph.add( collection, Rdfs.TYPE, Rdfs.SEQ );

            ManyAssociationState associatedIds = entityState.manyAssociationValueOf( associationType.qualifiedName() );
            for( EntityReference associatedId : associatedIds )
            {
                URI assocEntityURI = values.createURI( associatedId.toURI() );
                graph.add( collection, Rdfs.LIST_ITEM, assocEntityURI );
            }
        }
    }
}
