/*
 * 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.apache.xerces.impl.xs.traversers;

import java.util.ArrayList;
import java.util.Iterator;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.ProcessingInstruction;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import org.apache.xerces.impl.xs.opti.SchemaDOMParser;
import org.apache.xerces.util.JAXPNamespaceContextWrapper;
import org.apache.xerces.util.StAXLocationWrapper;
import org.apache.xerces.util.SymbolTable;
import org.apache.xerces.util.XMLAttributesImpl;
import org.apache.xerces.util.XMLStringBuffer;
import org.apache.xerces.util.XMLSymbols;
import org.apache.xerces.xni.NamespaceContext;
import org.apache.xerces.xni.QName;
import org.apache.xerces.xni.XMLString;
import org.apache.xerces.xni.XNIException;
import org.w3c.dom.Document;

/**
 * <p>StAXSchemaParser reads StAX events, converts them into XNI events
 * and passes them directly to the SchemaDOMParser.</p>
 * 
 * @xerces.internal
 * 
 * @version $Id$
 */
final class StAXSchemaParser {
    
    /** Chunk size (1024). */
    private static final int CHUNK_SIZE = (1 << 10);
    
    /** Chunk mask (CHUNK_SIZE - 1). */
    private static final int CHUNK_MASK = CHUNK_SIZE - 1;

    /** Array for holding character data. **/
    private final char [] fCharBuffer = new char[CHUNK_SIZE];
    
    /** Symbol table **/
    private SymbolTable fSymbolTable;
    
    /** SchemaDOMParser, events will be delegated to SchemaDOMParser to pass */
    private SchemaDOMParser fSchemaDOMParser;
    
    /** XML Locator wrapper for SAX. **/
    private final StAXLocationWrapper fLocationWrapper = new StAXLocationWrapper();
    
    /** The namespace context of this document: stores namespaces in scope */
    private final JAXPNamespaceContextWrapper fNamespaceContext = new JAXPNamespaceContextWrapper(fSymbolTable);
    
    /** Fields for start element, end element and characters. */
    private final QName fElementQName = new QName();
    private final QName fAttributeQName = new QName();
    private final XMLAttributesImpl fAttributes = new XMLAttributesImpl();
    private final XMLString fTempString = new XMLString();
    private final ArrayList fDeclaredPrefixes = new ArrayList();
    private final XMLStringBuffer fStringBuffer = new XMLStringBuffer();
    private int fDepth;
    
    public StAXSchemaParser() {
        fNamespaceContext.setDeclaredPrefixes(fDeclaredPrefixes);
    }
    
    public void reset(SchemaDOMParser schemaDOMParser, SymbolTable symbolTable) {
        fSchemaDOMParser = schemaDOMParser;
        fSymbolTable = symbolTable;
        fNamespaceContext.setSymbolTable(fSymbolTable);
        fNamespaceContext.reset();
    }

    public Document getDocument() {
        return fSchemaDOMParser.getDocument();
    }

    public void parse(XMLEventReader input) throws XMLStreamException, XNIException {
        XMLEvent currentEvent = input.peek();
        if (currentEvent != null) {
            int eventType = currentEvent.getEventType();
            if (eventType != XMLStreamConstants.START_DOCUMENT &&
                eventType != XMLStreamConstants.START_ELEMENT) {
                throw new XMLStreamException();
            }
            fLocationWrapper.setLocation(currentEvent.getLocation());
            fSchemaDOMParser.startDocument(fLocationWrapper, null, fNamespaceContext, null);
            loop: while (input.hasNext()) {
                currentEvent = input.nextEvent();
                eventType = currentEvent.getEventType();
                switch (eventType) {
                case XMLStreamConstants.START_ELEMENT:
                    ++fDepth;
                    StartElement start = currentEvent.asStartElement();
                    fillQName(fElementQName, start.getName());
                    fLocationWrapper.setLocation(start.getLocation());
                    fNamespaceContext.setNamespaceContext(start.getNamespaceContext());
                    fillXMLAttributes(start);
                    fillDeclaredPrefixes(start);
                    addNamespaceDeclarations();
                    fNamespaceContext.pushContext();
                    fSchemaDOMParser.startElement(fElementQName, fAttributes, null);
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    EndElement end = currentEvent.asEndElement();
                    fillQName(fElementQName, end.getName());
                    fillDeclaredPrefixes(end);
                    fLocationWrapper.setLocation(end.getLocation());
                    fSchemaDOMParser.endElement(fElementQName, null);
                    fNamespaceContext.popContext();
                    --fDepth;
                    if (fDepth <= 0) {
                        break loop;
                    }
                    break;
                case XMLStreamConstants.CHARACTERS:
                    sendCharactersToSchemaParser(currentEvent.asCharacters().getData(), false);
                    break;
                case XMLStreamConstants.SPACE:
                    sendCharactersToSchemaParser(currentEvent.asCharacters().getData(), true);
                    break;
                case XMLStreamConstants.CDATA:
                    fSchemaDOMParser.startCDATA(null);
                    sendCharactersToSchemaParser(currentEvent.asCharacters().getData(), false);
                    fSchemaDOMParser.endCDATA(null);
                    break;
                case XMLStreamConstants.PROCESSING_INSTRUCTION:
                    ProcessingInstruction pi = (ProcessingInstruction)currentEvent;
                    fillProcessingInstruction(pi.getData());
                    fSchemaDOMParser.processingInstruction(pi.getTarget(), fTempString, null);
                    break;
                case XMLStreamConstants.DTD:
                    /* There shouldn't be a DTD in the schema */
                    break;
                case XMLStreamConstants.ENTITY_REFERENCE:
                    /* Not needed for schemas */
                    break;
                case XMLStreamConstants.COMMENT:
                    /* No point in sending comments */
                    break;
                case XMLStreamConstants.START_DOCUMENT:
                    fDepth++;
                    /* We automatically call startDocument before the loop */
                    break;
                case XMLStreamConstants.END_DOCUMENT:
                    /* We automatically call endDocument after the loop */
                    break;
                }
            }
            fLocationWrapper.setLocation(null);
            fNamespaceContext.setNamespaceContext(null);
            fSchemaDOMParser.endDocument(null);
        }
    }
    
    public void parse(XMLStreamReader input) throws XMLStreamException, XNIException {
        if (input.hasNext()) {
            int eventType = input.getEventType();
            if (eventType != XMLStreamConstants.START_DOCUMENT &&
                eventType != XMLStreamConstants.START_ELEMENT) {
                throw new XMLStreamException();
            }
            fLocationWrapper.setLocation(input.getLocation());
            fSchemaDOMParser.startDocument(fLocationWrapper, null, fNamespaceContext, null);
            boolean first = true;
            loop: while (input.hasNext()) {
                if (!first) {
                    eventType = input.next();
                }
                else {
                    first = false;
                }
                switch (eventType) {
                case XMLStreamConstants.START_ELEMENT:
                    ++fDepth;
                    fLocationWrapper.setLocation(input.getLocation());
                    fNamespaceContext.setNamespaceContext(input.getNamespaceContext());
                    fillQName(fElementQName, input.getNamespaceURI(),
                        input.getLocalName(), input.getPrefix());
                    fillXMLAttributes(input);
                    fillDeclaredPrefixes(input);
                    addNamespaceDeclarations();
                    fNamespaceContext.pushContext();
                    fSchemaDOMParser.startElement(fElementQName, fAttributes, null);
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    fLocationWrapper.setLocation(input.getLocation());
                    fNamespaceContext.setNamespaceContext(input.getNamespaceContext());
                    fillQName(fElementQName, input.getNamespaceURI(), 
                        input.getLocalName(), input.getPrefix());
                    fillDeclaredPrefixes(input);
                    fSchemaDOMParser.endElement(fElementQName, null);
                    fNamespaceContext.popContext();
                    --fDepth;
                    if (fDepth <= 0) {
                        break loop;
                    }
                    break;
                case XMLStreamConstants.CHARACTERS:
                    fTempString.setValues(input.getTextCharacters(), 
                        input.getTextStart(), input.getTextLength());
                    fSchemaDOMParser.characters(fTempString, null);
                    break;
                case XMLStreamConstants.SPACE:
                    fTempString.setValues(input.getTextCharacters(), 
                        input.getTextStart(), input.getTextLength());
                    fSchemaDOMParser.ignorableWhitespace(fTempString, null);
                    break;
                case XMLStreamConstants.CDATA:
                    fSchemaDOMParser.startCDATA(null);
                    fTempString.setValues(input.getTextCharacters(), 
                        input.getTextStart(), input.getTextLength());
                    fSchemaDOMParser.characters(fTempString, null);
                    fSchemaDOMParser.endCDATA(null);
                    break;
                case XMLStreamConstants.PROCESSING_INSTRUCTION:
                    fillProcessingInstruction(input.getPIData());
                    fSchemaDOMParser.processingInstruction(input.getPITarget(), fTempString, null);
                    break;
                case XMLStreamConstants.DTD:
                    /* There shouldn't be a DTD in the schema */
                    break;
                case XMLStreamConstants.ENTITY_REFERENCE:
                    /* Not needed for schemas */
                    break;
                case XMLStreamConstants.COMMENT:
                    /* No point in sending comments */
                    break;
                case XMLStreamConstants.START_DOCUMENT:
                    ++fDepth;
                    /* We automatically call startDocument before the loop */
                    break;
                case XMLStreamConstants.END_DOCUMENT:
                    /* We automatically call endDocument after the loop */
                    break;
                }
            }
            fLocationWrapper.setLocation(null);
            fNamespaceContext.setNamespaceContext(null);
            fSchemaDOMParser.endDocument(null);
        }
    }
    
    /** Send characters to the validator in CHUNK_SIZE character chunks. */
    private void sendCharactersToSchemaParser(String str, boolean whitespace) {
        if (str != null) {
            final int length = str.length();
            final int remainder = length & CHUNK_MASK;
            if (remainder > 0) {
                str.getChars(0, remainder, fCharBuffer, 0);
                fTempString.setValues(fCharBuffer, 0, remainder);
                if (whitespace) {
                    fSchemaDOMParser.ignorableWhitespace(fTempString, null);
                }
                else {
                    fSchemaDOMParser.characters(fTempString, null);
                }
            }
            int i = remainder;
            while (i < length) {
                str.getChars(i, i += CHUNK_SIZE, fCharBuffer, 0);
                fTempString.setValues(fCharBuffer, 0, CHUNK_SIZE);
                if (whitespace) {
                    fSchemaDOMParser.ignorableWhitespace(fTempString, null);
                }
                else {
                    fSchemaDOMParser.characters(fTempString, null);
                }
            }
        }
    }
    
    // processing instructions must be sent all in one chunk
    private void fillProcessingInstruction(String data) {
        final int dataLength = data.length();
        char [] charBuffer = fCharBuffer;
        if (charBuffer.length < dataLength) {
            // toCharArray() creates a newly allocated array, so it's okay
            // to keep a reference to it.
            charBuffer = data.toCharArray();
        }
        else {
            data.getChars(0, dataLength, charBuffer, 0);
        }
        fTempString.setValues(charBuffer, 0, dataLength);
    }

    private void fillXMLAttributes(StartElement event) {
        fAttributes.removeAllAttributes();
        final Iterator attrs = event.getAttributes();
        while (attrs.hasNext()) {
            Attribute attr = (Attribute) attrs.next();
            fillQName(fAttributeQName, attr.getName());
            String type = attr.getDTDType();
            int idx = fAttributes.getLength();
            fAttributes.addAttributeNS(fAttributeQName, 
                    (type != null) ? type : XMLSymbols.fCDATASymbol, attr.getValue());
            fAttributes.setSpecified(idx, attr.isSpecified());
        }
    }
    
    private void fillXMLAttributes(XMLStreamReader input) {
        fAttributes.removeAllAttributes();
        final int len = input.getAttributeCount();
        for (int i = 0; i < len; ++i) {
            fillQName(fAttributeQName, input.getAttributeNamespace(i), 
                input.getAttributeLocalName(i), input.getAttributePrefix(i));
            String type = input.getAttributeType(i);
            fAttributes.addAttributeNS(fAttributeQName, 
                    (type != null) ? type : XMLSymbols.fCDATASymbol, input.getAttributeValue(i));
            fAttributes.setSpecified(i, input.isAttributeSpecified(i));
        }
    }
    
    private void addNamespaceDeclarations() {
        String prefix = null;
        String localpart = null;
        String rawname = null;
        String nsPrefix = null;
        String nsURI = null;
        
        final Iterator iter = fDeclaredPrefixes.iterator();
        while (iter.hasNext()) {
            nsPrefix = (String) iter.next();
            nsURI = fNamespaceContext.getURI(nsPrefix);
            if (nsPrefix.length() > 0) {
                prefix = XMLSymbols.PREFIX_XMLNS;
                localpart = nsPrefix;
                fStringBuffer.clear();
                fStringBuffer.append(prefix);
                fStringBuffer.append(':');
                fStringBuffer.append(localpart);
                rawname = fSymbolTable.addSymbol(fStringBuffer.ch, fStringBuffer.offset, fStringBuffer.length);
            }
            else {
                prefix = XMLSymbols.EMPTY_STRING;
                localpart = XMLSymbols.PREFIX_XMLNS;
                rawname = XMLSymbols.PREFIX_XMLNS;
            }
            fAttributeQName.setValues(prefix, localpart, rawname, NamespaceContext.XMLNS_URI);
            fAttributes.addAttribute(fAttributeQName, XMLSymbols.fCDATASymbol, 
                    (nsURI != null) ? nsURI : XMLSymbols.EMPTY_STRING);
        }
    }
    
    /** Fills in the list of declared prefixes. */
    private void fillDeclaredPrefixes(StartElement event) {
        fillDeclaredPrefixes(event.getNamespaces());
    }
    
    /** Fills in the list of declared prefixes. */
    private void fillDeclaredPrefixes(EndElement event) {
        fillDeclaredPrefixes(event.getNamespaces());
    }
    
    /** Fills in the list of declared prefixes. */
    private void fillDeclaredPrefixes(Iterator namespaces) {
        fDeclaredPrefixes.clear();
        while (namespaces.hasNext()) {
            Namespace ns = (Namespace) namespaces.next();
            String prefix = ns.getPrefix();
            fDeclaredPrefixes.add(prefix != null ? prefix : "");
        }
    }
    
    /** Fills in the list of declared prefixes. */
    private void fillDeclaredPrefixes(XMLStreamReader reader) {
        fDeclaredPrefixes.clear();
        final int len = reader.getNamespaceCount();
        for (int i = 0; i < len; ++i) {
            String prefix = reader.getNamespacePrefix(i);
            fDeclaredPrefixes.add(prefix != null ? prefix : "");
        }
    }

    /** Fills in a QName object. */
    private void fillQName(QName toFill, javax.xml.namespace.QName toCopy) {
        fillQName(toFill, toCopy.getNamespaceURI(), toCopy.getLocalPart(), toCopy.getPrefix());
    }

    /** Fills in a QName object. */
    final void fillQName(QName toFill, String uri, String localpart, String prefix) {
        uri = (uri != null && uri.length() > 0) ? fSymbolTable.addSymbol(uri) : null;
        localpart = (localpart != null) ? fSymbolTable.addSymbol(localpart) : XMLSymbols.EMPTY_STRING;
        prefix = (prefix != null && prefix.length() > 0) ? fSymbolTable.addSymbol(prefix) : XMLSymbols.EMPTY_STRING;
        String raw = localpart;
        if (prefix != XMLSymbols.EMPTY_STRING) {
            fStringBuffer.clear();
            fStringBuffer.append(prefix);
            fStringBuffer.append(':');
            fStringBuffer.append(localpart);
            raw = fSymbolTable.addSymbol(fStringBuffer.ch, fStringBuffer.offset, fStringBuffer.length);
        }
        toFill.setValues(prefix, localpart, raw, uri);
    }
    
} // StAXSchemaParser
