/*
 * Copyright (c) 2007, Rickard Öberg. All Rights Reserved.
 * Copyright (c) 2010, Niclas Hehdman. All Rights Reserved.
 * 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.orgjson;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
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.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 org.json.
 */
public class OrgJsonValueDeserializer
    extends ValueDeserializerAdapter<JSONTokener, Object>
{

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

    /* package */ OrgJsonValueDeserializer(
        Application application,
        Module module,
        Function<Application, Module> valuesModuleFinder )
    {
        super( application, module, valuesModuleFinder );
    }

    @Override
    protected JSONTokener adaptInput( InputStream input )
        throws Exception
    {
        return new JSONTokener( new InputStreamReader( input, "UTF-8" ) );
    }

    @Override
    protected Object readPlainValue( JSONTokener input )
        throws Exception
    {
        Object nextValue = input.nextValue();
        if( JSONObject.NULL.equals( nextValue ) )
        {
            return null;
        }
        else // Object or Array
        if( JSONObject.class.isAssignableFrom( nextValue.getClass() )
            || JSONArray.class.isAssignableFrom( nextValue.getClass() ) )
        {
            throw new ValueSerializationException( "Asked for a Value but found an Object or an Array." );
        }
        return nextValue;
    }

    @Override
    protected <T> Collection<T> readArrayInCollection( JSONTokener input,
                                                       Function<JSONTokener, T> deserializer,
                                                       Collection<T> collection )
        throws Exception
    {
        char c = input.nextClean();
        char q;
        if( c == 'n' ) // null?
        {
            /*
             * Handle unquoted text. This could be the values true, false, or
             * null, or it can be a number. An implementation (such as this one)
             * is allowed to also accept non-standard forms.
             *
             * Accumulate characters until we reach the end of the text or a
             * formatting character.
             */
            StringBuilder sb = new StringBuilder();
            sb.setLength( 0 );
            while( c >= ' ' && ",:]}/\\\"[{;=#".indexOf( c ) < 0 )
            {
                sb.append( c );
                c = input.next();
            }
            input.back();
            String s = sb.toString().trim();
            if( !"null".equals( s ) )
            {
                input.syntaxError( "Unknown value: '" + s + "'" );
            }
            return null;
        }
        else if( c == '[' )
        {
            q = ']';
        }
        else
        {
            throw input.syntaxError( "A JSONArray text must start with '['" );
        }
        if( input.nextClean() == ']' )
        {
            return collection;
        }
        input.back();
        for( ;; )
        {
            if( input.nextClean() == ',' )
            {
                input.back();
                collection.add( null );
            }
            else
            {
                input.back();
                collection.add( deserializer.map( input ) );
            }
            c = input.nextClean();
            switch( c )
            {
                case ';':
                case ',':
                    if( input.nextClean() == ']' )
                    {
                        return collection;
                    }
                    input.back();
                    break;
                case ']':
                case ')':
                    if( q != c )
                    {
                        throw input.syntaxError( "Expected a '" + Character.valueOf( q ) + "'" );
                    }
                    return collection;
                default:
                    throw input.syntaxError( "Expected a ',' or ']'" );
            }
        }
    }

    @Override
    protected <K, V> Map<K, V> readMapInMap( JSONTokener input,
                                             Function<JSONTokener, K> keyDeserializer,
                                             Function<JSONTokener, V> valueDeserializer,
                                             Map<K, V> map )
        throws Exception
    {
        char c = input.nextClean();
        char q;
        if( c == 'n' ) // null?
        {
            /*
             * Handle unquoted text. This could be the values true, false, or
             * null, or it can be a number. An implementation (such as this one)
             * is allowed to also accept non-standard forms.
             *
             * Accumulate characters until we reach the end of the text or a
             * formatting character.
             */
            StringBuilder sb = new StringBuilder();
            sb.setLength( 0 );
            while( c >= ' ' && ",:]}/\\\"[{;=#".indexOf( c ) < 0 )
            {
                sb.append( c );
                c = input.next();
            }
            input.back();
            String s = sb.toString().trim();
            if( !"null".equals( s ) )
            {
                input.syntaxError( "Unknown value: '" + s + "'" );
            }
            return null;
        }
        else if( c == '[' )
        {
            q = ']';
        }
        else
        {
            throw input.syntaxError( "A JSONArray text must start with '['" );
        }
        if( input.nextClean() == ']' )
        {
            return map;
        }
        input.back();

        for( ;; )
        {
            if( input.nextClean() == ',' )
            {
                input.back();
            }
            else
            {
                input.back();
                // Map entry!
                if( input.nextClean() != '{' )
                {
                    throw input.syntaxError( "A JSONObject text must begin with '{'" );
                }

                String objectKey;
                K key = null;
                V value = null;

                boolean breakIteration = false;
                while( !breakIteration )
                {
                    c = input.nextClean();
                    switch( c )
                    {
                        case 0:
                            throw input.syntaxError( "A JSONObject text must end with '}'" );
                        case '}':
                            breakIteration = true;
                            continue;
                        default:
                            input.back();
                            objectKey = input.nextValue().toString();
                    }

                    /*
                     * The key is followed by ':'. We will also tolerate '=' or '=>'.
                     */
                    c = input.nextClean();
                    if( c == '=' )
                    {
                        if( input.next() != '>' )
                        {
                            input.back();
                        }
                    }
                    else if( c != ':' )
                    {
                        throw input.syntaxError( "Expected a ':' after a key" );
                    }

                    if( "key".equals( objectKey ) )
                    {
                        key = keyDeserializer.map( input );
                    }
                    else if( "value".equals( objectKey ) )
                    {
                        value = valueDeserializer.map( input );
                    }
                    else
                    {
                        input.nextValue();
                    }

                    /*
                     * Pairs are separated by ','. We will also tolerate ';'.
                     */
                    switch( input.nextClean() )
                    {
                        case ';':
                        case ',':
                            if( input.nextClean() == '}' )
                            {
                                breakIteration = true;
                                continue;
                            }
                            input.back();
                            continue;
                        case '}':
                            breakIteration = true;
                            continue;
                        default:
                            throw input.syntaxError( "Expected a ',' or '}'" );
                    }
                }
                if( key != null )
                {
                    map.put( key, value );
                }
            }
            c = input.nextClean();
            switch( c )
            {
                case ';':
                case ',':
                    if( input.nextClean() == ']' )
                    {
                        return map;
                    }
                    input.back();
                    break;
                case ']':
                case ')':
                    if( q != c )
                    {
                        throw input.syntaxError( "Expected a '" + Character.valueOf( q ) + "'" );
                    }
                    return map;
                default:
                    throw input.syntaxError( "Expected a ',' or ']'" );
            }
        }
    }

    //
    // Deserialization - Tree parsing
    //
    @Override
    protected JSONObject readObjectTree( JSONTokener input )
        throws Exception
    {
        Object objectTree = input.nextValue();
        if( JSONObject.NULL.equals( objectTree ) )
        {
            return null;
        }
        return (JSONObject) objectTree;
    }

    @Override
    protected Object asSimpleValue( Object inputNode )
        throws Exception
    {
        if( JSONObject.NULL.equals( inputNode ) )
        {
            return null;
        }
        if( inputNode instanceof JSONObject || inputNode instanceof JSONArray )
        {
            throw new ValueSerializationException( "Expected a simple value but got " + inputNode );
        }
        return inputNode;
    }

    @Override
    protected boolean isObjectValue( Object inputNode )
        throws Exception
    {
        if( JSONObject.NULL.equals( inputNode ) )
        {
            return false;
        }
        return inputNode instanceof JSONObject;
    }

    @Override
    protected boolean objectHasField( Object inputNode, String key )
        throws Exception
    {
        if( JSONObject.NULL.equals( inputNode ) )
        {
            return false;
        }
        if( !( inputNode instanceof JSONObject ) )
        {
            throw new ValueSerializationException( "Expected an object but got " + inputNode );
        }
        JSONObject json = (JSONObject) inputNode;
        return json.has( key );
    }

    @Override
    protected <T> T getObjectFieldValue( Object inputNode, String key, Function<Object, T> valueDeserializer )
        throws Exception
    {
        JSONObject json = (JSONObject) inputNode;
        Object valueNode = json.opt( key );
        if( JSONObject.NULL.equals( valueNode ) )
        {
            return null;
        }
        T value = valueDeserializer.map( valueNode );
        return value;
    }

    @Override
    protected <T> void putArrayNodeInCollection( Object inputNode, Function<Object, T> deserializer, Collection<T> collection )
        throws Exception
    {
        if( JSONObject.NULL.equals( inputNode ) )
        {
            return;
        }
        if( !( inputNode instanceof JSONArray ) )
        {
            throw new ValueSerializationException( "Expected an array but got " + inputNode );
        }
        JSONArray array = (JSONArray) inputNode;
        for( int idx = 0; idx < array.length(); idx++ )
        {
            Object item = array.get( idx );
            T value = deserializer.map( item );
            collection.add( value );
        }
    }

    @Override
    protected <K, V> void putArrayNodeInMap( Object inputNode, Function<Object, K> keyDeserializer, Function<Object, V> valueDeserializer, Map<K, V> map )
        throws Exception
    {
        if( JSONObject.NULL.equals( inputNode ) )
        {
            return;
        }
        if( !( inputNode instanceof JSONArray ) )
        {
            throw new ValueSerializationException( "Expected an array but got " + inputNode );
        }
        JSONArray array = (JSONArray) inputNode;
        for( int idx = 0; idx < array.length(); idx++ )
        {
            Object item = array.get( idx );
            if( !( item instanceof JSONObject ) )
            {
                throw new ValueSerializationException( "Expected an object but got " + inputNode );
            }
            JSONObject object = (JSONObject) item;
            Object keyNode = object.get( "key" );
            Object valueNode = object.get( "value" );
            K key = keyDeserializer.map( keyNode );
            V value = valueDeserializer.map( valueNode );
            if( key != null )
            {
                map.put( key, value );
            }
        }
    }

    @Override
    protected <V> void putObjectNodeInMap( Object inputNode, Function<Object, V> valueDeserializer, Map<String, V> map )
        throws Exception
    {
        if( JSONObject.NULL.equals( inputNode ) )
        {
            return;
        }
        if( !( inputNode instanceof JSONObject ) )
        {
            throw new ValueSerializationException( "Expected an object but got " + inputNode );
        }
        JSONObject object = (JSONObject) inputNode;

        @SuppressWarnings( "unchecked" )
        Iterator<String> it = object.keys();
        while( it.hasNext() )
        {
            String key = it.next();
            Object item = object.get( key );
            V valueValue = valueDeserializer.map( item );
            if( key != null )
            {
                map.put( key, valueValue );
            }
        }
    }
}
