/*
 * Copyright (C) 2013, 2015 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.HierarchicalPathNavigationItem = class HierarchicalPathNavigationItem extends WI.NavigationItem
{
    constructor(identifier, components)
    {
        super(identifier);

        this._collapsedComponent = null;
        this._needsUpdate = false;

        this.components = components;
    }

    // Public

    get components()
    {
        return this._components;
    }

    set components(newComponents)
    {
        if (!newComponents)
            newComponents = [];

        let componentsEqual = function(a, b) {
            let getRepresentedObjects = (component) => component.representedObject;
            let representedObjectsA = a.map(getRepresentedObjects);
            let representedObjectsB = b.map(getRepresentedObjects);
            if (!Array.shallowEqual(representedObjectsA, representedObjectsB))
                return false;

            let getExtraComparisonData = (component) => component.comparisonData;
            let extraComparisonDataA = a.map(getExtraComparisonData);
            let extraComparisonDataB = b.map(getExtraComparisonData);
            return Array.shallowEqual(extraComparisonDataA, extraComparisonDataB);
        };

        if (this._components && componentsEqual(this._components, newComponents))
            return;

        if (this._components) {
            for (let component of this._components)
                component.removeEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._siblingPathComponentWasSelected, this);
        }

        this._components = newComponents.slice(0);

        for (let component of this._components)
            component.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._siblingPathComponentWasSelected, this);

        // Wait until layout to update the DOM.
        this._needsUpdate = true;

        // Update layout for the so other items can adjust to the extra space (or lack thereof) too.
        if (this.parentNavigationBar)
            this.parentNavigationBar.needsLayout();
    }

    get lastComponent()
    {
        return this._components.lastValue || null;
    }

    get alwaysShowLastPathComponentSeparator()
    {
        return this.element.classList.contains(WI.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName);
    }

    set alwaysShowLastPathComponentSeparator(flag)
    {
        if (flag)
            this.element.classList.add(WI.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName);
        else
            this.element.classList.remove(WI.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName);
    }

    update(options = {})
    {
        this._updateComponentsIfNeeded();

        super.update(options);

        var navigationBar = this.parentNavigationBar;
        if (!navigationBar)
            return;

        if (this._collapsedComponent) {
            this.element.removeChild(this._collapsedComponent.element);
            this._collapsedComponent = null;
        }

        let sizesToFit = navigationBar.sizesToFit;

        // Expand our components to full width to test if the items can fit at full width.
        for (var i = 0; i < this._components.length; ++i) {
            this._components[i].hidden = false;
            this._components[i].forcedWidth = null;
            this._components[i].hideTooltip = !sizesToFit;
        }

        if (options.expandOnly)
            return;

        if (sizesToFit)
            return;

        // Iterate over all the other navigation items in the bar and calculate their width.
        var totalOtherItemsWidth = 0;
        for (var i = 0; i < navigationBar.navigationItems.length; ++i) {
            // Skip ourself.
            if (navigationBar.navigationItems[i] === this)
                continue;

            // Skip flexible space items since they can take up no space at the minimum width.
            if (navigationBar.navigationItems[i] instanceof WI.FlexibleSpaceNavigationItem)
                continue;

            totalOtherItemsWidth += navigationBar.navigationItems[i].element.realOffsetWidth;
        }

        // Calculate the width for all the components.
        var thisItemWidth = 0;
        var componentWidths = [];
        for (var i = 0; i < this._components.length; ++i) {
            var componentWidth = this._components[i].element.realOffsetWidth;
            componentWidths.push(componentWidth);
            thisItemWidth += componentWidth;
        }

        // If all our components fit with the other navigation items in the width of the bar,
        // then we don't need to collapse any components.
        var barWidth = navigationBar.element.realOffsetWidth;
        if (totalOtherItemsWidth + thisItemWidth <= barWidth)
            return;

        // Calculate the width we need to remove from our components, then iterate over them
        // and force their width to be smaller.
        var widthToRemove = totalOtherItemsWidth + thisItemWidth - barWidth;
        for (var i = 0; i < this._components.length; ++i) {
            var componentWidth = componentWidths[i];

            this._components[i].hideTooltip = false;

            // Try to take the whole width we need to remove from each component.
            var forcedWidth = componentWidth - widthToRemove;
            this._components[i].forcedWidth = forcedWidth;

            // Since components have a minimum width, we need to see how much was actually
            // removed and subtract that from what remans to be removed.
            componentWidths[i] = Math.max(this._components[i].minimumWidth, forcedWidth);
            widthToRemove -= componentWidth - componentWidths[i];

            // If there is nothing else to remove, then we can stop.
            if (widthToRemove <= 0)
                break;
        }

        // If there is nothing else to remove, then we can stop.
        if (widthToRemove <= 0)
            return;

        // If there are 3 or fewer components, then we can stop. Collapsing the middle of 3 components
        // does not save more than a few pixels over just the icon, so it isn't worth it unless there
        // are 4 or more components.
        if (this._components.length <= 3)
            return;

        // We want to collapse the middle components, so find the nearest middle index.
        var middle = this._components.length >> 1;
        var distance = -1;
        var i = middle;

        // Create a component that will represent the hidden components with a ellipse as the display name.
        this._collapsedComponent = new WI.HierarchicalPathComponent(ellipsis, []);
        this._collapsedComponent.collapsed = true;

        // Insert it in the middle, it doesn't matter exactly where since the elements around it will be hidden soon.
        this.element.insertBefore(this._collapsedComponent.element, this._components[middle].element);

        // Add the width of the collapsed component to the width we need to remove.
        widthToRemove += this._collapsedComponent.minimumWidth;

        var hiddenDisplayNames = [];

        // Loop through the components starting at the middle and fanning out in each direction.
        while (i >= 0 && i <= this._components.length - 1) {
            // Only hide components in the middle and never the ends.
            if (i > 0 && i < this._components.length - 1) {
                var component = this._components[i];
                component.hidden = true;

                // Remember the displayName so it can be put in the tool tip of the collapsed component.
                if (distance > 0)
                    hiddenDisplayNames.unshift(component.displayName);
                else
                    hiddenDisplayNames.push(component.displayName);

                // Fully subtract the hidden component's width.
                widthToRemove -= componentWidths[i];

                // If there is nothing else to remove, then we can stop.
                if (widthToRemove <= 0)
                    break;
            }

            // Calculate the next index.
            i = middle + distance;

            // Increment the distance when it is in the positive direction.
            if (distance > 0)
                ++distance;

            // Flip the direction of the distance.
            distance *= -1;
        }

        // Set the tool tip of the collapsed component.
        this._collapsedComponent.element.title = hiddenDisplayNames.join("\n");
    }

    // Protected

    get additionalClassNames()
    {
        return ["hierarchical-path"];
    }

    // Private

    _updateComponentsIfNeeded()
    {
        if (!this._needsUpdate)
            return;

        this._needsUpdate = false;

        this.element.removeChildren();
        this._collapsedComponent = null;

        for (let component of this._components)
            this.element.appendChild(component.element);
    }

    _siblingPathComponentWasSelected(event)
    {
        this.dispatchEventToListeners(WI.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, event.data);
    }
};

WI.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName = "always-show-last-path-component-separator";

WI.HierarchicalPathNavigationItem.Event = {
    PathComponentWasSelected: "hierarchical-path-navigation-item-path-component-was-selected"
};
