/*
 * 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();
    },

    /**
     * @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;
}

WebInspector.ObjectPropertyTreeElement.prototype = {
    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");
        if (this.property.symbol)
            this.nameElement.addEventListener("contextmenu", this._contextMenuFired.bind(this, this.property.symbol), 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.value), 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);
    },

    _contextMenuFired: function(value, event)
    {
        var contextMenu = new WebInspector.ContextMenu(event);
        contextMenu.appendApplicableItems(value);
        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.keyIdentifier === "U+001B") { // Esc
            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;
    }
}


var isObject = require("@nathanfaucett/is_object"),
    isString = require("@nathanfaucett/is_string"),
    isUndefined = require("@nathanfaucett/is_undefined"),
    isNull = require("@nathanfaucett/is_null"),
    create = require("@nathanfaucett/create"),
    keys = require("@nathanfaucett/keys"),
    punycode = require("punycode"),
    qs = require("@nathanfaucett/qs");

// Reference: RFC 3986, RFC 1808, RFC 2396

// define these here so at least they only have to be
// compiled once on the first module load.
var url = exports,
    toASCII = punycode.toASCII,

    protocolPattern = /^([a-z0-9.+-]+:)/i,
    portPattern = /:[0-9]*$/,

    // Special case for a simple path URL
    simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,

    hostnameMaxLen = 255,
    // protocols that can allow "unsafe" and "unwise" chars.
    unsafeProtocol = {
        'javascript': true,
        'javascript:': true
    },
    // protocols that never have a hostname.
    hostlessProtocol = {
        'javascript': true,
        'javascript:': true
    },
    // protocols that always contain a // bit.
    slashedProtocol = {
        'http': true,
        'http:': true,
        'https': true,
        'https:': true,
        'ftp': true,
        'ftp:': true,
        'gopher': true,
        'gopher:': true,
        'file': true,
        'file:': true
    };

url.parse = urlParse;
url.resolve = urlResolve;
url.resolveObject = urlResolveObject;
url.format = urlFormat;

url.Url = Url;

function Url() {
    this.protocol = null;
    this.slashes = null;
    this.auth = null;
    this.host = null;
    this.port = null;
    this.hostname = null;
    this.hash = null;
    this.search = null;
    this.query = null;
    this.pathname = null;
    this.path = null;
    this.href = null;
}

// This constructor is used to store parsed query string values. Instantiating
// this is faster than explicitly calling `Object.create(null)` to get a
// "clean" empty object (tested with v8 v4.9).
function ParsedQueryString() {}
ParsedQueryString.prototype = create(null);

function urlParse(url, parseQueryString, slashesDenoteHost) {
    var u;

    if (url instanceof Url) {
        return url;
    } else {
        u = new Url();
        u.parse(url, parseQueryString, slashesDenoteHost);
        return u;
    }
}

Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
    if (!isString(url)) {
        throw new TypeError('Parameter "url" must be a string, not ' + typeof url);
    }
    // Copy chrome, IE, opera backslash-handling behavior.
    // Back slashes before the query string get converted to forward slashes
    // See: https://code.google.com/p/chromium/issues/detail?id=25916
    var hasHash = false,
        start = -1,
        end = -1,
        rest = '',
        lastPos = 0,
        i = 0,
        inWs, code, simplePath, proto, lowerProto, slashes, hostEnd, atSign,
        nonHost, hostname, ipv6Hostname, result, p, h, questionIdx, hashIdx,
        firstIdx, s;

    for (inWs = false, split = false; i < url.length; ++i) {
        code = url.charCodeAt(i);

        // Find first and last non-whitespace characters for trimming
        isWs = code === 32 /* */ ||
            code === 9 /*\t*/ ||
            code === 13 /*\r*/ ||
            code === 10 /*\n*/ ||
            code === 12 /*\f*/ ||
            code === 160 /*\u00A0*/ ||
            code === 65279 /*\uFEFF*/ ;

        if (start === -1) {
            if (isWs) {
                continue;
            }
            lastPos = start = i;
        } else {
            if (inWs) {
                if (!isWs) {
                    end = -1;
                    inWs = false;
                }
            } else if (isWs) {
                end = i;
                inWs = true;
            }
        }

        // Only convert backslashes while we haven't seen a split character
        if (!split) {
            switch (code) {
                case 35: // '#'
                    hasHash = true;
                    /* falls through */
                case 63: // '?'
                    split = true;
                    break;
                case 92: // '\\'
                    if (i - lastPos > 0) {
                        rest += url.slice(lastPos, i);
                    }
                    rest += '/';
                    lastPos = i + 1;
                    break;
            }
        } else if (!hasHash && code === 35 /*#*/ ) {
            hasHash = true;
        }
    }

    // Check if string was non-empty (including strings with only whitespace)
    if (start !== -1) {
        if (lastPos === start) {
            // We didn't convert any backslashes

            if (end === -1) {
                if (start === 0) {
                    rest = url;
                } else {
                    rest = url.slice(start);
                }
            } else {
                rest = url.slice(start, end);
            }
        } else if (end === -1 && lastPos < url.length) {
            // We converted some backslashes and have only part of the entire string
            rest += url.slice(lastPos);
        } else if (end !== -1 && lastPos < end) {
            // We converted some backslashes and have only part of the entire string
            rest += url.slice(lastPos, end);
        }
    }

    if (!slashesDenoteHost && !hasHash) {
        // Try fast path regexp
        simplePath = simplePathPattern.exec(rest);

        if (simplePath) {
            this.path = rest;
            this.href = rest;
            this.pathname = simplePath[1];
            if (simplePath[2]) {
                this.search = simplePath[2];
                if (parseQueryString) {
                    this.query = qs.parse(this.search.slice(1));
                } else {
                    this.query = this.search.slice(1);
                }
            } else if (parseQueryString) {
                this.search = '';
                this.query = new ParsedQueryString();
            }
            return this;
        }
    }

    proto = protocolPattern.exec(rest);
    if (proto) {
        proto = proto[0];
        lowerProto = proto.toLowerCase();
        this.protocol = lowerProto;
        rest = rest.slice(proto.length);
    }

    // figure out if it's got a host
    // user@server is *always* interpreted as a hostname, and url
    // resolution will treat //foo/bar as host=foo,path=bar because that's
    // how the browser resolves relative URLs.
    if (slashesDenoteHost || proto || /^\/\/[^@\/]+@[^@\/]+/.test(rest)) {
        slashes = rest.charCodeAt(0) === 47 && rest.charCodeAt(1) === 47;
        if (slashes && !(proto && hostlessProtocol[proto])) {
            rest = rest.slice(2);
            this.slashes = true;
        }
    }

    if (!hostlessProtocol[proto] &&
        (slashes || (proto && !slashedProtocol[proto]))) {

        // there's a hostname.
        // the first instance of /, ?, ;, or # ends the host.
        //
        // If there is an @ in the hostname, then non-host chars *are* allowed
        // to the left of the last @ sign, unless some host-ending character
        // comes *before* the @-sign.
        // URLs are obnoxious.
        //
        // ex:
        // http://a@b@c/ => user:a@b host:c
        // http://a@b?@c => user:a host:b path:/?@c

        // v0.12 TODO(isaacs): This is not quite how Chrome does things.
        // Review our test case against browsers more comprehensively.

        hostEnd = -1;
        atSign = -1;
        nonHost = -1;
        for (i = 0; i < rest.length; i++) {
            switch (rest.charCodeAt(i)) {
                case 9: // '\t'
                case 10: // '\n'
                case 13: // '\r'
                case 32: // ' '
                case 34: // '"'
                case 37: // '%'
                case 39: // '\''
                case 59: // ';'
                case 60: // '<'
                case 62: // '>'
                case 92: // '\\'
                case 94: // '^'
                case 96: // '`'
                case 123: // '{'
                case 124: // '|'
                case 125: // '}'
                    // Characters that are never ever allowed in a hostname from RFC 2396
                    if (nonHost === -1) {
                        nonHost = i;
                    }
                    break;
                case 35: // '#'
                case 47: // '/'
                case 63: // '?'
                    // Find the first instance of any host-ending characters
                    if (nonHost === -1) {
                        nonHost = i;
                    }
                    hostEnd = i;
                    break;
                case 64: // '@'
                    // At this point, either we have an explicit point where the
                    // auth portion cannot go past, or the last @ char is the decider.
                    atSign = i;
                    nonHost = -1;
                    break;
            }
            if (hostEnd !== -1) {
                break;
            }
        }
        start = 0;
        if (atSign !== -1) {
            this.auth = decodeURIComponent(rest.slice(0, atSign));
            start = atSign + 1;
        }
        if (nonHost === -1) {
            this.host = rest.slice(start);
            rest = '';
        } else {
            this.host = rest.slice(start, nonHost);
            rest = rest.slice(nonHost);
        }

        // pull out port.
        this.parseHost();

        // we've indicated that there is a hostname,
        // so even if it's empty, it has to be present.
        if (!isString(this.hostname)) {
            this.hostname = '';
        }

        hostname = this.hostname;

        // if hostname begins with [ and ends with ]
        // assume that it's an IPv6 address.
        ipv6Hostname = hostname.charCodeAt(0) === 91 && hostname.charCodeAt(hostname.length - 1) === 93;

        // validate a little.
        if (!ipv6Hostname) {
            result = validateHostname(this, rest, hostname);
            if (!isUndefined(result)) {
                rest = result;
            }
        }

        if (this.hostname.length > hostnameMaxLen) {
            this.hostname = '';
        } else {
            // hostnames are always lower case.
            this.hostname = this.hostname.toLowerCase();
        }

        if (!ipv6Hostname) {
            // IDNA Support: Returns a punycoded representation of "domain".
            // It only converts parts of the domain name that
            // have non-ASCII characters, i.e. it doesn't matter if
            // you call it with a domain that already is ASCII-only.
            this.hostname = toASCII(this.hostname);
        }

        p = this.port ? ':' + this.port : '';
        h = this.hostname || '';
        this.host = h + p;

        // strip [ and ] from the hostname
        // the host field still retains them, though
        if (ipv6Hostname) {
            this.hostname = this.hostname.slice(1, -1);
            if (rest[0] !== '/') {
                rest = '/' + rest;
            }
        }
    }

    // now rest is set to the post-host stuff.
    // chop off any delim chars.
    if (!unsafeProtocol[lowerProto]) {
        // First, make 100% sure that any "autoEscape" chars get
        // escaped, even if encodeURIComponent doesn't think they
        // need to be.
        result = autoEscapeStr(rest);
        if (!isUndefined(result)) {
            rest = result;
        }
    }

    questionIdx = -1;
    hashIdx = -1;
    for (i = 0; i < rest.length; ++i) {
        code = rest.charCodeAt(i);
        if (code === 35 /*#*/ ) {
            this.hash = rest.slice(i);
            hashIdx = i;
            break;
        } else if (code === 63 /*?*/ && questionIdx === -1) {
            questionIdx = i;
        }
    }

    if (questionIdx !== -1) {
        if (hashIdx === -1) {
            this.search = rest.slice(questionIdx);
            this.query = rest.slice(questionIdx + 1);
        } else {
            this.search = rest.slice(questionIdx, hashIdx);
            this.query = rest.slice(questionIdx + 1, hashIdx);
        }
        if (parseQueryString) {
            this.query = qs.parse(this.query);
        }
    } else if (parseQueryString) {
        // no query string, but parseQueryString still requested
        this.search = '';
        this.query = new ParsedQueryString();
    }

    firstIdx = (
        questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx) ?
        questionIdx :
        hashIdx
    );

    if (firstIdx === -1) {
        if (rest.length > 0) {
            this.pathname = rest;
        }
    } else if (firstIdx > 0) {
        this.pathname = rest.slice(0, firstIdx);
    }
    if (slashedProtocol[lowerProto] &&
        this.hostname && !this.pathname) {
        this.pathname = '/';
    }

    // to support http.request
    if (this.pathname || this.search) {
        p = this.pathname || '';
        s = this.search || '';
        this.path = p + s;
    }

    // finally, reconstruct the href based on what has been validated.
    this.href = this.format();

    return this;
};

function validateHostname(self, rest, hostname) {
    var i, lastPos, code;

    for (i = 0, lastPos; i <= hostname.length; ++i) {
        if (i < hostname.length) {
            code = hostname.charCodeAt(i);
        }
        if (code === 46 /*.*/ || i === hostname.length) {
            if (i - lastPos > 0) {
                if (i - lastPos > 63) {
                    self.hostname = hostname.slice(0, lastPos + 63);
                    return '/' + hostname.slice(lastPos + 63) + rest;
                }
            }
            lastPos = i + 1;
            continue;
        } else if ((code >= 48 /*0*/ && code <= 57 /*9*/ ) ||
            (code >= 97 /*a*/ && code <= 122 /*z*/ ) ||
            code === 45 /*-*/ ||
            (code >= 65 /*A*/ && code <= 90 /*Z*/ ) ||
            code === 43 /*+*/ ||
            code === 95 /*_*/ ||
            code > 127) {
            continue;
        }
        // Invalid host character
        self.hostname = hostname.slice(0, i);
        if (i < hostname.length) {
            return '/' + hostname.slice(i) + rest;
        }
        break;
    }
}

function autoEscapeStr(rest) {
    var newRest = '',
        lastPos = 0,
        i;
    for (i = 0; i < rest.length; i++) {
        // Automatically escape all delimiters and unwise characters from RFC 2396
        // Also escape single quotes in case of an XSS attack
        switch (rest.charCodeAt(i)) {
            case 9: // '\t'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%09';
                lastPos = i + 1;
                break;
            case 10: // '\n'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%0A';
                lastPos = i + 1;
                break;
            case 13: // '\r'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%0D';
                lastPos = i + 1;
                break;
            case 32: // ' '
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%20';
                lastPos = i + 1;
                break;
            case 34: // '"'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%22';
                lastPos = i + 1;
                break;
            case 39: // '\''
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%27';
                lastPos = i + 1;
                break;
            case 60: // '<'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%3C';
                lastPos = i + 1;
                break;
            case 62: // '>'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%3E';
                lastPos = i + 1;
                break;
            case 92: // '\\'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%5C';
                lastPos = i + 1;
                break;
            case 94: // '^'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%5E';
                lastPos = i + 1;
                break;
            case 96: // '`'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%60';
                lastPos = i + 1;
                break;
            case 123: // '{'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%7B';
                lastPos = i + 1;
                break;
            case 124: // '|'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%7C';
                lastPos = i + 1;
                break;
            case 125: // '}'
                if (i - lastPos > 0) {
                    newRest += rest.slice(lastPos, i);
                }
                newRest += '%7D';
                lastPos = i + 1;
                break;
        }
    }
    if (lastPos === 0) {
        return;
    }
    if (lastPos < rest.length) {
        return newRest + rest.slice(lastPos);
    } else {
        return newRest;
    }
}

// format a parsed object into a url string
function urlFormat(obj) {
    // ensure it's an object, and not a string url.
    // If it's an obj, this is a no-op.
    // this way, you can call url_format() on strings
    // to clean up potentially wonky urls.
    if (isString(obj)) {
        obj = urlParse(obj);
    } else if (!isObject(obj) || isNull(obj)) {
        throw new TypeError('Parameter "urlObj" must be an object, not ' +
            isNull(obj) ? 'null' : typeof obj);
    } else if (!(obj instanceof Url)) {
        return Url.prototype.format.call(obj);
    } else {
        return obj.format();
    }
}

Url.prototype.format = function() {
    var auth = this.auth || '',
        protocol, pathname, hash, host, query, search, newPathname, lastPos, i;

    if (auth) {
        auth = encodeAuth(auth);
        auth += '@';
    }

    protocol = this.protocol || '';
    pathname = this.pathname || '';
    hash = this.hash || '';
    host = '';
    query = '';

    if (this.host) {
        host = auth + this.host;
    } else if (this.hostname) {
        host = (
            auth + (this.hostname.indexOf(':') === -1 ?
                this.hostname :
                '[' + this.hostname + ']')
        );
        if (this.port) {
            host += ':' + this.port;
        }
    }

    if (isNull(this.query) && isObject(this.query)) {
        query = qs.stringify(this.query);
    }

    search = this.search || (query && ('?' + query)) || '';

    if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58 /*:*/ ) {
        protocol += ':';
    }

    newPathname = '';
    lastPos = 0;
    for (i = 0; i < pathname.length; ++i) {
        switch (pathname.charCodeAt(i)) {
            case 35: // '#'
                if (i - lastPos > 0) {
                    newPathname += pathname.slice(lastPos, i);
                }
                newPathname += '%23';
                lastPos = i + 1;
                break;
            case 63: // '?'
                if (i - lastPos > 0) {
                    newPathname += pathname.slice(lastPos, i);
                }
                newPathname += '%3F';
                lastPos = i + 1;
                break;
        }
    }
    if (lastPos > 0) {
        if (lastPos !== pathname.length) {
            pathname = newPathname + pathname.slice(lastPos);
        } else {
            pathname = newPathname;
        }
    }

    // only the slashedProtocols get the //.  Not mailto:, xmpp:, etc.
    // unless they had them to begin with.
    if (this.slashes || slashedProtocol[protocol]) {
        if (this.slashes || host) {
            if (pathname && pathname.charCodeAt(0) !== 47 /*/*/ ) {
                pathname = '/' + pathname;
            }
            host = '//' + host;
        } else if (protocol.length >= 4 &&
            protocol.charCodeAt(0) === 102 /*f*/ &&
            protocol.charCodeAt(1) === 105 /*i*/ &&
            protocol.charCodeAt(2) === 108 /*l*/ &&
            protocol.charCodeAt(3) === 101 /*e*/ ) {
            host = '//';
        }
    }

    search = search.replace(/#/g, '%23');

    if (hash && hash.charCodeAt(0) !== 35 /*#*/ ) {
        hash = '#' + hash;
    }
    if (search && search.charCodeAt(0) !== 63 /*?*/ ) {
        search = '?' + search;
    }

    return protocol + host + pathname + search + hash;
};

function urlResolve(source, relative) {
    return urlParse(source, false, true).resolve(relative);
}

Url.prototype.resolve = function(relative) {
    return this.resolveObject(urlParse(relative, false, true)).format();
};

function urlResolveObject(source, relative) {
    if (!source) {
        return relative;
    }
    return urlParse(source, false, true).resolveObject(relative);
}

Url.prototype.resolveObject = function(relative) {
    var rel, result, tkeys, tk, tkey, rkeys, rk, ks, v, k, relPath, p, s,
        isSourceAbs, isRelAbs, mustEndAbs, removeAllDots, srcPath, psychotic,
        authInHost, last, hasTrailingSlash, up, i, isAbsolute;

    if (isString(relative)) {
        rel = new Url();
        rel.parse(relative, false, true);
        relative = rel;
    }

    result = new Url();
    tkeys = keys(this);
    for (tk = 0; tk < tkeys.length; tk++) {
        tkey = tkeys[tk];
        result[tkey] = this[tkey];
    }

    // hash is always overridden, no matter what.
    // even href="" will remove it.
    result.hash = relative.hash;

    // if the relative url is empty, then there's nothing left to do here.
    if (relative.href === '') {
        result.href = result.format();
        return result;
    }

    // hrefs like //foo/bar always cut to the protocol.
    if (relative.slashes && !relative.protocol) {
        // take everything except the protocol from relative
        rkeys = keys(relative);

        for (rk = 0; rk < rkeys.length; rk++) {
            rkey = rkeys[rk];
            if (rkey !== 'protocol') {
                result[rkey] = relative[rkey];
            }
        }

        //urlParse appends trailing / to urls like http://www.example.com
        if (slashedProtocol[result.protocol] &&
            result.hostname && !result.pathname) {
            result.path = result.pathname = '/';
        }

        result.href = result.format();

        return result;
    }

    if (relative.protocol && relative.protocol !== result.protocol) {
        // if it's a known url protocol, then changing
        // the protocol does weird things
        // first, if it's not file:, then we MUST have a host,
        // and if there was a path
        // to begin with, then we MUST have a path.
        // if it is file:, then the host is dropped,
        // because that's known to be hostless.
        // anything else is assumed to be absolute.
        if (!slashedProtocol[relative.protocol]) {
            ks = keys(relative);
            for (v = 0; v < ks.length; v++) {
                k = ks[v];
                result[k] = relative[k];
            }
            result.href = result.format();
            return result;
        }

        result.protocol = relative.protocol;
        if (!relative.host &&
            !/^file:?$/.test(relative.protocol) &&
            !hostlessProtocol[relative.protocol]) {
            relPath = (relative.pathname || '').split('/');
            while (relPath.length && !(relative.host = relPath.shift())) {}
            if (!relative.host) {
                relative.host = '';
            }
            if (!relative.hostname) {
                relative.hostname = '';
            }
            if (relPath[0] !== '') {
                relPath.unshift('');
            }
            if (relPath.length < 2) {
                relPath.unshift('');
            }
            result.pathname = relPath.join('/');
        } else {
            result.pathname = relative.pathname;
        }
        result.search = relative.search;
        result.query = relative.query;
        result.host = relative.host || '';
        result.auth = relative.auth;
        result.hostname = relative.hostname || relative.host;
        result.port = relative.port;
        // to support http.request
        if (result.pathname || result.search) {
            p = result.pathname || '';
            s = result.search || '';
            result.path = p + s;
        }
        result.slashes = result.slashes || relative.slashes;
        result.href = result.format();
        return result;
    }

    isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/');
    isRelAbs = (
        relative.host ||
        relative.pathname && relative.pathname.charAt(0) === '/'
    );
    mustEndAbs = (isRelAbs || isSourceAbs || (result.host && relative.pathname));
    removeAllDots = mustEndAbs;
    srcPath = result.pathname && result.pathname.split('/') || [];
    relPath = relative.pathname && relative.pathname.split('/') || [];
    psychotic = result.protocol && !slashedProtocol[result.protocol];

    // if the url is a non-slashed url, then relative
    // links like ../.. should be able
    // to crawl up to the hostname, as well.  This is strange.
    // result.protocol has already been set by now.
    // Later on, put the first path part into the host field.
    if (psychotic) {
        result.hostname = '';
        result.port = null;
        if (result.host) {
            if (srcPath[0] === '') {
                srcPath[0] = result.host;
            } else {
                srcPath.unshift(result.host);
            }
        }
        result.host = '';
        if (relative.protocol) {
            relative.hostname = null;
            relative.port = null;
            result.auth = null;
            if (relative.host) {
                if (relPath[0] === '') {
                    relPath[0] = relative.host;
                } else {
                    relPath.unshift(relative.host);
                }
            }
            relative.host = null;
        }
        mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
    }

    if (isRelAbs) {
        // it's absolute.
        if (relative.host || relative.host === '') {
            if (result.host !== relative.host) {
                result.auth = null;
            }
            result.host = relative.host;
            result.port = relative.port;
        }
        if (relative.hostname || relative.hostname === '') {
            if (result.hostname !== relative.hostname) {
                result.auth = null;
            }
            result.hostname = relative.hostname;
        }
        result.search = relative.search;
        result.query = relative.query;
        srcPath = relPath;
        // fall through to the dot-handling below.
    } else if (relPath.length) {
        // it's relative
        // throw away the existing file, and take the new path instead.
        if (!srcPath) {
            srcPath = [];
        }
        srcPath.pop();
        srcPath = srcPath.concat(relPath);
        result.search = relative.search;
        result.query = relative.query;
    } else if (isNull(relative.search) && relative.search !== undefined) {
        // just pull out the search.
        // like href='?foo'.
        // Put this after the other two cases because it simplifies the booleans
        if (psychotic) {
            result.hostname = result.host = srcPath.shift();
            //occasionally the auth can get stuck only in host
            //this especially happens in cases like
            //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
            authInHost = result.host && result.host.indexOf('@') > 0 ?
                result.host.split('@') : false;
            if (authInHost) {
                result.auth = authInHost.shift();
                result.host = result.hostname = authInHost.shift();
            }
        }
        result.search = relative.search;
        result.query = relative.query;
        //to support http.request
        if (isNull(result.pathname) || isNull(result.search)) {
            result.path = (result.pathname ? result.pathname : '') +
                (result.search ? result.search : '');
        }
        result.href = result.format();
        return result;
    }

    if (!srcPath.length) {
        // no path at all.  easy.
        // we've already handled the other stuff above.
        result.pathname = null;
        //to support http.request
        if (result.search) {
            result.path = '/' + result.search;
        } else {
            result.path = null;
        }
        result.href = result.format();

        return result;
    }

    // if a url ENDs in . or .., then it must get a trailing slash.
    // however, if it ends in anything else non-slashy,
    // then it must NOT get a trailing slash.
    last = srcPath.slice(-1)[0];
    hasTrailingSlash = (
        (result.host || relative.host || srcPath.length > 1) &&
        (last === '.' || last === '..') || last === '');

    // strip single dots, resolve double dots to parent dir
    // if the path tries to go above the root, `up` ends up > 0
    up = 0;
    for (i = srcPath.length - 1; i >= 0; i--) {
        last = srcPath[i];
        if (last === '.') {
            spliceOne(srcPath, i);
        } else if (last === '..') {
            spliceOne(srcPath, i);
            up++;
        } else if (up) {
            spliceOne(srcPath, i);
            up--;
        }
    }

    // if the path is allowed to go above the root, restore leading ..s
    if (!mustEndAbs && !removeAllDots) {
        for (; up--; up) {
            srcPath.unshift('..');
        }
    }

    if (mustEndAbs && srcPath[0] !== '' && (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
        srcPath.unshift('');
    }

    if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
        srcPath.push('');
    }

    isAbsolute = srcPath[0] === '' || (srcPath[0] && srcPath[0].charAt(0) === '/');

    // put the host back
    if (psychotic) {
        result.hostname = result.host = isAbsolute ? '' : srcPath.length ? srcPath.shift() : '';
        //occasionally the auth can get stuck only in host
        //this especially happens in cases like
        //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
        authInHost = result.host && result.host.indexOf('@') > 0 ?
            result.host.split('@') : false;
        if (authInHost) {
            result.auth = authInHost.shift();
            result.host = result.hostname = authInHost.shift();
        }
    }

    mustEndAbs = mustEndAbs || (result.host && srcPath.length);

    if (mustEndAbs && !isAbsolute) {
        srcPath.unshift('');
    }

    if (!srcPath.length) {
        result.pathname = null;
        result.path = null;
    } else {
        result.pathname = srcPath.join('/');
    }

    //to support request.http
    if (isNull(result.pathname) || isNull(result.search)) {
        result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : '');
    }
    result.auth = relative.auth || result.auth;
    result.slashes = result.slashes || relative.slashes;
    result.href = result.format();

    return result;
};

Url.prototype.parseHost = function() {
    var host = this.host,
        port = portPattern.exec(host);

    if (port) {
        port = port[0];
        if (port !== ':') {
            this.port = port.slice(1);
        }
        host = host.slice(0, host.length - port.length);
    }
    if (host) {
        this.hostname = host;
    }
};

// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
    var i = index,
        k = i + 1,
        n = list.length;

    for (; k < n; i += 1, k += 1) {
        list[i] = list[k];
    }
    list.pop();
}

var hexTable = new Array(256);
for (var i = 0; i < 256; ++i) {
    hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
}

function encodeAuth(str) {
    // faster encodeURIComponent alternative for encoding auth uri components
    var out = '',
        lastPos = 0,
        i, c, c2;

    for (i = 0; i < str.length; ++i) {
        c = str.charCodeAt(i);

        // These characters do not need escaping:
        // ! - . _ ~
        // ' ( ) * :
        // digits
        // alpha (uppercase)
        // alpha (lowercase)
        if (c === 0x21 || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E ||
            (c >= 0x27 && c <= 0x2A) ||
            (c >= 0x30 && c <= 0x3A) ||
            (c >= 0x41 && c <= 0x5A) ||
            (c >= 0x61 && c <= 0x7A)) {
            continue;
        }

        if (i - lastPos > 0) {
            out += str.slice(lastPos, i);
        }

        lastPos = i + 1;

        // Other ASCII characters
        if (c < 0x80) {
            out += hexTable[c];
            continue;
        }

        // Multi-byte characters ...
        if (c < 0x800) {
            out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)];
            continue;
        }
        if (c < 0xD800 || c >= 0xE000) {
            out += hexTable[0xE0 | (c >> 12)] +
                hexTable[0x80 | ((c >> 6) & 0x3F)] +
                hexTable[0x80 | (c & 0x3F)];
            continue;
        }
        // Surrogate pair
        ++i;
        c2;
        if (i < str.length) {
            c2 = str.charCodeAt(i) & 0x3FF;
        } else {
            c2 = 0;
        }
        c = 0x10000 + (((c & 0x3FF) << 10) | c2);
        out += hexTable[0xF0 | (c >> 18)] +
            hexTable[0x80 | ((c >> 12) & 0x3F)] +
            hexTable[0x80 | ((c >> 6) & 0x3F)] +
            hexTable[0x80 | (c & 0x3F)];
    }
    if (lastPos === 0) {
        return str;
    }
    if (lastPos < str.length) {
        return out + str.slice(lastPos);
    }
    return out;
}


/**
 * CONSOLE
 *
 */

'use strict';

module.exports = function( grunt ){

    /**
     * writelnColor(msg, color, mgs2, color2,..,msgN, colorN) writeln msg in color
     */
    function writelnColor(){
        for(var i=0; i<arguments.length; i=i+2)
            grunt.log.write(arguments[i][arguments[i+1]]);
        grunt.log.writeln('');
    }

    /**
     * writelnYellow(msg) writeln msg in yellow
     */
    function writelnYellow(msg){
        writelnColor(msg, 'yellow');
    }

    //writelnRed(msg) writeln msg in red
    //function writelnRed(msg){ writelnColor(msg, 'red'); }

    //Return the module
    return {
        writelnColor : writelnColor,
        writelnYellow: writelnYellow

    };
};


var Line2 = require('line2');
var Vec2 = require('vec2');
var lineIntersect = require('line-intersect');

/**
* @param {object} ray - Object that looks like
* {
*   start: {x: number, y: number},
*   end: {x: number, y: number}
* }
*
* @param {object} circle - Object that looks like
* {
*   position: {x: number, y: number},
*   radius: number
* }
*
* @return {object} point (x/y) where ray hits circle or null if ray doesn't hit
*/
function rayVsCircle(ray, circle) {
  var rayStart = new Vec2(ray.start);

  if (circleContainsPoint(circle.position, circle.radius, ray.start)) {
    return rayStart;
  }

  var intersections = rayLineVsCircle(ray, circle);

  if (intersections.length) {
    return rayStart.nearest(intersections.filter(within(ray)));
  } else {
    return null;
  }
}

/**
* Treats ray like an infinite line see where it intersects circle.
*
* @param {object} ray - ray-like
* @param {Circle2} circle
* @return {Array} containing 0, 1, or 2 Vec2 where line hits circle
*/
function rayLineVsCircle(ray, circle) {
  var rayLine = new Line2(
    ray.start.x,
    ray.start.y,
    ray.end.x,
    ray.end.y
  );

  return rayLine.intersectCircle(circle.position, circle.radius);
}

function circleContainsPoint(circlePosition, radius, point) {
  return distance(circlePosition.x, circlePosition.y, point.x, point.y) <= radius;
}

function distance(x1, y1, x2, y2) {
  var x = x1 - x2;
  var y = y1 - y2;
  return Math.sqrt(x * x + y * y);
}

/**
* @param {object} ray - ray-like
* @return {Function} function that returns true when passed an x/y that is
*                    within ray
*/
function within(ray) {
  return function(vec) {
    return lineIntersect.colinearPointWithinSegment(
      vec.x, vec.y,
      ray.start.x, ray.start.y,
      ray.end.x, ray.end.y
    );
  };
}

module.exports = rayVsCircle;


"use strict"
var Url = require('url')
var _ = require('underscore')

module.exports = function(ngin) {
  var SportsModel = ngin.SportsModel
  var config = ngin.config
  var Super = SportsModel.prototype

  /**
   * Scopes the url to the tournament, league, or flight
   *
   * @param {Object} options
   * @returns {String}
   * @api public
   */

  function scopeUrl(options, inst) {
    options = _.extend(_.clone(options || {}), inst)
    if (typeof options !== 'object' && (!options.tournament_id || !options.flight_id))
      throw new Error('tournament_id or flight_id required to make tibreak preference api calls')

    var url = ''
    if (options.tournament_id) {
      url += ngin.Tournament.urlRoot() + '/' + options.tournament_id
    } else if (options.flight_id) {
      url += ngin.Flight.urlRoot() + '/' + options.flight_id
    }
    return url + TiebreakPreference.urlRoot()
  }

  /**
   * TiebreakPreference Class
   *
   * @param {Object} attr
   * @param {Object} options
   * @api public
   */

  var TiebreakPreference = SportsModel.extend({

    // Fetch is used by Tournament and
    // list is being used by Flight. Can those
    // be consolidated?
    fetch: function(options, callback) {
      var url = scopeUrl(options, this)
      return Super.fetch.call(this, url, options, callback)
    },

    save: function(options, callback) {
      var url = scopeUrl(options, this)
      options.method = options.method || 'PUT'
      return Super.save.call(this, url, options, callback)
    }

  }, {

    urlRoot: function() {
      // No need for the base portion of the url here
      // since this url must be scoped to another object.
      return '/tiebreak_preference'
    }

  })

  return TiebreakPreference

}


/**
 * jsonrpclib - main file
 * copyright (c) 2012 openmason.
 * MIT Licensed.
 */

var handy = require('handy');
var logger = require('util');
var _ = require('underscore');

// The rpc specification can be found at:
// - http://www.jsonrpc.org/specification
// - http://en.wikipedia.org/wiki/JSON-RPC

/*
 * Syntax:
 *
 * --> data sent to Server
 * <-- data sent to Client
 *
 * rpc call with positional parameters:
 *
 * --> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
 * <-- {"jsonrpc": "2.0", "result": 19, "id": 1}
 *
 * --> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
 * <-- {"jsonrpc": "2.0", "result": -19, "id": 2}
 *
 * rpc call with named parameters:
 *
 * --> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
 * <-- {"jsonrpc": "2.0", "result": 19, "id": 3}
 *
 * --> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
 * <-- {"jsonrpc": "2.0", "result": 19, "id": 4}
 *
 * a Notification:
 *
 * --> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
 * --> {"jsonrpc": "2.0", "method": "foobar"}
 *
 * rpc call of non-existent method:
 *
 * --> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
 * <-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": "1"}
 *
 * rpc call with invalid JSON:
 *
 * --> {"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]
 * <-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error."}, "id": null}
 *
 * rpc call with invalid Request object:
 *
 * --> {"jsonrpc": "2.0", "method": 1, "params": "bar"}
 * <-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
 *
 * rpc call Batch, invalid JSON:
 *
 * --> [
 *   {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
 *   {"jsonrpc": "2.0", "method"
 * ]
 * <-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error."}, "id": null}
 * rpc call with an empty Array:
 * --> []
 * <-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
 * rpc call with an invalid Batch (but not empty):
 * --> [1]
 * <-- [
 *   {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
 * ]
 *
 * rpc call with invalid Batch:
 *
 * --> [1,2,3]
 * <-- [
 *   {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
 *   {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
 *   {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
 * ]
 *
 * rpc call Batch:
 *
 * --> [
 *         {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
 *         {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
 *         {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
 *         {"foo": "boo"},
 *         {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
 *         {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
 *     ]
 * <-- [
 *         {"jsonrpc": "2.0", "result": 7, "id": "1"},
 *         {"jsonrpc": "2.0", "result": 19, "id": "2"},
 *         {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
 *         {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": "5"},
 *         {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
 *     ]
 *
 * rpc call Batch (all notifications):
 *
 * --> [
 *         {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
 *         {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}
 *     ]
 * <-- //Nothing is returned for all notification batches
 *
 */

var RpcErrors = {
  PARSE_ERROR     : { code:-32700, message: 'Parse error' },
  INVALID_REQUEST : { code:-32600, message: 'Invalid Request' },
  METHOD_NOT_FOUND: { code:-32601, message: 'Method not found' },
  INVALID_PARAMS  : { code:-32602, message: 'Invalid params' },
  INTERNAL_ERROR  : { code:-32603, message: 'Internal error' },
  SERVER_ERROR    : { code:-32000, message: 'Server error' }
};

// JSON RPC library entry point
var JSONRpc = function(module, debug) {
  this.debug = debug || false;
  this.jsonrpc = "2.0";
  // check & load the methods in module
  this.methods = module;
  if(handy.getType(module)=='string') {
    this.methods = require(module);
  }
  if(this.debug) {
    logger.debug('Loaded with methods:'+_.functions(this.methods));
  }
};

// main dispatcher for processing json-rpc
// requests
JSONRpc.prototype.request = function(jsonBody) {
  var self=this;
  self._debug(true, jsonBody);

  var id = null;
  var batch = false;
  var rpcObj;
  // first step is to parse the json
  try {
    rpcObj = JSON.parse(jsonBody);
  } catch(err) {
    return JSON.stringify(self.error(RpcErrors.PARSE_ERROR, id, err.message));
  }
  var requests = [];
  var results = [];
  // if rpcObj is array, then its a batch request
  if(handy.getType(rpcObj)=='array') {
    if(rpcObj.length==0) {
      return JSON.stringify(self.error(RpcErrors.INVALID_REQUEST, id));
    }
    batch = true;
    requests = rpcObj;
  } else {
    requests = [rpcObj];
  }
  // handle all the requests
  // 1. try to validate each of the request
  // 2. run them
  _.each(requests, function(reqObj) {
    // - first drop off all the requests without id
    //   those are notifications.
    if(handy.getType(reqObj)=='object' && _.size(reqObj)>0 && !_.has(reqObj, 'id')) {
      self._debug(true, 'Notification ' + JSON.stringify(reqObj));
    } else {
      var result = self._validate(reqObj);
      if(!result) {
        // invoke the function
        // @todo - what should be the value of 'this'?
        try {
          var res=self.methods[reqObj.method].apply(null, reqObj.params);
          result = self.result(reqObj.id, res);
        }
        catch(err) {
          result = self.error(RpcErrors.INTERNAL_ERROR, reqObj.id, err);
        }
      }
      results.push(result);
    }
  });
  // return back the result
  if(results.length<=0) return;
  if(batch==false) {
    return JSON.stringify(results[0]);
  }
  return JSON.stringify(results);
};

// return back the result object
JSONRpc.prototype.result = function(id, result) {
  var res = { jsonrpc: this.jsonrpc,
              result: result,
              id: id };
  this._debug(false, res);
  return res;
};

// return back the correct error object
JSONRpc.prototype.error = function(err, id, data) {
  var errorObj = { jsonrpc: this.jsonrpc,
                   error: { code: err.code, message: err.message },
                   id: id };
  if(data) {
    errorObj['data'] = data;
  }
  this._debug(false, errorObj);
  return errorObj;
};

// ---- private functions

// validate the request object
// - returns the error object, if any error
JSONRpc.prototype._validate = function(requestObj) {
  var self = this;
  // - check for jsonprc value
  if(_.has(requestObj, 'jsonrpc') && requestObj['jsonrpc']!='2.0') {
    return self.error(RpcErrors.INVALID_REQUEST, requestObj.id, 'unknown jsonrpc version');
  }
  // - check for id
  var idType = handy.getType(requestObj.id);
  if(idType != 'string' && idType != 'number') {
    return self.error(RpcErrors.INVALID_REQUEST, requestObj.id, 'id should be a valid number/string');
  }
  // - check for method
  if(!_.has(requestObj, 'method')) {
    return self.error(RpcErrors.INVALID_REQUEST, requestObj.id, 'missing method to call');
  }
  // - check if method is present
  var fns = _.functions(self.methods);
  if(!_.include(fns, requestObj.method)) {
    return self.error(RpcErrors.METHOD_NOT_FOUND, requestObj.id, requestObj.method + " - unknown method");
  }

  // - parameter checks
  var params=_getParamNames(self.methods[requestObj.method]) || [];
  // - if params are absent and required for the method, its an error
  if(!_.has(requestObj,'params') && params && params.length>0) {
    return self.error(RpcErrors.INVALID_PARAMS, requestObj.id, 'params expected:'+params);
  }
  // - if params are present
  //   it has to be either array or object
  if(_.has(requestObj, 'params')) {
    var ptype = handy.getType(requestObj.params);
    if(ptype!='array' && ptype!='object') {
      return self.error(RpcErrors.INVALID_PARAMS, requestObj.id, 'params should be either array or object');
    }

    // @todo - not sure if this check needs to be enabled
    // sometimes it might be by design that less valus can be passed

    // check if array matches the arguments
    if(ptype=='array' && requestObj.params.length != params.length) {
      return self.error(RpcErrors.INVALID_PARAMS, requestObj.id, 'total params expected:'+params.length);
    }
    // check if the object has matching params
    if(ptype=='object') {
      var requestValues = _.keys(requestObj.params);
      if(!handy.isArrayEqual(params, requestValues)) {
        return self.error(RpcErrors.INVALID_PARAMS, requestObj.id, 'params expected:'+params);
      }
      // lets convert the params to array
      // in the order expected
      requestObj.params = _.values(_.pick(requestObj.params, params));
    }
  }
};

// returns the function parameters
function _getParamNames(func) {
  var funStr = func.toString();
  return funStr.slice(funStr.indexOf('(')+1, funStr.indexOf(')')).match(/([^\s,]+)/g);
}

// debug request/response statements
JSONRpc.prototype._debug = function(isRequest, value) {
  if(this.debug) {
    if(handy.getType(value)!='string') {
      value = JSON.stringify(value);
    }
    logger.debug((isRequest?'-->':'<--')+' ' + value);
  }
};

module.exports = JSONRpc;

// -- EOF


/*!
 * Module dependencies
 */
var inflection = require('inflection')
  ;

/*!
 * Expose `_getChangedProperties()` and `update()`.
 */
exports._getChangedProperties = _getChangedProperties;
exports.update = update;

/**
 * Get only changed properties as a hash.
 *
 * @return {Object}
 * @api private
 */
function _getChangedProperties() {
  var retVal = {}, key;
  for (key in this._changed) {
    retVal[key] = this._changed[key];
  }
  return retVal;
}

/**
 * Allows a resource to be updated.
 *
 * @param {Object|Function} properties A hash of fields to be updated or the callback.
 * @param {Function} callback The callback to call with the updated resource or an error message.
 * @api public
 */
function update(properties, callback) {
  if (typeof properties == 'function') {
    callback = properties;
    properties = {};
  }

  var key, changed;
  var exceptions = ['addresses_update_action', 'emails_update_action', 'phone_numbers_update_action'];
  for (key in properties) {
    if ('set' + inflection.camelize(key) in this) {
      this['set' + inflection.camelize(key)](properties[key]);
    } else if(exceptions.indexOf(key) != -1) {
      this._changed[key] = properties[key];
    }
  }

  changed = this._getChangedProperties();
  this.getClient().patch(this.definition._links.self.href, changed, function(err, definition, response) {
    if (err) return callback(err);
    this.definition = definition;
    this._setup();
    callback(null, this);
  }.bind(this));
}