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

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import org.w3c.dom.Attr;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

/**
 * A Rhino wrapper around org.w3c.dom.Node. Not comprehensive, but enough to test CXF JavaScript.
 */
public class JsSimpleDomNode extends ScriptableObject {
    private static final long serialVersionUID = -299162863985870752L;
    private Node wrappedNode;
    private boolean childrenWrapped;
    private boolean attributesWrapped;
    private JsSimpleDomNode previousSibling;
    private JsSimpleDomNode nextSibling;
    private List<JsSimpleDomNode> children;
    private JsNamedNodeMap attributes;

    /**
     * Only exists to make Rhino happy. Should never be used.
     */
    public JsSimpleDomNode() {
    }

    public static void register(ScriptableObject scope) {
        try {
            ScriptableObject.defineClass(scope, JsSimpleDomNode.class);
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String getClassName() {
        return "Node";
    }

    public Node getWrappedNode() {
        return wrappedNode;
    }

    // CHECKSTYLE:OFF
    public String jsGet_localName() {
        return wrappedNode.getLocalName();
    }

    public String jsGet_namespaceURI() {
        return wrappedNode.getNamespaceURI();
    }

    public Object jsGet_firstChild() {
        establishChildren();
        if (!children.isEmpty()) {
            return children.get(0);
        }
        return null;
    }

    public Object jsGet_nextSibling() {
        return nextSibling;
    }

    public Object jsGet_previousSibling() {
        return previousSibling;
    }

    public Object jsGet_parentNode() {
        // risk errors in object equality ...
        return wrapNode(this, wrappedNode.getParentNode());
    }

    public int jsGet_nodeType() {
        return wrappedNode.getNodeType();
    }

    public String jsGet_nodeValue() {
        return wrappedNode.getNodeValue();
    }

    public String jsGet_nodeName() {
        return wrappedNode.getNodeName();
    }

    // in a more complete version of this, we'd use a different object type to wrap documents.
    public Object jsGet_documentElement() {
        if (9 /* Document */!= wrappedNode.getNodeType()) {
            return null;
        }
        establishChildren();
        return children.get(0); // it is, after all, just a convenience feature.
    }

    public Object[] jsGet_childNodes() {
        establishChildren();
        return children.toArray();
    }

    public Object jsGet_attributes() {
        establishAttributes();
        return attributes;
    }

    public String jsFunction_getAttributeNS(String namespaceURI, String localName) {
        NamedNodeMap attributes = wrappedNode.getAttributes();
        Node attrNode = attributes.getNamedItemNS(namespaceURI, localName);
        if (attrNode == null) {
            return null;
        }
        Attr attribute = (Attr)attrNode;
        return attribute.getValue();
    }

    public String jsFunction_getAttribute(String localName) {
        NamedNodeMap attributes = wrappedNode.getAttributes();
        Node attrNode = attributes.getNamedItem(localName);
        if (attrNode == null) {
            return null;
        }
        Attr attribute = (Attr)attrNode;
        return attribute.getValue();
    }

    // CHECKSTYLE:ON

    public static JsSimpleDomNode wrapNode(Scriptable scope, Node node) {
        if (node == null) {
            return null;
        }

        Context cx = ContextFactory.getGlobal().enterContext();
        try {
            JsSimpleDomNode newObject = (JsSimpleDomNode)cx.newObject(scope, "Node");
            newObject.initialize(node, null);
            return newObject;
        } finally {
            Context.exit();
        }
    }

    private JsSimpleDomNode newObject(Node node, JsSimpleDomNode prev) {
        Context cx = ContextFactory.getGlobal().enterContext();
        try {
            JsSimpleDomNode newObject = (JsSimpleDomNode)cx.newObject(getParentScope(), "Node");
            newObject.initialize(node, prev);
            return newObject;
        } finally {
            Context.exit();
        }
    }

    private void establishChildren() {
        if (!childrenWrapped) {
            if (wrappedNode.hasChildNodes()) {
                children = new ArrayList<>();
                Node node = wrappedNode.getFirstChild();
                int x = 0;
                while (node != null) {
                    JsSimpleDomNode prev = null;
                    if (x > 0) {
                        prev = children.get(x - 1);
                    }
                    children.add(x, newObject(node, prev));
                    if (x > 0) {
                        children.get(x - 1).setNext(children.get(x));
                    }
                    node = node.getNextSibling();
                    x++;
                }
            } else {
                children = new ArrayList<>();
            }
            childrenWrapped = true;
        }
    }

    private void establishAttributes() {
        if (!attributesWrapped) {
            NamedNodeMap nodeAttributes = wrappedNode.getAttributes();
            attributes = JsNamedNodeMap.wrapMap(getParentScope(), nodeAttributes);
            attributesWrapped = true;
        }
    }

    // rhino won't let us use a constructor.
    void initialize(Node node, JsSimpleDomNode prev) {
        wrappedNode = node;
        childrenWrapped = false;
        previousSibling = prev;
    }

    void setNext(JsSimpleDomNode next) {
        nextSibling = next;
    }

}
