/*
 * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.xml.internal.stream;

import com.sun.xml.internal.stream.events.XMLEventAllocatorImpl;
import java.util.NoSuchElementException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.EntityReference;
import javax.xml.stream.events.XMLEvent;
import javax.xml.stream.util.XMLEventAllocator;

/**
 * @author  @author  Neeraj Bajaj Sun Microsystems
 *
 */

public class XMLEventReaderImpl implements javax.xml.stream.XMLEventReader{

    protected XMLStreamReader fXMLReader ;
    protected XMLEventAllocator fXMLEventAllocator;

    //only constructor will do because we delegate everything to underlying XMLStreamReader
    public XMLEventReaderImpl(XMLStreamReader reader) throws  XMLStreamException {
        fXMLReader = reader ;
        fXMLEventAllocator = (XMLEventAllocator)reader.getProperty(XMLInputFactory.ALLOCATOR);
        if(fXMLEventAllocator == null){
            fXMLEventAllocator = new XMLEventAllocatorImpl();
        }
        fPeekedEvent = fXMLEventAllocator.allocate(fXMLReader);
    }


    public boolean hasNext() {
        //if we have the peeked event return 'true'
        if(fPeekedEvent != null)return true;
        //this is strange XMLStreamReader throws XMLStreamException
        //XMLEventReader doesn't throw XMLStreamException
        boolean next = false ;
        try{
            next = fXMLReader.hasNext();
        }catch(XMLStreamException ex){
            return false;
        }
        return next ;
    }


    public XMLEvent nextEvent() throws XMLStreamException {
        //if application peeked return the peeked event
        if(fPeekedEvent != null){
            fLastEvent = fPeekedEvent ;
            fPeekedEvent = null;
            return fLastEvent ;
        }
        else if(fXMLReader.hasNext()){
            //advance the reader to next state.
            fXMLReader.next();
            return fLastEvent = fXMLEventAllocator.allocate(fXMLReader);
        }
        else{
            fLastEvent = null;
            throw new NoSuchElementException();
        }
    }

    public void remove(){
        //remove of the event is not supported.
        throw new java.lang.UnsupportedOperationException();
    }


    public void close() throws XMLStreamException {
        fXMLReader.close();
    }

    /** Reads the content of a text-only element. Precondition:
     * the current event is START_ELEMENT. Postcondition:
     * The current event is the corresponding END_ELEMENT.
     * @throws XMLStreamException if the current event is not a START_ELEMENT
     * or if a non text element is encountered
     */
    public String getElementText() throws XMLStreamException {
        //we have to keep reference to the 'last event' of the stream to be able
        //to make this check - is there another way ? - nb.
        if(fLastEvent.getEventType() != XMLEvent.START_ELEMENT){
            throw new XMLStreamException(
            "parser must be on START_ELEMENT to read next text", fLastEvent.getLocation());
        }

        // STag content ETag
        //[43]   content   ::=   CharData? ((element | Reference | CDSect | PI | Comment) CharData?)*

        //<foo>....some long text say in KB and underlying parser reports multiple character
        // but getElementText() events....</foo>

        String data = null;
        //having a peeked event makes things really worse -- we have to test the first event
        if(fPeekedEvent != null){
            XMLEvent event = fPeekedEvent ;
            fPeekedEvent = null;
            int type = event.getEventType();

            if(  type == XMLEvent.CHARACTERS || type == XMLEvent.SPACE ||
            type == XMLEvent.CDATA){
                data = event.asCharacters().getData();
            }
            else if(type == XMLEvent.ENTITY_REFERENCE){
                data = ((EntityReference)event).getDeclaration().getReplacementText();
            }
            else if(type == XMLEvent.COMMENT || type == XMLEvent.PROCESSING_INSTRUCTION){
                //ignore
            } else if(type == XMLEvent.START_ELEMENT) {
                throw new XMLStreamException(
                "elementGetText() function expects text only elment but START_ELEMENT was encountered.", event.getLocation());
            }else if(type == XMLEvent.END_ELEMENT){
                return "";
            }

            //create the string buffer and add initial data
            StringBuffer buffer = new StringBuffer();
            if(data != null && data.length() > 0 ) {
                buffer.append(data);
            }
            //get the next event -- we should stop at END_ELEMENT but it can be any thing
            //things are worse when implementing this function in XMLEventReader because
            //there isn't any function called getText() which can get values for
            //space, cdata, characters and entity reference
            //nextEvent() would also set the last event.
            event = nextEvent();
            while(event.getEventType() != XMLEvent.END_ELEMENT){
                if(  type == XMLEvent.CHARACTERS || type == XMLEvent.SPACE ||
                type == XMLEvent.CDATA){
                    data = event.asCharacters().getData();
                }
                else if(type == XMLEvent.ENTITY_REFERENCE){
                    data = ((EntityReference)event).getDeclaration().getReplacementText();
                }
                else if(type == XMLEvent.COMMENT || type == XMLEvent.PROCESSING_INSTRUCTION){
                    //ignore
                } else if(type == XMLEvent.END_DOCUMENT) {
                    throw new XMLStreamException("unexpected end of document when reading element text content");
                } else if(type == XMLEvent.START_ELEMENT) {
                    throw new XMLStreamException(
                    "elementGetText() function expects text only elment but START_ELEMENT was encountered.", event.getLocation());
                } else {
                    throw new XMLStreamException(
                    "Unexpected event type "+ type, event.getLocation());
                }
                //add the data to the buffer
                if(data != null && data.length() > 0 ) {
                    buffer.append(data);
                }
                event = nextEvent();
            }
            return buffer.toString();
        }//if (fPeekedEvent != null)

        //if there was no peeked, delegate everything to fXMLReader
        //update the last event before returning the text
        data = fXMLReader.getElementText();
        fLastEvent = fXMLEventAllocator.allocate(fXMLReader);
        return data;
    }

    /** Get the value of a feature/property from the underlying implementation
     * @param name The name of the property
     * @return The value of the property
     * @throws IllegalArgumentException if the property is not supported
     */
    public Object getProperty(java.lang.String name) throws java.lang.IllegalArgumentException {
        return fXMLReader.getProperty(name) ;
    }

    /** Skips any insignificant space events until a START_ELEMENT or
     * END_ELEMENT is reached. If anything other than space characters are
     * encountered, an exception is thrown. This method should
     * be used when processing element-only content because
     * the parser is not able to recognize ignorable whitespace if
     * the DTD is missing or not interpreted.
     * @throws XMLStreamException if anything other than space characters are encountered
     */
    public XMLEvent nextTag() throws XMLStreamException {
        //its really a pain if there is peeked event before calling nextTag()
        if(fPeekedEvent != null){
            //check the peeked event first.
            XMLEvent event = fPeekedEvent;
            fPeekedEvent = null ;
            int eventType = event.getEventType();
            //if peeked event is whitespace move to the next event
            //if peeked event is PI or COMMENT move to the next event
            if( (event.isCharacters() && event.asCharacters().isWhiteSpace())
            || eventType == XMLStreamConstants.PROCESSING_INSTRUCTION
            || eventType == XMLStreamConstants.COMMENT
            || eventType == XMLStreamConstants.START_DOCUMENT){
                event = nextEvent();
                eventType = event.getEventType();
            }

            //we have to have the while loop because there can be many PI or comment event in sucession
            while((event.isCharacters() && event.asCharacters().isWhiteSpace())
            || eventType == XMLStreamConstants.PROCESSING_INSTRUCTION
            || eventType == XMLStreamConstants.COMMENT){

                event = nextEvent();
                eventType = event.getEventType();
            }

            if (eventType != XMLStreamConstants.START_ELEMENT && eventType != XMLStreamConstants.END_ELEMENT) {
                throw new XMLStreamException("expected start or end tag", event.getLocation());
            }
            return event;
        }

        //if there is no peeked event -- delegate the work of getting next event to fXMLReader
        fXMLReader.nextTag();
        return (fLastEvent = fXMLEventAllocator.allocate(fXMLReader));
    }

    public Object next() {
        Object object = null;
        try{
            object = nextEvent();
        }catch(XMLStreamException streamException){
            fLastEvent = null ;
            //don't swallow the cause
            NoSuchElementException e = new NoSuchElementException(streamException.getMessage());
            e.initCause(streamException.getCause());
            throw e;
        }
        return object;
    }

    public XMLEvent peek() throws XMLStreamException{
        //if someone call peek() two times we should just return the peeked event
        //this is reset if we call next() or nextEvent()
        if(fPeekedEvent != null) return fPeekedEvent;

        if(hasNext()){
            //revisit: we can implement peek() by calling underlying reader to advance
            // the stream and returning the event without the knowledge of the user
            // that the stream was advanced but the point is we are advancing the stream
            //here. -- nb.

            // Is there any application that relies on this behavior ?
            //Can it be an application knows that there is particularly very large 'comment' section
            //or character data which it doesn't want to read or to be returned as event
            //But as of now we are creating every event but it can be optimized not to create
            // the event.
            fXMLReader.next();
            fPeekedEvent = fXMLEventAllocator.allocate(fXMLReader);
            return fPeekedEvent;
        }else{
            return null;
        }
    }//peek()

    private XMLEvent fPeekedEvent;
    private XMLEvent fLastEvent;

}//XMLEventReaderImpl
