// Copyright 2014 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.HBox}
 */
WebInspector.ElementsBreadcrumbs = function()
{
    WebInspector.HBox.call(this, true);
    this.registerRequiredCSS("elements/breadcrumbs.css");

    this.crumbsElement = this.contentElement.createChild("div", "crumbs");
    this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false);
    this.crumbsElement.addEventListener("mouseleave", this._mouseMovedOutOfCrumbs.bind(this), false);
    this._nodeSymbol = Symbol("node");
}

/** @enum {string} */
WebInspector.ElementsBreadcrumbs.Events = {
    NodeSelected: "NodeSelected"
}

WebInspector.ElementsBreadcrumbs.prototype = {
    wasShown: function()
    {
        this.update();
    },

    /**
     * @param {!Array.<!WebInspector.DOMNode>} nodes
     */
    updateNodes: function(nodes)
    {
        if (!nodes.length)
            return;

        var crumbs = this.crumbsElement;
        for (var crumb = crumbs.firstChild; crumb; crumb = crumb.nextSibling) {
            if (nodes.indexOf(crumb[this._nodeSymbol]) !== -1) {
                this.update(true);
                return;
            }
        }
    },

    /**
     * @param {?WebInspector.DOMNode} node
     */
    setSelectedNode: function(node)
    {
        this._currentDOMNode = node;
        this.update();
    },

    _mouseMovedInCrumbs: function(event)
    {
        var nodeUnderMouse = event.target;
        var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb");
        var node = /** @type {?WebInspector.DOMNode} */ (crumbElement ? crumbElement[this._nodeSymbol] : null);
        if (node)
            node.highlight();
    },

    _mouseMovedOutOfCrumbs: function(event)
    {
        if (this._currentDOMNode)
            WebInspector.DOMModel.hideDOMNodeHighlight();
    },

    /**
     * @param {boolean=} force
     */
    update: function(force)
    {
        if (!this.isShowing())
            return;

        var currentDOMNode = this._currentDOMNode;
        var crumbs = this.crumbsElement;

        var handled = false;
        var crumb = crumbs.firstChild;
        while (crumb) {
            if (crumb[this._nodeSymbol] === currentDOMNode) {
                crumb.classList.add("selected");
                handled = true;
            } else {
                crumb.classList.remove("selected");
            }

            crumb = crumb.nextSibling;
        }

        if (handled && !force) {
            // We don't need to rebuild the crumbs, but we need to adjust sizes
            // to reflect the new focused or root node.
            this.updateSizes();
            return;
        }

        crumbs.removeChildren();

        var panel = this;

        /**
         * @param {!Event} event
         * @this {WebInspector.ElementsBreadcrumbs}
         */
        function selectCrumb(event)
        {
            event.preventDefault();
            var crumb = /** @type {!Element} */ (event.currentTarget);
            if (!crumb.classList.contains("collapsed")) {
                this.dispatchEventToListeners(WebInspector.ElementsBreadcrumbs.Events.NodeSelected, crumb[this._nodeSymbol]);
                return;
            }

            // Clicking a collapsed crumb will expose the hidden crumbs.
            if (crumb === panel.crumbsElement.firstChild) {
                // If the focused crumb is the first child, pick the farthest crumb
                // that is still hidden. This allows the user to expose every crumb.
                var currentCrumb = crumb;
                while (currentCrumb) {
                    var hidden = currentCrumb.classList.contains("hidden");
                    var collapsed = currentCrumb.classList.contains("collapsed");
                    if (!hidden && !collapsed)
                        break;
                    crumb = currentCrumb;
                    currentCrumb = currentCrumb.nextSiblingElement;
                }
            }

            this.updateSizes(crumb);
        }

        var boundSelectCrumb = selectCrumb.bind(this);
        for (var current = currentDOMNode; current; current = current.parentNode) {
            if (current.nodeType() === Node.DOCUMENT_NODE)
                continue;

            crumb = createElementWithClass("span", "crumb");
            crumb[this._nodeSymbol] = current;
            crumb.addEventListener("mousedown", boundSelectCrumb, false);

            var crumbTitle = "";
            switch (current.nodeType()) {
            case Node.ELEMENT_NODE:
                if (current.pseudoType())
                    crumbTitle = "::" + current.pseudoType();
                else
                    WebInspector.DOMPresentationUtils.decorateNodeLabel(current, crumb);
                break;

            case Node.TEXT_NODE:
                crumbTitle = WebInspector.UIString("(text)");
                break;

            case Node.COMMENT_NODE:
                crumbTitle = "<!-->";
                break;

            case Node.DOCUMENT_TYPE_NODE:
                crumbTitle = "<!DOCTYPE>";
                break;

            case Node.DOCUMENT_FRAGMENT_NODE:
                crumbTitle = current.shadowRootType() ? "#shadow-root" : current.nodeNameInCorrectCase();
                break;

            default:
                crumbTitle = current.nodeNameInCorrectCase();
            }

            if (!crumb.childNodes.length) {
                var nameElement = createElement("span");
                nameElement.textContent = crumbTitle;
                crumb.appendChild(nameElement);
                crumb.title = crumbTitle;
            }

            if (current === currentDOMNode)
                crumb.classList.add("selected");
            crumbs.insertBefore(crumb, crumbs.firstChild);
        }

        this.updateSizes();
    },

    /**
     * @param {!Element=} focusedCrumb
     */
    updateSizes: function(focusedCrumb)
    {
        if (!this.isShowing())
            return;

        var crumbs = this.crumbsElement;
        if (!crumbs.firstChild)
            return;

        var selectedIndex = 0;
        var focusedIndex = 0;
        var selectedCrumb;

        // Reset crumb styles.
        for (var i = 0; i < crumbs.childNodes.length; ++i) {
            var crumb = crumbs.children[i];
            // Find the selected crumb and index.
            if (!selectedCrumb && crumb.classList.contains("selected")) {
                selectedCrumb = crumb;
                selectedIndex = i;
            }

            // Find the focused crumb index.
            if (crumb === focusedCrumb)
                focusedIndex = i;

            crumb.classList.remove("compact", "collapsed", "hidden");
        }

        // Layout 1: Measure total and normal crumb sizes
        var contentElementWidth = this.contentElement.offsetWidth;
        var normalSizes = [];
        for (var i = 0; i < crumbs.childNodes.length; ++i) {
            var crumb = crumbs.childNodes[i];
            normalSizes[i] = crumb.offsetWidth;
        }

        // Layout 2: Measure collapsed crumb sizes
        var compactSizes = [];
        for (var i = 0; i < crumbs.childNodes.length; ++i) {
            var crumb = crumbs.childNodes[i];
            crumb.classList.add("compact");
        }
        for (var i = 0; i < crumbs.childNodes.length; ++i) {
            var crumb = crumbs.childNodes[i];
            compactSizes[i] = crumb.offsetWidth;
        }

        // Layout 3: Measure collapsed crumb size
        crumbs.firstChild.classList.add("collapsed");
        var collapsedSize = crumbs.firstChild.offsetWidth;

        // Clean up.
        for (var i = 0; i < crumbs.childNodes.length; ++i) {
            var crumb = crumbs.childNodes[i];
            crumb.classList.remove("compact", "collapsed");
        }

        function crumbsAreSmallerThanContainer()
        {
            var totalSize = 0;
            for (var i = 0; i < crumbs.childNodes.length; ++i) {
                var crumb = crumbs.childNodes[i];
                if (crumb.classList.contains("hidden"))
                    continue;
                if (crumb.classList.contains("collapsed")) {
                    totalSize += collapsedSize;
                    continue;
                }
                totalSize += crumb.classList.contains("compact") ? compactSizes[i] : normalSizes[i];
            }
            const rightPadding = 10;
            return totalSize + rightPadding < contentElementWidth;
        }

        if (crumbsAreSmallerThanContainer())
            return; // No need to compact the crumbs, they all fit at full size.

        var BothSides = 0;
        var AncestorSide = -1;
        var ChildSide = 1;

        /**
         * @param {function(!Element)} shrinkingFunction
         * @param {number} direction
         */
        function makeCrumbsSmaller(shrinkingFunction, direction)
        {
            var significantCrumb = focusedCrumb || selectedCrumb;
            var significantIndex = significantCrumb === selectedCrumb ? selectedIndex : focusedIndex;

            function shrinkCrumbAtIndex(index)
            {
                var shrinkCrumb = crumbs.children[index];
                if (shrinkCrumb && shrinkCrumb !== significantCrumb)
                    shrinkingFunction(shrinkCrumb);
                if (crumbsAreSmallerThanContainer())
                    return true; // No need to compact the crumbs more.
                return false;
            }

            // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
            // fit in the container or we run out of crumbs to shrink.
            if (direction) {
                // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
                var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
                while (index !== significantIndex) {
                    if (shrinkCrumbAtIndex(index))
                        return true;
                    index += (direction > 0 ? 1 : -1);
                }
            } else {
                // Crumbs are shrunk in order of descending distance from the signifcant crumb,
                // with a tie going to child crumbs.
                var startIndex = 0;
                var endIndex = crumbs.childNodes.length - 1;
                while (startIndex !== significantIndex || endIndex !== significantIndex) {
                    var startDistance = significantIndex - startIndex;
                    var endDistance = endIndex - significantIndex;
                    if (startDistance >= endDistance)
                        var index = startIndex++;
                    else
                        var index = endIndex--;
                    if (shrinkCrumbAtIndex(index))
                        return true;
                }
            }

            // We are not small enough yet, return false so the caller knows.
            return false;
        }

        function coalesceCollapsedCrumbs()
        {
            var crumb = crumbs.firstChild;
            var collapsedRun = false;
            var newStartNeeded = false;
            var newEndNeeded = false;
            while (crumb) {
                var hidden = crumb.classList.contains("hidden");
                if (!hidden) {
                    var collapsed = crumb.classList.contains("collapsed");
                    if (collapsedRun && collapsed) {
                        crumb.classList.add("hidden");
                        crumb.classList.remove("compact");
                        crumb.classList.remove("collapsed");

                        if (crumb.classList.contains("start")) {
                            crumb.classList.remove("start");
                            newStartNeeded = true;
                        }

                        if (crumb.classList.contains("end")) {
                            crumb.classList.remove("end");
                            newEndNeeded = true;
                        }

                        continue;
                    }

                    collapsedRun = collapsed;

                    if (newEndNeeded) {
                        newEndNeeded = false;
                        crumb.classList.add("end");
                    }
                } else {
                    collapsedRun = true;
                }
                crumb = crumb.nextSibling;
            }

            if (newStartNeeded) {
                crumb = crumbs.lastChild;
                while (crumb) {
                    if (!crumb.classList.contains("hidden")) {
                        crumb.classList.add("start");
                        break;
                    }
                    crumb = crumb.previousSibling;
                }
            }
        }

        /**
         * @param {!Element} crumb
         */
        function compact(crumb)
        {
            if (crumb.classList.contains("hidden"))
                return;
            crumb.classList.add("compact");
        }

        /**
         * @param {!Element} crumb
         * @param {boolean=} dontCoalesce
         */
        function collapse(crumb, dontCoalesce)
        {
            if (crumb.classList.contains("hidden"))
                return;
            crumb.classList.add("collapsed");
            crumb.classList.remove("compact");
            if (!dontCoalesce)
                coalesceCollapsedCrumbs();
        }

        if (!focusedCrumb) {
            // When not focused on a crumb we can be biased and collapse less important
            // crumbs that the user might not care much about.

            // Compact child crumbs.
            if (makeCrumbsSmaller(compact, ChildSide))
                return;

            // Collapse child crumbs.
            if (makeCrumbsSmaller(collapse, ChildSide))
                return;
        }

        // Compact ancestor crumbs, or from both sides if focused.
        if (makeCrumbsSmaller(compact, focusedCrumb ? BothSides : AncestorSide))
            return;

        // Collapse ancestor crumbs, or from both sides if focused.
        if (makeCrumbsSmaller(collapse, focusedCrumb ? BothSides : AncestorSide))
            return;

        if (!selectedCrumb)
            return;

        // Compact the selected crumb.
        compact(selectedCrumb);
        if (crumbsAreSmallerThanContainer())
            return;

        // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
        collapse(selectedCrumb, true);
    },

    __proto__: WebInspector.HBox.prototype
}
