/*
 * Copyright (C) 2014-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. 
 */

WebInspector. FolderizedTreeElement = class FolderizedTreeElement extends WebInspector. GeneralTreeElement
{
    constructor(classNames, title, subtitle, representedObject, hasChildren)
    {
        super(classNames, title, subtitle, representedObject, hasChildren);

        this.shouldRefreshChildren = true;

        this._folderExpandedSettingMap = new Map;
        this._folderSettingsKey = "";
        this._folderTypeMap = new Map;
        this._folderizeSettingsMap = new Map;
        this._groupedIntoFolders = false;
        this._clearNewChildQueue();
    }

    // Public

    get groupedIntoFolders()
    {
        return this._groupedIntoFolders;
    }

    set folderSettingsKey(x)
    {
        this._folderSettingsKey = x;
    }

    registerFolderizeSettings(type, folderDisplayName, validateRepresentedObjectCallback, countChildrenCallback, treeElementConstructor)
    {
        console.assert(type);
        console.assert(folderDisplayName);
        console.assert(typeof validateRepresentedObjectCallback === "function");
        console.assert(typeof countChildrenCallback === "function");
        console.assert(typeof treeElementConstructor === "function");

        var settings = {
            type,
            folderDisplayName,
            validateRepresentedObjectCallback,
            countChildrenCallback,
            treeElementConstructor
        };

        this._folderizeSettingsMap.set(type, settings);
    }

    // Overrides from TreeElement (Private). 

    removeChildren()
    {
        super.removeChildren();

        this._clearNewChildQueue();

        for (var folder of this._folderTypeMap.values())
            folder.removeChildren();

        this._folderExpandedSettingMap.clear();
        this._folderTypeMap.clear();

        this._groupedIntoFolders = false;
    }

    // Protected

    addChildForRepresentedObject(representedObject)
    {
        var settings = this._settingsForRepresentedObject(representedObject);
        console.assert(settings);
        if (! settings) {
            console.error("No settings for represented object", representedObject);
            return;
        }

        if (! this.treeOutline) {
            // Just mark as needing to update to avoid doing work that might not be needed. 
            this.shouldRefreshChildren = true;
            return;
        }

        var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
        if (! childTreeElement)
            childTreeElement = new settings.treeElementConstructor(representedObject);

        this._addTreeElement(childTreeElement);
    }

    addRepresentedObjectToNewChildQueue(representedObject)
    {
        // This queue reduces flashing as resources load and change folders when their type becomes known. 

        this._newChildQueue.push(representedObject);
        if (! this._newChildQueueTimeoutIdentifier)
            this._newChildQueueTimeoutIdentifier = setTimeout(this._populateFromNewChildQueue.bind(this), WebInspector. FolderizedTreeElement. NewChildQueueUpdateInterval);
    }

    removeChildForRepresentedObject(representedObject)
    {
        this._removeRepresentedObjectFromNewChildQueue(representedObject);
        this.updateParentStatus();

        if (! this.treeOutline) {
            // Just mark as needing to update to avoid doing work that might not be needed. 
            this.shouldRefreshChildren = true;
            return;
        }

        // Find the tree element for the frame by using getCachedTreeElement
        // to only get the item if it has been created already. 
        var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
        if (! childTreeElement || ! childTreeElement.parent)
            return;

        this._removeTreeElement(childTreeElement);
    }

    compareChildTreeElements(a, b)
    {
        return this._compareTreeElementsByMainTitle(a, b);
    }

    updateParentStatus()
    {
        var hasChildren = false;
        for (var settings of this._folderizeSettingsMap.values()) {
            if (settings.countChildrenCallback()) {
                hasChildren = true;
                break;
            }
        }

        this.hasChildren = hasChildren;
        if (! this.hasChildren)
            this.removeChildren();
    }

    prepareToPopulate()
    {
        if (! this._groupedIntoFolders && this._shouldGroupIntoFolders()) {
            this._groupedIntoFolders = true;
            return true;
        }

        return false;
    }

    // Private

    _clearNewChildQueue()
    {
        this._newChildQueue = [];
        if (this._newChildQueueTimeoutIdentifier) {
            clearTimeout(this._newChildQueueTimeoutIdentifier);
            this._newChildQueueTimeoutIdentifier = null;
        }
    }

    _populateFromNewChildQueue()
    {
        if (! this.children.length) {
            this.updateParentStatus();
            this.shouldRefreshChildren = true;
            return;
        }

        if (this.prepareToPopulate()) {
            // Will now folderize, repopulate children. 
            this._clearNewChildQueue();
            this.shouldRefreshChildren = true;
            return;
        }

        for (var i = 0; i < this._newChildQueue.length; ++i)
            this.addChildForRepresentedObject(this._newChildQueue[i]);

        this._clearNewChildQueue();
    }

    _removeRepresentedObjectFromNewChildQueue(representedObject)
    {
        this._newChildQueue.remove(representedObject);
    }

    _addTreeElement(childTreeElement)
    {
        console.assert(childTreeElement);
        if (! childTreeElement)
            return;

        var wasSelected = childTreeElement.selected;

        this._removeTreeElement(childTreeElement, true, true);

        var parentTreeElement = this._parentTreeElementForRepresentedObject(childTreeElement.representedObject);
        if (parentTreeElement ! == this && ! parentTreeElement.parent)
            this._insertFolderTreeElement(parentTreeElement);

        this._insertChildTreeElement(parentTreeElement, childTreeElement);

        if (wasSelected)
            childTreeElement.revealAndSelect(true, false, true, true);
    }

    _compareTreeElementsByMainTitle(a, b)
    {
        return a.mainTitle.localeCompare(b.mainTitle);
    }

    _insertFolderTreeElement(folderTreeElement)
    {
        console.assert(this._groupedIntoFolders);
        console.assert(!folderTreeElement.parent);
        this.insertChild(folderTreeElement, insertionIndexForObjectInListSortedByFunction(folderTreeElement, this.children, this._compareTreeElementsByMainTitle));
    }

    _insertChildTreeElement(parentTreeElement, childTreeElement)
    {
        console.assert(!childTreeElement.parent);
        parentTreeElement.insertChild(childTreeElement, insertionIndexForObjectInListSortedByFunction(childTreeElement, parentTreeElement.children, this.compareChildTreeElements.bind(this)));
    }

    _removeTreeElement(childTreeElement, suppressOnDeselect, suppressSelectSibling)
    {
        var oldParent = childTreeElement.parent;
        if (! oldParent)
            return;

        oldParent.removeChild(childTreeElement, suppressOnDeselect, suppressSelectSibling);

        if (oldParent === this)
            return;

        console.assert(oldParent instanceof WebInspector. FolderTreeElement);
        if (!( oldParent instanceof WebInspector. FolderTreeElement))
            return;

        // Remove the old parent folder if it is now empty. 
        if (! oldParent.children.length)
            oldParent.parent.removeChild(oldParent);
    }

    _parentTreeElementForRepresentedObject(representedObject)
    {
        if (! this._groupedIntoFolders)
            return this;

        console.assert(this._folderSettingsKey ! == "");

        function createFolderTreeElement(type, displayName)
        {
            let folderTreeElement = new WebInspector. FolderTreeElement(displayName);
            let folderExpandedSetting = new WebInspector. Setting(type + "-folder-expanded-" + this._folderSettingsKey, false);
            this._folderExpandedSettingMap.set(folderTreeElement, folderExpandedSetting);

            if (folderExpandedSetting.value)
                folderTreeElement.expand();

            folderTreeElement.onexpand = this._folderTreeElementExpandedStateChange.bind(this);
            folderTreeElement.oncollapse = this._folderTreeElementExpandedStateChange.bind(this);
            return folderTreeElement;
        }

        var settings = this._settingsForRepresentedObject(representedObject);
        if (! settings) {
            console.error("Unknown representedObject", representedObject);
            return this;
        }

        var folder = this._folderTypeMap.get(settings.type);
        if (folder)
            return folder;

        folder = createFolderTreeElement.call(this, settings.type, settings.folderDisplayName);
        this._folderTypeMap.set(settings.type, folder);
        return folder;
    }

    _folderTreeElementExpandedStateChange(folderTreeElement)
    {
        let expandedSetting = this._folderExpandedSettingMap.get(folderTreeElement);
        console.assert(expandedSetting, "No expanded setting for folderTreeElement", folderTreeElement);
        if (! expandedSetting)
            return;

        expandedSetting.value = folderTreeElement.expanded;
    }

    _settingsForRepresentedObject(representedObject)
    {
        for (var settings of this._folderizeSettingsMap.values()) {
            if (settings.validateRepresentedObjectCallback(representedObject))
                return settings;
        }
        return null;
    }

    _shouldGroupIntoFolders()
    {
        // Already grouped into folders, keep it that way. 
        if (this._groupedIntoFolders)
            return true;

        // Child objects are grouped into folders if one of two thresholds are met:
        // 1) Once the number of medium categories passes NumberOfMediumCategoriesThreshold. 
        // 2) When there is a category that passes LargeChildCountThreshold and there are
        //    any child objects in another category. 

        // Folders are avoided when there is only one category or most categories are small. 

        var numberOfSmallCategories = 0;
        var numberOfMediumCategories = 0;
        var foundLargeCategory = false;

        function pushCategory(childCount)
        {
            if (! childCount)
                return false;

            // If this type has any resources and there is a known large category, make folders. 
            if (foundLargeCategory)
                return true;

            // If there are lots of this resource type, then count it as a large category. 
            if (childCount >= WebInspector. FolderizedTreeElement. LargeChildCountThreshold) {
                // If we already have other resources in other small or medium categories, make folders. 
                if (numberOfSmallCategories || numberOfMediumCategories)
                    return true;

                foundLargeCategory = true;
                return false;
            }

            // Check if this is a medium category. 
            if (childCount >= WebInspector. FolderizedTreeElement. MediumChildCountThreshold) {
                // If this is the medium category that puts us over the maximum allowed, make folders. 
                return ++numberOfMediumCategories >= WebInspector. FolderizedTreeElement. NumberOfMediumCategoriesThreshold;
            }

            // This is a small category. 
            ++numberOfSmallCategories;
            return false;
        }

        // Iterate over all the available child object types. 
        for (var settings of this._folderizeSettingsMap.values()) {
            if (pushCategory(settings.countChildrenCallback()))
                return true;
        }
        return false;
    }
};

WebInspector. FolderizedTreeElement. MediumChildCountThreshold = 5;
WebInspector. FolderizedTreeElement. LargeChildCountThreshold = 15;
WebInspector. FolderizedTreeElement. NumberOfMediumCategoriesThreshold = 2;
WebInspector. FolderizedTreeElement. NewChildQueueUpdateInterval = 500;
