/**
 * 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.cxf.aegis.util.stax;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

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

import org.apache.cxf.staxutils.AbstractDOMStreamReader;
import org.apache.cxf.staxutils.FastStack;
import org.jdom.Attribute;
import org.jdom.CDATA;
import org.jdom.Comment;
import org.jdom.Content;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.EntityRef;
import org.jdom.Namespace;
import org.jdom.Text;

/**
 * Facade for DOMStreamReader using JDOM implmentation.
 */
public class JDOMStreamReader extends AbstractDOMStreamReader<Element, Integer> {

    private Content content;

    private FastStack<Map<String, Namespace>> namespaceStack
        = new FastStack<Map<String, Namespace>>();

    private List<Namespace> namespaces = new ArrayList<>();

    private Map<String, Namespace> prefix2decNs;

    private JDOMNamespaceContext namespaceContext;

    /**
     * @param element
     */
    public JDOMStreamReader(Element element) {
        super(new ElementFrame<Element, Integer>(element, null, -1));

        namespaceContext = new JDOMNamespaceContext();
        setupNamespaces(element);
    }

    /**
     * @param document
     */
    public JDOMStreamReader(Document document) {
        this(document.getRootElement());
    }

    public static String toStaxType(int jdom) {
        String val;
        switch (jdom) {
        case Attribute.CDATA_TYPE:
            val = "CDATA";
            break;
        case Attribute.ID_TYPE:
            val = "ID";
            break;
        case Attribute.IDREF_TYPE:
            val = "IDREF";
            break;
        case Attribute.IDREFS_TYPE:
            val = "IDREFS";
            break;
        case Attribute.ENTITY_TYPE:
            val = "ENTITY";
            break;
        case Attribute.ENTITIES_TYPE:
            val = "ENTITIES";
            break;
        case Attribute.ENUMERATED_TYPE:
            val = "ENUMERATED";
            break;
        case Attribute.NMTOKEN_TYPE:
            val = "NMTOKEN";
            break;
        case Attribute.NMTOKENS_TYPE:
            val = "NMTOKENS";
            break;
        case Attribute.NOTATION_TYPE:
            val = "NOTATION";
            break;
        default:
            val = null;
        }
        return val;
    }

    private void setupNamespaces(Element element) {
        namespaceContext.setElement(element);

        if (prefix2decNs != null) {
            namespaceStack.push(prefix2decNs);
        }

        prefix2decNs = new HashMap<>();
        namespaces.clear();

        for (Iterator<?> itr = element.getAdditionalNamespaces().iterator(); itr.hasNext();) {
            declare((Namespace)itr.next());
        }

        Namespace ns = element.getNamespace();

        if (shouldDeclare(ns)) {
            declare(ns);
        }

        for (Iterator<?> itr = element.getAttributes().iterator(); itr.hasNext();) {
            ns = ((Attribute)itr.next()).getNamespace();
            if (shouldDeclare(ns)) {
                declare(ns);
            }
        }
    }

    private void declare(Namespace ns) {
        prefix2decNs.put(ns.getPrefix(), ns);
        namespaces.add(ns);
    }

    private boolean shouldDeclare(Namespace ns) {
        if (ns == Namespace.XML_NAMESPACE) {
            return false;
        }

        if (ns == Namespace.NO_NAMESPACE && getDeclaredURI("") == null) {
            return false;
        }

        String decUri = getDeclaredURI(ns.getPrefix());

        return !(decUri != null && decUri.equals(ns.getURI()));
    }

    private String getDeclaredURI(String string) {
        for (int i = namespaceStack.size() - 1; i == 0; i--) {
            Map<String, Namespace> nmspaces = namespaceStack.get(i);

            Namespace dec = nmspaces.get(string);

            if (dec != null) {
                return dec.getURI();
            }
        }
        return null;
    }

    @Override
    protected void endElement() {
        if (!namespaceStack.isEmpty()) {
            prefix2decNs = namespaceStack.pop();
        }
    }

    public Element getCurrentElement() {
        return getCurrentFrame().getElement();
    }

    @Override
    protected ElementFrame<Element, Integer> getChildFrame() {
        int currentChild = getCurrentFrame().getCurrentChild();
        return new ElementFrame<Element, Integer>((Element)getCurrentElement().getContent(currentChild),
                                                  getCurrentFrame(),
                                                  -1);
    }

    @Override
    protected boolean hasMoreChildren() {
        int currentChild = getCurrentFrame().getCurrentChild();
        return currentChild < (getCurrentElement().getContentSize() - 1);
    }

    @Override
    protected int nextChild() {
        int currentChild = getCurrentFrame().getCurrentChild();
        currentChild++;
        getCurrentFrame().setCurrentChild(currentChild);
        this.content = getCurrentElement().getContent(currentChild);

        if (content instanceof Text) {
            return CHARACTERS;
        } else if (content instanceof Element) {
            setupNamespaces((Element)content);
            return START_ELEMENT;
        } else if (content instanceof CDATA) {
            return CHARACTERS;
        } else if (content instanceof Comment) {
            return CHARACTERS;
        } else if (content instanceof EntityRef) {
            return ENTITY_REFERENCE;
        }

        throw new IllegalStateException();
    }

    @Override
    public String getElementText() throws XMLStreamException {
        return ((Text)content).getText();
    }

    @Override
    public String getNamespaceURI(String prefix) {
        return getCurrentElement().getNamespace(prefix).getURI();
    }

    public String getAttributeValue(String ns, String local) {
        return getCurrentElement().getAttributeValue(local, Namespace.getNamespace(ns));
    }

    public int getAttributeCount() {
        return getCurrentElement().getAttributes().size();
    }

    Attribute getAttribute(int i) {
        return (Attribute)getCurrentElement().getAttributes().get(i);
    }

    public QName getAttributeName(int i) {
        Attribute at = getAttribute(i);

        return new QName(at.getNamespaceURI(), at.getName(), at.getNamespacePrefix());
    }

    public String getAttributeNamespace(int i) {
        return getAttribute(i).getNamespaceURI();
    }

    public String getAttributeLocalName(int i) {
        return getAttribute(i).getName();
    }

    public String getAttributePrefix(int i) {
        return getAttribute(i).getNamespacePrefix();
    }

    public String getAttributeType(int i) {
        return toStaxType(getAttribute(i).getAttributeType());
    }

    public String getAttributeValue(int i) {
        return getAttribute(i).getValue();
    }

    public boolean isAttributeSpecified(int i) {
        return getAttribute(i).getValue() != null;
    }

    public int getNamespaceCount() {
        return namespaces.size();
    }

    Namespace getNamespace(int i) {
        return namespaces.get(i);
    }

    public String getNamespacePrefix(int i) {
        return getNamespace(i).getPrefix();
    }

    public String getNamespaceURI(int i) {
        return getNamespace(i).getURI();
    }

    public NamespaceContext getNamespaceContext() {
        return namespaceContext;
    }

    public String getText() {
        return content.getValue();
    }

    public char[] getTextCharacters() {
        return getText().toCharArray();
    }

    public int getTextStart() {
        return 0;
    }

    public int getTextLength() {
        return getText().length();
    }

    public String getEncoding() {
        return null;
    }

    public QName getName() {
        Element el = getCurrentElement();

        return new QName(el.getNamespaceURI(), el.getName(), el.getNamespacePrefix());
    }

    public String getLocalName() {
        return getCurrentElement().getName();
    }

    public String getNamespaceURI() {
        return getCurrentElement().getNamespaceURI();
    }

    public String getPrefix() {
        return getCurrentElement().getNamespacePrefix();
    }

    public String getPITarget() {
        throw new UnsupportedOperationException();
    }

    public String getPIData() {
        throw new UnsupportedOperationException();
    }
}
