/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.stax;

import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Map;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.lang.StringEscapeUtils;
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;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * ValueDeserializer reading Values from XML documents.
 */
public class StaxValueDeserializer
    extends ValueDeserializerAdapter<XMLEventReader, Node>
{

    private final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
    private final TransformerFactory transformerFactory = TransformerFactory.newInstance();

    public StaxValueDeserializer( @Structure Application application,
                                  @Structure Module module,
                                  @Service ServiceReference<ValueDeserializer> serviceRef )
    {
        super( application, module, serviceRef );
        // Input Factory setup
        inputFactory.setProperty( "javax.xml.stream.isValidating", Boolean.FALSE );
        inputFactory.setProperty( "javax.xml.stream.isNamespaceAware", Boolean.FALSE );
        inputFactory.setProperty( "javax.xml.stream.supportDTD", Boolean.FALSE );
        inputFactory.setProperty( "javax.xml.stream.isCoalescing", Boolean.TRUE );
        inputFactory.setProperty( "javax.xml.stream.isReplacingEntityReferences", Boolean.FALSE );
    }

    @Override
    protected XMLEventReader adaptInput( InputStream input )
        throws Exception
    {
        return inputFactory.createXMLEventReader( input, "UTF-8" );
    }

    @Override
    protected Object readPlainValue( XMLEventReader input )
        throws Exception
    {
        if( !input.hasNext() )
        {
            return null;
        }
        XMLEvent nextEvent = input.nextEvent();
        if( nextEvent.getEventType() == XMLEvent.START_ELEMENT
            && "null".equals( nextEvent.asStartElement().getName().getLocalPart() ) )
        {
            input.nextTag();
            return null;
        }
        if( nextEvent.getEventType() != XMLEvent.CHARACTERS )
        {
            throw new ValueSerializationException( "Expected characters but got: " + nextEvent );
        }
        String stringValue = nextEvent.asCharacters().getData();
        return detectAndConvertStringValue( stringValue );
    }

    @Override
    protected <T> Collection<T> readArrayInCollection( XMLEventReader input,
                                                       Function<XMLEventReader, T> deserializer,
                                                       Collection<T> collection )
        throws Exception
    {
        if( !input.hasNext() )
        {
            return null;
        }
        XMLEvent nextTag = input.nextTag();
        if( nextTag.isStartElement() && "null".equals( nextTag.asStartElement().getName().getLocalPart() ) )
        {
            input.nextTag();
            return null;
        }
        if( !nextTag.isStartElement() || !"array".equals( nextTag.asStartElement().getName().getLocalPart() ) )
        {
            throw new ValueSerializationException( "Expected an <array/> but got: " + nextTag );
        }
        WHILE:
        while( input.hasNext() )
        {
            XMLEvent currentTag = input.nextTag();
            if( currentTag.isEndElement() )
            {
                String endElementName = currentTag.asEndElement().getName().getLocalPart();
                switch( endElementName )
                {
                    case "array":
                        break WHILE;
                    case "value":
                        continue;
                }
            }
            if( !"value".equals( currentTag.asStartElement().getName().getLocalPart() ) )
            {
                throw new ValueSerializationException( "Expected a <value/> but got: " + currentTag );
            }
            T item = deserializer.map( input );
            collection.add( item );
        }
        return collection;
    }

    @Override
    protected <K, V> Map<K, V> readMapInMap( XMLEventReader input,
                                             Function<XMLEventReader, K> keyDeserializer,
                                             Function<XMLEventReader, V> valueDeserializer,
                                             Map<K, V> map )
        throws Exception
    {
        if( !input.hasNext() )
        {
            return null;
        }
        XMLEvent nextTag = input.nextTag();
        if( nextTag.isStartElement() && "null".equals( nextTag.asStartElement().getName().getLocalPart() ) )
        {
            input.nextTag();
            return null;
        }
        if( !nextTag.isStartElement() || !"array".equals( nextTag.asStartElement().getName().getLocalPart() ) )
        {
            throw new ValueSerializationException( "Expected an <array/> but got: " + nextTag );
        }
        XMLEvent currentTag = input.nextTag(); // <object>
        while( !currentTag.isEndElement() || !"array".equals( currentTag.asEndElement().getName().getLocalPart() ) )
        {
            if( !currentTag.isStartElement() || !"object".equals( currentTag.asStartElement().getName().getLocalPart() ) )
            {
                throw new ValueSerializationException( "Expected an <object/> but got: " + nextTag );
            }
            currentTag = input.nextTag(); // <field>
            K key = null;
            V value = null;
            while( !currentTag.isEndElement() || !"object".equals( currentTag.asEndElement().getName().getLocalPart() ) )
            {
                input.nextTag(); // <name>
                String keyOrValue = input.nextEvent().asCharacters().getData();
                input.nextTag(); // </name>
                input.nextTag(); // <value>
                switch( keyOrValue )
                {
                    case "key":
                        key = keyDeserializer.map( input );
                        break;
                    case "value":
                        value = valueDeserializer.map( input );
                        break;
                    default:
                        readObjectTree( input );
                        break;
                }
                input.nextTag(); // </value>
                input.nextTag(); // </field>
                currentTag = input.nextTag();
            }
            if( key != null )
            {
                map.put( key, value );
            }
            currentTag = input.nextTag();
        }
        return map;
    }

    @Override
    protected Node readObjectTree( XMLEventReader input )
        throws Exception
    {
        XMLEvent peek = input.peek();
        if( peek.isStartElement() && "null".equals( peek.asStartElement().getName().getLocalPart() ) )
        {
            input.nextTag();// <null>
            input.nextTag();// </null>
            return null;
        }
        String elementBody = readElementBody( input );
        Transformer transformer = transformerFactory.newTransformer();
        DOMResult domResult = new DOMResult();
        transformer.transform( new StreamSource( new StringReader( elementBody ) ), domResult );
        return ( (Document) domResult.getNode() ).getDocumentElement();

    }

    private static String readElementBody( XMLEventReader input )
        throws XMLStreamException
    {
        StringWriter buf = new StringWriter( 1024 );
        int depth = 0;
        while( input.hasNext() )
        {
            // peek event
            XMLEvent xmlEvent = input.peek();
            if( xmlEvent.isStartElement() )
            {
                ++depth;
            }
            else if( xmlEvent.isEndElement() )
            {
                --depth;
                // reached END_ELEMENT tag?
                // break loop, leave event in stream
                if( depth < 0 )
                {
                    break;
                }
            }
            // consume event
            xmlEvent = input.nextEvent();
            // print out event
            xmlEvent.writeAsEncodedUnicode( buf );
        }
        return buf.getBuffer().toString();
    }

    @Override
    protected Object asSimpleValue( Node inputNode )
        throws Exception
    {
        if( inputNode == null )
        {
            return null;
        }
        if( inputNode.getNodeType() == Node.ELEMENT_NODE && "null".equals( inputNode.getLocalName() ) )
        {
            return null;
        }
        if( inputNode.getNodeType() != Node.TEXT_NODE && inputNode.getNodeType() != Node.CDATA_SECTION_NODE )
        {
            throw new ValueSerializationException( "Expected a TEXT or CDATA node but got " + inputNode );
        }
        String stringValue = inputNode.getNodeValue();
        return detectAndConvertStringValue( stringValue );
    }

    @Override
    protected boolean isObjectValue( Node inputNode )
        throws Exception
    {
        if( inputNode == null )
        {
            return false;
        }
        if( "object".equals( inputNode.getLocalName() ) )
        {
            return true;
        }
        if( !"value".equals( inputNode.getLocalName() ) )
        {
            return false;
        }
        return getDirectChildNode( inputNode, "object" ) != null;
    }

    @Override
    protected boolean objectHasField( Node inputNode, String key )
        throws Exception
    {
        if( inputNode == null )
        {
            return false;
        }
        Node objectNode;
        if( "value".equals( inputNode.getLocalName() ) )
        {
            objectNode = getDirectChildNode( inputNode, "object" );
        }
        else
        {
            objectNode = inputNode;
        }
        if( objectNode == null )
        {
            return false;
        }
        if( !"object".equals( objectNode.getLocalName() ) )
        {
            throw new ValueSerializationException( "Expected an object value but got: " + objectNode );
        }
        return getObjectFieldNode( objectNode, key ) != null;
    }

    @Override
    @SuppressWarnings( "unchecked" )
    protected <T> T getObjectFieldValue( Node inputNode,
                                         String key,
                                         Function<Node, T> valueDeserializer )
        throws Exception
    {
        if( inputNode == null )
        {
            return null;
        }
        Node objectNode;
        if( "value".equals( inputNode.getLocalName() ) )
        {
            objectNode = getDirectChildNode( inputNode, "object" );
        }
        else
        {
            objectNode = inputNode;
        }
        if( objectNode == null )
        {
            return null;
        }
        if( !"object".equals( objectNode.getLocalName() ) )
        {
            throw new ValueSerializationException( "Expected an object value but got: " + objectNode );
        }
        Node fieldNode = getObjectFieldNode( objectNode, key );
        if( fieldNode == null )
        {
            return null;
        }
        Node valueElement = getDirectChildNode( fieldNode, "value" );
        Node valueNode = valueElement.getFirstChild();
        if( valueNode == null )
        {
            return (T) "";
        }
        if( valueNode.getNodeType() == Node.ELEMENT_NODE && "null".equals( valueNode.getLocalName() ) )
        {
            return null;
        }
        T value = valueDeserializer.map( valueNode );
        return value;
    }

    @Override
    protected <T> void putArrayNodeInCollection( Node inputNode,
                                                 Function<Node, T> deserializer,
                                                 Collection<T> collection )
        throws Exception
    {
        if( inputNode == null )
        {
            return;
        }
        if( !( inputNode instanceof Element ) )
        {
            throw new ValueSerializationException( "Expected an Element but got " + inputNode );
        }
        NodeList arrayValues = inputNode.getChildNodes();
        for( int arrayValuesIndex = 0; arrayValuesIndex < arrayValues.getLength(); arrayValuesIndex++ )
        {
            Node arrayValue = arrayValues.item( arrayValuesIndex );
            T value = deserializer.map( arrayValue.getFirstChild() );
            collection.add( value );
        }
    }

    @Override
    protected <K, V> void putArrayNodeInMap( Node inputNode,
                                             Function<Node, K> keyDeserializer,
                                             Function<Node, V> valueDeserializer, Map<K, V> map )
        throws Exception
    {
        if( inputNode == null )
        {
            return;
        }
        if( !"array".equals( inputNode.getLocalName() ) )
        {
            throw new ValueSerializationException( "Expected an <array/> but got " + inputNode );
        }
        NodeList entriesNodes = inputNode.getChildNodes();
        for( int idx = 0; idx < entriesNodes.getLength(); idx++ )
        {
            Node entryNode = entriesNodes.item( idx );
            K key = getObjectFieldValue( entryNode, "key", keyDeserializer );
            V value = getObjectFieldValue( entryNode, "value", valueDeserializer );
            if( key != null )
            {
                map.put( key, value );
            }
        }
    }

    @Override
    protected <V> void putObjectNodeInMap( Node inputNode,
                                           Function<Node, V> valueDeserializer,
                                           Map<String, V> map )
        throws Exception
    {
        if( inputNode == null )
        {
            return;
        }
        if( !"object".equals( inputNode.getLocalName() ) )
        {
            throw new ValueSerializationException( "Expected an <object/> but got " + inputNode );
        }
        NodeList fieldsNodes = inputNode.getChildNodes();
        for( int idx = 0; idx < fieldsNodes.getLength(); idx++ )
        {
            Node fieldNode = fieldsNodes.item( idx );
            String key = getDirectChildNode( fieldNode, "name" ).getTextContent();
            if( key != null && key.length() > 0 )
            {
                V value = getObjectFieldValue( inputNode, key, valueDeserializer );
                map.put( key, value );
            }
        }
    }

    @SuppressWarnings( "AssignmentToMethodParameter" )
    private Object detectAndConvertStringValue( String stringValue )
    {
        if( stringValue == null || stringValue.length() == 0 )
        {
            return "";
        }
        stringValue = StringEscapeUtils.unescapeXml( stringValue );
        if( stringValue.matches( "[+-]?\\d+(\\.\\d+)?([eE][+-]?\\d+(\\.\\d+)?)?" ) )
        {
            if( stringValue.indexOf( '.' ) != -1 )
            {
                return new BigDecimal( stringValue );
            }
            return new BigInteger( stringValue );
        }
        if( "true".equalsIgnoreCase( stringValue ) || "false".equalsIgnoreCase( stringValue ) )
        {
            return Boolean.parseBoolean( stringValue );
        }
        return stringValue;
    }

    private static Node getObjectFieldNode( Node inputNode, String key )
    {
        if( inputNode == null )
        {
            return null;
        }
        if( !( inputNode instanceof Element ) )
        {
            throw new ValueSerializationException( "Excpected an Element but got " + inputNode );
        }
        NodeList fieldNodes = inputNode.getChildNodes();
        for( int idx = 0; idx < fieldNodes.getLength(); idx++ )
        {
            Node fieldNode = fieldNodes.item( idx );
            Node nameNode = getDirectChildNode( fieldNode, "name" );
            if( nameNode != null && key.equals( nameNode.getTextContent() ) )
            {
                return fieldNode;
            }
        }
        return null;
    }

    private static Node getDirectChildNode( Node parent, String name )
    {
        for( Node child = parent.getFirstChild(); child != null; child = child.getNextSibling() )
        {
            if( name.equals( child.getNodeName() ) )
            {
                return child;
            }
        }
        return null;
    }
}
