/*
 * 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:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.DOMNodeDetailsSidebarPanel = class DOMNodeDetailsSidebarPanel extends WI.DOMDetailsSidebarPanel
{
    constructor()
    {
        super("dom-node-details", WI.UIString("Node"));

        this._eventListenerGroupingMethodSetting = new WI.Setting("dom-node-event-listener-grouping-method", WI.DOMNodeDetailsSidebarPanel.EventListenerGroupingMethod.Event);

        this.element.classList.add("dom-node");

        this._nodeRemoteObject = null;
    }

    // Public

    closed()
    {
        if (this.didInitialLayout) {
            WI.domManager.removeEventListener(WI.DOMManager.Event.AttributeModified, this._attributesChanged, this);
            WI.domManager.removeEventListener(WI.DOMManager.Event.AttributeRemoved, this._attributesChanged, this);
            WI.domManager.removeEventListener(WI.DOMManager.Event.CharacterDataModified, this._characterDataModified, this);
            WI.domManager.removeEventListener(WI.DOMManager.Event.CustomElementStateChanged, this._customElementStateChanged, this);
        }

        super.closed();
    }

    // Protected

    initialLayout()
    {
        super.initialLayout();

        WI.domManager.addEventListener(WI.DOMManager.Event.AttributeModified, this._attributesChanged, this);
        WI.domManager.addEventListener(WI.DOMManager.Event.AttributeRemoved, this._attributesChanged, this);
        WI.domManager.addEventListener(WI.DOMManager.Event.CharacterDataModified, this._characterDataModified, this);
        WI.domManager.addEventListener(WI.DOMManager.Event.CustomElementStateChanged, this._customElementStateChanged, this);

        this._identityNodeTypeRow = new WI.DetailsSectionSimpleRow(WI.UIString("Type"));
        this._identityNodeNameRow = new WI.DetailsSectionSimpleRow(WI.UIString("Name"));
        this._identityNodeValueRow = new WI.DetailsSectionSimpleRow(WI.UIString("Value"));
        this._identityNodeContentSecurityPolicyHashRow = new WI.DetailsSectionSimpleRow(WI.UIString("CSP Hash"));

        var identityGroup = new WI.DetailsSectionGroup([this._identityNodeTypeRow, this._identityNodeNameRow, this._identityNodeValueRow, this._identityNodeContentSecurityPolicyHashRow]);
        var identitySection = new WI.DetailsSection("dom-node-identity", WI.UIString("Identity"), [identityGroup]);
        this.contentView.element.appendChild(identitySection.element);

        this._attributesDataGridRow = new WI.DetailsSectionDataGridRow(null, WI.UIString("No Attributes"));

        var attributesGroup = new WI.DetailsSectionGroup([this._attributesDataGridRow]);
        var attributesSection = new WI.DetailsSection("dom-node-attributes", WI.UIString("Attributes"), [attributesGroup]);
        this.contentView.element.appendChild(attributesSection.element);

        if (InspectorBackend.hasCommand("DOM.resolveNode")) {
            this._propertiesRow = new WI.DetailsSectionRow;
            let propertiesGroup = new WI.DetailsSectionGroup([this._propertiesRow]);
            let propertiesSection = new WI.DetailsSection("dom-node-properties", WI.UIString("Properties"), [propertiesGroup]);
            this.contentView.element.appendChild(propertiesSection.element);
        }

        let eventListenersFilterElement = WI.ImageUtilities.useSVGSymbol("Images/FilterFieldGlyph.svg", "filter", WI.UIString("Grouping Method"));
        WI.addMouseDownContextMenuHandlers(eventListenersFilterElement, this._populateEventListenersFilterContextMenu.bind(this));

        this._eventListenersSectionGroup = new WI.DetailsSectionGroup;
        let eventListenersSection = new WI.DetailsSection("dom-node-event-listeners", WI.UIString("Event Listeners"), [this._eventListenersSectionGroup], eventListenersFilterElement);
        this.contentView.element.appendChild(eventListenersSection.element);

        if (InspectorBackend.hasCommand("DOM.getDataBindingsForNode")) {
            this._dataBindingsSection = new WI.DetailsSection("dom-node-data-bindings", WI.UIString("Data Bindings"), []);
            this.contentView.element.appendChild(this._dataBindingsSection.element);
        }

        if (InspectorBackend.hasCommand("DOM.getAssociatedDataForNode")) {
            this._associatedDataGrid = new WI.DetailsSectionRow(WI.UIString("No Associated Data"));

            let associatedDataGroup = new WI.DetailsSectionGroup([this._associatedDataGrid]);

            let associatedSection = new WI.DetailsSection("dom-node-associated-data", WI.UIString("Associated Data"), [associatedDataGroup]);
            this.contentView.element.appendChild(associatedSection.element);
        }

        if (this._accessibilitySupported()) {
            this._accessibilityEmptyRow = new WI.DetailsSectionRow(WI.UIString("No Accessibility Information"));
            this._accessibilityNodeActiveDescendantRow = new WI.DetailsSectionSimpleRow(WI.UIString("Shared Focus"));
            this._accessibilityNodeBusyRow = new WI.DetailsSectionSimpleRow(WI.UIString("Busy"));
            this._accessibilityNodeCheckedRow = new WI.DetailsSectionSimpleRow(WI.UIString("Checked"));
            this._accessibilityNodeChildrenRow = new WI.DetailsSectionSimpleRow(WI.UIString("Children"));
            this._accessibilityNodeControlsRow = new WI.DetailsSectionSimpleRow(WI.UIString("Controls"));
            this._accessibilityNodeCurrentRow = new WI.DetailsSectionSimpleRow(WI.UIString("Current"));
            this._accessibilityNodeDisabledRow = new WI.DetailsSectionSimpleRow(WI.UIString("Disabled"));
            this._accessibilityNodeExpandedRow = new WI.DetailsSectionSimpleRow(WI.UIString("Expanded"));
            this._accessibilityNodeFlowsRow = new WI.DetailsSectionSimpleRow(WI.UIString("Flows"));
            this._accessibilityNodeFocusedRow = new WI.DetailsSectionSimpleRow(WI.UIString("Focused"));
            this._accessibilityNodeHeadingLevelRow = new WI.DetailsSectionSimpleRow(WI.UIString("Heading Level"));
            this._accessibilityNodehierarchyLevelRow = new WI.DetailsSectionSimpleRow(WI.UIString("Hierarchy Level"));
            this._accessibilityNodeIgnoredRow = new WI.DetailsSectionSimpleRow(WI.UIString("Ignored"));
            this._accessibilityNodeInvalidRow = new WI.DetailsSectionSimpleRow(WI.UIString("Invalid"));
            this._accessibilityNodeLiveRegionStatusRow = new WI.DetailsSectionSimpleRow(WI.UIString("Live"));
            this._accessibilityNodeMouseEventRow = new WI.DetailsSectionSimpleRow("");
            this._accessibilityNodeLabelRow = new WI.DetailsSectionSimpleRow(WI.UIString("Label"));
            this._accessibilityNodeOwnsRow = new WI.DetailsSectionSimpleRow(WI.UIString("Owns"));
            this._accessibilityNodeParentRow = new WI.DetailsSectionSimpleRow(WI.UIString("Parent"));
            this._accessibilityNodePressedRow = new WI.DetailsSectionSimpleRow(WI.UIString("Pressed"));
            this._accessibilityNodeReadonlyRow = new WI.DetailsSectionSimpleRow(WI.UIString("Readonly"));
            this._accessibilityNodeRequiredRow = new WI.DetailsSectionSimpleRow(WI.UIString("Required"));
            this._accessibilityNodeRoleRow = new WI.DetailsSectionSimpleRow(WI.UIString("Role"));
            this._accessibilityNodeSelectedRow = new WI.DetailsSectionSimpleRow(WI.UIString("Selected"));
            this._accessibilityNodeSelectedChildrenRow = new WI.DetailsSectionSimpleRow(WI.UIString("Selected Items"));

            this._accessibilityGroup = new WI.DetailsSectionGroup([this._accessibilityEmptyRow]);
            var accessibilitySection = new WI.DetailsSection("dom-node-accessibility", WI.UIString("Accessibility"), [this._accessibilityGroup]);

            this.contentView.element.appendChild(accessibilitySection.element);
        }
    }

    layout()
    {
        super.layout();

        if (!this.domNode || this.domNode.destroyed)
            return;

        this._refreshIdentity();
        this._refreshAttributes();
        this._refreshProperties();
        this._refreshEventListeners();
        this._refreshDataBindings();
        this._refreshAssociatedData();
        this._refreshAccessibility();
    }

    sizeDidChange()
    {
        super.sizeDidChange();

        // FIXME: <https://webkit.org/b/152269> Web Inspector: Convert DetailsSection classes to use View
        this._attributesDataGridRow.sizeDidChange();
    }

    attached()
    {
        super.attached();

        WI.DOMNode.addEventListener(WI.DOMNode.Event.EventListenersChanged, this._eventListenersChanged, this);
    }

    detached()
    {
        WI.DOMNode.removeEventListener(WI.DOMNode.Event.EventListenersChanged, this._eventListenersChanged, this);

        super.detached();
    }

    // Private

    _accessibilitySupported()
    {
        return InspectorBackend.hasCommand("DOM.getAccessibilityPropertiesForNode");
    }

    _refreshIdentity()
    {
        const domNode = this.domNode;
        this._identityNodeTypeRow.value = this._nodeTypeDisplayName();
        this._identityNodeNameRow.value = domNode.nodeNameInCorrectCase();
        this._identityNodeValueRow.value = domNode.nodeValue();
        this._identityNodeContentSecurityPolicyHashRow.value = domNode.contentSecurityPolicyHash();
    }

    _refreshAttributes()
    {
        let domNode = this.domNode;
        if (!domNode || !domNode.hasAttributes()) {
            // Remove the DataGrid to show the placeholder text.
            this._attributesDataGridRow.dataGrid = null;
            return;
        }

        let dataGrid = this._attributesDataGridRow.dataGrid;
        if (!dataGrid) {
            const columns = {
                name: {title: WI.UIString("Name"), width: "30%"},
                value: {title: WI.UIString("Value")},
            };
            dataGrid = this._attributesDataGridRow.dataGrid = new WI.DataGrid(columns);
        }

        dataGrid.removeChildren();

        let attributes = domNode.attributes();
        attributes.sort((a, b) => a.name.extendedLocaleCompare(b.name));
        for (let attribute of attributes) {
            let dataGridNode = new WI.EditableDataGridNode(attribute);
            dataGridNode.addEventListener(WI.EditableDataGridNode.Event.ValueChanged, this._attributeNodeValueChanged, this);
            dataGrid.appendChild(dataGridNode);
        }

        dataGrid.updateLayoutIfNeeded();
    }

    _refreshProperties()
    {
        if (!this._propertiesRow)
            return;

        if (this._nodeRemoteObject) {
            this._nodeRemoteObject.release();
            this._nodeRemoteObject = null;
        }

        let target = WI.assumingMainTarget();

        const objectGroup = "dom-node-details-sidebar-properties-object-group";
        target.RuntimeAgent.releaseObjectGroup(objectGroup);

        let domNode = this.domNode;
        WI.RemoteObject.resolveNode(domNode, objectGroup).then((object) => {
            // Bail if the DOM node changed while we were waiting for the async response.
            if (this.domNode !== domNode)
                return;

            this._nodeRemoteObject = object;

            function inspectedPage_node_collectPrototypes()
            {
                // This builds an object with numeric properties. This is easier than dealing with arrays
                // with the way RemoteObject works. Start at 1 since we use parseInt later and parseInt
                // returns 0 for non-numeric strings make it ambiguous.
                var prototype = this;
                var result = [];
                var counter = 1;

                while (prototype) {
                    result[counter++] = prototype;
                    prototype = prototype.__proto__;
                }

                return result;
            }

            const args = undefined;
            const generatePreview = false;
            object.callFunction(inspectedPage_node_collectPrototypes, args, generatePreview, nodePrototypesReady.bind(this));
        }).catch((error) => {
            // Bail if the DOM node changed while we were waiting for the async response.
            if (this.domNode !== domNode)
                return;

            console.assert(false, "Cannot resolve node.", error, domNode);
        });

        function nodePrototypesReady(error, object, wasThrown)
        {
            if (error || wasThrown || !object)
                return;

            // Bail if the DOM node changed while we were waiting for the async response.
            if (this.domNode !== domNode)
                return;

            object.getPropertyDescriptors(fillSection.bind(this), {ownProperties: true});
        }

        function fillSection(prototypes)
        {
            if (!prototypes)
                return;

            // Bail if the DOM node changed while we were waiting for the async response.
            if (this.domNode !== domNode)
                return;

            let element = this._propertiesRow.element;
            element.removeChildren();

            let propertyPath = new WI.PropertyPath(this._nodeRemoteObject, "node");

            let initialSection = true;
            for (let i = 0; i < prototypes.length; ++i) {
                // The only values we care about are numeric, as assigned in collectPrototypes.
                if (!parseInt(prototypes[i].name, 10))
                    continue;

                let prototype = prototypes[i].value;
                let prototypeName = prototype.description;
                let title = prototypeName;
                if (/Prototype$/.test(title)) {
                    prototypeName = prototypeName.replace(/Prototype$/, "");
                    title = prototypeName + WI.UIString(" (Prototype)");
                } else if (title === "Object")
                    title = title + WI.UIString(" (Prototype)");

                let mode = initialSection ? WI.ObjectTreeView.Mode.Properties : WI.ObjectTreeView.Mode.PureAPI;
                let objectTree = new WI.ObjectTreeView(prototype, mode, propertyPath);
                objectTree.showOnlyProperties();
                objectTree.setPrototypeNameOverride(prototypeName);

                let detailsSection = new WI.DetailsSection(prototype.description.hash + "-prototype-properties", title, null, null, true);
                detailsSection.groups[0].rows = [new WI.ObjectPropertiesDetailSectionRow(objectTree, detailsSection)];

                element.appendChild(detailsSection.element);

                initialSection = false;
            }
        }
    }

    _refreshEventListeners()
    {
        var domNode = this.domNode;
        if (!domNode)
            return;

        const windowTargetIdentifier = Symbol("window");

        function createEventListenerSection(title, eventListeners, options = {}) {
            let groups = eventListeners.map((eventListener) => new WI.EventListenerSectionGroup(eventListener, options));

            let optionsElement = null;
            if ((WI.DOMManager.supportsDisablingEventListeners() || WI.DOMManager.supportsEventListenerBreakpoints()) && groups.some((group) => group.supportsStateModification)) {
                optionsElement = WI.ImageUtilities.useSVGSymbol("Images/Gear.svg", "event-listener-options", WI.UIString("Options"));
                WI.addMouseDownContextMenuHandlers(optionsElement, (contextMenu) => {
                    if (WI.DOMManager.supportsDisablingEventListeners()) {
                        let shouldDisable = groups.some((eventListener) => !eventListener.isEventListenerDisabled);
                        contextMenu.appendItem(shouldDisable ? WI.UIString("Disable Event Listeners") : WI.UIString("Enable Event Listeners"), () => {
                            for (let group of groups)
                                group.isEventListenerDisabled = shouldDisable;
                        });
                    }

                    if (WI.DOMManager.supportsEventListenerBreakpoints()) {
                        let shouldBreakpoint = groups.some((eventListener) => !eventListener.hasEventListenerBreakpoint);
                        contextMenu.appendItem(shouldBreakpoint ? WI.UIString("Add Breakpoints") : WI.UIString("Delete Breakpoints"), () => {
                            for (let group of groups)
                                group.hasEventListenerBreakpoint = shouldBreakpoint;
                        });
                    }
                });
            }

            const defaultCollapsedSettingValue = true;
            let identifier = `${options.identifier ?? title}-event-listener-section`;
            let section = new WI.DetailsSection(identifier, title, groups, optionsElement, defaultCollapsedSettingValue);
            section.element.classList.add("event-listener-section");
            return section;
        }

        function generateGroupsByEvent(eventListeners) {
            let eventListenerTypes = new Map;
            for (let eventListener of eventListeners) {
                console.assert(eventListener.nodeId || eventListener.onWindow);
                if (eventListener.nodeId)
                    eventListener.node = WI.domManager.nodeForId(eventListener.nodeId);

                let eventListenersForType = eventListenerTypes.get(eventListener.type);
                if (!eventListenersForType)
                    eventListenerTypes.set(eventListener.type, eventListenersForType = []);
                eventListenersForType.push(eventListener);
            }

            let rows = [];

            let types = Array.from(eventListenerTypes.keys());
            types.sort();
            for (let type of types)
                rows.push(createEventListenerSection(type, eventListenerTypes.get(type), {hideType: true}));

            return rows;
        }

        function generateGroupsByTarget(eventListeners) {
            let eventListenerTargets = new Map;
            for (let eventListener of eventListeners) {
                console.assert(eventListener.nodeId || eventListener.onWindow);
                if (eventListener.nodeId)
                    eventListener.node = WI.domManager.nodeForId(eventListener.nodeId);

                let target = eventListener.onWindow ? windowTargetIdentifier : eventListener.node;
                let eventListenersForTarget = eventListenerTargets.get(target);
                if (!eventListenersForTarget)
                    eventListenerTargets.set(target, eventListenersForTarget = []);
                eventListenersForTarget.push(eventListener);
            }

            let rows = [];

            function generateSectionForTarget(target) {
                let eventListenersForTarget = eventListenerTargets.get(target);
                if (!eventListenersForTarget)
                    return;

                eventListenersForTarget.sort((a, b) => a.type.toLowerCase().extendedLocaleCompare(b.type.toLowerCase()));

                let title = target === windowTargetIdentifier ? WI.unlocalizedString("window") : target.displayName;
                let identifier = target === windowTargetIdentifier ? WI.unlocalizedString("window") : target.unescapedSelector;

                let section = createEventListenerSection(title, eventListenersForTarget, {hideTarget: true, identifier});
                if (target instanceof WI.DOMNode)
                    WI.bindInteractionsForNodeToElement(target, section.titleElement, {ignoreClick: true});
                rows.push(section);
            }

            let currentNode = domNode;
            do {
                generateSectionForTarget(currentNode);
            } while (currentNode = currentNode.parentNode);

            generateSectionForTarget(windowTargetIdentifier);

            return rows;
        }

        function eventListenersCallback(error, eventListeners)
        {
            if (error)
                return;

            // Bail if the DOM node changed while we were waiting for the async response.
            if (this.domNode !== domNode)
                return;

            if (!eventListeners.length) {
                var emptyRow = new WI.DetailsSectionRow(WI.UIString("No Event Listeners"));
                emptyRow.showEmptyMessage();
                this._eventListenersSectionGroup.rows = [emptyRow];
                return;
            }

            switch (this._eventListenerGroupingMethodSetting.value) {
            case WI.DOMNodeDetailsSidebarPanel.EventListenerGroupingMethod.Event:
                this._eventListenersSectionGroup.rows = generateGroupsByEvent.call(this, eventListeners);
                break;

            case WI.DOMNodeDetailsSidebarPanel.EventListenerGroupingMethod.Target:
                this._eventListenersSectionGroup.rows = generateGroupsByTarget.call(this, eventListeners);
                break;
            }
        }

        domNode.getEventListeners(eventListenersCallback.bind(this));
    }

    _refreshDataBindings()
    {
        if (!this._dataBindingsSection)
            return;

        let domNode = this.domNode;
        if (!domNode)
            return;

        let target = WI.assumingMainTarget();
        target.DOMAgent.getDataBindingsForNode(this.domNode.id).then(({dataBindings}) => {
            if (this.domNode !== domNode)
                return;

            if (!dataBindings.length) {
                let emptyRow = new WI.DetailsSectionRow(WI.UIString("No Data Bindings"));
                emptyRow.showEmptyMessage();
                this._dataBindingsSection.groups = [new WI.DetailsSectionGroup([emptyRow])];
                return;
            }

            let groups = [];
            for (let {binding, type, value} of dataBindings) {
                groups.push(new WI.DetailsSectionGroup([
                    new WI.DetailsSectionSimpleRow(WI.UIString("Binding"), binding),
                    new WI.DetailsSectionSimpleRow(WI.UIString("Type"), type),
                    new WI.DetailsSectionSimpleRow(WI.UIString("Value"), value),
                ]));
            }
            this._dataBindingsSection.groups = groups;
        });
    }

    _refreshAssociatedData()
    {
        if (!this._associatedDataGrid)
            return;

        let target = WI.assumingMainTarget();

        const objectGroup = "dom-node-details-sidebar-associated-data-object-group";
        target.RuntimeAgent.releaseObjectGroup(objectGroup);

        let domNode = this.domNode;
        if (!domNode)
            return;

        target.DOMAgent.getAssociatedDataForNode(domNode.id).then(({associatedData}) => {
            if (this.domNode !== domNode)
                return;

            if (!associatedData) {
                this._associatedDataGrid.showEmptyMessage();
                return;
            }

            let expression = associatedData;
            const options = {
                objectGroup,
                doNotPauseOnExceptionsAndMuteConsole: true,
            };
            WI.runtimeManager.evaluateInInspectedWindow(expression, options, (result, wasThrown) => {
                console.assert(!wasThrown);

                if (!result) {
                    this._associatedDataGrid.showEmptyMessage();
                    return;
                }

                this._associatedDataGrid.hideEmptyMessage();

                const propertyPath = null;
                const forceExpanding = true;
                let element = WI.FormattedValue.createObjectTreeOrFormattedValueForRemoteObject(result, propertyPath, forceExpanding);

                let objectTree = element.__objectTree;
                if (objectTree) {
                    objectTree.showOnlyJSON();
                    objectTree.expand();
                }

                this._associatedDataGrid.element.appendChild(element);
            });
        });
    }

    _refreshAccessibility()
    {
        if (!this._accessibilitySupported())
            return;

        var domNode = this.domNode;
        if (!domNode)
            return;

        var properties = {};

        function booleanValueToLocalizedStringIfTrue(property) {
            if (properties[property])
                return WI.UIString("Yes");
            return "";
        }

        function booleanValueToLocalizedStringIfPropertyDefined(property) {
            if (properties[property] !== undefined) {
                if (properties[property])
                    return WI.UIString("Yes");
                else
                    return WI.UIString("No");
            }
            return "";
        }

        function linkForNodeId(nodeId) {
            var link = null;
            if (nodeId !== undefined && typeof nodeId === "number") {
                var node = WI.domManager.nodeForId(nodeId);
                if (node)
                    link = WI.linkifyAccessibilityNodeReference(node);
            }
            return link;
        }

        function linkListForNodeIds(nodeIds) {
            if (!nodeIds)
                return null;

            const itemsToShow = 5;
            let hasLinks = false;
            let listItemCount = 0;
            let container = document.createElement("div");
            container.classList.add("list-container");
            let linkList = container.createChild("ul", "node-link-list");
            let initiallyHiddenItems = [];
            for (let nodeId of nodeIds) {
                let node = WI.domManager.nodeForId(nodeId);
                if (!node)
                    continue;
                let link = WI.linkifyAccessibilityNodeReference(node);
                hasLinks = true;
                let li = linkList.createChild("li");
                li.appendChild(link);
                if (listItemCount >= itemsToShow) {
                    li.hidden = true;
                    initiallyHiddenItems.push(li);
                }
                listItemCount++;
            }
            container.appendChild(linkList);
            if (listItemCount > itemsToShow) {
                let moreNodesButton = container.createChild("button", "expand-list-button");
                moreNodesButton.textContent = WI.UIString("%d More\u2026").format(listItemCount - itemsToShow);
                moreNodesButton.addEventListener("click", () => {
                    initiallyHiddenItems.forEach((element) => { element.hidden = false; });
                    moreNodesButton.remove();
                });
            }
            if (hasLinks)
                return container;

            return null;
        }

        function accessibilityPropertiesCallback(accessibilityProperties) {
            if (this.domNode !== domNode)
                return;

            // Make sure the current set of properties is available in the closure scope for the helper functions.
            properties = accessibilityProperties;

            if (accessibilityProperties && accessibilityProperties.exists) {

                var activeDescendantLink = linkForNodeId(accessibilityProperties.activeDescendantNodeId);
                var busy = booleanValueToLocalizedStringIfPropertyDefined("busy");

                var checked = "";
                if (accessibilityProperties.checked !== undefined) {
                    if (accessibilityProperties.checked === InspectorBackend.Enum.DOM.AccessibilityPropertiesChecked.True)
                        checked = WI.UIString("Yes");
                    else if (accessibilityProperties.checked === InspectorBackend.Enum.DOM.AccessibilityPropertiesChecked.Mixed)
                        checked = WI.UIString("Mixed");
                    else // InspectorBackend.Enum.DOM.AccessibilityPropertiesChecked.False
                        checked = WI.UIString("No");
                }

                // Accessibility tree children are not a 1:1 mapping with DOM tree children.
                var childNodeLinkList = linkListForNodeIds(accessibilityProperties.childNodeIds);
                var controlledNodeLinkList = linkListForNodeIds(accessibilityProperties.controlledNodeIds);

                var current = "";
                switch (accessibilityProperties.current) {
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesCurrent.True:
                    current = WI.UIString("True");
                    break;
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesCurrent.Page:
                    current = WI.UIString("Page");
                    break;
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesCurrent.Location:
                    current = WI.UIString("Location");
                    break;
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesCurrent.Step:
                    current = WI.UIString("Step");
                    break;
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesCurrent.Date:
                    current = WI.UIString("Date");
                    break;
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesCurrent.Time:
                    current = WI.UIString("Time");
                    break;
                default:
                    current = "";
                }

                var disabled = booleanValueToLocalizedStringIfTrue("disabled");
                var expanded = booleanValueToLocalizedStringIfPropertyDefined("expanded");
                var flowedNodeLinkList = linkListForNodeIds(accessibilityProperties.flowedNodeIds);
                var focused = booleanValueToLocalizedStringIfPropertyDefined("focused");

                var ignored = "";
                if (accessibilityProperties.ignored) {
                    ignored = WI.UIString("Yes");
                    if (accessibilityProperties.hidden)
                        ignored = WI.UIString("%s (hidden)").format(ignored);
                    else if (accessibilityProperties.ignoredByDefault)
                        ignored = WI.UIString("%s (default)").format(ignored);
                }

                var invalid = "";
                if (accessibilityProperties.invalid === InspectorBackend.Enum.DOM.AccessibilityPropertiesInvalid.True)
                    invalid = WI.UIString("Yes");
                else if (accessibilityProperties.invalid === InspectorBackend.Enum.DOM.AccessibilityPropertiesInvalid.Grammar)
                    invalid = WI.UIString("Grammar");
                else if (accessibilityProperties.invalid === InspectorBackend.Enum.DOM.AccessibilityPropertiesInvalid.Spelling)
                    invalid = WI.UIString("Spelling");

                var label = accessibilityProperties.label;

                var liveRegionStatus = "";
                var liveRegionStatusNode = null;
                var liveRegionStatusToken = accessibilityProperties.liveRegionStatus;
                switch (liveRegionStatusToken) {
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesLiveRegionStatus.Assertive:
                    liveRegionStatus = WI.UIString("Assertive");
                    break;
                case InspectorBackend.Enum.DOM.AccessibilityPropertiesLiveRegionStatus.Polite:
                    liveRegionStatus = WI.UIString("Polite");
                    break;
                default:
                    liveRegionStatus = "";
                }
                if (liveRegionStatus) {
                    var liveRegionRelevant = accessibilityProperties.liveRegionRelevant;
                    // Append @aria-relevant values. E.g. "Live: Assertive (Additions, Text)".
                    if (liveRegionRelevant && liveRegionRelevant.length) {
                        // @aria-relevant="all" is exposed as ["additions","removals","text"], in order.
                        // This order is controlled in WebCore and expected in WebInspectorUI.
                        if (liveRegionRelevant.length === 3
                            && liveRegionRelevant[0] === InspectorBackend.Enum.DOM.LiveRegionRelevant.Additions
                            && liveRegionRelevant[1] === InspectorBackend.Enum.DOM.LiveRegionRelevant.Removals
                            && liveRegionRelevant[2] === InspectorBackend.Enum.DOM.LiveRegionRelevant.Text)
                            liveRegionRelevant = [WI.UIString("All Changes")];
                        else {
                            // Reassign localized strings in place: ["additions","text"] becomes ["Additions","Text"].
                            liveRegionRelevant = liveRegionRelevant.map(function(value) {
                                switch (value) {
                                case InspectorBackend.Enum.DOM.LiveRegionRelevant.Additions:
                                    return WI.UIString("Additions");
                                case InspectorBackend.Enum.DOM.LiveRegionRelevant.Removals:
                                    return WI.UIString("Removals");
                                case InspectorBackend.Enum.DOM.LiveRegionRelevant.Text:
                                    return WI.UIString("Text");
                                default: // If WebCore sends a new unhandled value, display as a String.
                                    return "\"" + value + "\"";
                                }
                            });
                        }
                        liveRegionStatus += " (" + liveRegionRelevant.join(", ") + ")";
                    }
                    // Clarify @aria-atomic if necessary.
                    if (accessibilityProperties.liveRegionAtomic) {
                        liveRegionStatusNode = document.createElement("div");
                        liveRegionStatusNode.className = "value-with-clarification";
                        liveRegionStatusNode.setAttribute("role", "text");
                        liveRegionStatusNode.append(liveRegionStatus);
                        var clarificationNode = document.createElement("div");
                        clarificationNode.className = "clarification";
                        clarificationNode.append(WI.UIString("Region announced in its entirety."));
                        liveRegionStatusNode.appendChild(clarificationNode);
                    }
                }

                var mouseEventNodeId = accessibilityProperties.mouseEventNodeId;
                var mouseEventTextValue = "";
                var mouseEventNodeLink = null;
                if (mouseEventNodeId) {
                    if (mouseEventNodeId === accessibilityProperties.nodeId)
                        mouseEventTextValue = WI.UIString("Yes");
                    else
                        mouseEventNodeLink = linkForNodeId(mouseEventNodeId);
                }

                var ownedNodeLinkList = linkListForNodeIds(accessibilityProperties.ownedNodeIds);

                // Accessibility tree parent is not a 1:1 mapping with the DOM tree parent.
                var parentNodeLink = linkForNodeId(accessibilityProperties.parentNodeId);

                var pressed = booleanValueToLocalizedStringIfPropertyDefined("pressed");
                var readonly = booleanValueToLocalizedStringIfTrue("readonly");
                var required = booleanValueToLocalizedStringIfPropertyDefined("required");

                var role = accessibilityProperties.role;
                let hasPopup = accessibilityProperties.isPopupButton;
                let roleType = null;
                let buttonType = null;
                let buttonTypePopupString = WI.UIString("popup");
                let buttonTypeToggleString = WI.UIString("toggle");
                let buttonTypePopupToggleString = WI.UIString("popup, toggle");

                if (role === "" || role === "unknown")
                    role = WI.UIString("No matching ARIA role");
                else if (role) {
                    if (role === "button") {
                        if (pressed)
                            buttonType = buttonTypeToggleString;

                        // In cases where an element is a toggle button, but it also has
                        // aria-haspopup, we concatenate the button types. If it is just
                        // a popup button, we only include "popup".
                        if (hasPopup)
                            buttonType = buttonType ? buttonTypePopupToggleString : buttonTypePopupString;
                    }

                    if (!domNode.getAttribute("role"))
                        roleType = WI.UIString("default");
                    else if (buttonType || domNode.getAttribute("role") !== role)
                        roleType = WI.UIString("computed");

                    if (buttonType && roleType)
                        role = WI.UIString("%s (%s, %s)").format(role, buttonType, roleType);
                    else if (roleType || buttonType) {
                        let extraInfo = roleType || buttonType;
                        role = WI.UIString("%s (%s)").format(role, extraInfo);
                    }
                }

                var selected = booleanValueToLocalizedStringIfTrue("selected");
                var selectedChildNodeLinkList = linkListForNodeIds(accessibilityProperties.selectedChildNodeIds);

                var headingLevel = accessibilityProperties.headingLevel;
                var hierarchyLevel = accessibilityProperties.hierarchyLevel;
                // Assign all the properties to their respective views.
                this._accessibilityNodeActiveDescendantRow.value = activeDescendantLink || "";
                this._accessibilityNodeBusyRow.value = busy;
                this._accessibilityNodeCheckedRow.value = checked;
                this._accessibilityNodeChildrenRow.value = childNodeLinkList || "";
                this._accessibilityNodeControlsRow.value = controlledNodeLinkList || "";
                this._accessibilityNodeCurrentRow.value = current;
                this._accessibilityNodeDisabledRow.value = disabled;
                this._accessibilityNodeExpandedRow.value = expanded;
                this._accessibilityNodeFlowsRow.value = flowedNodeLinkList || "";
                this._accessibilityNodeFocusedRow.value = focused;
                this._accessibilityNodeHeadingLevelRow.value = headingLevel || "";
                this._accessibilityNodehierarchyLevelRow.value = hierarchyLevel || "";
                this._accessibilityNodeIgnoredRow.value = ignored;
                this._accessibilityNodeInvalidRow.value = invalid;
                this._accessibilityNodeLabelRow.value = label;
                this._accessibilityNodeLiveRegionStatusRow.value = liveRegionStatusNode || liveRegionStatus;

                // Row label changes based on whether the value is a delegate node link.
                this._accessibilityNodeMouseEventRow.label = mouseEventNodeLink ? WI.UIString("Click Listener") : WI.UIString("Clickable");
                this._accessibilityNodeMouseEventRow.value = mouseEventNodeLink || mouseEventTextValue;

                this._accessibilityNodeOwnsRow.value = ownedNodeLinkList || "";
                this._accessibilityNodeParentRow.value = parentNodeLink || "";
                this._accessibilityNodePressedRow.value = pressed;
                this._accessibilityNodeReadonlyRow.value = readonly;
                this._accessibilityNodeRequiredRow.value = required;
                this._accessibilityNodeRoleRow.value = role;
                this._accessibilityNodeSelectedRow.value = selected;

                this._accessibilityNodeSelectedChildrenRow.label = WI.UIString("Selected Items");
                this._accessibilityNodeSelectedChildrenRow.value = selectedChildNodeLinkList || "";
                if (selectedChildNodeLinkList && accessibilityProperties.selectedChildNodeIds.length === 1)
                    this._accessibilityNodeSelectedChildrenRow.label = WI.UIString("Selected Item");

                // Display order, not alphabetical as above.
                this._accessibilityGroup.rows = [
                    // Global properties for all elements.
                    this._accessibilityNodeIgnoredRow,
                    this._accessibilityNodeRoleRow,
                    this._accessibilityNodeLabelRow,
                    this._accessibilityNodeParentRow,
                    this._accessibilityNodeActiveDescendantRow,
                    this._accessibilityNodeSelectedChildrenRow,
                    this._accessibilityNodeChildrenRow,
                    this._accessibilityNodeOwnsRow,
                    this._accessibilityNodeControlsRow,
                    this._accessibilityNodeFlowsRow,
                    this._accessibilityNodeMouseEventRow,
                    this._accessibilityNodeFocusedRow,
                    this._accessibilityNodeHeadingLevelRow,
                    this._accessibilityNodehierarchyLevelRow,
                    this._accessibilityNodeBusyRow,
                    this._accessibilityNodeLiveRegionStatusRow,
                    this._accessibilityNodeCurrentRow,

                    // Properties exposed for all input-type elements.
                    this._accessibilityNodeDisabledRow,
                    this._accessibilityNodeInvalidRow,
                    this._accessibilityNodeRequiredRow,

                    // Role-specific properties.
                    this._accessibilityNodeCheckedRow,
                    this._accessibilityNodeExpandedRow,
                    this._accessibilityNodePressedRow,
                    this._accessibilityNodeReadonlyRow,
                    this._accessibilityNodeSelectedRow
                ];

                this._accessibilityEmptyRow.hideEmptyMessage();

            } else {
                this._accessibilityGroup.rows = [this._accessibilityEmptyRow];
                this._accessibilityEmptyRow.showEmptyMessage();
            }
        }

        domNode.accessibilityProperties(accessibilityPropertiesCallback.bind(this));
    }

    _populateEventListenersFilterContextMenu(contextMenu)
    {
        let addGroupingMethodCheckboxItem = (label, groupingMethod) => {
            contextMenu.appendCheckboxItem(label, () => {
                this._eventListenerGroupingMethodSetting.value = groupingMethod;

                this._refreshEventListeners();
            }, this._eventListenerGroupingMethodSetting.value === groupingMethod);
        };

        addGroupingMethodCheckboxItem(WI.UIString("Group by Event", "Group by Event @ Node Event Listeners", "Group DOM event listeners by DOM event"), WI.DOMNodeDetailsSidebarPanel.EventListenerGroupingMethod.Event);
        addGroupingMethodCheckboxItem(WI.UIString("Group by Target", "Group by Target @ Node Event Listeners", "Group DOM event listeners by DOM node"), WI.DOMNodeDetailsSidebarPanel.EventListenerGroupingMethod.Target);
    }

    _eventListenersChanged(event)
    {
        if (event.target === this.domNode || event.target.isAncestor(this.domNode))
            this._refreshEventListeners();
    }

    _attributesChanged(event)
    {
        if (event.data.node !== this.domNode)
            return;
        this._refreshAttributes();
        this._refreshAccessibility();
        this._refreshDataBindings();
    }

    _attributeNodeValueChanged(event)
    {
        let change = event.data;
        let data = event.target.data;

        if (change.columnIdentifier === "name") {
            this.domNode.removeAttribute(data[change.columnIdentifier], (error) => {
                this.domNode.setAttribute(change.value, `${change.value}="${data.value}"`);
            });
        } else if (change.columnIdentifier === "value")
            this.domNode.setAttributeValue(data.name, change.value);
    }

    _characterDataModified(event)
    {
        if (event.data.node !== this.domNode)
            return;
        this._identityNodeValueRow.value = this.domNode.nodeValue();
    }

    _customElementStateChanged(event)
    {
        if (event.data.node !== this.domNode)
            return;
        this._refreshIdentity();
    }

    _nodeTypeDisplayName()
    {
        switch (this.domNode.nodeType()) {
        case Node.ELEMENT_NODE: {
            const nodeName = WI.UIString("Element");
            const state = this._customElementState();
            return state === null ? nodeName : `${nodeName} (${state})`;
        }
        case Node.TEXT_NODE:
            return WI.UIString("Text Node");
        case Node.COMMENT_NODE:
            return WI.UIString("Comment");
        case Node.DOCUMENT_NODE:
            return WI.UIString("Document");
        case Node.DOCUMENT_TYPE_NODE:
            return WI.UIString("Document Type");
        case Node.DOCUMENT_FRAGMENT_NODE:
            return WI.UIString("Document Fragment");
        case Node.CDATA_SECTION_NODE:
            return WI.UIString("Character Data");
        case Node.PROCESSING_INSTRUCTION_NODE:
            return WI.UIString("Processing Instruction");
        default:
            console.error("Unknown DOM node type: ", this.domNode.nodeType());
            return this.domNode.nodeType();
        }
    }

    _customElementState()
    {
        const state = this.domNode.customElementState();
        switch (state) {
        case WI.DOMNode.CustomElementState.Builtin:
            return null;
        case WI.DOMNode.CustomElementState.Custom:
            return WI.UIString("Custom");
        case WI.DOMNode.CustomElementState.Waiting:
            return WI.UIString("Undefined custom element");
        case WI.DOMNode.CustomElementState.Failed:
            return WI.UIString("Failed to upgrade");
        }
        console.error("Unknown DOM custom element state: ", state);
        return null;
    }
};

WI.DOMNodeDetailsSidebarPanel.EventListenerGroupingMethod = {
    Event: "event",
    Target: "target",
};
