/*
 * Copyright (C) 2008 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.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``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
 * 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 {TreeOutlineInShadow}
 * @param {!WebInspector.RemoteObject} object
 * @param {?string|!Element=} title
 * @param {?string=} emptyPlaceholder
 * @param {boolean=} ignoreHasOwnProperty
 * @param {!Array.<!WebInspector.RemoteObjectProperty>=} extraProperties
 */
WebInspector.ObjectPropertiesSection = function(object, title, emptyPlaceholder, ignoreHasOwnProperty, extraProperties)
{
    this._object = object;
    this._editable = true;
    TreeOutlineInShadow.call(this);
    this.hideOverflow();
    this.setFocusable(false);
    this._objectTreeElement = new WebInspector.ObjectPropertiesSection.RootElement(object, emptyPlaceholder, ignoreHasOwnProperty, extraProperties);
    this.appendChild(this._objectTreeElement);
    if (typeof title === "string" || !title)
        this.element.createChild("span").textContent = title || "";
    else
        this.element.appendChild(title);

    this.element._section = this;
    this.registerRequiredCSS("components/objectValue.css");
    this.registerRequiredCSS("components/objectPropertiesSection.css");
    this.rootElement().childrenListElement.classList.add("source-code", "object-properties-section");
}

/** @const */
WebInspector.ObjectPropertiesSection._arrayLoadThreshold = 100;

/**
 * @param {!WebInspector.RemoteObject} object
 * @param {boolean=} skipProto
 * @return {!Element}
 */
WebInspector.ObjectPropertiesSection.defaultObjectPresentation = function(object, skipProto)
{
    var componentRoot = createElementWithClass("span", "source-code");
    var shadowRoot = WebInspector.createShadowRootWithCoreStyles(componentRoot, "components/objectValue.css");
    shadowRoot.appendChild(WebInspector.ObjectPropertiesSection.createValueElement(object, false));
    if (!object.hasChildren)
        return componentRoot;

    var objectPropertiesSection = new WebInspector.ObjectPropertiesSection(object, componentRoot);
    objectPropertiesSection.editable = false;
    if (skipProto)
        objectPropertiesSection.skipProto();

    return objectPropertiesSection.element;
}

WebInspector.ObjectPropertiesSection.prototype = {
    skipProto: function()
    {
        this._skipProto = true;
    },

    expand: function()
    {
        this._objectTreeElement.expand();
    },

    /**
     * @param {boolean} value
     */
    setEditable: function(value)
    {
        this._editable = value;
    },

    /**
     * @return {!TreeElement}
     */
    objectTreeElement: function()
    {
        return this._objectTreeElement;
    },

    enableContextMenu: function()
    {
        this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), false);
    },

    _contextMenuEventFired: function(event)
    {
        var contextMenu = new WebInspector.ContextMenu(event);
        contextMenu.appendApplicableItems(this._object);
        contextMenu.show();
    },

    titleLessMode: function()
    {
        this._objectTreeElement.listItemElement.classList.add("hidden");
        this._objectTreeElement.childrenListElement.classList.add("title-less-mode");
        this._objectTreeElement.expand();
    },

    __proto__: TreeOutlineInShadow.prototype
}

/**
 * @param {!WebInspector.RemoteObjectProperty} propertyA
 * @param {!WebInspector.RemoteObjectProperty} propertyB
 * @return {number}
 */
WebInspector.ObjectPropertiesSection.CompareProperties = function(propertyA, propertyB)
{
    var a = propertyA.name;
    var b = propertyB.name;
    if (a === "__proto__")
        return 1;
    if (b === "__proto__")
        return -1;
    if (propertyA.symbol && !propertyB.symbol)
        return 1;
    if (propertyB.symbol && !propertyA.symbol)
        return -1;
    return String.naturalOrderComparator(a, b);
}

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!WebInspector.RemoteObject} object
 * @param {?string=} emptyPlaceholder
 * @param {boolean=} ignoreHasOwnProperty
 * @param {!Array.<!WebInspector.RemoteObjectProperty>=} extraProperties
 */
WebInspector.ObjectPropertiesSection.RootElement = function(object, emptyPlaceholder, ignoreHasOwnProperty, extraProperties)
{
    this._object = object;
    this._extraProperties = extraProperties || [];
    this._ignoreHasOwnProperty = !!ignoreHasOwnProperty;
    this._emptyPlaceholder = emptyPlaceholder;
    var contentElement = createElement("content");
    TreeElement.call(this, contentElement);
    this.setExpandable(true);
    this.selectable = false;
    this.toggleOnClick = true;
    this.listItemElement.classList.add("object-properties-section-root-element");
}

WebInspector.ObjectPropertiesSection.RootElement.prototype = {

    onexpand: function()
    {
        if (this.treeOutline)
            this.treeOutline.element.classList.add("expanded");
    },

    oncollapse: function()
    {
        if (this.treeOutline)
            this.treeOutline.element.classList.remove("expanded");
    },

    /**
     * @override
     * @param {!Event} e
     * @return {boolean}
     */
    ondblclick: function(e)
    {
        return true;
    },

    onpopulate: function()
    {
        WebInspector.ObjectPropertyTreeElement._populate(this, this._object, !!this.treeOutline._skipProto, this._emptyPlaceholder, this._ignoreHasOwnProperty, this._extraProperties);
    },

    __proto__: TreeElement.prototype
}

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!WebInspector.RemoteObjectProperty} property
 */
WebInspector.ObjectPropertyTreeElement = function(property)
{
    this.property = property;

    // Pass an empty title, the title gets made later in onattach.
    TreeElement.call(this);
    this.toggleOnClick = true;
    this.selectable = false;
    /** @type {!Array.<!Object>} */
    this._highlightChanges = [];
}

WebInspector.ObjectPropertyTreeElement.prototype = {
    /**
     * @param {!RegExp} regex
     * @param {string=} additionalCssClassName
     * @return {boolean}
     */
    setSearchRegex: function(regex, additionalCssClassName) {
        var cssClasses = WebInspector.highlightedSearchResultClassName;
        if (additionalCssClassName)
            cssClasses += " " + additionalCssClassName;
        this.revertHighlightChanges();

        this._applySearch(regex, this.nameElement, cssClasses);
        var valueType = this.property.value.type;
        if (valueType !== "object" && valueType !== "array")
            this._applySearch(regex, this.valueElement, cssClasses);

        return !!this._highlightChanges.length;
    },

    /**
     * @param {!RegExp} regex
     * @param {!Element} element
     * @param {string} cssClassName
     */
    _applySearch: function(regex, element, cssClassName)
    {
        var ranges = [];
        var content = element.textContent;
        regex.lastIndex = 0;
        var match = regex.exec(content);
        while (match) {
            ranges.push(new WebInspector.SourceRange(match.index, match[0].length));
            match = regex.exec(content);
        }
        if (ranges.length)
            WebInspector.highlightRangesWithStyleClass(element, ranges, cssClassName, this._highlightChanges);
    },

    revertHighlightChanges: function()
    {
        WebInspector.revertDomChanges(this._highlightChanges);
        this._highlightChanges = [];
    },

    onpopulate: function()
    {
        var propertyValue = /** @type {!WebInspector.RemoteObject} */ (this.property.value);
        console.assert(propertyValue);
        var skipProto = this.treeOutline ? this.treeOutline._skipProto : true;
        var targetValue = this.property.name !== "__proto__" ? propertyValue : this.property.parentObject;
        WebInspector.ObjectPropertyTreeElement._populate(this, propertyValue, skipProto, undefined, undefined, undefined, targetValue);
    },

    /**
     * @override
     * @return {boolean}
     */
    ondblclick: function(event)
    {
        var editableElement = this.valueElement;
        if (!this.property.value.customPreview() && (this.property.writable || this.property.setter) && event.target.isSelfOrDescendant(editableElement))
            this._startEditing();
        return false;
    },

    /**
     * @override
     */
    onattach: function()
    {
        this.update();
        this._updateExpandable();
    },

    update: function()
    {
        this.nameElement = WebInspector.ObjectPropertiesSection.createNameElement(this.property.name);
        if (!this.property.enumerable)
            this.nameElement.classList.add("object-properties-section-dimmed");
        if (this.property.isAccessorProperty())
            this.nameElement.classList.add("properties-accessor-property-name");
        if (this.property.synthetic)
            this.nameElement.classList.add("synthetic-property");

        this._updatePropertyPath();
        this.nameElement.addEventListener("contextmenu", this._contextMenuFired.bind(this, this.property), false);

        var separatorElement = createElementWithClass("span", "object-properties-section-separator");
        separatorElement.textContent = ": ";

        if (this.property.value) {
            this.valueElement = WebInspector.ObjectPropertiesSection.createValueElementWithCustomSupport(this.property.value, this.property.wasThrown, this.listItemElement);
            this.valueElement.addEventListener("contextmenu", this._contextMenuFired.bind(this, this.property), false);
        } else if (this.property.getter) {
            this.valueElement = WebInspector.ObjectPropertyTreeElement.createRemoteObjectAccessorPropertySpan(this.property.parentObject, [this.property.name], this._onInvokeGetterClick.bind(this));
        } else {
            this.valueElement = createElementWithClass("span", "object-value-undefined");
            this.valueElement.textContent = WebInspector.UIString("<unreadable>");
            this.valueElement.title = WebInspector.UIString("No property getter");
        }

        this.listItemElement.removeChildren();
        this.listItemElement.appendChildren(this.nameElement, separatorElement, this.valueElement);
    },

    _updatePropertyPath: function()
    {
        if (this.nameElement.title)
            return;

        var useDotNotation = /^(_|\$|[A-Z])(_|\$|[A-Z]|\d)*$/i;
        var isInteger = /^[1-9]\d*$/;
        var name = this.property.name;
        var parentPath = this.parent.nameElement ? this.parent.nameElement.title : "";
        if (useDotNotation.test(name))
            this.nameElement.title = parentPath + "." + name;
        else if (isInteger.test(name))
            this.nameElement.title = parentPath + "[" + name + "]";
        else
            this.nameElement.title = parentPath + "[\"" + name + "\"]";
    },

    /**
     * @param {!WebInspector.RemoteObjectProperty} property
     * @param {!Event} event
     */
    _contextMenuFired: function(property, event)
    {
        var contextMenu = new WebInspector.ContextMenu(event);
        if (property.symbol)
            contextMenu.appendApplicableItems(property.symbol);
        if (property.value)
            contextMenu.appendApplicableItems(property.value);
        var copyPathHandler = InspectorFrontendHost.copyText.bind(InspectorFrontendHost, this.nameElement.title);
        contextMenu.beforeShow(() => { contextMenu.appendItem(WebInspector.UIString.capitalize("Copy ^property ^path"), copyPathHandler); });
        contextMenu.show();
    },

    _startEditing: function()
    {
        if (this._prompt || !this.treeOutline._editable || this._readOnly)
            return;

        this._editableDiv = this.listItemElement.createChild("span");

        var text = this.property.value.description;
        if (this.property.value.type === "string" && typeof text === "string")
            text = "\"" + text + "\"";

        this._editableDiv.setTextContentTruncatedIfNeeded(text, WebInspector.UIString("<string is too large to edit>"));
        var originalContent = this._editableDiv.textContent;

        this.valueElement.classList.add("hidden");

        // Lie about our children to prevent expanding on double click and to collapse subproperties.
        this.setExpandable(false);
        this.listItemElement.classList.add("editing-sub-part");

        this._prompt = new WebInspector.ObjectPropertyPrompt();

        var proxyElement = this._prompt.attachAndStartEditing(this._editableDiv, this._editingCommitted.bind(this, originalContent));
        this.listItemElement.getComponentSelection().setBaseAndExtent(this._editableDiv, 0, this._editableDiv, 1);
        proxyElement.addEventListener("keydown", this._promptKeyDown.bind(this, originalContent), false);
    },

    _editingEnded: function()
    {
        this._prompt.detach();
        delete this._prompt;
        this._editableDiv.remove();
        this._updateExpandable();
        this.listItemElement.scrollLeft = 0;
        this.listItemElement.classList.remove("editing-sub-part");
    },

    _editingCancelled: function()
    {
        this.valueElement.classList.remove("hidden");
        this._editingEnded();
    },

    /**
     * @param {string} originalContent
     */
    _editingCommitted: function(originalContent)
    {
        var userInput = this._prompt.text();
        if (userInput === originalContent) {
            this._editingCancelled(); // nothing changed, so cancel
            return;
        }

        this._editingEnded();
        this._applyExpression(userInput);
    },

    /**
     * @param {string} originalContent
     * @param {!Event} event
     */
    _promptKeyDown: function(originalContent, event)
    {
        if (isEnterKey(event)) {
            event.consume(true);
            this._editingCommitted(originalContent);
            return;
        }
        if (event.key === "Escape") {
            event.consume();
            this._editingCancelled();
            return;
        }
    },

    /**
     * @param {string} expression
     */
    _applyExpression: function(expression)
    {
        var property = WebInspector.RemoteObject.toCallArgument(this.property.symbol || this.property.name);
        expression = expression.trim();
        if (expression)
            this.property.parentObject.setPropertyValue(property, expression, callback.bind(this));
        else
            this.property.parentObject.deleteProperty(property, callback.bind(this));

        /**
         * @param {?Protocol.Error} error
         * @this {WebInspector.ObjectPropertyTreeElement}
         */
        function callback(error)
        {
            if (error) {
                this.update();
                return;
            }

            if (!expression) {
                // The property was deleted, so remove this tree element.
                this.parent.removeChild(this);
            } else {
                // Call updateSiblings since their value might be based on the value that just changed.
                var parent = this.parent;
                parent.invalidateChildren();
                parent.expand();
            }
        };
    },

    /**
     * @param {?WebInspector.RemoteObject} result
     * @param {boolean=} wasThrown
     */
    _onInvokeGetterClick: function(result, wasThrown)
    {
        if (!result)
            return;
        this.property.value = result;
        this.property.wasThrown = wasThrown;

        this.update();
        this.invalidateChildren();
        this._updateExpandable();
    },

    _updateExpandable: function()
    {
        if (this.property.value)
            this.setExpandable(!this.property.value.customPreview() && this.property.value.hasChildren && !this.property.wasThrown);
        else
            this.setExpandable(false);
    },

    __proto__: TreeElement.prototype
}

/**
 * @param {!TreeElement} treeElement
 * @param {!WebInspector.RemoteObject} value
 * @param {boolean} skipProto
 * @param {?string=} emptyPlaceholder
 * @param {boolean=} flattenProtoChain
 * @param {!Array.<!WebInspector.RemoteObjectProperty>=} extraProperties
 * @param {!WebInspector.RemoteObject=} targetValue
 */
WebInspector.ObjectPropertyTreeElement._populate = function(treeElement, value, skipProto, emptyPlaceholder, flattenProtoChain, extraProperties, targetValue)
{
    if (value.arrayLength() > WebInspector.ObjectPropertiesSection._arrayLoadThreshold) {
        treeElement.removeChildren();
        WebInspector.ArrayGroupingTreeElement._populateArray(treeElement, value, 0, value.arrayLength() - 1);
        return;
    }

    /**
     * @param {?Array.<!WebInspector.RemoteObjectProperty>} properties
     * @param {?Array.<!WebInspector.RemoteObjectProperty>} internalProperties
     */
    function callback(properties, internalProperties)
    {
        treeElement.removeChildren();
        if (!properties)
            return;

        extraProperties = extraProperties || [];
        for (var i = 0; i < extraProperties.length; ++i)
            properties.push(extraProperties[i]);

        WebInspector.ObjectPropertyTreeElement.populateWithProperties(treeElement, properties, internalProperties,
            skipProto, targetValue || value, emptyPlaceholder);
    }

    if (flattenProtoChain)
        value.getAllProperties(false, callback);
    else
        WebInspector.RemoteObject.loadFromObjectPerProto(value, callback);
}

/**
 * @param {!TreeElement} treeNode
 * @param {!Array.<!WebInspector.RemoteObjectProperty>} properties
 * @param {?Array.<!WebInspector.RemoteObjectProperty>} internalProperties
 * @param {boolean} skipProto
 * @param {?WebInspector.RemoteObject} value
 * @param {?string=} emptyPlaceholder
 */
WebInspector.ObjectPropertyTreeElement.populateWithProperties = function(treeNode, properties, internalProperties, skipProto, value, emptyPlaceholder) {
    properties.sort(WebInspector.ObjectPropertiesSection.CompareProperties);

    for (var i = 0; i < properties.length; ++i) {
        var property = properties[i];
        if (skipProto && property.name === "__proto__")
            continue;
        if (property.isAccessorProperty()) {
            if (property.name !== "__proto__" && property.getter) {
                property.parentObject = value;
                treeNode.appendChild(new WebInspector.ObjectPropertyTreeElement(property));
            }
            if (property.isOwn) {
                if (property.getter) {
                    var getterProperty = new WebInspector.RemoteObjectProperty("get " + property.name, property.getter);
                    getterProperty.parentObject = value;
                    treeNode.appendChild(new WebInspector.ObjectPropertyTreeElement(getterProperty));
                }
                if (property.setter) {
                    var setterProperty = new WebInspector.RemoteObjectProperty("set " + property.name, property.setter);
                    setterProperty.parentObject = value;
                    treeNode.appendChild(new WebInspector.ObjectPropertyTreeElement(setterProperty));
                }
            }
        } else {
            property.parentObject = value;
            treeNode.appendChild(new WebInspector.ObjectPropertyTreeElement(property));
        }
    }
    if (internalProperties) {
        for (var i = 0; i < internalProperties.length; i++) {
            internalProperties[i].parentObject = value;
            treeNode.appendChild(new WebInspector.ObjectPropertyTreeElement(internalProperties[i]));
        }
    }
    if (value && value.type === "function") {
        // Whether function has TargetFunction internal property.
        // This is a simple way to tell that the function is actually a bound function (we are not told).
        // Bound function never has inner scope and doesn't need corresponding UI node.
        var hasTargetFunction = false;

        if (internalProperties) {
            for (var i = 0; i < internalProperties.length; i++) {
                if (internalProperties[i].name === "[[TargetFunction]]") {
                    hasTargetFunction = true;
                    break;
                }
            }
        }
        if (!hasTargetFunction)
            treeNode.appendChild(new WebInspector.FunctionScopeMainTreeElement(value));
    }
    if (value && value.type === "object" && (value.subtype === "map" || value.subtype === "set" || value.subtype === "iterator"))
        treeNode.appendChild(new WebInspector.CollectionEntriesMainTreeElement(value));

    WebInspector.ObjectPropertyTreeElement._appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder);
}

/**
 * @param {!TreeElement} treeNode
 * @param {?string=} emptyPlaceholder
 */
WebInspector.ObjectPropertyTreeElement._appendEmptyPlaceholderIfNeeded = function(treeNode, emptyPlaceholder)
{
    if (treeNode.childCount())
        return;
    var title = createElementWithClass("div", "info");
    title.textContent = emptyPlaceholder || WebInspector.UIString("No Properties");
    var infoElement = new TreeElement(title);
    treeNode.appendChild(infoElement);
}

/**
 * @param {?WebInspector.RemoteObject} object
 * @param {!Array.<string>} propertyPath
 * @param {function(?WebInspector.RemoteObject, boolean=)} callback
 * @return {!Element}
 */
WebInspector.ObjectPropertyTreeElement.createRemoteObjectAccessorPropertySpan = function(object, propertyPath, callback)
{
    var rootElement = createElement("span");
    var element = rootElement.createChild("span");
    element.textContent = WebInspector.UIString("(...)");
    if (!object)
        return rootElement;
    element.classList.add("object-value-calculate-value-button");
    element.title = WebInspector.UIString("Invoke property getter");
    element.addEventListener("click", onInvokeGetterClick, false);

    function onInvokeGetterClick(event)
    {
        event.consume();
        object.getProperty(propertyPath, callback);
    }

    return rootElement;
}

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!WebInspector.RemoteObject} remoteObject
 */
WebInspector.FunctionScopeMainTreeElement = function(remoteObject)
{
    TreeElement.call(this, "<function scope>", true);
    this.toggleOnClick = true;
    this.selectable = false;
    this._remoteObject = remoteObject;
}

WebInspector.FunctionScopeMainTreeElement.prototype = {
    onpopulate: function()
    {
        /**
         * @param {?WebInspector.DebuggerModel.FunctionDetails} response
         * @this {WebInspector.FunctionScopeMainTreeElement}
         */
        function didGetDetails(response)
        {
            if (!response)
                return;
            this.removeChildren();

            var scopeChain = response.scopeChain || [];
            for (var i = 0; i < scopeChain.length; ++i) {
                var scope = scopeChain[i];
                var title = null;
                var isTrueObject = false;

                switch (scope.type) {
                case DebuggerAgent.ScopeType.Local:
                    // Not really expecting this scope type here.
                    title = WebInspector.UIString("Local");
                    break;
                case DebuggerAgent.ScopeType.Closure:
                    title = WebInspector.UIString("Closure");
                    break;
                case DebuggerAgent.ScopeType.Catch:
                    title = WebInspector.UIString("Catch");
                    break;
                case DebuggerAgent.ScopeType.Block:
                    title = WebInspector.UIString("Block");
                    break;
                case DebuggerAgent.ScopeType.Script:
                    title = WebInspector.UIString("Script");
                    break;
                case DebuggerAgent.ScopeType.With:
                    title = WebInspector.UIString("With Block");
                    isTrueObject = true;
                    break;
                case DebuggerAgent.ScopeType.Global:
                    title = WebInspector.UIString("Global");
                    isTrueObject = true;
                    break;
                default:
                    console.error("Unknown scope type: " + scope.type);
                    continue;
                }

                var runtimeModel = this._remoteObject.target().runtimeModel;
                if (isTrueObject) {
                    var remoteObject = runtimeModel.createRemoteObject(scope.object);
                    var property = new WebInspector.RemoteObjectProperty(title, remoteObject);
                    property.writable = false;
                    property.parentObject = null;
                    this.appendChild(new WebInspector.ObjectPropertyTreeElement(property));
                } else {
                    var scopeRef = new WebInspector.ScopeRef(i, undefined);
                    var remoteObject = runtimeModel.createScopeRemoteObject(scope.object, scopeRef);
                    var scopeTreeElement = new WebInspector.ScopeTreeElement(title, remoteObject);
                    this.appendChild(scopeTreeElement);
                }
            }

            WebInspector.ObjectPropertyTreeElement._appendEmptyPlaceholderIfNeeded(this, WebInspector.UIString("No Scopes"));
        }

        this._remoteObject.functionDetails(didGetDetails.bind(this));
    },

    __proto__: TreeElement.prototype
}

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!WebInspector.RemoteObject} remoteObject
 */
WebInspector.CollectionEntriesMainTreeElement = function(remoteObject)
{
    TreeElement.call(this, "<entries>", true);
    this.toggleOnClick = true;
    this.selectable = false;
    this._remoteObject = remoteObject;
    this.expand();
}

WebInspector.CollectionEntriesMainTreeElement.prototype = {
    onpopulate: function()
    {
        /**
         * @param {?Array.<!DebuggerAgent.CollectionEntry>} entries
         * @this {WebInspector.CollectionEntriesMainTreeElement}
         */
        function didGetCollectionEntries(entries)
        {
            if (!entries)
                return;
            this.removeChildren();

            var entriesLocalObject = [];
            var runtimeModel = this._remoteObject.target().runtimeModel;
            for (var i = 0; i < entries.length; ++i) {
                var entry = entries[i];
                if (entry.key) {
                    entriesLocalObject.push(new WebInspector.MapEntryLocalJSONObject({
                        key: runtimeModel.createRemoteObject(entry.key),
                        value: runtimeModel.createRemoteObject(entry.value)
                    }));
                } else {
                    entriesLocalObject.push(runtimeModel.createRemoteObject(entry.value));
                }
            }
            WebInspector.ObjectPropertyTreeElement._populate(this, WebInspector.RemoteObject.fromLocalObject(entriesLocalObject), true, WebInspector.UIString("No Entries"));
            this.title = "<entries>[" + entriesLocalObject.length + "]";
        }

        this._remoteObject.collectionEntries(didGetCollectionEntries.bind(this));
    },

    __proto__: TreeElement.prototype
}

/**
 * @constructor
 * @extends {TreeElement}
 * @param {string} title
 * @param {!WebInspector.RemoteObject} remoteObject
 */
WebInspector.ScopeTreeElement = function(title, remoteObject)
{
    TreeElement.call(this, title, true);
    this.toggleOnClick = true;
    this.selectable = false;
    this._remoteObject = remoteObject;
}

WebInspector.ScopeTreeElement.prototype = {
    onpopulate: function()
    {
        WebInspector.ObjectPropertyTreeElement._populate(this, this._remoteObject, false);
    },

    __proto__: TreeElement.prototype
}

/**
 * @constructor
 * @extends {TreeElement}
 * @param {!WebInspector.RemoteObject} object
 * @param {number} fromIndex
 * @param {number} toIndex
 * @param {number} propertyCount
 */
WebInspector.ArrayGroupingTreeElement = function(object, fromIndex, toIndex, propertyCount)
{
    TreeElement.call(this, String.sprintf("[%d \u2026 %d]", fromIndex, toIndex), true);
    this.toggleOnClick = true;
    this.selectable = false;
    this._fromIndex = fromIndex;
    this._toIndex = toIndex;
    this._object = object;
    this._readOnly = true;
    this._propertyCount = propertyCount;
}

WebInspector.ArrayGroupingTreeElement._bucketThreshold = 100;
WebInspector.ArrayGroupingTreeElement._sparseIterationThreshold = 250000;
WebInspector.ArrayGroupingTreeElement._getOwnPropertyNamesThreshold = 500000;

/**
 * @param {!TreeElement} treeNode
 * @param {!WebInspector.RemoteObject} object
 * @param {number} fromIndex
 * @param {number} toIndex
 */
WebInspector.ArrayGroupingTreeElement._populateArray = function(treeNode, object, fromIndex, toIndex)
{
    WebInspector.ArrayGroupingTreeElement._populateRanges(treeNode, object, fromIndex, toIndex, true);
}

/**
 * @param {!TreeElement} treeNode
 * @param {!WebInspector.RemoteObject} object
 * @param {number} fromIndex
 * @param {number} toIndex
 * @param {boolean} topLevel
 * @this {WebInspector.ArrayGroupingTreeElement}
 */
WebInspector.ArrayGroupingTreeElement._populateRanges = function(treeNode, object, fromIndex, toIndex, topLevel)
{
    object.callFunctionJSON(packRanges, [
        { value: fromIndex },
        { value: toIndex },
        { value: WebInspector.ArrayGroupingTreeElement._bucketThreshold },
        { value: WebInspector.ArrayGroupingTreeElement._sparseIterationThreshold },
        { value: WebInspector.ArrayGroupingTreeElement._getOwnPropertyNamesThreshold }
    ], callback);

    /**
     * Note: must declare params as optional.
     * @param {number=} fromIndex
     * @param {number=} toIndex
     * @param {number=} bucketThreshold
     * @param {number=} sparseIterationThreshold
     * @param {number=} getOwnPropertyNamesThreshold
     * @suppressReceiverCheck
     * @this {Object}
     */
    function packRanges(fromIndex, toIndex, bucketThreshold, sparseIterationThreshold, getOwnPropertyNamesThreshold)
    {
        var ownPropertyNames = null;
        var consecutiveRange = (toIndex - fromIndex >= sparseIterationThreshold) && ArrayBuffer.isView(this);
        var skipGetOwnPropertyNames = consecutiveRange && (toIndex - fromIndex >= getOwnPropertyNamesThreshold);

        function* arrayIndexes(object)
        {
            if (toIndex - fromIndex < sparseIterationThreshold) {
                for (var i = fromIndex; i <= toIndex; ++i) {
                    if (i in object)
                        yield i;
                }
            } else {
                ownPropertyNames = ownPropertyNames || Object.getOwnPropertyNames(object);
                for (var i = 0; i < ownPropertyNames.length; ++i) {
                    var name = ownPropertyNames[i];
                    var index = name >>> 0;
                    if (("" + index) === name && fromIndex <= index && index <= toIndex)
                        yield index;
                }
            }
        }

        var count = 0;
        if (consecutiveRange) {
            count = toIndex - fromIndex + 1;
        } else {
            for (var i of arrayIndexes(this))
                ++count;
        }

        var bucketSize = count;
        if (count <= bucketThreshold)
            bucketSize = count;
        else
            bucketSize = Math.pow(bucketThreshold, Math.ceil(Math.log(count) / Math.log(bucketThreshold)) - 1);

        var ranges = [];
        if (consecutiveRange) {
            for (var i = fromIndex; i <= toIndex; i += bucketSize) {
                var groupStart = i;
                var groupEnd = groupStart + bucketSize - 1;
                if (groupEnd > toIndex)
                    groupEnd = toIndex;
                ranges.push([groupStart, groupEnd, groupEnd - groupStart + 1]);
            }
        } else {
            count = 0;
            var groupStart = -1;
            var groupEnd = 0;
            for (var i of arrayIndexes(this)) {
                if (groupStart === -1)
                    groupStart = i;
                groupEnd = i;
                if (++count === bucketSize) {
                    ranges.push([groupStart, groupEnd, count]);
                    count = 0;
                    groupStart = -1;
                }
            }
            if (count > 0)
                ranges.push([groupStart, groupEnd, count]);
        }

        return { ranges: ranges, skipGetOwnPropertyNames: skipGetOwnPropertyNames };
    }

    function callback(result)
    {
        if (!result)
            return;
        var ranges = /** @type {!Array.<!Array.<number>>} */ (result.ranges);
        if (ranges.length === 1) {
            WebInspector.ArrayGroupingTreeElement._populateAsFragment(treeNode, object, ranges[0][0], ranges[0][1]);
        } else {
            for (var i = 0; i < ranges.length; ++i) {
                var fromIndex = ranges[i][0];
                var toIndex = ranges[i][1];
                var count = ranges[i][2];
                if (fromIndex === toIndex)
                    WebInspector.ArrayGroupingTreeElement._populateAsFragment(treeNode, object, fromIndex, toIndex);
                else
                    treeNode.appendChild(new WebInspector.ArrayGroupingTreeElement(object, fromIndex, toIndex, count));
            }
        }
        if (topLevel)
            WebInspector.ArrayGroupingTreeElement._populateNonIndexProperties(treeNode, object, result.skipGetOwnPropertyNames);
    }
}

/**
 * @param {!TreeElement} treeNode
 * @param {!WebInspector.RemoteObject} object
 * @param {number} fromIndex
 * @param {number} toIndex
 * @this {WebInspector.ArrayGroupingTreeElement}
 */
WebInspector.ArrayGroupingTreeElement._populateAsFragment = function(treeNode, object, fromIndex, toIndex)
{
    object.callFunction(buildArrayFragment, [{value: fromIndex}, {value: toIndex}, {value: WebInspector.ArrayGroupingTreeElement._sparseIterationThreshold}], processArrayFragment.bind(this));

    /**
     * @suppressReceiverCheck
     * @this {Object}
     * @param {number=} fromIndex // must declare optional
     * @param {number=} toIndex // must declare optional
     * @param {number=} sparseIterationThreshold // must declare optional
     */
    function buildArrayFragment(fromIndex, toIndex, sparseIterationThreshold)
    {
        var result = Object.create(null);
        if (toIndex - fromIndex < sparseIterationThreshold) {
            for (var i = fromIndex; i <= toIndex; ++i) {
                if (i in this)
                    result[i] = this[i];
            }
        } else {
            var ownPropertyNames = Object.getOwnPropertyNames(this);
            for (var i = 0; i < ownPropertyNames.length; ++i) {
                var name = ownPropertyNames[i];
                var index = name >>> 0;
                if (String(index) === name && fromIndex <= index && index <= toIndex)
                    result[index] = this[index];
            }
        }
        return result;
    }

    /**
     * @param {?WebInspector.RemoteObject} arrayFragment
     * @param {boolean=} wasThrown
     * @this {WebInspector.ArrayGroupingTreeElement}
     */
    function processArrayFragment(arrayFragment, wasThrown)
    {
        if (!arrayFragment || wasThrown)
            return;
        arrayFragment.getAllProperties(false, processProperties.bind(this));
    }

    /** @this {WebInspector.ArrayGroupingTreeElement} */
    function processProperties(properties, internalProperties)
    {
        if (!properties)
            return;

        properties.sort(WebInspector.ObjectPropertiesSection.CompareProperties);
        for (var i = 0; i < properties.length; ++i) {
            properties[i].parentObject = this._object;
            var childTreeElement = new WebInspector.ObjectPropertyTreeElement(properties[i]);
            childTreeElement._readOnly = true;
            treeNode.appendChild(childTreeElement);
        }
    }
}

/**
 * @param {!TreeElement} treeNode
 * @param {!WebInspector.RemoteObject} object
 * @param {boolean} skipGetOwnPropertyNames
 * @this {WebInspector.ArrayGroupingTreeElement}
 */
WebInspector.ArrayGroupingTreeElement._populateNonIndexProperties = function(treeNode, object, skipGetOwnPropertyNames)
{
    object.callFunction(buildObjectFragment, [{value: skipGetOwnPropertyNames}], processObjectFragment.bind(this));

    /**
     * @param {boolean=} skipGetOwnPropertyNames
     * @suppressReceiverCheck
     * @this {Object}
     */
    function buildObjectFragment(skipGetOwnPropertyNames)
    {
        var result = { __proto__: this.__proto__ };
        if (skipGetOwnPropertyNames)
            return result;
        var names = Object.getOwnPropertyNames(this);
        for (var i = 0; i < names.length; ++i) {
            var name = names[i];
            // Array index check according to the ES5-15.4.
            if (String(name >>> 0) === name && name >>> 0 !== 0xffffffff)
                continue;
            var descriptor = Object.getOwnPropertyDescriptor(this, name);
            if (descriptor)
                Object.defineProperty(result, name, descriptor);
        }
        return result;
    }

    /**
     * @param {?WebInspector.RemoteObject} arrayFragment
     * @param {boolean=} wasThrown
     * @this {WebInspector.ArrayGroupingTreeElement}
     */
    function processObjectFragment(arrayFragment, wasThrown)
    {
        if (!arrayFragment || wasThrown)
            return;
        arrayFragment.getOwnProperties(processProperties.bind(this));
    }

    /**
     * @param {?Array.<!WebInspector.RemoteObjectProperty>} properties
     * @param {?Array.<!WebInspector.RemoteObjectProperty>=} internalProperties
     * @this {WebInspector.ArrayGroupingTreeElement}
     */
    function processProperties(properties, internalProperties)
    {
        if (!properties)
            return;
        properties.sort(WebInspector.ObjectPropertiesSection.CompareProperties);
        for (var i = 0; i < properties.length; ++i) {
            properties[i].parentObject = this._object;
            var childTreeElement = new WebInspector.ObjectPropertyTreeElement(properties[i]);
            childTreeElement._readOnly = true;
            treeNode.appendChild(childTreeElement);
        }
    }
}

WebInspector.ArrayGroupingTreeElement.prototype = {
    onpopulate: function()
    {
        if (this._propertyCount >= WebInspector.ArrayGroupingTreeElement._bucketThreshold) {
            WebInspector.ArrayGroupingTreeElement._populateRanges(this, this._object, this._fromIndex, this._toIndex, false);
            return;
        }
        WebInspector.ArrayGroupingTreeElement._populateAsFragment(this, this._object, this._fromIndex, this._toIndex);
    },

    onattach: function()
    {
        this.listItemElement.classList.add("object-properties-section-name");
    },

    __proto__: TreeElement.prototype
}

/**
 * @constructor
 * @extends {WebInspector.TextPrompt}
 */
WebInspector.ObjectPropertyPrompt = function()
{
    WebInspector.TextPrompt.call(this, WebInspector.ExecutionContextSelector.completionsForTextPromptInCurrentContext);
    this.setSuggestBoxEnabled(true);
}

WebInspector.ObjectPropertyPrompt.prototype = {
    __proto__: WebInspector.TextPrompt.prototype
}

/**
 * @param {?string} name
 * @return {!Element}
 */
WebInspector.ObjectPropertiesSection.createNameElement = function(name)
{
    var nameElement = createElementWithClass("span", "name");
    if (/^\s|\s$|^$|\n/.test(name))
        nameElement.createTextChildren("\"", name.replace(/\n/g, "\u21B5"), "\"");
    else
        nameElement.textContent = name;
    return nameElement;
}

/**
 * @param {?string=} description
 * @return {string} valueText
 */
WebInspector.ObjectPropertiesSection.valueTextForFunctionDescription = function(description)
{
    var text = description.replace(/^function [gs]et /, "function ");
    var matches = /function\s([^)]*)/.exec(text);
    if (!matches) {
        // process shorthand methods
        matches = /[^(]*(\([^)]*)/.exec(text);
    }
    var match = matches ? matches[1] : null;
    return match ? match.replace(/\n/g, " ") + ")" : (text || "");
}

/**
 * @param {!WebInspector.RemoteObject} value
 * @param {boolean} wasThrown
 * @param {!Element=} parentElement
 * @return {!Element}
 */
WebInspector.ObjectPropertiesSection.createValueElementWithCustomSupport = function(value, wasThrown, parentElement)
{
    if (value.customPreview()) {
        var result = (new WebInspector.CustomPreviewComponent(value)).element;
        result.classList.add("object-properties-section-custom-section");
        return result
    }
    return WebInspector.ObjectPropertiesSection.createValueElement(value, wasThrown, parentElement);
}

/**
 * @param {!WebInspector.RemoteObject} value
 * @param {boolean} wasThrown
 * @param {!Element=} parentElement
 * @return {!Element}
 */
WebInspector.ObjectPropertiesSection.createValueElement = function(value, wasThrown, parentElement)
{
    var valueElement = createElementWithClass("span", "value");
    var type = value.type;
    var subtype = value.subtype;
    var description = value.description;
    var prefix;
    var valueText;
    var suffix;
    if (wasThrown) {
        prefix = "[Exception: ";
        valueText = description;
        suffix = "]";
    } else if (type === "string" && typeof description === "string") {
        // Render \n as a nice unicode cr symbol.
        prefix = "\"";
        valueText = description.replace(/\n/g, "\u21B5");
        suffix = "\"";
    } else if (type === "function") {
        valueText = WebInspector.ObjectPropertiesSection.valueTextForFunctionDescription(description);
    } else if (type !== "object" || subtype !== "node") {
        valueText = description;
    }
    if (type !== "number" || valueText.indexOf("e") === -1) {
        valueElement.setTextContentTruncatedIfNeeded(valueText || "");
        if (prefix)
            valueElement.insertBefore(createTextNode(prefix), valueElement.firstChild);
        if (suffix)
            valueElement.createTextChild(suffix);
    } else {
        var numberParts = valueText.split("e");
        var mantissa = valueElement.createChild("span", "object-value-scientific-notation-mantissa");
        mantissa.textContent = numberParts[0];
        var exponent = valueElement.createChild("span", "object-value-scientific-notation-exponent");
        exponent.textContent = "e" + numberParts[1];
        valueElement.classList.add("object-value-scientific-notation-number");
        if (parentElement)  // FIXME: do it in the caller.
            parentElement.classList.add("hbox");
    }

    if (wasThrown)
        valueElement.classList.add("error");
    if (subtype || type)
        valueElement.classList.add("object-value-" + (subtype || type));

    if (type === "object" && subtype === "node" && description) {
        WebInspector.DOMPresentationUtils.createSpansForNodeTitle(valueElement, description);
        valueElement.addEventListener("click", mouseClick, false);
        valueElement.addEventListener("mousemove", mouseMove, false);
        valueElement.addEventListener("mouseleave", mouseLeave, false);
    } else {
        valueElement.title = description || "";
    }

    function mouseMove()
    {
        WebInspector.DOMModel.highlightObjectAsDOMNode(value);
    }

    function mouseLeave()
    {
        WebInspector.DOMModel.hideDOMNodeHighlight();
    }

    /**
     * @param {!Event} event
     */
    function mouseClick(event)
    {
        WebInspector.Revealer.reveal(value);
        event.consume(true);
    }

    return valueElement;
}

/**
 * @param {!WebInspector.RemoteObject} func
 * @param {!Element} element
 * @param {boolean} linkify
 * @param {boolean=} includePreview
 */
WebInspector.ObjectPropertiesSection.formatObjectAsFunction = function(func, element, linkify, includePreview)
{
    func.functionDetails(didGetDetails);

    /**
     * @param {?WebInspector.DebuggerModel.FunctionDetails} response
     */
    function didGetDetails(response)
    {
        if (!response) {
            var valueText = WebInspector.ObjectPropertiesSection.valueTextForFunctionDescription(func.description);
            element.createTextChild(valueText);
            return;
        }

        if (linkify && response && response.location) {
            var anchor = createElement("span");
            element.classList.add("linkified");
            element.appendChild(anchor);
            element.addEventListener("click", WebInspector.Revealer.reveal.bind(WebInspector.Revealer, response.location, undefined));
            element = anchor;
        }

        var text = func.description.substring(0, 200);
        if (includePreview) {
            element.textContent = text.replace(/^function /, "") + (func.description.length > 200 ? "\u2026" : "");
            return;
        }

        // Now parse description and get the real params and title.
        self.runtime.instancePromise(WebInspector.TokenizerFactory).then(processTokens);

        var params = null;
        var functionName = response ? response.functionName : "";

        /**
         * @param {!WebInspector.TokenizerFactory} tokenizerFactory
         */
        function processTokens(tokenizerFactory)
        {
            var tokenize = tokenizerFactory.createTokenizer("text/javascript");
            tokenize(text, processToken);
            element.textContent = (functionName || "anonymous") + "(" + (params || []).join(", ") + ")";
        }

        var doneProcessing = false;

        /**
         * @param {string} token
         * @param {?string} tokenType
         * @param {number} column
         * @param {number} newColumn
         */
        function processToken(token, tokenType, column, newColumn)
        {
            if (!params && tokenType === "js-def" && !functionName)
                functionName = token;
            doneProcessing = doneProcessing || token === ")";
            if (doneProcessing)
                return;
            if (token === "(") {
                params = [];
                return;
            }
            if (params && tokenType === "js-def")
                params.push(token);
        }
    }
}

/**
 * @constructor
 */
WebInspector.ObjectPropertiesSectionExpandController = function()
{
    /** @type {!Set.<string>} */
    this._expandedProperties = new Set();
}

WebInspector.ObjectPropertiesSectionExpandController._cachedPathSymbol = Symbol("cachedPath");
WebInspector.ObjectPropertiesSectionExpandController._treeOutlineId = Symbol("treeOutlineId");

WebInspector.ObjectPropertiesSectionExpandController.prototype = {
    /**
     * @param {string} id
     * @param {!WebInspector.ObjectPropertiesSection} section
     */
    watchSection: function(id, section)
    {
        section.addEventListener(TreeOutline.Events.ElementAttached, this._elementAttached, this);
        section.addEventListener(TreeOutline.Events.ElementExpanded, this._elementExpanded, this);
        section.addEventListener(TreeOutline.Events.ElementCollapsed, this._elementCollapsed, this);
        section[WebInspector.ObjectPropertiesSectionExpandController._treeOutlineId] = id;

        if (this._expandedProperties.has(id))
            section.expand();
    },

    /**
     * @param {string} id
     */
    stopWatchSectionsWithId: function(id)
    {
        for (var property of this._expandedProperties) {
            if (property.startsWith(id + ":"))
                this._expandedProperties.delete(property);
        }
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _elementAttached: function(event)
    {
        var element = /** @type {!TreeElement} */ (event.data);
        if (element.isExpandable() && this._expandedProperties.has(this._propertyPath(element)))
            element.expand();
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _elementExpanded: function(event)
    {
        var element = /** @type {!TreeElement} */ (event.data);
        this._expandedProperties.add(this._propertyPath(element));
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _elementCollapsed: function(event)
    {
        var element = /** @type {!TreeElement} */ (event.data);
        this._expandedProperties.delete(this._propertyPath(element));
    },

    /**
     * @param {!TreeElement} treeElement
     * @return {string}
     */
    _propertyPath: function(treeElement)
    {
        var cachedPropertyPath = treeElement[WebInspector.ObjectPropertiesSectionExpandController._cachedPathSymbol];
        if (cachedPropertyPath)
            return cachedPropertyPath;

        var current = treeElement;
        var rootElement = treeElement.treeOutline.objectTreeElement();

        var result;

        while (current !== rootElement) {
            var currentName = "";
            if (current.property)
                currentName = current.property.name;
            else
                currentName = typeof current.title === "string" ? current.title : current.title.textContent;

            result = currentName + (result ? "." + result : "");
            current = current.parent;
        }
        var treeOutlineId = treeElement.treeOutline[WebInspector.ObjectPropertiesSectionExpandController._treeOutlineId];
        result = treeOutlineId + (result ? ":" + result : "");
        treeElement[WebInspector.ObjectPropertiesSectionExpandController._cachedPathSymbol] = result;
        return result;
    }
}
