/*
 * Copyright (c) 2012, 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.valueserialization.jackson;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingJsonFactory;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.InputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.service.ServiceReference;
import org.qi4j.api.structure.Application;
import org.qi4j.api.structure.Module;
import org.qi4j.api.type.ValueType;
import org.qi4j.api.value.ValueDeserializer;
import org.qi4j.api.value.ValueSerializationException;
import org.qi4j.functional.Function;
import org.qi4j.spi.value.ValueDeserializerAdapter;

/**
 * ValueDeserializer reading Values from JSON documents using Jackson.
 */
public class JacksonValueDeserializer
    extends ValueDeserializerAdapter<JsonParser, JsonNode>
{

    private final JsonFactory jsonFactory = new MappingJsonFactory();

    public JacksonValueDeserializer( @Structure Application application,
                                     @Structure Module module,
                                     @Service ServiceReference<ValueDeserializer> serviceRef )
    {
        super( application, module, serviceRef );
    }

    @Override
    protected JsonParser adaptInput( InputStream input )
        throws Exception
    {
        return jsonFactory.createParser( input );
    }

    @Override
    protected void onDeserializationEnd( ValueType valueType, JsonParser input )
        throws Exception
    {
        input.close();
    }

    @Override
    protected Object readPlainValue( JsonParser input )
        throws Exception
    {
        JsonNode jsonNode = input.readValueAsTree();
        if( jsonNode.isArray() || jsonNode.isObject() )
        {
            throw new ValueSerializationException( "Asked for a Value but found an Object or an Array at "
                                                   + input.getCurrentLocation().toString() );
        }
        if( jsonNode.isDouble() )
        {
            return jsonNode.asDouble();
        }
        if( jsonNode.isLong() )
        {
            return jsonNode.asLong();
        }
        if( jsonNode.isInt() )
        {
            return jsonNode.asInt();
        }
        if( jsonNode.isBoolean() )
        {
            return jsonNode.asBoolean();
        }
        if( jsonNode.isNull() )
        {
            return null;
        }
        return jsonNode.asText();
    }

    @Override
    protected <T> Collection<T> readArrayInCollection( JsonParser input,
                                                       Function<JsonParser, T> deserializer,
                                                       Collection<T> collection )
        throws Exception
    {
        JsonToken token = input.getCurrentToken();
        if( token == JsonToken.VALUE_NULL )
        {
            return null;
        }
        if( token != JsonToken.START_ARRAY )
        {
            token = input.nextToken();
        }
        if( token == JsonToken.VALUE_NULL )
        {
            return null;
        }
        if( token != JsonToken.START_ARRAY )
        {
            throw new ValueSerializationException( "Expected an array start at "
                                                   + input.getCurrentLocation().toString() );
        }
        while( input.nextToken() != JsonToken.END_ARRAY )
        {
            T element = deserializer.map( input );
            collection.add( element );
        }
        return collection;
    }

    @Override
    protected <K, V> Map<K, V> readMapInMap( JsonParser input,
                                             Function<JsonParser, K> keyDeserializer,
                                             Function<JsonParser, V> valueDeserializer,
                                             Map<K, V> map )
        throws Exception
    {
        JsonToken token = input.getCurrentToken();
        if( token == JsonToken.VALUE_NULL )
        {
            return null;
        }
        if( token != JsonToken.START_ARRAY )
        {
            token = input.nextToken();
        }
        if( token == JsonToken.VALUE_NULL )
        {
            return null;
        }
        if( token != JsonToken.START_ARRAY )
        {
            throw new ValueSerializationException( "Expected an array start at "
                                                   + input.getCurrentLocation().toString() );
        }
        JsonToken currentToken = input.nextToken();
        while( currentToken != JsonToken.END_ARRAY )
        {
            if( currentToken != JsonToken.START_OBJECT )
            {
                throw new ValueSerializationException( "Expected an object start at "
                                                       + input.getCurrentLocation().toString() );
            }
            currentToken = input.nextToken();
            K key = null;
            V value = null;
            while( currentToken != JsonToken.END_OBJECT )
            {
                String objectKey = input.getCurrentName();
                input.nextToken();
                if( "key".equals( objectKey ) )
                {
                    key = keyDeserializer.map( input );
                }
                else if( "value".equals( objectKey ) )
                {
                    value = valueDeserializer.map( input );
                }
                else
                {
                    //input.nextToken();
                    input.skipChildren();
                }
                currentToken = input.nextToken();
            }
            if( key != null )
            {
                map.put( key, value );
            }
            currentToken = input.nextToken();
        }
        return map;
    }

    @Override
    protected ObjectNode readObjectTree( JsonParser input )
        throws Exception
    {
        JsonToken token = input.getCurrentToken();
        if( token == JsonToken.VALUE_NULL )
        {
            return null;
        }
        if( token != JsonToken.START_OBJECT )
        {
            token = input.nextToken();
        }
        if( token != JsonToken.START_OBJECT )
        {
            throw new ValueSerializationException( "Expected an object start at "
                                                   + input.getCurrentLocation().toString() );
        }
        return (ObjectNode) input.readValueAsTree();
    }

    @Override
    protected Object asSimpleValue( JsonNode inputNode )
        throws Exception
    {
        if( isNullOrMissing( inputNode ) )
        {
            return null;
        }
        if( !inputNode.isValueNode() )
        {
            throw new ValueSerializationException( "Expected a value node but got a container node " + inputNode );
        }
        if( inputNode.isDouble() )
        {
            return inputNode.asDouble();
        }
        if( inputNode.isLong() )
        {
            return inputNode.asLong();
        }
        if( inputNode.isInt() )
        {
            return inputNode.asInt();
        }
        if( inputNode.isBoolean() )
        {
            return inputNode.asBoolean();
        }
        return inputNode.asText();
    }

    @Override
    protected boolean isObjectValue( JsonNode inputNode )
        throws Exception
    {
        if( isNullOrMissing( inputNode ) )
        {
            return false;
        }
        return inputNode.isObject();
    }

    @Override
    protected boolean objectHasField( JsonNode inputNode, String key )
        throws Exception
    {
        if( isNullOrMissing( inputNode ) )
        {
            return false;
        }
        if( !inputNode.isObject() )
        {
            throw new ValueSerializationException( "Expected an object but got " + inputNode );
        }
        return inputNode.has( key );
    }

    @Override
    protected <T> T getObjectFieldValue( JsonNode inputNode, String key, Function<JsonNode, T> valueDeserializer )
        throws Exception
    {
        JsonNode valueNode = inputNode.get( key );
        if( isNullOrMissing( valueNode ) )
        {
            return null;
        }
        T value = valueDeserializer.map( valueNode );
        return value;
    }

    @Override
    protected <T> void putArrayNodeInCollection( JsonNode inputNode,
                                                 Function<JsonNode, T> deserializer,
                                                 Collection<T> collection )
        throws Exception
    {
        if( isNullOrMissing( inputNode ) )
        {
            return;
        }
        if( !inputNode.isArray() )
        {
            throw new ValueSerializationException( "Expected an array but got " + inputNode );
        }
        ArrayNode array = (ArrayNode) inputNode;
        for( JsonNode item : array )
        {
            T value = deserializer.map( item );
            collection.add( value );
        }
    }

    @Override
    protected <K, V> void putArrayNodeInMap( JsonNode inputNode,
                                             Function<JsonNode, K> keyDeserializer,
                                             Function<JsonNode, V> valueDeserializer,
                                             Map<K, V> map )
        throws Exception
    {
        if( isNullOrMissing( inputNode ) )
        {
            return;
        }
        if( !inputNode.isArray() )
        {
            throw new ValueSerializationException( "Expected an array but got " + inputNode );
        }
        ArrayNode array = (ArrayNode) inputNode;
        for( JsonNode item : array )
        {
            if( !item.isObject() )
            {
                throw new ValueSerializationException( "Expected an object but got " + inputNode );
            }
            JsonNode keyNode = item.get( "key" );
            JsonNode valueNode = item.get( "value" );
            K key = keyDeserializer.map( keyNode );
            V value = valueDeserializer.map( valueNode );
            if( key != null )
            {
                map.put( key, value );
            }
        }
    }

    @Override
    protected <V> void putObjectNodeInMap( JsonNode inputNode,
                                           Function<JsonNode, V> valueDeserializer,
                                           Map<String, V> map
    )
        throws Exception
    {
        if( isNullOrMissing( inputNode ) )
        {
            return;
        }
        if( !inputNode.isObject() )
        {
            throw new ValueSerializationException( "Expected an object but got " + inputNode );
        }
        ObjectNode object = (ObjectNode) inputNode;
        Iterator<Map.Entry<String, JsonNode>> fields = object.fields();
        while( fields.hasNext() )
        {
            Map.Entry<String, JsonNode> entry = fields.next();
            V value = valueDeserializer.map( entry.getValue() );
            map.put( entry.getKey(), value );
        }
    }

    private static boolean isNullOrMissing( JsonNode inputNode )
    {
        return inputNode == null || inputNode.isNull() || inputNode.isMissingNode();
    }
}
