/*
 * Copyright (C) 2007 Apple Inc.  All rights reserved.
 * Copyright (C) 2009 Joseph Pecoraro
 *
 * 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.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE 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 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.
 */

/**
 * @constructor
 * @extends {WebInspector.ThrottledWidget}
 * @param {!WebInspector.StylesSidebarPane} stylesSidebarPane
 * @param {!WebInspector.SharedSidebarModel} sharedModel
 * @param {function(!WebInspector.CSSProperty)} revealCallback
 */
WebInspector.ComputedStyleWidget = function(stylesSidebarPane, sharedModel, revealCallback)
{
    WebInspector.ThrottledWidget.call(this);
    this.element.classList.add("computed-style-sidebar-pane");

    this.registerRequiredCSS("elements/computedStyleSidebarPane.css");
    this._alwaysShowComputedProperties = { "display": true, "height": true, "width": true };

    this._sharedModel = sharedModel;
    this._sharedModel.addEventListener(WebInspector.SharedSidebarModel.Events.ComputedStyleChanged, this.update, this);

    this._showInheritedComputedStylePropertiesSetting = WebInspector.settings.createSetting("showInheritedComputedStyleProperties", false);
    this._showInheritedComputedStylePropertiesSetting.addChangeListener(this._showInheritedComputedStyleChanged.bind(this));

    var hbox = this.element.createChild("div", "hbox styles-sidebar-pane-toolbar");
    var filterContainerElement = hbox.createChild("div", "styles-sidebar-pane-filter-box");
    var filterInput = WebInspector.StylesSidebarPane.createPropertyFilterElement(WebInspector.UIString("Filter"), hbox, filterCallback.bind(this));
    filterContainerElement.appendChild(filterInput);

    var toolbar = new WebInspector.Toolbar("styles-pane-toolbar", hbox);
    toolbar.appendToolbarItem(new WebInspector.ToolbarCheckbox(WebInspector.UIString("Show all"), undefined, this._showInheritedComputedStylePropertiesSetting));

    this._propertiesOutline = new TreeOutlineInShadow();
    this._propertiesOutline.hideOverflow();
    this._propertiesOutline.registerRequiredCSS("elements/computedStyleSidebarPane.css");
    this._propertiesOutline.element.classList.add("monospace", "computed-properties");
    this.element.appendChild(this._propertiesOutline.element);

    this._stylesSidebarPane = stylesSidebarPane;
    this._linkifier = new WebInspector.Linkifier(new WebInspector.Linkifier.DefaultCSSFormatter());
    this._revealCallback = revealCallback;

    /**
     * @param {?RegExp} regex
     * @this {WebInspector.ComputedStyleWidget}
     */
    function filterCallback(regex)
    {
        this._filterRegex = regex;
        this._updateFilter(regex);
    }
}

/**
 * @param {!WebInspector.StylesSidebarPane} stylesSidebarPane
 * @param {!WebInspector.SharedSidebarModel} sharedModel
 * @param {function(!WebInspector.CSSProperty)} revealCallback
 * @return {!WebInspector.ElementsSidebarViewWrapperPane}
 */
WebInspector.ComputedStyleWidget.createSidebarWrapper = function(stylesSidebarPane, sharedModel, revealCallback)
{
    var widget = new WebInspector.ComputedStyleWidget(stylesSidebarPane, sharedModel, revealCallback);
    return new WebInspector.ElementsSidebarViewWrapperPane(WebInspector.UIString("Computed Style"), widget)
}

WebInspector.ComputedStyleWidget._propertySymbol = Symbol("property");

WebInspector.ComputedStyleWidget.prototype = {
    _showInheritedComputedStyleChanged: function()
    {
        this.update();
    },

    /**
     * @override
     * @return {!Promise.<?>}
     */
    doUpdate: function()
    {
        var promises = [
            this._sharedModel.fetchComputedStyle(),
            this._stylesSidebarPane.fetchMatchedCascade()
        ];
        return Promise.all(promises)
            .spread(this._innerRebuildUpdate.bind(this));
    },

    /**
     * @param {string} text
     * @return {!Node}
     */
    _processColor: function(text)
    {
        var color = WebInspector.Color.parse(text);
        if (!color)
            return createTextNode(text);
        var swatch = WebInspector.ColorSwatch.create();
        swatch.setColorText(text);
        swatch.setFormat(WebInspector.Color.detectColorFormat(color));
        return swatch;
    },

    /**
     * @param {?WebInspector.SharedSidebarModel.ComputedStyle} nodeStyle
     * @param {?WebInspector.CSSMatchedStyles} matchedStyles
     */
    _innerRebuildUpdate: function(nodeStyle, matchedStyles)
    {
        this._propertiesOutline.removeChildren();
        this._linkifier.reset();
        var cssModel = this._sharedModel.cssModel();
        if (!nodeStyle || !matchedStyles || !cssModel)
            return;

        var uniqueProperties = nodeStyle.computedStyle.keysArray();
        uniqueProperties.sort(propertySorter);

        var propertyTraces = this._computePropertyTraces(matchedStyles);
        var inhertiedProperties = this._computeInheritedProperties(matchedStyles);
        var showInherited = this._showInheritedComputedStylePropertiesSetting.get();
        for (var i = 0; i < uniqueProperties.length; ++i) {
            var propertyName = uniqueProperties[i];
            var propertyValue = nodeStyle.computedStyle.get(propertyName);
            var canonicalName = WebInspector.CSSMetadata.canonicalPropertyName(propertyName);
            var inherited = !inhertiedProperties.has(canonicalName);
            if (!showInherited && inherited && !(propertyName in this._alwaysShowComputedProperties))
                continue;
            if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName))
                continue;

            var propertyElement = createElement("div");
            propertyElement.classList.add("computed-style-property");
            propertyElement.classList.toggle("computed-style-property-inherited", inherited);
            var renderer = new WebInspector.StylesSidebarPropertyRenderer(null, nodeStyle.node, propertyName, /** @type {string} */(propertyValue));
            renderer.setColorHandler(this._processColor.bind(this));
            var propertyNameElement = renderer.renderName();
            propertyNameElement.classList.add("property-name");
            propertyElement.appendChild(propertyNameElement);

            var colon = createElementWithClass("span", "delimeter");
            colon.textContent = ":";
            propertyNameElement.appendChild(colon);

            var propertyValueElement = propertyElement.createChild("span", "property-value");

            var propertyValueText = renderer.renderValue();
            propertyValueText.classList.add("property-value-text");
            propertyValueElement.appendChild(propertyValueText);

            var semicolon = createElementWithClass("span", "delimeter");
            semicolon.textContent = ";";
            propertyValueElement.appendChild(semicolon);

            var treeElement = new TreeElement();
            treeElement.selectable = false;
            treeElement.title = propertyElement;
            treeElement[WebInspector.ComputedStyleWidget._propertySymbol] = {
                name: propertyName,
                value: propertyValue
            };
            var isOdd = this._propertiesOutline.rootElement().children().length % 2 === 0;
            treeElement.listItemElement.classList.toggle("odd-row", isOdd);
            this._propertiesOutline.appendChild(treeElement);

            var trace = propertyTraces.get(propertyName);
            if (trace) {
                var activeProperty = this._renderPropertyTrace(cssModel, matchedStyles, nodeStyle.node, treeElement, trace);
                treeElement.listItemElement.addEventListener("mousedown", consumeEvent, false);
                treeElement.listItemElement.addEventListener("dblclick", consumeEvent, false);
                treeElement.listItemElement.addEventListener("click", handleClick.bind(null, treeElement), false);
                var gotoSourceElement = propertyValueElement.createChild("div", "goto-source-icon");
                gotoSourceElement.addEventListener("click", this._navigateToSource.bind(this, activeProperty));
            }
        }

        this._updateFilter(this._filterRegex);

        /**
         * @param {string} a
         * @param {string} b
         * @return {number}
         */
        function propertySorter(a, b)
        {
            if (a.startsWith("-webkit") ^ b.startsWith("-webkit"))
                return a.startsWith("-webkit") ? 1 : -1;
            var canonicalName = WebInspector.CSSMetadata.canonicalPropertyName;
            return canonicalName(a).compareTo(canonicalName(b));
        }

        /**
         * @param {!TreeElement} treeElement
         * @param {!Event} event
         */
        function handleClick(treeElement, event)
        {
            if (!treeElement.expanded)
                treeElement.expand();
            else
                treeElement.collapse();
            consumeEvent(event);
        }
    },

    /**
     * @param {!WebInspector.CSSProperty} cssProperty
     * @param {!Event} event
     */
    _navigateToSource: function(cssProperty, event)
    {
        if (this._revealCallback)
            this._revealCallback.call(null, cssProperty);
        event.consume(true);
    },

    /**
     * @param {!WebInspector.CSSModel} cssModel
     * @param {!WebInspector.CSSMatchedStyles} matchedStyles
     * @param {!WebInspector.DOMNode} node
     * @param {!TreeElement} rootTreeElement
     * @param {!Array<!WebInspector.CSSProperty>} tracedProperties
     * @return {!WebInspector.CSSProperty}
     */
    _renderPropertyTrace: function(cssModel, matchedStyles, node, rootTreeElement, tracedProperties)
    {
        var activeProperty = null;
        for (var property of tracedProperties) {
            var trace = createElement("div");
            trace.classList.add("property-trace");
            if (matchedStyles.propertyState(property) === WebInspector.CSSMatchedStyles.PropertyState.Overloaded)
                trace.classList.add("property-trace-inactive");
            else
                activeProperty = property;

            var renderer = new WebInspector.StylesSidebarPropertyRenderer(null, node, property.name, /** @type {string} */(property.value));
            renderer.setColorHandler(this._processColor.bind(this));
            var valueElement = renderer.renderValue();
            valueElement.classList.add("property-trace-value");
            valueElement.addEventListener("click", this._navigateToSource.bind(this, property), false);
            var gotoSourceElement = createElement("div");
            gotoSourceElement.classList.add("goto-source-icon");
            gotoSourceElement.addEventListener("click", this._navigateToSource.bind(this, property));
            valueElement.insertBefore(gotoSourceElement, valueElement.firstChild);

            trace.appendChild(valueElement);

            var rule = property.ownerStyle.parentRule;
            if (rule) {
                var linkSpan = trace.createChild("span", "trace-link");
                linkSpan.appendChild(WebInspector.StylePropertiesSection.createRuleOriginNode(matchedStyles, this._linkifier, rule));
            }

            var selectorElement = trace.createChild("span", "property-trace-selector");
            selectorElement.textContent = rule ? rule.selectorText() : "element.style";
            selectorElement.title = selectorElement.textContent;

            var traceTreeElement = new TreeElement();
            traceTreeElement.title = trace;
            traceTreeElement.selectable = false;
            rootTreeElement.appendChild(traceTreeElement);
        }
        return /** @type {!WebInspector.CSSProperty} */(activeProperty);
    },

    /**
     * @param {!WebInspector.CSSMatchedStyles} matchedStyles
     * @return {!Map<string, !Array<!WebInspector.CSSProperty>>}
     */
    _computePropertyTraces: function(matchedStyles)
    {
        var result = new Map();
        for (var style of matchedStyles.nodeStyles()) {
            var allProperties = style.allProperties;
            for (var property of allProperties) {
                if (!property.activeInStyle() || !matchedStyles.propertyState(property))
                    continue;
                if (!result.has(property.name))
                    result.set(property.name, []);
                result.get(property.name).push(property);
            }
        }
        return result;
    },

    /**
     * @param {!WebInspector.CSSMatchedStyles} matchedStyles
     * @return {!Set<string>}
     */
    _computeInheritedProperties: function(matchedStyles)
    {
        var result = new Set();
        for (var style of matchedStyles.nodeStyles()) {
            for (var property of style.allProperties) {
                if (!matchedStyles.propertyState(property))
                    continue;
                result.add(WebInspector.CSSMetadata.canonicalPropertyName(property.name));
            }
        }
        return result;
    },

    /**
     * @param {?RegExp} regex
     */
    _updateFilter: function(regex)
    {
        var children = this._propertiesOutline.rootElement().children();
        for (var child of children) {
            var property = child[WebInspector.ComputedStyleWidget._propertySymbol];
            var matched = !regex || regex.test(property.name) || regex.test(property.value);
            child.hidden = !matched;
        }
    },

    __proto__: WebInspector.ThrottledWidget.prototype
}
