/*
 * Copyright (c) 2002-2010 Gargoyle Software Inc.
 *
 * 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 com.gargoylesoftware.htmlunit.javascript.host;

import java.util.ArrayList;
import java.util.List;

import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.Interpreter;
import net.sourceforge.htmlunit.corejs.javascript.JavaScriptException;
import net.sourceforge.htmlunit.corejs.javascript.RhinoException;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.Undefined;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

import com.gargoylesoftware.htmlunit.BrowserVersionFeatures;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
import com.gargoylesoftware.htmlunit.html.DomNode;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLCollection;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLHtmlElement;
import com.gargoylesoftware.htmlunit.javascript.host.xml.XMLSerializer;
import com.gargoylesoftware.htmlunit.xml.XmlPage;

/**
 * The JavaScript object "Node" which is the base class for all DOM
 * objects. This will typically wrap an instance of {@link DomNode}.
 *
 * @version $Revision: 5864 $
 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
 * @author David K. Taylor
 * @author Barnaby Court
 * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
 * @author <a href="mailto:george@murnock.com">George Murnock</a>
 * @author Chris Erskine
 * @author Bruce Faulkner
 * @author Ahmed Ashour
 */
public class Node extends SimpleScriptable {

    private static final long serialVersionUID = -5695262053081637445L;

    /** "Live" child nodes collection; has to be a member to have equality (==) working. */
    private HTMLCollection childNodes_;

    private EventListenersContainer eventListenersContainer_;

    /** @see org.w3c.dom.Node#ELEMENT_NODE */
    public static final short ELEMENT_NODE = org.w3c.dom.Node.ELEMENT_NODE;

    /** @see org.w3c.dom.Node#ATTRIBUTE_NODE */
    public static final short ATTRIBUTE_NODE = org.w3c.dom.Node.ATTRIBUTE_NODE;

    /** @see org.w3c.dom.Node#TEXT_NODE */
    public static final short TEXT_NODE = org.w3c.dom.Node.TEXT_NODE;

    /** @see org.w3c.dom.Node#CDATA_SECTION_NODE */
    public static final short CDATA_SECTION_NODE = org.w3c.dom.Node.CDATA_SECTION_NODE;

    /** @see org.w3c.dom.Node#ENTITY_REFERENCE_NODE */
    public static final short ENTITY_REFERENCE_NODE = org.w3c.dom.Node.ENTITY_REFERENCE_NODE;

    /** @see org.w3c.dom.Node#ENTITY_NODE */
    public static final short ENTITY_NODE = org.w3c.dom.Node.ENTITY_NODE;

    /** @see org.w3c.dom.Node#PROCESSING_INSTRUCTION_NODE */
    public static final short PROCESSING_INSTRUCTION_NODE = org.w3c.dom.Node.PROCESSING_INSTRUCTION_NODE;

    /** @see org.w3c.dom.Node#COMMENT_NODE */
    public static final short COMMENT_NODE = org.w3c.dom.Node.COMMENT_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_NODE */
    public static final short DOCUMENT_NODE = org.w3c.dom.Node.DOCUMENT_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_TYPE_NODE */
    public static final short DOCUMENT_TYPE_NODE = org.w3c.dom.Node.DOCUMENT_TYPE_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_FRAGMENT_NODE */
    public static final short DOCUMENT_FRAGMENT_NODE = org.w3c.dom.Node.DOCUMENT_FRAGMENT_NODE;

    /** @see org.w3c.dom.Node#NOTATION_NODE */
    public static final short NOTATION_NODE = org.w3c.dom.Node.NOTATION_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_DISCONNECTED */
    public static final short DOCUMENT_POSITION_DISCONNECTED = org.w3c.dom.Node.DOCUMENT_POSITION_DISCONNECTED;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_PRECEDING */
    public static final short DOCUMENT_POSITION_PRECEDING = org.w3c.dom.Node.DOCUMENT_POSITION_PRECEDING;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_FOLLOWING */
    public static final short DOCUMENT_POSITION_FOLLOWING = org.w3c.dom.Node.DOCUMENT_POSITION_FOLLOWING;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_CONTAINS */
    public static final short DOCUMENT_POSITION_CONTAINS = org.w3c.dom.Node.DOCUMENT_POSITION_CONTAINS;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_CONTAINED_BY */
    public static final short DOCUMENT_POSITION_CONTAINED_BY = org.w3c.dom.Node.DOCUMENT_POSITION_CONTAINED_BY;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC */
    public static final short DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
        = org.w3c.dom.Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;

    /**
     * Creates an instance.
     */
    public Node() {
        // Empty.
    }

    /**
     * Gets the JavaScript property "nodeType" for the current node.
     * @return the node type
     */
    public short jsxGet_nodeType() {
        return this.<DomNode>getDomNodeOrDie().getNodeType();
    }

    /**
     * Gets the JavaScript property "nodeName" for the current node.
     * @return the node name
     */
    public String jsxGet_nodeName() {
        return this.<DomNode>getDomNodeOrDie().getNodeName();
    }

    /**
     * Gets the JavaScript property "nodeValue" for the current node.
     * @return the node value
     */
    public String jsxGet_nodeValue() {
        return this.<DomNode>getDomNodeOrDie().getNodeValue();
    }

    /**
     * Sets the JavaScript property "nodeValue" for the current node.
     * @param newValue the new node value
     */
    public void jsxSet_nodeValue(final String newValue) {
        this.<DomNode>getDomNodeOrDie().setNodeValue(newValue);
    }

    /**
     * Adds a DOM node to the node.
     * @param childObject the node to add to this node
     * @return the newly added child node
     */
    public Object jsxFunction_appendChild(final Object childObject) {
        Object appendedChild = null;
        if (childObject instanceof Node) {
            final Node childNode = (Node) childObject;

            // is the node allowed here?
            if (!isNodeInsertable(childNode)) {
                if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_117)) {
                    return childObject; // IE silently ignores it
                }

                throw asJavaScriptException(
                    new DOMException("Node cannot be inserted at the specified point in the hierarchy",
                        DOMException.HIERARCHY_REQUEST_ERR));
            }

            // Get XML node for the DOM node passed in
            final DomNode childDomNode = childNode.getDomNodeOrDie();

            // Get the parent XML node that the child should be added to.
            final DomNode parentNode = getDomNodeOrDie();

            // Append the child to the parent node
            parentNode.appendChild(childDomNode);
            appendedChild = childObject;

            //if the parentNode has null parentNode in IE,
            //create a DocumentFragment to be the parentNode's parentNode.
            if (!(parentNode instanceof SgmlPage)
                    && !(this instanceof DocumentFragment) && parentNode.getParentNode() == null
                    && getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_118)) {
                final DomDocumentFragment fragment = parentNode.getPage().createDomDocumentFragment();
                fragment.appendChild(parentNode);
            }
        }
        return appendedChild;
    }

    private RhinoException asJavaScriptException(final DOMException exception) {
        exception.setPrototype(getWindow().getPrototype(exception.getClass()));
        exception.setParentScope(getWindow());

        // get current line and file name
        // this method can only be used in interpreted mode. If one day we choose to use compiled mode,
        // then we'll have to find an other way here.
        final String fileName;
        final int lineNumber;
        if (Context.getCurrentContext().getOptimizationLevel() == -1) {
            final int[] linep = new int[1];
            final String sourceName = new Interpreter().getSourcePositionFromStack(Context.getCurrentContext(), linep);
            fileName = sourceName.replaceFirst("script in (.*) from .*", "$1");
            lineNumber = linep[0];
        }
        else {
            throw new Error("HtmlUnit not ready to run in compiled mode");
        }

        exception.setLocation(fileName, lineNumber);

        return new JavaScriptException(exception, fileName, lineNumber);
    }

    /**
     * Indicates if the node can be inserted.
     * @param childObject the node
     * @return <code>false</code> if it is not allowed here
     */
    private boolean isNodeInsertable(final Node childObject) {
        return !(childObject instanceof HTMLHtmlElement);
    }

    /**
     * Clones this node.
     * @param deep if <tt>true</tt>, recursively clones all descendants
     * @return the newly cloned node
     */
    public Object jsxFunction_cloneNode(final boolean deep) {
        final DomNode domNode = getDomNodeOrDie();
        final DomNode clonedNode = domNode.cloneNode(deep);

        final Node jsClonedNode = getJavaScriptNode(clonedNode);
        if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_119)) {
            // need to copy the event listener when they exist
            copyEventListenersWhenNeeded(domNode, clonedNode);
        }
        return jsClonedNode;
    }

    private void copyEventListenersWhenNeeded(final DomNode domNode, final DomNode clonedNode) {
        final Node jsNode = (Node) domNode.getScriptObject();
        if (jsNode != null) {
            final Node jsClonedNode = getJavaScriptNode(clonedNode);
            jsClonedNode.getEventListenersContainer().copyFrom(jsNode.getEventListenersContainer());
        }

        // look through the children
        DomNode child =  domNode.getFirstChild();
        DomNode clonedChild =  clonedNode.getFirstChild();
        while (child != null && clonedChild != null) {
            copyEventListenersWhenNeeded(child, clonedChild);
            child = child.getNextSibling();
            clonedChild = clonedChild.getNextSibling();
        }
    }

    /**
     * Add a DOM node as a child to this node before the referenced node.
     * If the referenced node is null, append to the end.
     * @param context the JavaScript context
     * @param thisObj the scriptable
     * @param args the arguments passed into the method
     * @param function the function
     * @return the newly added child node
     */
    public static Object jsxFunction_insertBefore(
            final Context context, final Scriptable thisObj, final Object[] args, final Function function) {
        return ((Node) thisObj).jsxFunction_insertBefore(args);
    }

    /**
     * Add a DOM node as a child to this node before the referenced node.
     * If the referenced node is null, append to the end.
     * @param args the arguments
     * @return the newly added child node
     */
    protected Object jsxFunction_insertBefore(final Object[] args) {
        final Object newChildObject = args[0];
        final Object refChildObject;
        if (args.length > 1) {
            refChildObject = args[1];
        }
        else {
            refChildObject = Undefined.instance;
        }
        Object appendedChild = null;

        if (newChildObject instanceof Node) {
            final Node newChild = (Node) newChildObject;
            final DomNode newChildNode = newChild.getDomNodeOrDie();

            // is the node allowed here?
            if (!isNodeInsertable(newChild)) {
                if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_120)) {
                    return newChildNode; // IE silently ignores it
                }
                throw Context.reportRuntimeError("Node cannot be inserted at the specified point in the hierarchy");
            }

            if (newChildNode instanceof DomDocumentFragment) {
                final DomDocumentFragment fragment = (DomDocumentFragment) newChildNode;
                for (final DomNode child : fragment.getChildren()) {
                    jsxFunction_insertBefore(new Object[] {child.getScriptObject(), refChildObject});
                }
                return newChildObject;
            }
            final DomNode refChildNode;
            // IE accepts non standard calls with only one arg
            if (refChildObject == Undefined.instance) {
                if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_121)) {
                    if (args.length > 1) {
                        throw Context.reportRuntimeError("Invalid argument.");
                    }
                    refChildNode = null;
                }
                else {
                    if (args.length == 2) {
                        refChildNode = null;
                    }
                    else {
                        throw Context.reportRuntimeError("insertBefore: not enough arguments");
                    }
                }
            }
            else if (refChildObject != null) {
                refChildNode = ((Node) refChildObject).getDomNodeOrDie();
            }
            else {
                refChildNode = null;
            }

            final DomNode domNode = getDomNodeOrDie();
            // Append the child to the parent node
            if (refChildNode != null) {
                refChildNode.insertBefore(newChildNode);
                appendedChild = newChildObject;
            }
            else {
                domNode.appendChild(newChildNode);
                appendedChild = newChildObject;
            }

            //if parentNode is null in IE, create a DocumentFragment to be the parentNode
            if (domNode.getParentNode() == null
                    && getWindow().getWebWindow().getWebClient().getBrowserVersion()
                    .hasFeature(BrowserVersionFeatures.GENERATED_122)) {
                final DomDocumentFragment fragment = domNode.getPage().createDomDocumentFragment();
                fragment.appendChild(domNode);
            }
        }
        return appendedChild;
    }

    /**
     * This method provides a way to determine whether two Node references returned by
     * the implementation reference the same object.
     * When two Node references are references to the same object, even if through a proxy,
     * the references may be used completely interchangeably, such that all attributes
     * have the same values and calling the same DOM method on either reference always has exactly the same effect.
     *
     * @param other the node to test against
     *
     * @return whether this node is the same node as the given one
     */
    public boolean jsxFunction_isSameNode(final Object other) {
        return other == this;
    }

    /**
     * Removes a DOM node from this node.
     * @param childObject the node to remove from this node
     * @return the removed child node
     */
    public Object jsxFunction_removeChild(final Object childObject) {
        Object removedChild = null;

        if (childObject instanceof Node) {
            // Get XML node for the DOM node passed in
            final DomNode childNode = ((Node) childObject).getDomNodeOrDie();

            // Remove the child from the parent node
            childNode.remove();
            removedChild = childObject;
        }
        return removedChild;
    }

    /**
     * Returns whether this node has any children.
     * @return boolean true if this node has any children, false otherwise
     */
    public boolean jsxFunction_hasChildNodes() {
        return this.<DomNode>getDomNodeOrDie().getChildren().iterator().hasNext();
    }

    /**
     * Returns the child nodes of the current element.
     * @return the child nodes of the current element
     */
    public HTMLCollection jsxGet_childNodes() {
        if (childNodes_ == null) {
            childNodes_ = new HTMLCollection(this);
            childNodes_.initFromChildren(getDomNodeOrDie());
        }
        return childNodes_;
    }

    /**
     * Replaces a child DOM node with another DOM node.
     * @param newChildObject the node to add as a child of this node
     * @param oldChildObject the node to remove as a child of this node
     * @return the removed child node
     */
    public Object jsxFunction_replaceChild(final Object newChildObject, final Object oldChildObject) {
        Object removedChild = null;

        if (newChildObject instanceof DocumentFragment) {
            final DocumentFragment fragment = (DocumentFragment) newChildObject;
            Node firstNode = null;
            final Node refChildObject = ((Node) oldChildObject).jsxGet_nextSibling();
            for (final DomNode node : fragment.<DomNode>getDomNodeOrDie().getChildren()) {
                if (firstNode == null) {
                    jsxFunction_replaceChild(node.getScriptObject(), oldChildObject);
                    firstNode = (Node) node.getScriptObject();
                }
                else {
                    jsxFunction_insertBefore(new Object[] {node.getScriptObject(), refChildObject});
                }
            }
            if (firstNode == null) {
                jsxFunction_removeChild(oldChildObject);
            }
            removedChild = oldChildObject;
        }
        else if (newChildObject instanceof Node && oldChildObject instanceof Node) {
            final Node newChild = (Node) newChildObject;

            // is the node allowed here?
            if (!isNodeInsertable(newChild)) {
                throw Context.reportRuntimeError("Node cannot be inserted at the specified point in the hierarchy");
            }

            // Get XML nodes for the DOM nodes passed in
            final DomNode newChildNode = newChild.getDomNodeOrDie();
            final DomNode oldChildNode;
            // Replace the old child with the new child.
            oldChildNode = ((Node) oldChildObject).getDomNodeOrDie();
            oldChildNode.replace(newChildNode);
            removedChild = oldChildObject;
        }

        return removedChild;
    }

    /**
     * Returns this node's parent node.
     * @return this node's parent node
     */
    public Node getParent() {
        return getJavaScriptNode(this.<DomNode>getDomNodeOrDie().getParentNode());
    }

    /**
     * Gets the JavaScript property "parentNode" for the node that
     * contains the current node.
     * @return the parent node
     */
    public Object jsxGet_parentNode() {
        return getJavaScriptNode(this.<DomNode>getDomNodeOrDie().getParentNode());
    }

    /**
     * Gets the JavaScript property "nextSibling" for the node that
     * contains the current node.
     * @return the next sibling node or null if the current node has
     * no next sibling.
     */
    public Node jsxGet_nextSibling() {
        return getJavaScriptNode(this.<DomNode>getDomNodeOrDie().getNextSibling());
    }

    /**
     * Gets the JavaScript property "previousSibling" for the node that
     * contains the current node.
     * @return the previous sibling node or null if the current node has
     * no previous sibling.
     */
    public Node jsxGet_previousSibling() {
        return getJavaScriptNode(this.<DomNode>getDomNodeOrDie().getPreviousSibling());
    }

    /**
     * Gets the JavaScript property "firstChild" for the node that
     * contains the current node.
     * @return the first child node or null if the current node has
     * no children.
     */
    public Node jsxGet_firstChild() {
        return getJavaScriptNode(this.<DomNode>getDomNodeOrDie().getFirstChild());
    }

    /**
     * Gets the JavaScript property "lastChild" for the node that
     * contains the current node.
     * @return the last child node or null if the current node has
     * no children.
     */
    public Node jsxGet_lastChild() {
        return getJavaScriptNode(this.<DomNode>getDomNodeOrDie().getLastChild());
    }

    /**
     * Gets the JavaScript node for a given DomNode.
     * @param domNode the DomNode
     * @return the JavaScript node or null if the DomNode was null
     */
    protected Node getJavaScriptNode(final DomNode domNode) {
        if (domNode == null) {
            return null;
        }
        return (Node) getScriptableFor(domNode);
    }

    /**
     * Allows the registration of event listeners on the event target.
     * @param type the event type to listen for (like "onclick")
     * @param listener the event listener
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536343.aspx">MSDN documentation</a>
     * @return <code>true</code> if the listener has been added
     */
    public boolean jsxFunction_attachEvent(final String type, final Function listener) {
        return getEventListenersContainer().addEventListener(StringUtils.substring(type, 2), listener, false);
    }

    /**
     * Allows the registration of event listeners on the event target.
     * @param type the event type to listen for (like "click")
     * @param listener the event listener
     * @param useCapture If <code>true</code>, indicates that the user wishes to initiate capture
     * @see <a href="http://developer.mozilla.org/en/docs/DOM:element.addEventListener">Mozilla documentation</a>
     */
    public void jsxFunction_addEventListener(final String type, final Function listener, final boolean useCapture) {
        getEventListenersContainer().addEventListener(type, listener, useCapture);
    }

    /**
     * Gets the container for event listeners.
     * @return the container (newly created if needed)
     */
    private EventListenersContainer getEventListenersContainer() {
        if (eventListenersContainer_ == null) {
            eventListenersContainer_ = new EventListenersContainer(this);
        }
        return eventListenersContainer_;
    }

    /**
     * Allows the removal of event listeners on the event target.
     * @param type the event type to listen for (like "onclick")
     * @param listener the event listener
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536411.aspx">MSDN documentation</a>
     */
    public void jsxFunction_detachEvent(final String type, final Function listener) {
        jsxFunction_removeEventListener(StringUtils.substring(type, 2), listener, false);
    }

    /**
     * Allows the removal of event listeners on the event target.
     * @param type the event type to listen for (like "click")
     * @param listener the event listener
     * @param useCapture If <code>true</code>, indicates that the user wishes to initiate capture (not yet implemented)
     * @see <a href="http://developer.mozilla.org/en/docs/DOM:element.removeEventListener">Mozilla documentation</a>
     */
    public void jsxFunction_removeEventListener(final String type, final Function listener, final boolean useCapture) {
        getEventListenersContainer().removeEventListener(type, listener, useCapture);
    }

    /**
     * Executes the event on this object only (needed for instance for onload on (i)frame tags).
     * @param event the event
     * @return the result
     */
    public ScriptResult executeEvent(final Event event) {
        if (eventListenersContainer_ != null) {

            final HtmlPage page = (HtmlPage) this.<DomNode>getDomNodeOrDie().getPage();
            final boolean isIE = getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_123);
            final Window window = (Window) page.getEnclosingWindow().getScriptObject();
            final Object[] args = new Object[] {event};

            // handlers declared as property on a node don't receive the event as argument for IE
            final Object[] propHandlerArgs;
            if (isIE) {
                propHandlerArgs = ArrayUtils.EMPTY_OBJECT_ARRAY;
            }
            else {
                propHandlerArgs = args;
            }

            window.setCurrentEvent(event);
            try {
                return eventListenersContainer_.executeListeners(event, args, propHandlerArgs);
            }
            finally {
                window.setCurrentEvent(null); // reset event
            }
        }

        return null;
    }

    /**
     * Fires the event on the node with capturing and bubbling phase.
     * @param event the event
     * @return the result
     */
    public ScriptResult fireEvent(final Event event) {
        final HtmlPage page = (HtmlPage) this.<DomNode>getDomNodeOrDie().getPage();
        final boolean ie = getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_124);
        final Window window = (Window) page.getEnclosingWindow().getScriptObject();
        final Object[] args = new Object[] {event};

        event.startFire();
        ScriptResult result = null;
        final Event previousEvent = window.getCurrentEvent();
        window.setCurrentEvent(event);

        try {
            // window's listeners
            final EventListenersContainer windowsListeners = getWindow().getEventListenersContainer();

            // capturing phase
            event.setEventPhase(Event.CAPTURING_PHASE);
            result = windowsListeners.executeCapturingListeners(event, args);
            if (event.isPropagationStopped()) {
                return result;
            }
            final List<DomNode> parents = new ArrayList<DomNode>();
            DomNode node = getDomNodeOrDie();
            while (node != null) {
                parents.add(node);
                node = node.getParentNode();
            }
            for (int i = parents.size() - 1; i >= 0; i--) {
                final DomNode curNode = parents.get(i);
                final Node jsNode = (Node) curNode.getScriptObject();
                final EventListenersContainer elc = jsNode.eventListenersContainer_;
                if (elc != null) {
                    final ScriptResult r = elc.executeCapturingListeners(event, args);
                    result = ScriptResult.combine(r, result, ie);
                    if (event.isPropagationStopped()) {
                        return result;
                    }
                }
            }

            // handlers declared as property on a node don't receive the event as argument for IE
            final Object[] propHandlerArgs;
            if (ie) {
                propHandlerArgs = ArrayUtils.EMPTY_OBJECT_ARRAY;
            }
            else {
                propHandlerArgs = args;
            }

            // bubbling phase
            event.setEventPhase(Event.AT_TARGET);
            node = getDomNodeOrDie();
            while (node != null) {
                final Node jsNode = (Node) node.getScriptObject();
                final EventListenersContainer elc = jsNode.eventListenersContainer_;
                if (elc != null) {
                    final ScriptResult r = elc.executeBubblingListeners(event, args, propHandlerArgs);
                    result = ScriptResult.combine(r, result, ie);
                    if (event.isPropagationStopped()) {
                        return result;
                    }
                }
                node = node.getParentNode();
                event.setEventPhase(Event.BUBBLING_PHASE);
            }

            final ScriptResult r = windowsListeners.executeBubblingListeners(event, args, propHandlerArgs);
            result = ScriptResult.combine(r, result, ie);
        }
        finally {
            event.endFire();
            window.setCurrentEvent(previousEvent); // reset event
        }

        return result;
    }

    /**
     * Returns the specified event handler.
     * @param eventName the event name (e.g. "onclick")
     * @return the handler function, or <tt>null</tt> if the property is null or not a function
     */
    public Function getEventHandler(final String eventName) {
        if (eventListenersContainer_ == null) {
            return null;
        }
        return eventListenersContainer_.getEventHandler(StringUtils.substring(eventName, 2));
    }

    /**
     * Returns <tt>true</tt> if there are any event handlers for the specified event.
     * @param eventName the event name (e.g. "onclick")
     * @return <tt>true</tt> if there are any event handlers for the specified event, <tt>false</tt> otherwise
     */
    public boolean hasEventHandlers(final String eventName) {
        if (eventListenersContainer_ == null) {
            return false;
        }
        return eventListenersContainer_.hasEventHandlers(StringUtils.substring(eventName, 2));
    }

    /**
     * Defines an event handler.
     * @param eventName the event name (e.g. "onclick")
     * @param eventHandler the handler (<code>null</code> to reset it)
     */
    public void setEventHandler(final String eventName, final Function eventHandler) {
        setEventHandlerProp(eventName, eventHandler);
    }

    /**
     * Defines an event handler (or maybe any other object).
     * @param eventName the event name (e.g. "onclick")
     * @param value the property (<code>null</code> to reset it)
     */
    protected void setEventHandlerProp(final String eventName, final Object value) {
        getEventListenersContainer().setEventHandlerProp(StringUtils.substring(eventName.toLowerCase(), 2), value);
    }

    /**
     * Gets the property defined as event handler (not necessary a Function if something else has been set).
     * @param eventName the event name (e.g. "onclick")
     * @return the property
     */
    protected Object getEventHandlerProp(final String eventName) {
        if (eventListenersContainer_ == null) {
            return null;
        }
        return eventListenersContainer_.getEventHandlerProp(StringUtils.substring(eventName.toLowerCase(), 2));
    }

    /**
     * Returns the owner document.
     * @return the document
     */
    public Object jsxGet_ownerDocument() {
        final Object document = this.<DomNode>getDomNodeOrDie().getOwnerDocument();
        if (document == null) {
            return null;
        }
        return ((SgmlPage) document).getScriptObject();
    }

    /**
     * Returns the namespace prefix.
     * @return the namespace prefix
     */
    public String jsxGet_prefix() {
        final DomNode domNode = getDomNodeOrDie();
        final String prefix = domNode.getPrefix();
        if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_125)
                && (prefix == null || domNode.getPage() instanceof HtmlPage)) {
            return "";
        }
        return prefix;
    }

    /**
     * Returns the local name of this element.
     * @return the local name of this element
     */
    public String jsxGet_localName() {
        return this.<DomNode>getDomNodeOrDie().getLocalName();
    }

    /**
     * Returns The URI that identifies an XML namespace.
     * @return the URI that identifies an XML namespace
     */
    public String jsxGet_namespaceURI() {
        final String namespaceURI = this.<DomNode>getDomNodeOrDie().getNamespaceURI();
        if (namespaceURI == null && getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_126)) {
            return "";
        }
        return namespaceURI;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDomNode(final DomNode domNode) {
        super.setDomNode(domNode);
        if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_127)
                && !(this.<DomNode>getDomNodeOrDie().getPage() instanceof HtmlPage)) {
            ActiveXObject.addProperty(this, "namespaceURI", true, false);
            ActiveXObject.addProperty(this, "prefix", true, false);
        }
    }

    /**
     * Compares the positions of this node and the provided node within the document.
     * @param node node object that specifies the node to check
     * @return how the node is positioned relatively to the reference node.
     * @see <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition">DOM level 3</a>
     * @see org.w3c.dom.Node#compareDocumentPosition(org.w3c.dom.Node)
     */
    public short jsxFunction_compareDocumentPosition(final Node node) {
        return this.<DomNode>getDomNodeOrDie().compareDocumentPosition(node.getDomNodeOrDie());
    }

    /**
     * Merges adjacent TextNode objects to produce a normalized document object model.
     */
    public void jsxFunction_normalize() {
        this.<DomNode>getDomNodeOrDie().normalize();
    }

    /**
     * Represents the xml content of the node and its descendants.
     * @return the xml content of the node and its descendants
     */
    public Object jsxGet_xml() {
        final DomNode node = getDomNodeOrDie();
        if (node.getPage() instanceof XmlPage) {
            if (this instanceof Element) {
                final XMLSerializer serializer = new XMLSerializer();
                serializer.setParentScope(getParentScope());
                String xml = serializer.jsxFunction_serializeToString(this);
                if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_128) && xml.endsWith("\r\n")) {
                    xml = xml.substring(0, xml.length() - 2);
                }
                return xml;
            }
            return node.asXml();
        }
        return Undefined.instance;
    }

    /**
     * Gets the textContent attribute.
     * @return the contents of this node as text
     */
    public String jsxGet_textContent() {
        return this.<DomNode>getDomNodeOrDie().getTextContent();
    }

    /**
     * Replace all children elements of this element with the supplied value.
     * @param value - the new value for the contents of this node
     */
    public void jsxSet_textContent(final Object value) {
        this.<DomNode>getDomNodeOrDie().setTextContent(value == null ? null : Context.toString(value));
    }
}
