/*
 * 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.base;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLStreamException;

/**
 * Represent document/element scope. Used to store namespace bindings and
 * attributes, implements {@link NamespaceContext}.
 */
public abstract class AbstractXMLStreamScope implements NamespaceContext {
    class Attr {
        private final String prefix;
        private final String localName;
        private final String namespaceURI;
        private final String value;

        Attr(String prefix, String localName, String namespaceURI, String value) {
            this.prefix = prefix;
            this.localName = localName;
            this.namespaceURI = namespaceURI;
            this.value = value;
        }

        String getPrefix() {
            if (prefix != null) {
                return prefix;
            } else if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
                return XMLConstants.DEFAULT_NS_PREFIX;
            } else {
                return AbstractXMLStreamScope.this.getNonEmptyPrefix(namespaceURI);
            }
        }

        String getLocalName() {
            return localName;
        }

        String getNamespaceURI() {
            if (namespaceURI != null) {
                return namespaceURI;
            } else if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                return XMLConstants.NULL_NS_URI;
            } else {
                return AbstractXMLStreamScope.this.getNamespaceURI(prefix);
            }
        }

        String getValue() {
            return value;
        }

        void verify() throws XMLStreamException {
            if (prefix == null) {
                if (!XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
                    String prefix = AbstractXMLStreamScope.this.getNonEmptyPrefix(namespaceURI);
                    if (prefix == null) {
                        throw new XMLStreamException("No prefix found for attribute namespace: " + namespaceURI);
                    }
                }
            } else if (namespaceURI == null) {
                if (!XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                    String namespaceURI = AbstractXMLStreamScope.this.getNamespaceURI(prefix);
                    if (namespaceURI == null || XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
                        throw new XMLStreamException("Unbound attribute prefix: " + prefix);
                    }
                }
            } else {
                if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                    if (!XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
                        throw new XMLStreamException("Illegal namespace for unprefixed attribute: " + namespaceURI);
                    }
                } else if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
                    if (!XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                        throw new XMLStreamException("Illegal prefix for null namespace: " + prefix);
                    }
                } else {
                    if (!AbstractXMLStreamScope.this.getNamespaceURI(prefix).equals(namespaceURI)) {
                        throw new XMLStreamException("Prefix '" + prefix + "' is not bound to: " + namespaceURI);
                    }
                }
            }
        }
    }

    private final NamespaceContext parent;
    private final String prefix;
    private final String localName;
    private final String namespaceURI;

    private String defaultNamespace;
    private List<Attr> attributes;
    private List<Pair<String, String>> prefixes;
    private AbstractXMLStreamScope lastChild;
    private boolean startTagClosed;

    /**
     * Create root scope.
     *
     * @param defaultNamespace
     */
    public AbstractXMLStreamScope(String defaultNamespace) {
        this.parent = null;
        this.prefix = null;
        this.localName = null;
        this.namespaceURI = XMLConstants.NULL_NS_URI;
        this.defaultNamespace = defaultNamespace;
        this.startTagClosed = true;
    }

    /**
     * Create root scope.
     *
     * @param parent root namespace context
     */
    public AbstractXMLStreamScope(NamespaceContext parent) {
        this.parent = parent;
        this.prefix = null;
        this.localName = null;
        this.namespaceURI = XMLConstants.NULL_NS_URI;
        this.defaultNamespace = parent.getNamespaceURI(XMLConstants.NULL_NS_URI);
        this.startTagClosed = true;
    }

    /**
     * Create element scope.
     *
     * @param parent
     * @param prefix
     * @param localName
     */
    public AbstractXMLStreamScope(AbstractXMLStreamScope parent, String prefix, String localName, String namespaceURI) {
        this.parent = parent;
        this.prefix = prefix;
        this.localName = localName;
        this.namespaceURI = namespaceURI;
        this.startTagClosed = false;
        this.defaultNamespace = parent.getNamespaceURI(XMLConstants.NULL_NS_URI);

        parent.lastChild = this;
        parent.startTagClosed = true;
    }

    void addAttribute(String prefix, String localName, String namespaceURI, String value) {
        if (attributes == null) {
            attributes = new LinkedList<Attr>();
        }
        attributes.add(new Attr(prefix, localName, namespaceURI, value));
    }

    List<Attr> getAttributes() {
        return attributes;
    }

    public String getPrefix() {
        return prefix == null ? getPrefix(namespaceURI) : prefix;
    }

    public String getLocalName() {
        return localName;
    }

    public String getNamespaceURI() {
        return namespaceURI == null ? getNamespaceURI(prefix) : namespaceURI;
    }

    public boolean isRoot() {
        return localName == null;
    }

    public AbstractXMLStreamScope getParent() {
        return isRoot() ? null : (AbstractXMLStreamScope) parent;
    }

    public AbstractXMLStreamScope getLastChild() {
        return lastChild;
    }

    public boolean isStartTagClosed() {
        return startTagClosed;
    }

    private void verify() throws XMLStreamException {
        if (prefix == null) {
            if (!XMLConstants.NULL_NS_URI.equals(namespaceURI) && getPrefix(namespaceURI) == null) {
                throw new XMLStreamException("No prefix for namespace URI: " + namespaceURI);
            }
        } else if (namespaceURI == null) {
            if (!XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) && XMLConstants.NULL_NS_URI.equals(getNamespaceURI(prefix))) {
                throw new XMLStreamException("Unbound prefix: " + prefix);
            }
        } else {
            if (!namespaceURI.equals(getNamespaceURI(prefix))) {
                if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                    throw new XMLStreamException("Prefix required for namespace URI: '" + namespaceURI);
                } else if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
                    throw new XMLStreamException("Prefix '" + prefix + "' is bound to: " + getNamespaceURI(prefix));
                } else {
                    throw new XMLStreamException("Prefix '" + prefix + "' is not bound to: " + namespaceURI);
                }
            }
        }
        if (attributes != null) {
            for (Attr attribute : attributes) {
                attribute.verify();
            }
        }
    }

    void setStartTagClosed(boolean startTagClosed) throws XMLStreamException {
        if (startTagClosed) {
            verify();
        }
        this.startTagClosed = startTagClosed;
    }

    private String findNonEmptyPrefix(String namespaceURI, AbstractXMLStreamScope descendent) {
        if (prefixes != null) {
            for (Pair<String, String> pair : prefixes) {
                if (pair.getSecond().equals(namespaceURI)) {
                    if (descendent == this || descendent.getNamespaceURI(pair.getFirst()).equals(namespaceURI)) {
                        return pair.getFirst();
                    }
                }
            }
        }
        if (isRoot()) {
            if (parent == null) {
                return null;
            } else {
                Iterator<?> prefixes = parent.getPrefixes(namespaceURI);
                while (prefixes.hasNext()) {
                    String prefix = prefixes.next().toString();
                    if (!XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                        if (descendent == this || descendent.getNamespaceURI(prefix).equals(namespaceURI)) {
                            return prefix;
                        }
                    }
                }
                return null;
            }
        } else {
            return getParent().findNonEmptyPrefix(namespaceURI, descendent);
        }
    }

    String getNonEmptyPrefix(String namespaceURI) {
        if (namespaceURI == null) {
            throw new IllegalArgumentException("Namespace URI must not be null");
        } else if (XMLConstants.XML_NS_URI.equals(namespaceURI)) {
            return XMLConstants.XML_NS_PREFIX;
        } else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI)) {
            return XMLConstants.XMLNS_ATTRIBUTE;
        } else {
            return findNonEmptyPrefix(namespaceURI, this);
        }
    }

    @Override
    public String getPrefix(String namespaceURI) {
        if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
            return null;
        } else if (defaultNamespace.equals(namespaceURI)) {
            return XMLConstants.DEFAULT_NS_PREFIX;
        } else {
            return getNonEmptyPrefix(namespaceURI);
        }
    }

    public void setPrefix(String prefix, String namespaceURI) {
        if (prefix == null || namespaceURI == null) {
            throw new IllegalArgumentException("Prefix and namespace URI must not be null");
        }
        if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
            defaultNamespace = namespaceURI;
        } else if (XMLConstants.XML_NS_PREFIX.equals(namespaceURI)) {
            throw new IllegalArgumentException("Cannot bind to prefix: " + prefix);
        } else if (XMLConstants.XMLNS_ATTRIBUTE.equals(namespaceURI)) {
            throw new IllegalArgumentException("Cannot bind to prefix: " + prefix);
        } else {
            if (prefixes == null) {
                prefixes = new LinkedList<Pair<String, String>>();
            } else {
                Iterator<Pair<String, String>> iterator = prefixes.iterator();
                while (iterator.hasNext()) {
                    if (iterator.next().getFirst().equals(prefix)) {
                        iterator.remove();
                    }
                }
            }
            prefixes.add(new Pair<String, String>(prefix, namespaceURI));
        }
    }

    @Override
    public Iterator<String> getPrefixes(final String namespaceURI) {
        if (namespaceURI == null) {
            throw new IllegalArgumentException("Namespace URI must not be null");
        } else if (XMLConstants.XML_NS_URI.equals(namespaceURI)) {
            return Arrays.asList(XMLConstants.XML_NS_PREFIX).iterator();
        } else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI)) {
            return Arrays.asList(XMLConstants.XMLNS_ATTRIBUTE).iterator();
        } else {
            return new Iterator<String>() {
                int state = 0;
                String next = null;
                Iterator<Pair<String, String>> pairs;
                Iterator<?> above;

                private String next0() {
                    if (state == 0) { // check default
                        state = 1;
                        if (namespaceURI.equals(defaultNamespace)) {
                            return XMLConstants.DEFAULT_NS_PREFIX;
                        }
                    }
                    if (state == 1) { // check pairs
                        if (prefixes != null) {
                            if (pairs == null) {
                                pairs = prefixes.iterator();
                            }
                            while (pairs.hasNext()) {
                                Pair<String, String> pair = pairs.next();
                                if (namespaceURI.equals(pair.getSecond())) {
                                    return pair.getFirst();
                                }
                            }
                        }
                        state = 2;
                    }
                    if (state == 2) { // check above
                        if (parent != null) {
                            if (above == null) {
                                above = parent.getPrefixes(namespaceURI);
                            }
                            while (above.hasNext()) {
                                String prefix = above.next().toString();
                                if (getNamespaceURI(prefix).equals(namespaceURI)) {
                                    return prefix;
                                }
                            }
                        }
                        state = 3;
                    }
                    if (state == 3) { // check out...
                        return null;
                    }
                    throw new IllegalStateException(); // should not happen
                }

                @Override
                public boolean hasNext() {
                    if (next == null) {
                        next = next0();
                    }
                    return next != null;
                }

                @Override
                public String next() {
                    if (!hasNext()) {
                        throw new NoSuchElementException();
                    }
                    String result = next;
                    next = null;
                    return result;
                }

                @Override
                public void remove() {
                    throw new UnsupportedOperationException("Cannot remove prefix");
                }
            };
        }
    }

    @Override
    public String getNamespaceURI(String prefix) {
        if (prefix == null) {
            throw new IllegalArgumentException("Prefix must not be null");
        } else if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
            return defaultNamespace;
        } else if (XMLConstants.XML_NS_PREFIX.equals(prefix)) {
            return XMLConstants.XML_NS_URI;
        } else if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
            return XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
        } else {
            if (prefixes != null) {
                for (Pair<String, String> pair : prefixes) {
                    if (pair.getFirst().equals(prefix)) {
                        return pair.getSecond();
                    }
                }
            }
            return parent == null ? XMLConstants.NULL_NS_URI : parent.getNamespaceURI(prefix);
        }
    }
}
