/*
 * Copyright 2011, 2012 Odysseus Software GmbH
 *
 * 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.apache.synapse.commons.staxon.core.xml;

import java.io.IOException;
import java.io.Reader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.XMLConstants;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;

import org.apache.synapse.commons.staxon.core.base.AbstractXMLStreamReader;
import org.apache.synapse.commons.staxon.core.base.XMLStreamReaderScope;

/**
 * Simple XML Stream Reader.
 * <p/>
 * The scope info is the element tag name.
 */
public class SimpleXMLStreamReader extends AbstractXMLStreamReader<String> {
    private final Reader reader;
    private int ch;

    public SimpleXMLStreamReader(Reader reader) throws XMLStreamException {
        super(null);
        this.reader = reader;
        try {
            nextChar();
        } catch (IOException e) {
            throw new XMLStreamException(e);
        }
        initialize();
    }

    private void readAttrNsDecl(String name, String value) throws XMLStreamException {
        int separator = name.indexOf(':');
        if (separator < 0) {
            if (XMLConstants.XMLNS_ATTRIBUTE.equals(name)) {
                readNsDecl(XMLConstants.DEFAULT_NS_PREFIX, value);
            } else {
                readAttr(XMLConstants.DEFAULT_NS_PREFIX, name, null, value);
            }
        } else {
            if (name.startsWith(XMLConstants.XMLNS_ATTRIBUTE) && separator == XMLConstants.XMLNS_ATTRIBUTE.length()) {
                readNsDecl(name.substring(separator + 1), value);
            } else {
                readAttr(name.substring(0, separator), name.substring(separator + 1), null, value);
            }
        }
    }

    @Override
    protected boolean consume() throws XMLStreamException, IOException {
        XMLStreamReaderScope<String> scope = getScope();
        skipWhitespace();

        if (ch == -1) {
            if (isStartDocumentRead()) {
                readEndDocument();
            }
            return false;
        }

        if (ch == '<') {
            nextChar();
            if (ch == '/') { // END_ELEMENT
                nextChar();
                String tagName = readName('>');
                if (scope.getInfo().equals(tagName)) {
                    readEndElementTag();
                } else {
                    throw new XMLStreamException("not well-formed");
                }
            } else if (ch == '?') { // START_DOPCUMENT | PROCESSING_INSTRUCTION
                nextChar();
                String target = readName('?');
                String data = null;
                if (ch != '?') {
                    data = readText('?');
                }
                nextChar(); // please, let it be '>'
                if ("xml".equals(target)) {
                    String version = null;
                    String encoding = null;
                    Boolean standalone = null;
                    if (data != null) {
                        Matcher matcher = Pattern.compile("(\\w+)\\s*=\\s*\"(\\S+)\"").matcher(data);
                        while (matcher.find()) {
                            String name = matcher.group(1);
                            String value = matcher.group(2);
                            if ("version".equals(name)) {
                                if (!"1.0".equals(value) && !"1.1".equals(value)) {
                                    throw new XMLStreamException("Bad XML version: " + value);
                                }
                                version = value;
                            } else if ("encoding".equals(name)) {
                                encoding = value;
                            } else if ("standalone".equals(name)) {
                                if (!"yes".equals(value) && !"no".equals(value)) {
                                    throw new XMLStreamException("Bad XML version: " + value);
                                }
                                standalone = value.equals("yes");
                            } else {
                                throw new XMLStreamException("Bad xml XML declaration attribute: " + name);
                            }
                        }
                    }
                    readStartDocument(version, encoding, standalone);
                } else {
                    readPI(target, data);
                }
            } else if (ch == '!') { // COMMENT | CDATA
                nextChar();
                switch (ch) {
                    case '-': // COMMENT
                        String comment = readData('-');
                        if (!comment.startsWith("-")) {
                            throw new XMLStreamException("expected comment");
                        }
                        readData(comment.substring(1, comment.length() - 3), null, XMLStreamConstants.COMMENT);
                        break;
                    case '[': // CDATA
                        String cdata = readData(']');
                        if (!cdata.startsWith("CDATA[")) {
                            throw new XMLStreamException("expected cdata");
                        }
                        readData(cdata.substring(6, cdata.length() - 3), null, XMLStreamConstants.CDATA);
                        break;
                }
            } else { // START_ELEMENT
                String tagName = readName(' ');
                int colon = tagName.indexOf(':');
                if (colon < 0) {
                    readStartElementTag(XMLConstants.DEFAULT_NS_PREFIX, tagName, null, tagName);
                } else {
                    readStartElementTag(tagName.substring(0, colon), tagName.substring(colon + 1), null, tagName);
                }
                scope = getScope();
                while (ch != '>' && ch != '/') {
                    String name = readName('=');
                    nextChar();
                    skipWhitespace();
                    int quote = ch;
                    nextChar();
                    String value = readText(quote);
                    nextChar();
                    skipWhitespace();
                    readAttrNsDecl(name, value);
                }
                if (ch == '/') {
                    nextChar(); // please, let it be '>'
                    readEndElementTag();
                } else {
                    nextChar();
                    return consume();
                }
            }
            nextChar();
        } else {
            String text = readText('<');
            readData(text, null, XMLStreamConstants.CHARACTERS);
        }
        return true;
    }


    private void nextChar() throws IOException {
        ch = reader.read();
    }

    private void skipWhitespace() throws IOException {
        if (Character.isWhitespace(ch)) {
            do {
                nextChar();
            } while (Character.isWhitespace(ch));
        }
    }

    private String readData(final int end) throws IOException {
        StringBuilder data = new StringBuilder();
        int state = 0;
        do {
            nextChar();
            data.append((char) ch);
            if ((ch == end && state != 2) || (ch == '>' && state == 2)) {
                state++;
            } else {
                state = 0;
            }
        } while (state < 3);
        return data.toString();
    }

    private String readText(final int end) throws IOException {
        final StringBuilder builder = new StringBuilder();
        while (ch != end && ch >= 0) {
            if (ch == '&') {
                nextChar();
                String entity = readName(';');
                if ("lt".equals(entity)) {
                    builder.append('<');
                } else if ("gt".equals(entity)) {
                    builder.append('>');
                } else if ("amp".equals(entity)) {
                    builder.append('&');
                } else if ("quot".equals(entity)) {
                    builder.append('"');
                } else if ("apos".equals(entity)) {
                    builder.append('\'');
                } else {
                    builder.append('?');
                }
            } else {
                builder.append((char) ch);
            }
            nextChar();
        }
        return builder.toString();
    }

    private String readName(final int end) throws IOException {
        skipWhitespace();
        final StringBuilder builder = new StringBuilder();
        do {
            builder.append((char) ch);
            nextChar();
        } while (ch != end && ch != '>' && ch != '/' && !Character.isWhitespace(ch));
        skipWhitespace();
        return builder.toString();
    }
}
