/*
 * Copyright (C) 2009, 2010 Google Inc. All rights reserved.
 * Copyright (C) 2009 Joseph Pecoraro
 * Copyright (C) 2013, 2016 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

WI.DOMNode = class DOMNode extends WI.Object
{
    constructor(domManager, doc, isInShadowTree, payload)
    {
        super();

        this._destroyed = false;

        this._domManager = domManager;
        this._isInShadowTree = isInShadowTree;

        this.id = payload.nodeId;
        this._domManager._idToDOMNode[this.id] = this;

        this._nodeType = payload.nodeType;
        this._nodeName = payload.nodeName;
        this._localName = payload.localName;
        this._nodeValue = payload.nodeValue;
        this._pseudoType = payload.pseudoType;
        this._shadowRootType = payload.shadowRootType;
        this._computedRole = null;
        this._contentSecurityPolicyHash = payload.contentSecurityPolicyHash;
        this._layoutContextType = null;

        if (this._nodeType === Node.DOCUMENT_NODE)
            this.ownerDocument = this;
        else
            this.ownerDocument = doc;

        this._frame = null;

        // COMPATIBILITY (iOS 12.2): DOM.Node.frameId was changed to represent the owner frame, not the content frame.
        // Since support can't be tested directly, check for Audit (iOS 13.0+).
        // FIXME: Use explicit version checking once https://webkit.org/b/148680 is fixed.
        if (InspectorBackend.hasDomain("Audit")) {
            if (payload.frameId)
                this._frame = WI.networkManager.frameForIdentifier(payload.frameId);
        }

        if (!this._frame && this.ownerDocument)
            this._frame = WI.networkManager.frameForIdentifier(this.ownerDocument.frameIdentifier);

        this._attributes = [];
        this._attributesMap = new Map;
        if (payload.attributes)
            this._setAttributesPayload(payload.attributes);

        this._childNodeCount = payload.childNodeCount;
        this._children = null;

        this._nextSibling = null;
        this._previousSibling = null;
        this.parentNode = null;

        this._enabledPseudoClasses = [];

        // FIXME: The logic around this._shadowRoots and this._children is very confusing.
        // We eventually include shadow roots at the start of _children. However we might
        // not have our actual children yet. So we try to defer initializing _children until
        // we have both shadowRoots and child nodes.
        this._shadowRoots = [];
        if (payload.shadowRoots) {
            for (var i = 0; i < payload.shadowRoots.length; ++i) {
                var root = payload.shadowRoots[i];
                var node = new WI.DOMNode(this._domManager, this.ownerDocument, true, root);
                node.parentNode = this;
                this._shadowRoots.push(node);
            }
        }

        if (payload.children)
            this._setChildrenPayload(payload.children);
        else if (this._shadowRoots.length && !this._childNodeCount)
            this._children = this._shadowRoots.slice();

        if (this._nodeType === Node.ELEMENT_NODE)
            this._customElementState = payload.customElementState || WI.DOMNode.CustomElementState.Builtin;
        else
            this._customElementState = null;

        if (payload.templateContent) {
            this._templateContent = new WI.DOMNode(this._domManager, this.ownerDocument, false, payload.templateContent);
            this._templateContent.parentNode = this;
        }

        this._pseudoElements = new Map;
        if (payload.pseudoElements) {
            for (var i = 0; i < payload.pseudoElements.length; ++i) {
                var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payload.pseudoElements[i]);
                node.parentNode = this;
                this._pseudoElements.set(node.pseudoType(), node);
            }
        }

        if (payload.contentDocument) {
            this._contentDocument = new WI.DOMNode(this._domManager, null, false, payload.contentDocument);
            this._children = [this._contentDocument];
            this._renumber();
        }

        if (this._nodeType === Node.ELEMENT_NODE) {
            // HTML and BODY from internal iframes should not overwrite top-level ones.
            if (this.ownerDocument && !this.ownerDocument.documentElement && this._nodeName === "HTML")
                this.ownerDocument.documentElement = this;
            if (this.ownerDocument && !this.ownerDocument.body && this._nodeName === "BODY")
                this.ownerDocument.body = this;
            if (payload.documentURL)
                this.documentURL = payload.documentURL;
        } else if (this._nodeType === Node.DOCUMENT_TYPE_NODE) {
            this.publicId = payload.publicId;
            this.systemId = payload.systemId;
        } else if (this._nodeType === Node.DOCUMENT_NODE) {
            this.documentURL = payload.documentURL;
            this.xmlVersion = payload.xmlVersion;
        } else if (this._nodeType === Node.ATTRIBUTE_NODE) {
            this.name = payload.name;
            this.value = payload.value;
        }

        this._domEvents = [];
        this._powerEfficientPlaybackRanges = [];

        if (this.isMediaElement())
            WI.DOMNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);

        // Setting layoutContextType to anything other than null will dispatch an event.
        this.layoutContextType = payload.layoutContextType;
    }

    // Static

    static getFullscreenDOMEvents(domEvents)
    {
        return domEvents.reduce((accumulator, current) => {
            if (current.eventName === "webkitfullscreenchange" && current.data && (!accumulator.length || accumulator.lastValue.data.enabled !== current.data.enabled))
                accumulator.push(current);
            return accumulator;
        }, []);
    }

    static isPlayEvent(eventName)
    {
        return eventName === "play"
            || eventName === "playing";
    }

    static isPauseEvent(eventName)
    {
        return eventName === "pause"
            || eventName === "stall";
    }

    static isStopEvent(eventName)
    {
        return eventName === "emptied"
            || eventName === "ended"
            || eventName === "suspend";
    }


    // Public

    get destroyed() { return this._destroyed; }
    get frame() { return this._frame; }
    get nextSibling() { return this._nextSibling; }
    get previousSibling() { return this._previousSibling; }
    get children() { return this._children; }
    get domEvents() { return this._domEvents; }
    get powerEfficientPlaybackRanges() { return this._powerEfficientPlaybackRanges; }

    get attached()
    {
        if (this._destroyed)
            return false;

        for (let node = this; node; node = node.parentNode) {
            if (node.ownerDocument === node)
                return true;
        }
        return false;
    }

    get firstChild()
    {
        var children = this.children;

        if (children && children.length > 0)
            return children[0];

        return null;
    }

    get lastChild()
    {
        var children = this.children;

        if (children && children.length > 0)
            return children.lastValue;

        return null;
    }

    get childNodeCount()
    {
        var children = this.children;
        if (children)
            return children.length;

        return this._childNodeCount + this._shadowRoots.length;
    }

    set childNodeCount(count)
    {
        this._childNodeCount = count;
    }

    get layoutContextType()
    {
        return this._layoutContextType;
    }

    set layoutContextType(layoutContextType)
    {
        layoutContextType ||= null;
        if (layoutContextType === this._layoutContextType)
            return;

        this._layoutContextType = layoutContextType;
        this.dispatchEventToListeners(WI.DOMNode.Event.LayoutContextTypeChanged);
    }

    markDestroyed()
    {
        console.assert(!this._destroyed, this);
        this._destroyed = true;

        this.layoutContextType = null;
    }

    computedRole()
    {
        return this._computedRole;
    }

    contentSecurityPolicyHash()
    {
        return this._contentSecurityPolicyHash;
    }

    hasAttributes()
    {
        return this._attributes.length > 0;
    }

    hasChildNodes()
    {
        return this.childNodeCount > 0;
    }

    hasShadowRoots()
    {
        return !!this._shadowRoots.length;
    }

    isInShadowTree()
    {
        return this._isInShadowTree;
    }

    isInUserAgentShadowTree()
    {
        return this._isInShadowTree && this.ancestorShadowRoot().isUserAgentShadowRoot();
    }

    isCustomElement()
    {
        return this._customElementState === WI.DOMNode.CustomElementState.Custom;
    }

    customElementState()
    {
        return this._customElementState;
    }

    isShadowRoot()
    {
        return !!this._shadowRootType;
    }

    isUserAgentShadowRoot()
    {
        return this._shadowRootType === WI.DOMNode.ShadowRootType.UserAgent;
    }

    ancestorShadowRoot()
    {
        if (!this._isInShadowTree)
            return null;

        let node = this;
        while (node && !node.isShadowRoot())
            node = node.parentNode;
        return node;
    }

    ancestorShadowHost()
    {
        let shadowRoot = this.ancestorShadowRoot();
        return shadowRoot ? shadowRoot.parentNode : null;
    }

    isPseudoElement()
    {
        return this._pseudoType !== undefined;
    }

    nodeType()
    {
        return this._nodeType;
    }

    nodeName()
    {
        return this._nodeName;
    }

    nodeNameInCorrectCase()
    {
        return this.isXMLNode() ? this.nodeName() : this.nodeName().toLowerCase();
    }

    setNodeName(name, callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.setNodeName(this.id, name, this._makeUndoableCallback(callback));
    }

    localName()
    {
        return this._localName;
    }

    templateContent()
    {
        return this._templateContent || null;
    }

    pseudoType()
    {
        return this._pseudoType;
    }

    hasPseudoElements()
    {
        return this._pseudoElements.size > 0;
    }

    pseudoElements()
    {
        return this._pseudoElements;
    }

    beforePseudoElement()
    {
        return this._pseudoElements.get(WI.DOMNode.PseudoElementType.Before) || null;
    }

    afterPseudoElement()
    {
        return this._pseudoElements.get(WI.DOMNode.PseudoElementType.After) || null;
    }

    shadowRoots()
    {
        return this._shadowRoots;
    }

    shadowRootType()
    {
        return this._shadowRootType;
    }

    nodeValue()
    {
        return this._nodeValue;
    }

    setNodeValue(value, callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.setNodeValue(this.id, value, this._makeUndoableCallback(callback));
    }

    getAttribute(name)
    {
        let attr = this._attributesMap.get(name);
        return attr ? attr.value : undefined;
    }

    setAttribute(name, text, callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.setAttributesAsText(this.id, text, name, this._makeUndoableCallback(callback));
    }

    setAttributeValue(name, value, callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.setAttributeValue(this.id, name, value, this._makeUndoableCallback(callback));
    }

    attributes()
    {
        return this._attributes;
    }

    removeAttribute(name, callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        function mycallback(error, success)
        {
            if (!error) {
                this._attributesMap.delete(name);
                for (var i = 0; i < this._attributes.length; ++i) {
                    if (this._attributes[i].name === name) {
                        this._attributes.splice(i, 1);
                        break;
                    }
                }
            }

            this._makeUndoableCallback(callback)(error);
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.removeAttribute(this.id, name, mycallback.bind(this));
    }

    toggleClass(className, flag)
    {
        if (!className || !className.length)
            return;

        if (this.isPseudoElement()) {
            this.parentNode.toggleClass(className, flag);
            return;
        }

        if (this.nodeType() !== Node.ELEMENT_NODE)
            return;

        WI.RemoteObject.resolveNode(this).then((object) => {
            function inspectedPage_node_toggleClass(className, flag) {
                this.classList.toggle(className, flag);
            }

            object.callFunction(inspectedPage_node_toggleClass, [className, flag]);
            object.release();
        });
    }

    querySelector(selector, callback)
    {
        console.assert(!this._destroyed, this);

        let target = WI.assumingMainTarget();

        if (typeof callback !== "function") {
            if (this._destroyed)
                return Promise.reject("ERROR: node is destroyed");
            return target.DOMAgent.querySelector(this.id, selector).then(({nodeId}) => nodeId);
        }

        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        target.DOMAgent.querySelector(this.id, selector, WI.DOMManager.wrapClientCallback(callback));
    }

    querySelectorAll(selector, callback)
    {
        console.assert(!this._destroyed, this);

        let target = WI.assumingMainTarget();

        if (typeof callback !== "function") {
            if (this._destroyed)
                return Promise.reject("ERROR: node is destroyed");
            return target.DOMAgent.querySelectorAll(this.id, selector).then(({nodeIds}) => nodeIds);
        }

        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        target.DOMAgent.querySelectorAll(this.id, selector, WI.DOMManager.wrapClientCallback(callback));
    }

    highlight(mode)
    {
        if (this._destroyed)
            return;

        if (this._hideDOMNodeHighlightTimeout) {
            clearTimeout(this._hideDOMNodeHighlightTimeout);
            this._hideDOMNodeHighlightTimeout = undefined;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.highlightNode(WI.DOMManager.buildHighlightConfig(mode), this.id);
    }

    showGridOverlay(color, {showLineNames, showLineNumbers, showExtendedGridLines, showTrackSizes, showAreaNames} = {})
    {
        console.assert(color instanceof WI.Color, color);

        if (this._destroyed)
            return Promise.reject("Cannot show overlay, node is destroyed");

        let target = WI.assumingMainTarget();
        return target.DOMAgent.showGridOverlay.invoke({
            nodeId: this.id,
            gridColor: color.toProtocol(),
            showLineNames: !!showLineNames,
            showLineNumbers: !!showLineNumbers,
            showExtendedGridLines: !!showExtendedGridLines,
            showTrackSizes: !!showTrackSizes,
            showAreaNames: !!showAreaNames,
        });
    }

    hideGridOverlay()
    {
        if (this._destroyed)
            return Promise.reject("Cannot hide overlay, node is destroyed");

        let target = WI.assumingMainTarget();
        return target.DOMAgent.hideGridOverlay(this.id);
    }

    scrollIntoView()
    {
        WI.RemoteObject.resolveNode(this).then((object) => {
            function inspectedPage_node_scrollIntoView() {
                this.scrollIntoViewIfNeeded(true);
            }

            object.callFunction(inspectedPage_node_scrollIntoView);
            object.release();
        });
    }

    getChildNodes(callback)
    {
        if (this.children) {
            if (callback)
                callback(this.children);
            return;
        }

        if (this._destroyed) {
            callback(this.children);
            return;
        }

        function mycallback(error) {
            if (!error && callback)
                callback(this.children);
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.requestChildNodes(this.id, mycallback.bind(this));
    }

    getSubtree(depth, callback)
    {
        if (this._destroyed) {
            callback(this.children);
            return;
        }

        function mycallback(error)
        {
            if (callback)
                callback(error ? null : this.children);
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.requestChildNodes(this.id, depth, mycallback.bind(this));
    }

    getOuterHTML(callback)
    {
        console.assert(!this._destroyed, this);

        let target = WI.assumingMainTarget();

        if (typeof callback !== "function") {
            if (this._destroyed)
                return Promise.reject("ERROR: node is destroyed");
            return target.DOMAgent.getOuterHTML(this.id).then(({outerHTML}) => outerHTML);
        }

        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        target.DOMAgent.getOuterHTML(this.id, callback);
    }

    setOuterHTML(html, callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.setOuterHTML(this.id, html, this._makeUndoableCallback(callback));
    }

    insertAdjacentHTML(position, html)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed)
            return;

        if (this.nodeType() !== Node.ELEMENT_NODE)
            return;

        let target = WI.assumingMainTarget();

        // COMPATIBILITY (iOS 11.0): DOM.insertAdjacentHTML did not exist.
        if (!target.hasCommand("DOM.insertAdjacentHTML")) {
            WI.RemoteObject.resolveNode(this).then((object) => {
                function inspectedPage_node_insertAdjacentHTML(position, html) {
                    this.insertAdjacentHTML(position, html);
                }

                object.callFunction(inspectedPage_node_insertAdjacentHTML, [position, html]);
                object.release();
            });
            return;
        }

        target.DOMAgent.insertAdjacentHTML(this.id, position, html, this._makeUndoableCallback());
    }

    removeNode(callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.removeNode(this.id, this._makeUndoableCallback(callback));
    }

    getEventListeners(callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        console.assert(WI.domManager.inspectedNode === this);

        let target = WI.assumingMainTarget();
        target.DOMAgent.getEventListenersForNode(this.id, callback);
    }

    accessibilityProperties(callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback({});
            return;
        }

        function accessibilityPropertiesCallback(error, accessibilityProperties)
        {
            if (!error && callback && accessibilityProperties) {
                this._computedRole = accessibilityProperties.role;

                callback({
                    activeDescendantNodeId: accessibilityProperties.activeDescendantNodeId,
                    busy: accessibilityProperties.busy,
                    checked: accessibilityProperties.checked,
                    childNodeIds: accessibilityProperties.childNodeIds,
                    controlledNodeIds: accessibilityProperties.controlledNodeIds,
                    current: accessibilityProperties.current,
                    disabled: accessibilityProperties.disabled,
                    exists: accessibilityProperties.exists,
                    expanded: accessibilityProperties.expanded,
                    flowedNodeIds: accessibilityProperties.flowedNodeIds,
                    focused: accessibilityProperties.focused,
                    ignored: accessibilityProperties.ignored,
                    ignoredByDefault: accessibilityProperties.ignoredByDefault,
                    invalid: accessibilityProperties.invalid,
                    isPopupButton: accessibilityProperties.isPopUpButton,
                    headingLevel: accessibilityProperties.headingLevel,
                    hierarchyLevel: accessibilityProperties.hierarchyLevel,
                    hidden: accessibilityProperties.hidden,
                    label: accessibilityProperties.label,
                    liveRegionAtomic: accessibilityProperties.liveRegionAtomic,
                    liveRegionRelevant: accessibilityProperties.liveRegionRelevant,
                    liveRegionStatus: accessibilityProperties.liveRegionStatus,
                    mouseEventNodeId: accessibilityProperties.mouseEventNodeId,
                    nodeId: accessibilityProperties.nodeId,
                    ownedNodeIds: accessibilityProperties.ownedNodeIds,
                    parentNodeId: accessibilityProperties.parentNodeId,
                    pressed: accessibilityProperties.pressed,
                    readonly: accessibilityProperties.readonly,
                    required: accessibilityProperties.required,
                    role: accessibilityProperties.role,
                    selected: accessibilityProperties.selected,
                    selectedChildNodeIds: accessibilityProperties.selectedChildNodeIds
                });
            }
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.getAccessibilityPropertiesForNode(this.id, accessibilityPropertiesCallback.bind(this));
    }

    path()
    {
        var path = [];
        var node = this;
        while (node && "index" in node && node._nodeName.length) {
            path.push([node.index, node._nodeName]);
            node = node.parentNode;
        }
        path.reverse();
        return path.join(",");
    }

    get escapedIdSelector()
    {
        return this._idSelector(true);
    }

    get escapedClassSelector()
    {
        return this._classSelector(true);
    }

    get displayName()
    {
        if (this.isPseudoElement())
            return "::" + this._pseudoType;
        return this.nodeNameInCorrectCase() + this.escapedIdSelector + this.escapedClassSelector;
    }

    get unescapedSelector()
    {
        if (this.isPseudoElement())
            return "::" + this._pseudoType;

        const shouldEscape = false;
        return this.nodeNameInCorrectCase() + this._idSelector(shouldEscape) + this._classSelector(shouldEscape);
    }

    appropriateSelectorFor(justSelector)
    {
        if (this.isPseudoElement())
            return this.parentNode.appropriateSelectorFor() + "::" + this._pseudoType;

        let lowerCaseName = this.localName() || this.nodeName().toLowerCase();

        let id = this.escapedIdSelector;
        if (id.length)
            return justSelector ? id : lowerCaseName + id;

        let classes = this.escapedClassSelector;
        if (classes.length)
            return justSelector ? classes : lowerCaseName + classes;

        if (lowerCaseName === "input" && this.getAttribute("type"))
            return lowerCaseName + "[type=\"" + this.getAttribute("type") + "\"]";

        return lowerCaseName;
    }

    isAncestor(node)
    {
        if (!node)
            return false;

        var currentNode = node.parentNode;
        while (currentNode) {
            if (this === currentNode)
                return true;
            currentNode = currentNode.parentNode;
        }
        return false;
    }

    isDescendant(descendant)
    {
        return descendant !== null && descendant.isAncestor(this);
    }

    get ownerSVGElement()
    {
        if (this._nodeName === "svg")
            return this;

        if (!this.parentNode)
            return null;

        return this.parentNode.ownerSVGElement;
    }

    isSVGElement()
    {
        return !!this.ownerSVGElement;
    }

    isMediaElement()
    {
        let lowerCaseName = this.localName() || this.nodeName().toLowerCase();
        return lowerCaseName === "video" || lowerCaseName === "audio";
    }

    didFireEvent(eventName, timestamp, data)
    {
        // Called from WI.DOMManager.

        this._addDOMEvent({
            eventName,
            timestamp: WI.timelineManager.computeElapsedTime(timestamp),
            data,
        });
    }

    powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient)
    {
        // Called from WI.DOMManager.

        console.assert(this.canEnterPowerEfficientPlaybackState());

        let lastValue = this._powerEfficientPlaybackRanges.lastValue;

        if (isPowerEfficient) {
            console.assert(!lastValue || lastValue.endTimestamp);
            if (!lastValue || lastValue.endTimestamp)
                this._powerEfficientPlaybackRanges.push({startTimestamp: timestamp});
        } else {
            console.assert(!lastValue || lastValue.startTimestamp);
            if (!lastValue)
                this._powerEfficientPlaybackRanges.push({endTimestamp: timestamp});
            else if (lastValue.startTimestamp)
                lastValue.endTimestamp = timestamp;
        }

        this.dispatchEventToListeners(DOMNode.Event.PowerEfficientPlaybackStateChanged, {isPowerEfficient, timestamp});
    }

    canEnterPowerEfficientPlaybackState()
    {
        return this.localName() === "video" || this.nodeName().toLowerCase() === "video";
    }

    _handleDOMNodeDidFireEvent(event)
    {
        if (event.target === this || !event.target.isAncestor(this))
            return;

        let domEvent = Object.shallowCopy(event.data.domEvent);
        domEvent.originator = event.target;

        this._addDOMEvent(domEvent);
    }

    _addDOMEvent(domEvent)
    {
        this._domEvents.push(domEvent);

        this.dispatchEventToListeners(WI.DOMNode.Event.DidFireEvent, {domEvent});
    }

    _setAttributesPayload(attrs)
    {
        this._attributes = [];
        this._attributesMap = new Map;
        for (var i = 0; i < attrs.length; i += 2)
            this._addAttribute(attrs[i], attrs[i + 1]);
    }

    _insertChild(prev, payload)
    {
        var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payload);
        if (!prev) {
            if (!this._children) {
                // First node
                this._children = this._shadowRoots.concat([node]);
            } else
                this._children.unshift(node);
        } else
            this._children.splice(this._children.indexOf(prev) + 1, 0, node);
        this._renumber();
        return node;
    }

    _removeChild(node)
    {
        // FIXME: Handle removal if this is a shadow root.
        if (node.isPseudoElement()) {
            this._pseudoElements.delete(node.pseudoType());
            node.parentNode = null;
        } else {
            this._children.splice(this._children.indexOf(node), 1);
            node.parentNode = null;
            this._renumber();
        }
    }

    _setChildrenPayload(payloads)
    {
        // We set children in the constructor.
        if (this._contentDocument)
            return;

        this._children = this._shadowRoots.slice();
        for (var i = 0; i < payloads.length; ++i) {
            var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payloads[i]);
            this._children.push(node);
        }
        this._renumber();
    }

    _renumber()
    {
        var childNodeCount = this._children.length;
        if (childNodeCount === 0)
            return;

        for (var i = 0; i < childNodeCount; ++i) {
            var child = this._children[i];
            child.index = i;
            child._nextSibling = i + 1 < childNodeCount ? this._children[i + 1] : null;
            child._previousSibling = i - 1 >= 0 ? this._children[i - 1] : null;
            child.parentNode = this;
        }
    }

    _addAttribute(name, value)
    {
        let attr = {name, value, _node: this};
        this._attributesMap.set(name, attr);
        this._attributes.push(attr);
    }

    _setAttribute(name, value)
    {
        let attr = this._attributesMap.get(name);
        if (attr)
            attr.value = value;
        else
            this._addAttribute(name, value);
    }

    _removeAttribute(name)
    {
        let attr = this._attributesMap.get(name);
        if (attr) {
            this._attributes.remove(attr);
            this._attributesMap.delete(name);
        }
    }

    moveTo(targetNode, anchorNode, callback)
    {
        console.assert(!this._destroyed, this);
        if (this._destroyed) {
            callback("ERROR: node is destroyed");
            return;
        }

        let target = WI.assumingMainTarget();
        target.DOMAgent.moveTo(this.id, targetNode.id, anchorNode ? anchorNode.id : undefined, this._makeUndoableCallback(callback));
    }

    isXMLNode()
    {
        return !!this.ownerDocument && !!this.ownerDocument.xmlVersion;
    }

    get enabledPseudoClasses()
    {
        return this._enabledPseudoClasses;
    }

    setPseudoClassEnabled(pseudoClass, enabled)
    {
        var pseudoClasses = this._enabledPseudoClasses;
        if (enabled) {
            if (pseudoClasses.includes(pseudoClass))
                return;
            pseudoClasses.push(pseudoClass);
        } else {
            if (!pseudoClasses.includes(pseudoClass))
                return;
            pseudoClasses.remove(pseudoClass);
        }

        function changed(error)
        {
            if (!error)
                this.dispatchEventToListeners(WI.DOMNode.Event.EnabledPseudoClassesChanged);
        }

        let target = WI.assumingMainTarget();
        target.CSSAgent.forcePseudoState(this.id, pseudoClasses, changed.bind(this));
    }

    _makeUndoableCallback(callback)
    {
        return (...args) => {
            if (!args[0]) { // error
                let target = WI.assumingMainTarget();
                if (target.hasCommand("DOM.markUndoableState"))
                    target.DOMAgent.markUndoableState();
            }

            if (callback)
                callback.apply(null, args);
        };
    }

    _idSelector(shouldEscape)
    {
        let id = this.getAttribute("id");
        if (!id)
            return "";

        id = id.trim();
        if (!id.length)
            return "";

        if (shouldEscape)
            id = CSS.escape(id);
        if (/[\s'"]/.test(id))
            return `[id="${id}"]`;

        return `#${id}`;
    }

    _classSelector(shouldEscape) {
        let classes = this.getAttribute("class");
        if (!classes)
            return "";

        classes = classes.trim();
        if (!classes.length)
            return "";

        let foundClasses = new Set;
        return classes.split(/\s+/).reduce((selector, className) => {
            if (!className.length || foundClasses.has(className))
                return selector;

            foundClasses.add(className);
            return `${selector}.${(shouldEscape ? CSS.escape(className) : className)}`;
        }, "");
    }
};

WI.DOMNode.Event = {
    EnabledPseudoClassesChanged: "dom-node-enabled-pseudo-classes-did-change",
    AttributeModified: "dom-node-attribute-modified",
    AttributeRemoved: "dom-node-attribute-removed",
    EventListenersChanged: "dom-node-event-listeners-changed",
    DidFireEvent: "dom-node-did-fire-event",
    PowerEfficientPlaybackStateChanged: "dom-node-power-efficient-playback-state-changed",
    LayoutContextTypeChanged: "dom-node-layout-context-type-changed",
};

WI.DOMNode.PseudoElementType = {
    Before: "before",
    After: "after",
};

WI.DOMNode.ShadowRootType = {
    UserAgent: "user-agent",
    Closed: "closed",
    Open: "open",
};

WI.DOMNode.CustomElementState = {
    Builtin: "builtin",
    Custom: "custom",
    Waiting: "waiting",
    Failed: "failed",
};

// Corresponds to `CSS.LayoutContextType`.
WI.DOMNode.LayoutContextType = {
    Grid: "grid",
};
