// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @constructor
 * @extends {WebInspector.ThrottledWidget}
 */
WebInspector.AccessibilitySidebarView = function()
{
    WebInspector.ThrottledWidget.call(this);
    this._computedTextSubPane = null;
    this._axNodeSubPane = null;
    this._node = null;
    this._sidebarPaneStack = null;
    WebInspector.context.addFlavorChangeListener(WebInspector.DOMNode, this._pullNode, this);
    this._pullNode();
}

WebInspector.AccessibilitySidebarView.prototype = {
    /**
     * @return {?WebInspector.DOMNode}
     */
    node: function()
    {
        return this._node;
    },

    /**
     * @override
     * @param {!WebInspector.Throttler.FinishCallback} finishCallback
     * @protected
     */
    doUpdate: function(finishCallback)
    {
        /**
         * @param {?AccessibilityAgent.AXNode} accessibilityNode
         * @this {WebInspector.AccessibilitySidebarView}
         */
        function accessibilityNodeCallback(accessibilityNode)
        {
            if (this._computedTextSubPane)
                this._computedTextSubPane.setAXNode(accessibilityNode);
            if (this._axNodeSubPane)
                this._axNodeSubPane.setAXNode(accessibilityNode);
            finishCallback();
        }
        var node = this.node();
        WebInspector.AccessibilityModel.fromTarget(node.target()).getAXNode(node.id, accessibilityNodeCallback.bind(this));
    },

    /**
     * @override
     */
    wasShown: function()
    {
        WebInspector.ThrottledWidget.prototype.wasShown.call(this);

        if (!this._sidebarPaneStack) {
            this._computedTextSubPane = new WebInspector.AXComputedTextSubPane();
            this._computedTextSubPane.setNode(this.node());
            this._computedTextSubPane.show(this.element);
            this._computedTextSubPane.expand();

            this._axNodeSubPane = new WebInspector.AXNodeSubPane();
            this._axNodeSubPane.setNode(this.node());
            this._axNodeSubPane.show(this.element);
            this._axNodeSubPane.expand();

            this._sidebarPaneStack = new WebInspector.SidebarPaneStack();
            this._sidebarPaneStack.element.classList.add("flex-auto");
            this._sidebarPaneStack.show(this.element);
            this._sidebarPaneStack.addPane(this._computedTextSubPane);
            this._sidebarPaneStack.addPane(this._axNodeSubPane);
        }

        WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrModified, this._onAttrChange, this);
        WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrRemoved, this._onAttrChange, this);
        WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.CharacterDataModified, this._onNodeChange, this);
        WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._onNodeChange, this);
    },

    /**
     * @override
     */
    willHide: function()
    {
        WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrModified, this._onAttrChange, this);
        WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrRemoved, this._onAttrChange, this);
        WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.CharacterDataModified, this._onNodeChange, this);
        WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._onNodeChange, this);
    },

    _pullNode: function()
    {
        this._node = WebInspector.context.flavor(WebInspector.DOMNode);
        if (this._computedTextSubPane)
            this._computedTextSubPane.setNode(this._node);
        if (this._axNodeSubPane)
            this._axNodeSubPane.setNode(this._node);
        this.update();
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _onAttrChange: function(event)
    {
        if (!this.node())
            return;
        var node = event.data.node;
        if (this.node() !== node)
            return;
        this.update();
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _onNodeChange: function(event)
    {
        if (!this.node())
            return;
        var node = event.data;
        if (this.node() !== node)
            return;
        this.update();
    },


    __proto__: WebInspector.ThrottledWidget.prototype
};

/**
 * @constructor
 * @extends {WebInspector.SidebarPane}
 * @param {string} name
 */
WebInspector.AccessibilitySubPane = function(name)
{
    WebInspector.SidebarPane.call(this, name);

    this._axNode = null;
    this.registerRequiredCSS("accessibility/accessibilityNode.css");
}

WebInspector.AccessibilitySubPane.prototype = {
    /**
     * @param {?AccessibilityAgent.AXNode} axNode
     * @protected
     */
    setAXNode: function(axNode)
    {
    },

    /**
     * @return {?WebInspector.DOMNode}
     */
    node: function()
    {
        return this._node;
    },

    /**
     * @param {?WebInspector.DOMNode} node
     */
    setNode: function(node)
    {
        this._node = node;
    },

    /**
     * @param {string} textContent
     * @param {string=} className
     * @return {!Element}
     */
    createInfo: function(textContent, className)
    {
        var classNameOrDefault = className || "info";
        var info = createElementWithClass("div", classNameOrDefault);
        info.textContent = textContent;
        this.element.appendChild(info);
        return info;
    },

    /**
     * @param {string=} className
     * @return {!TreeOutline}
     */
    createTreeOutline: function(className)
    {
        var treeOutline = new TreeOutlineInShadow(className);
        treeOutline.registerRequiredCSS("accessibility/accessibilityNode.css");
        treeOutline.registerRequiredCSS("components/objectValue.css");
        treeOutline.element.classList.add("hidden");
        this.element.appendChild(treeOutline.element);
        return treeOutline;
    },

    __proto__: WebInspector.SidebarPane.prototype
}

/**
 * @constructor
 * @extends {WebInspector.AccessibilitySubPane}
 */
WebInspector.AXComputedTextSubPane = function()
{
    WebInspector.AccessibilitySubPane.call(this, WebInspector.UIString("Computed Text"));

    this._computedTextElement = this.element.createChild("div", "ax-computed-text hidden");

    this._noTextInfo = this.createInfo(WebInspector.UIString("Node has no text alternative."));
    this._treeOutline = this.createTreeOutline();
};


WebInspector.AXComputedTextSubPane.prototype = {
    /**
     * @param {?AccessibilityAgent.AXNode} axNode
     * @override
     */
    setAXNode: function(axNode)
    {
        if (this._axNode === axNode)
            return;
        this._axNode = axNode;

        var treeOutline = this._treeOutline;
        treeOutline.removeChildren();
        var target = this.node().target();

        if (!axNode || axNode.ignored) {
            this._computedTextElement.classList.add("hidden");
            treeOutline.element.classList.add("hidden");

            this._noTextInfo.classList.remove("hidden");
            return;
        }
        this._computedTextElement.removeChildren();

        // TODO(aboxhall): include contents where appropriate (requires protocol change)
        this._computedTextElement.classList.toggle("hidden", !axNode.name || !axNode.name.value);
        if (axNode.name && axNode.name.value)
            this._computedTextElement.createChild("div").textContent = axNode.name.value;

        var foundProperty = false;
        /**
         * @param {!AccessibilityAgent.AXProperty} property
         */
        function addProperty(property)
        {
            foundProperty = true;
            treeOutline.appendChild(new WebInspector.AXNodePropertyTreeElement(property, target));
        }

        if ("value" in axNode && axNode.value.type === "string")
            addProperty(/** @type {!AccessibilityAgent.AXProperty} */ ({name: "value", value: axNode.value}));

        var propertiesArray = /** @type {!Array.<!AccessibilityAgent.AXProperty> } */ (axNode.properties);
        for (var property of propertiesArray) {
            if (property.name == AccessibilityAgent.AXWidgetAttributes.Valuetext) {
                addProperty(property);
                break;
            }
        }

        treeOutline.element.classList.toggle("hidden", !foundProperty)
        this._noTextInfo.classList.toggle("hidden", !treeOutline.element.classList.contains("hidden") || !this._computedTextElement.classList.contains("hidden"));
    },

    __proto__: WebInspector.AccessibilitySubPane.prototype
};

/**
 * @constructor
 * @extends {WebInspector.AccessibilitySubPane}
 */
WebInspector.AXNodeSubPane = function()
{
    WebInspector.AccessibilitySubPane.call(this, WebInspector.UIString("Accessibility Node"));

    this._noNodeInfo = this.createInfo(WebInspector.UIString("No accessibility node"));
    this._ignoredInfo = this.createInfo(WebInspector.UIString("Accessibility node not exposed"), "ax-ignored-info hidden");

    this._treeOutline = this.createTreeOutline();
    this._ignoredReasonsTree = this.createTreeOutline();
};


WebInspector.AXNodeSubPane.prototype = {
    /**
     * @param {?AccessibilityAgent.AXNode} axNode
     * @override
     */
    setAXNode: function(axNode)
    {
        if (this._axNode === axNode)
            return;
        this._axNode = axNode;

        var treeOutline = this._treeOutline;
        treeOutline.removeChildren();
        var ignoredReasons = this._ignoredReasonsTree;
        ignoredReasons.removeChildren();
        var target = this.node().target();

        if (!axNode) {
            treeOutline.element.classList.add("hidden");
            this._ignoredInfo.classList.add("hidden");
            ignoredReasons.element.classList.add("hidden");

            this._noNodeInfo.classList.remove("hidden");
            this.element.classList.add("ax-ignored-node-pane");

            return;
        } else if (axNode.ignored) {
            this._noNodeInfo.classList.add("hidden");
            treeOutline.element.classList.add("hidden");
            this.element.classList.add("ax-ignored-node-pane");

            this._ignoredInfo.classList.remove("hidden");
            ignoredReasons.element.classList.remove("hidden");
            /**
             * @param {!AccessibilityAgent.AXProperty} property
             */
            function addIgnoredReason(property)
            {
                ignoredReasons.appendChild(new WebInspector.AXNodeIgnoredReasonTreeElement(property, axNode, target));
            }
            var ignoredReasonsArray = /** @type {!Array.<!Object>} */(axNode.ignoredReasons);
            for (var reason of ignoredReasonsArray)
                addIgnoredReason(reason);
            if (!ignoredReasons.firstChild())
                ignoredReasons.element.classList.add("hidden");
            return;
        }
        this.element.classList.remove("ax-ignored-node-pane");

        this._ignoredInfo.classList.add("hidden");
        ignoredReasons.element.classList.add("hidden");
        this._noNodeInfo.classList.add("hidden");

        treeOutline.element.classList.remove("hidden");

        /**
         * @param {!AccessibilityAgent.AXProperty} property
         */
        function addProperty(property)
        {
            treeOutline.appendChild(new WebInspector.AXNodePropertyTreeElement(property, target));
        }

        for (var propertyName of ["name", "description", "help", "value"]) {
            if (propertyName in axNode) {
                var defaultProperty = /** @type {!AccessibilityAgent.AXProperty} */ ({name: propertyName, value: axNode[propertyName]});
                addProperty(defaultProperty);
            }
        }

        var roleProperty = /** @type {!AccessibilityAgent.AXProperty} */ ({name: "role", value: axNode.role});
        addProperty(roleProperty);

        var propertyMap = {};
        var propertiesArray = /** @type {!Array.<!AccessibilityAgent.AXProperty>} */ (axNode.properties);
        for (var property of propertiesArray)
            propertyMap[property.name] = property;

        for (var propertySet of [AccessibilityAgent.AXWidgetAttributes, AccessibilityAgent.AXWidgetStates, AccessibilityAgent.AXGlobalStates, AccessibilityAgent.AXLiveRegionAttributes, AccessibilityAgent.AXRelationshipAttributes]) {
            for (var propertyKey in propertySet) {
                var property = propertySet[propertyKey];
                if (property in propertyMap)
                    addProperty(propertyMap[property]);
            }
        }
    },

    __proto__: WebInspector.AccessibilitySubPane.prototype
};

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!AccessibilityAgent.AXProperty} property
 * @param {!WebInspector.Target} target
 */
WebInspector.AXNodePropertyTreeElement = function(property, target)
{
    this._property = property;
    this._target = target;

    // Pass an empty title, the title gets made later in onattach.
    TreeElement.call(this, "");
    this.toggleOnClick = true;
    this.selectable = false;
}

WebInspector.AXNodePropertyTreeElement.prototype = {
    /**
     * @override
     */
    onattach: function()
    {
        this._update();
    },


    _update: function()
    {
        this._nameElement = WebInspector.AXNodePropertyTreeElement.createNameElement(this._property.name);

        var value = this._property.value;
        if (value.type === "idref") {
            this._valueElement = WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement(value, this._target);
        } else if (value.type === "idrefList") {
            var relatedNodes = value.relatedNodeArrayValue;
            var numNodes = relatedNodes.length;
            var description = "(" + numNodes + (numNodes == 1 ? " node" : " nodes") + ")";
            value.value = description;
            for (var i = 0; i < relatedNodes.length; i++) {
                var backendId = relatedNodes[i].backendNodeId;
                var deferredNode = new WebInspector.DeferredDOMNode(this._target, relatedNodes[i].backendNodeId);
                var child = new WebInspector.AXRelatedNodeTreeElement(deferredNode);
                this.appendChild(child);
            }
            this._valueElement = WebInspector.AXNodePropertyTreeElement.createValueElement(value, this.listItemElement);
            if (relatedNodes.length <= 3)
                this.expand();
            else
                this.collapse();
        } else {
            this._valueElement = WebInspector.AXNodePropertyTreeElement.createValueElement(value, this.listItemElement);
        }

        var separatorElement = createElementWithClass("span", "separator");
        separatorElement.textContent = ": ";

        this.listItemElement.removeChildren();
        this.listItemElement.appendChildren(this._nameElement, separatorElement, this._valueElement);
    },

    __proto__: TreeElement.prototype
}

/**
 * @param {!TreeElement} treeNode
 * @param {?AccessibilityAgent.AXNode} axNode
 * @param {!WebInspector.Target} target
 */
WebInspector.AXNodePropertyTreeElement.populateWithNode = function(treeNode, axNode, target)
{
}

/**
 * @param {?string} name
 * @return {!Element}
 */
WebInspector.AXNodePropertyTreeElement.createNameElement = function(name)
{
    var nameElement = createElement("span");
    var AXAttributes = WebInspector.AccessibilityStrings.AXAttributes;
    if (name in AXAttributes) {
        nameElement.textContent = WebInspector.UIString(AXAttributes[name].name);
        nameElement.title = WebInspector.UIString(AXAttributes[name].description);
        nameElement.classList.add("ax-readable-name");
    } else {
        nameElement.textContent = name;
        nameElement.classList.add("ax-name");
    }
    return nameElement;
}

/**
 * @param {!AccessibilityAgent.AXValue} value
 * @param {!WebInspector.Target} target
 * @return {?Element}
 */
WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement = function(value, target)
{
    var deferredNode = new WebInspector.DeferredDOMNode(target, value.relatedNodeValue.backendNodeId);
    var valueElement = createElement("span");

    /**
     * @param {?WebInspector.DOMNode} node
     */
    function onNodeResolved(node)
    {
        valueElement.appendChild(WebInspector.DOMPresentationUtils.linkifyNodeReference(node));
    }
    deferredNode.resolve(onNodeResolved);

    return valueElement;
}

/**
 * @param {!AccessibilityAgent.AXValue} value
 * @param {!Element} parentElement
 * @return {!Element}
 */
WebInspector.AXNodePropertyTreeElement.createValueElement = function(value, parentElement)
{
    var valueElement = createElementWithClass("span", "monospace");
    var type = value.type;
    var prefix;
    var valueText;
    var suffix;
    if (type === "string") {
        // Render \n as a nice unicode cr symbol.
        prefix = "\"";
        valueText = value.value.replace(/\n/g, "\u21B5");
        suffix = "\"";
        valueElement._originalTextContent = "\"" + value.value + "\"";
    } else {
        valueText = String(value.value);
    }

    if (type in WebInspector.AXNodePropertyTreeElement.TypeStyles)
        valueElement.classList.add(WebInspector.AXNodePropertyTreeElement.TypeStyles[type]);

    valueElement.setTextContentTruncatedIfNeeded(valueText || "");
    if (prefix)
        valueElement.insertBefore(createTextNode(prefix), valueElement.firstChild);
    if (suffix)
        valueElement.createTextChild(suffix);

    valueElement.title = String(value.value) || "";

    return valueElement;
}

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!WebInspector.DeferredDOMNode} deferredNode
 */
WebInspector.AXRelatedNodeTreeElement = function(deferredNode)
{
    this._deferredNode = deferredNode;

    TreeElement.call(this, "");
};

WebInspector.AXRelatedNodeTreeElement.prototype = {
    onattach: function()
    {
        this._update();
    },

    _update: function()
    {
        var valueElement = createElement("div");
        this.listItemElement.appendChild(valueElement);

        /**
         * @param {?WebInspector.DOMNode} node
         */
        function onNodeResolved(node)
        {
            valueElement.appendChild(WebInspector.DOMPresentationUtils.linkifyNodeReference(node));
        }
        this._deferredNode.resolve(onNodeResolved);
    },

    __proto__: TreeElement.prototype
};

/** @type {!Object<string, string>} */
WebInspector.AXNodePropertyTreeElement.TypeStyles = {
    boolean: "object-value-boolean",
    booleanOrUndefined: "object-value-boolean",
    tristate: "object-value-boolean",
    number: "object-value-number",
    integer: "object-value-number",
    string: "object-value-string",
    role: "ax-role",
    internalRole: "ax-internal-role"
};

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!AccessibilityAgent.AXProperty} property
 * @param {?AccessibilityAgent.AXNode} axNode
 * @param {!WebInspector.Target} target
 */
WebInspector.AXNodeIgnoredReasonTreeElement = function(property, axNode, target)
{
    this._property = property;
    this._axNode = axNode;
    this._target = target;

    // Pass an empty title, the title gets made later in onattach.
    TreeElement.call(this, "");
    this.toggleOnClick = true;
    this.selectable = false;
}

WebInspector.AXNodeIgnoredReasonTreeElement.prototype = {
    /**
     * @override
     */
    onattach: function()
    {
        this.listItemElement.removeChildren();

        this._reasonElement = WebInspector.AXNodeIgnoredReasonTreeElement.createReasonElement(this._property.name, this._axNode);
        this.listItemElement.appendChild(this._reasonElement);

        var value = this._property.value;
        if (value.type === "idref") {
            this._valueElement = WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement(value, this._target);
            this.listItemElement.appendChild(this._valueElement);
        }
    },

    __proto__: TreeElement.prototype
};

/**
 * @param {?string} reason
 * @param {?AccessibilityAgent.AXNode} axNode
 * @return {?Element}
 */
WebInspector.AXNodeIgnoredReasonTreeElement.createReasonElement = function(reason, axNode)
{
    var reasonElement = null;
    switch(reason) {
    case "activeModalDialog":
        reasonElement = WebInspector.formatLocalized("Element is hidden by active modal dialog: ", [], "");
        break;
    case "ancestorDisallowsChild":
        reasonElement = WebInspector.formatLocalized("Element is not permitted as child of ", [], "");
        break;
    // http://www.w3.org/TR/wai-aria/roles#childrenArePresentational
    case "ancestorIsLeafNode":
        reasonElement = WebInspector.formatLocalized("Ancestor's children are all presentational: ", [], "");
        break;
    case "ariaHidden":
        var ariaHiddenSpan = createElement("span", "source-code").textContent = "aria-hidden";
        reasonElement = WebInspector.formatLocalized("Element is %s.", [ ariaHiddenSpan ], "");
        break;
    case "ariaHiddenRoot":
        var ariaHiddenSpan = createElement("span", "source-code").textContent = "aria-hidden";
        var trueSpan = createElement("span", "source-code").textContent = "true";
        reasonElement = WebInspector.formatLocalized("%s is %s on ancestor: ", [ ariaHiddenSpan, trueSpan ], "");
        break;
    case "emptyAlt":
        reasonElement = WebInspector.formatLocalized("Element has empty alt text.", [], "");
        break;
    case "emptyText":
        reasonElement = WebInspector.formatLocalized("No text content.", [], "");
        break;
    case "inert":
        reasonElement = WebInspector.formatLocalized("Element is inert.", [], "");
        break;
    case "inheritsPresentation":
        reasonElement = WebInspector.formatLocalized("Element inherits presentational role from ", [], "");
        break;
    case "labelContainer":
        reasonElement = WebInspector.formatLocalized("Part of label element: ", [], "");
        break;
    case "labelFor":
        reasonElement = WebInspector.formatLocalized("Label for ", [], "");
        break;
    case "notRendered":
        reasonElement = WebInspector.formatLocalized("Element is not rendered.", [], "");
        break;
    case "notVisible":
        reasonElement = WebInspector.formatLocalized("Element is not visible.", [], "");
        break;
    case "presentationalRole":
        var rolePresentationSpan = createElement("span", "source-code").textContent = "role=" + axNode.role.value;
        reasonElement = WebInspector.formatLocalized("Element has %s.", [ rolePresentationSpan ], "");
        break;
    case "probablyPresentational":
        reasonElement = WebInspector.formatLocalized("Element is presentational.", [], "");
        break;
    case "staticTextUsedAsNameFor":
        reasonElement = WebInspector.formatLocalized("Static text node is used as name for ", [], "");
        break;
    case "uninteresting":
        reasonElement = WebInspector.formatLocalized("Element not interesting for accessibility.", [], "")
        break;
    }
    if (reasonElement)
        reasonElement.classList.add("ax-reason");
    return reasonElement;
}
