/*
 * Copyright (C) 2012 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER 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.
 */

function defineCommonExtensionSymbols(apiPrivate)
{
    if (!apiPrivate.audits)
        apiPrivate.audits = {};
    apiPrivate.audits.Severity = {
        Info: "info",
        Warning: "warning",
        Severe: "severe"
    };

    if (!apiPrivate.console)
        apiPrivate.console = {};
    apiPrivate.console.Severity = {
        Debug: "debug",
        Log: "log",
        Warning: "warning",
        Error: "error"
    };

    if (!apiPrivate.panels)
        apiPrivate.panels = {};
    apiPrivate.panels.SearchAction = {
        CancelSearch: "cancelSearch",
        PerformSearch: "performSearch",
        NextSearchResult: "nextSearchResult",
        PreviousSearchResult: "previousSearchResult"
    };

    apiPrivate.Events = {
        AuditStarted: "audit-started-",
        ButtonClicked: "button-clicked-",
        ConsoleMessageAdded: "console-message-added",
        PanelObjectSelected: "panel-objectSelected-",
        NetworkRequestFinished: "network-request-finished",
        OpenResource: "open-resource",
        PanelSearch: "panel-search-",
        ResourceAdded: "resource-added",
        ResourceContentCommitted: "resource-content-committed",
        ViewShown: "view-shown-",
        ViewHidden: "view-hidden-"
    };

    apiPrivate.Commands = {
        AddAuditCategory: "addAuditCategory",
        AddAuditResult: "addAuditResult",
        AddConsoleMessage: "addConsoleMessage",
        AddRequestHeaders: "addRequestHeaders",
        ApplyStyleSheet: "applyStyleSheet",
        CreatePanel: "createPanel",
        CreateSidebarPane: "createSidebarPane",
        CreateStatusBarButton: "createStatusBarButton",
        EvaluateOnInspectedPage: "evaluateOnInspectedPage",
        ForwardKeyboardEvent: "_forwardKeyboardEvent",
        GetConsoleMessages: "getConsoleMessages",
        GetHAR: "getHAR",
        GetPageResources: "getPageResources",
        GetRequestContent: "getRequestContent",
        GetResourceContent: "getResourceContent",
        InspectedURLChanged: "inspectedURLChanged",
        OpenResource: "openResource",
        Reload: "Reload",
        Subscribe: "subscribe",
        SetOpenResourceHandler: "setOpenResourceHandler",
        SetResourceContent: "setResourceContent",
        SetSidebarContent: "setSidebarContent",
        SetSidebarHeight: "setSidebarHeight",
        SetSidebarPage: "setSidebarPage",
        ShowPanel: "showPanel",
        StopAuditCategoryRun: "stopAuditCategoryRun",
        Unsubscribe: "unsubscribe",
        UpdateAuditProgress: "updateAuditProgress",
        UpdateButton: "updateButton"
    };
}

/**
 * @param {number} injectedScriptId
 * @return {!Object}
 * @suppressGlobalPropertiesCheck
 */
function injectedExtensionAPI(injectedScriptId)
{

var apiPrivate = {};

defineCommonExtensionSymbols(apiPrivate);

var commands = apiPrivate.Commands;
var events = apiPrivate.Events;
var userAction = false;

// Here and below, all constructors are private to API implementation.
// For a public type Foo, if internal fields are present, these are on
// a private FooImpl type, an instance of FooImpl is used in a closure
// by Foo consutrctor to re-bind publicly exported members to an instance
// of Foo.

/**
 * @constructor
 */
function EventSinkImpl(type, customDispatch)
{
    this._type = type;
    this._listeners = [];
    this._customDispatch = customDispatch;
}

EventSinkImpl.prototype = {
    addListener: function(callback)
    {
        if (typeof callback !== "function")
            throw "addListener: callback is not a function";
        if (this._listeners.length === 0)
            extensionServer.sendRequest({ command: commands.Subscribe, type: this._type });
        this._listeners.push(callback);
        extensionServer.registerHandler("notify-" + this._type, this._dispatch.bind(this));
    },

    removeListener: function(callback)
    {
        var listeners = this._listeners;

        for (var i = 0; i < listeners.length; ++i) {
            if (listeners[i] === callback) {
                listeners.splice(i, 1);
                break;
            }
        }
        if (this._listeners.length === 0)
            extensionServer.sendRequest({ command: commands.Unsubscribe, type: this._type });
    },

    /**
     * @param {...} vararg
     */
    _fire: function(vararg)
    {
        var listeners = this._listeners.slice();
        for (var i = 0; i < listeners.length; ++i)
            listeners[i].apply(null, arguments);
    },

    _dispatch: function(request)
    {
         if (this._customDispatch)
             this._customDispatch.call(this, request);
         else
             this._fire.apply(this, request.arguments);
    }
}

/**
 * @constructor
 */
function InspectorExtensionAPI()
{
    this.audits = new Audits();
    this.inspectedWindow = new InspectedWindow();
    this.panels = new Panels();
    this.network = new Network();
    defineDeprecatedProperty(this, "webInspector", "resources", "network");
    this.console = new ConsoleAPI();
}

/**
 * @constructor
 */
function ConsoleAPI()
{
    this.onMessageAdded = new EventSink(events.ConsoleMessageAdded);
}

ConsoleAPI.prototype = {
    getMessages: function(callback)
    {
        extensionServer.sendRequest({ command: commands.GetConsoleMessages }, callback);
    },

    addMessage: function(severity, text, url, line)
    {
        extensionServer.sendRequest({ command: commands.AddConsoleMessage, severity: severity, text: text, url: url, line: line });
    },

    get Severity()
    {
        return apiPrivate.console.Severity;
    }
}

/**
 * @constructor
 */
function Network()
{
    /**
     * @this {EventSinkImpl}
     */
    function dispatchRequestEvent(message)
    {
        var request = message.arguments[1];
        request.__proto__ = new Request(message.arguments[0]);
        this._fire(request);
    }
    this.onRequestFinished = new EventSink(events.NetworkRequestFinished, dispatchRequestEvent);
    defineDeprecatedProperty(this, "network", "onFinished", "onRequestFinished");
    this.onNavigated = new EventSink(events.InspectedURLChanged);
}

Network.prototype = {
    getHAR: function(callback)
    {
        function callbackWrapper(result)
        {
            var entries = (result && result.entries) || [];
            for (var i = 0; i < entries.length; ++i) {
                entries[i].__proto__ = new Request(entries[i]._requestId);
                delete entries[i]._requestId;
            }
            callback(result);
        }
        extensionServer.sendRequest({ command: commands.GetHAR }, callback && callbackWrapper);
    },

    addRequestHeaders: function(headers)
    {
        extensionServer.sendRequest({ command: commands.AddRequestHeaders, headers: headers, extensionId: window.location.hostname });
    }
}

/**
 * @constructor
 */
function RequestImpl(id)
{
    this._id = id;
}

RequestImpl.prototype = {
    getContent: function(callback)
    {
        function callbackWrapper(response)
        {
            callback(response.content, response.encoding);
        }
        extensionServer.sendRequest({ command: commands.GetRequestContent, id: this._id }, callback && callbackWrapper);
    }
}

/**
 * @constructor
 */
function Panels()
{
    var panels = {
        elements: new ElementsPanel(),
        sources: new SourcesPanel(),
    };

    function panelGetter(name)
    {
        return panels[name];
    }
    for (var panel in panels)
        this.__defineGetter__(panel, panelGetter.bind(null, panel));
    this.applyStyleSheet = function(styleSheet) { extensionServer.sendRequest({ command: commands.ApplyStyleSheet, styleSheet: styleSheet }); };
}

Panels.prototype = {
    create: function(title, icon, page, callback)
    {
        var id = "extension-panel-" + extensionServer.nextObjectId();
        var request = {
            command: commands.CreatePanel,
            id: id,
            title: title,
            icon: icon,
            page: page
        };
        extensionServer.sendRequest(request, callback && callback.bind(this, new ExtensionPanel(id)));
    },

    setOpenResourceHandler: function(callback)
    {
        var hadHandler = extensionServer.hasHandler(events.OpenResource);

        function callbackWrapper(message)
        {
            // Allow the panel to show itself when handling the event.
            userAction = true;
            try {
                callback.call(null, new Resource(message.resource), message.lineNumber);
            } finally {
                userAction = false;
            }
        }

        if (!callback)
            extensionServer.unregisterHandler(events.OpenResource);
        else
            extensionServer.registerHandler(events.OpenResource, callbackWrapper);

        // Only send command if we either removed an existing handler or added handler and had none before.
        if (hadHandler === !callback)
            extensionServer.sendRequest({ command: commands.SetOpenResourceHandler, "handlerPresent": !!callback });
    },

    openResource: function(url, lineNumber, callback)
    {
        extensionServer.sendRequest({ command: commands.OpenResource, "url": url, "lineNumber": lineNumber }, callback);
    },

    get SearchAction()
    {
        return apiPrivate.panels.SearchAction;
    }
}

/**
 * @constructor
 */
function ExtensionViewImpl(id)
{
    this._id = id;

    /**
     * @this {EventSinkImpl}
     */
    function dispatchShowEvent(message)
    {
        var frameIndex = message.arguments[0];
        if (typeof frameIndex === "number")
            this._fire(window.parent.frames[frameIndex]);
        else
            this._fire();
    }

    if (id) {
        this.onShown = new EventSink(events.ViewShown + id, dispatchShowEvent);
        this.onHidden = new EventSink(events.ViewHidden + id);
    }
}

/**
 * @constructor
 * @extends {ExtensionViewImpl}
 * @param {string} hostPanelName
 */
function PanelWithSidebarImpl(hostPanelName)
{
    ExtensionViewImpl.call(this, null);
    this._hostPanelName = hostPanelName;
    this.onSelectionChanged = new EventSink(events.PanelObjectSelected + hostPanelName);
}

PanelWithSidebarImpl.prototype = {
    createSidebarPane: function(title, callback)
    {
        var id = "extension-sidebar-" + extensionServer.nextObjectId();
        var request = {
            command: commands.CreateSidebarPane,
            panel: this._hostPanelName,
            id: id,
            title: title
        };
        function callbackWrapper()
        {
            callback(new ExtensionSidebarPane(id));
        }
        extensionServer.sendRequest(request, callback && callbackWrapper);
    },

    __proto__: ExtensionViewImpl.prototype
}

function declareInterfaceClass(implConstructor)
{
    return function()
    {
        var impl = { __proto__: implConstructor.prototype };
        implConstructor.apply(impl, arguments);
        populateInterfaceClass(this, impl);
    }
}

function defineDeprecatedProperty(object, className, oldName, newName)
{
    var warningGiven = false;
    function getter()
    {
        if (!warningGiven) {
            console.warn(className + "." + oldName + " is deprecated. Use " + className + "." + newName + " instead");
            warningGiven = true;
        }
        return object[newName];
    }
    object.__defineGetter__(oldName, getter);
}

function extractCallbackArgument(args)
{
    var lastArgument = args[args.length - 1];
    return typeof lastArgument === "function" ? lastArgument : undefined;
}

var AuditCategory = declareInterfaceClass(AuditCategoryImpl);
var AuditResult = declareInterfaceClass(AuditResultImpl);
var Button = declareInterfaceClass(ButtonImpl);
var EventSink = declareInterfaceClass(EventSinkImpl);
var ExtensionPanel = declareInterfaceClass(ExtensionPanelImpl);
var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl);
var Request = declareInterfaceClass(RequestImpl);
var Resource = declareInterfaceClass(ResourceImpl);

/**
 * @constructor
 * @extends {PanelWithSidebar}
 */
function ElementsPanel()
{
    PanelWithSidebar.call(this, "elements");
}

ElementsPanel.prototype = {
    __proto__: PanelWithSidebar.prototype
}

/**
 * @constructor
 * @extends {PanelWithSidebar}
 */
function SourcesPanel()
{
    PanelWithSidebar.call(this, "sources");
}

SourcesPanel.prototype = {
    __proto__: PanelWithSidebar.prototype
}

/**
 * @constructor
 * @extends {ExtensionViewImpl}
 */
function ExtensionPanelImpl(id)
{
    ExtensionViewImpl.call(this, id);
    this.onSearch = new EventSink(events.PanelSearch + id);
}

ExtensionPanelImpl.prototype = {
    /**
     * @return {!Object}
     */
    createStatusBarButton: function(iconPath, tooltipText, disabled)
    {
        var id = "button-" + extensionServer.nextObjectId();
        var request = {
            command: commands.CreateStatusBarButton,
            panel: this._id,
            id: id,
            icon: iconPath,
            tooltip: tooltipText,
            disabled: !!disabled
        };
        extensionServer.sendRequest(request);
        return new Button(id);
    },

    show: function()
    {
        if (!userAction)
            return;

        var request = {
            command: commands.ShowPanel,
            id: this._id
        };
        extensionServer.sendRequest(request);
    },

    __proto__: ExtensionViewImpl.prototype
}

/**
 * @constructor
 * @extends {ExtensionViewImpl}
 */
function ExtensionSidebarPaneImpl(id)
{
    ExtensionViewImpl.call(this, id);
}

ExtensionSidebarPaneImpl.prototype = {
    setHeight: function(height)
    {
        extensionServer.sendRequest({ command: commands.SetSidebarHeight, id: this._id, height: height });
    },

    setExpression: function(expression, rootTitle, evaluateOptions)
    {
        var request = {
            command: commands.SetSidebarContent,
            id: this._id,
            expression: expression,
            rootTitle: rootTitle,
            evaluateOnPage: true,
        };
        if (typeof evaluateOptions === "object")
            request.evaluateOptions = evaluateOptions;
        extensionServer.sendRequest(request, extractCallbackArgument(arguments));
    },

    setObject: function(jsonObject, rootTitle, callback)
    {
        extensionServer.sendRequest({ command: commands.SetSidebarContent, id: this._id, expression: jsonObject, rootTitle: rootTitle }, callback);
    },

    setPage: function(page)
    {
        extensionServer.sendRequest({ command: commands.SetSidebarPage, id: this._id, page: page });
    },

    __proto__: ExtensionViewImpl.prototype
}

/**
 * @constructor
 */
function ButtonImpl(id)
{
    this._id = id;
    this.onClicked = new EventSink(events.ButtonClicked + id);
}

ButtonImpl.prototype = {
    update: function(iconPath, tooltipText, disabled)
    {
        var request = {
            command: commands.UpdateButton,
            id: this._id,
            icon: iconPath,
            tooltip: tooltipText,
            disabled: !!disabled
        };
        extensionServer.sendRequest(request);
    }
};

/**
 * @constructor
 */
function Audits()
{
}

Audits.prototype = {
    /**
     * @return {!AuditCategory}
     */
    addCategory: function(displayName, resultCount)
    {
        var id = "extension-audit-category-" + extensionServer.nextObjectId();
        if (typeof resultCount !== "undefined")
            console.warn("Passing resultCount to audits.addCategory() is deprecated. Use AuditResult.updateProgress() instead.");
        extensionServer.sendRequest({ command: commands.AddAuditCategory, id: id, displayName: displayName, resultCount: resultCount });
        return new AuditCategory(id);
    }
}

/**
 * @constructor
 */
function AuditCategoryImpl(id)
{
    /**
     * @this {EventSinkImpl}
     */
    function dispatchAuditEvent(request)
    {
        var auditResult = new AuditResult(request.arguments[0]);
        try {
            this._fire(auditResult);
        } catch (e) {
            console.error("Uncaught exception in extension audit event handler: " + e);
            auditResult.done();
        }
    }
    this._id = id;
    this.onAuditStarted = new EventSink(events.AuditStarted + id, dispatchAuditEvent);
}

/**
 * @constructor
 */
function AuditResultImpl(id)
{
    this._id = id;

    this.createURL = this._nodeFactory.bind(this, "url");
    this.createSnippet = this._nodeFactory.bind(this, "snippet");
    this.createText = this._nodeFactory.bind(this, "text");
    this.createObject = this._nodeFactory.bind(this, "object");
    this.createNode = this._nodeFactory.bind(this, "node");
}

AuditResultImpl.prototype = {
    addResult: function(displayName, description, severity, details)
    {
        // shorthand for specifying details directly in addResult().
        if (details && !(details instanceof AuditResultNode))
            details = new AuditResultNode(details instanceof Array ? details : [details]);

        var request = {
            command: commands.AddAuditResult,
            resultId: this._id,
            displayName: displayName,
            description: description,
            severity: severity,
            details: details
        };
        extensionServer.sendRequest(request);
    },

    /**
     * @return {!Object}
     */
    createResult: function()
    {
        return new AuditResultNode(Array.prototype.slice.call(arguments));
    },

    updateProgress: function(worked, totalWork)
    {
        extensionServer.sendRequest({ command: commands.UpdateAuditProgress, resultId: this._id, progress: worked / totalWork });
    },

    done: function()
    {
        extensionServer.sendRequest({ command: commands.StopAuditCategoryRun, resultId: this._id });
    },

    /**
     * @type {!Object.<string, string>}
     */
    get Severity()
    {
        return apiPrivate.audits.Severity;
    },

    /**
     * @return {!{type: string, arguments: !Array.<string|number>}}
     */
    createResourceLink: function(url, lineNumber)
    {
        return {
            type: "resourceLink",
            arguments: [url, lineNumber && lineNumber - 1]
        };
    },

    /**
     * @return {!{type: string, arguments: !Array.<string|number>}}
     */
    _nodeFactory: function(type)
    {
        return {
            type: type,
            arguments: Array.prototype.slice.call(arguments, 1)
        };
    }
}

/**
 * @constructor
 */
function AuditResultNode(contents)
{
    this.contents = contents;
    this.children = [];
    this.expanded = false;
}

AuditResultNode.prototype = {
    /**
     * @return {!Object}
     */
    addChild: function()
    {
        var node = new AuditResultNode(Array.prototype.slice.call(arguments));
        this.children.push(node);
        return node;
    }
};

/**
 * @constructor
 */
function InspectedWindow()
{
    /**
     * @this {EventSinkImpl}
     */
    function dispatchResourceEvent(message)
    {
        this._fire(new Resource(message.arguments[0]));
    }

    /**
     * @this {EventSinkImpl}
     */
    function dispatchResourceContentEvent(message)
    {
        this._fire(new Resource(message.arguments[0]), message.arguments[1]);
    }

    this.onResourceAdded = new EventSink(events.ResourceAdded, dispatchResourceEvent);
    this.onResourceContentCommitted = new EventSink(events.ResourceContentCommitted, dispatchResourceContentEvent);
}

InspectedWindow.prototype = {
    reload: function(optionsOrUserAgent)
    {
        var options = null;
        if (typeof optionsOrUserAgent === "object")
            options = optionsOrUserAgent;
        else if (typeof optionsOrUserAgent === "string") {
            options = { userAgent: optionsOrUserAgent };
            console.warn("Passing userAgent as string parameter to inspectedWindow.reload() is deprecated. " +
                         "Use inspectedWindow.reload({ userAgent: value}) instead.");
        }
        extensionServer.sendRequest({ command: commands.Reload, options: options });
    },

    /**
     * @return {?Object}
     */
    eval: function(expression, evaluateOptions)
    {
        var callback = extractCallbackArgument(arguments);
        function callbackWrapper(result)
        {
            if (result.isError || result.isException)
                callback(undefined, result);
            else
                callback(result.value);
        }
        var request = {
            command: commands.EvaluateOnInspectedPage,
            expression: expression
        };
        if (typeof evaluateOptions === "object")
            request.evaluateOptions = evaluateOptions;
        extensionServer.sendRequest(request, callback && callbackWrapper);
        return null;
    },

    getResources: function(callback)
    {
        function wrapResource(resourceData)
        {
            return new Resource(resourceData);
        }
        function callbackWrapper(resources)
        {
            callback(resources.map(wrapResource));
        }
        extensionServer.sendRequest({ command: commands.GetPageResources }, callback && callbackWrapper);
    }
}

/**
 * @constructor
 */
function ResourceImpl(resourceData)
{
    this._url = resourceData.url
    this._type = resourceData.type;
}

ResourceImpl.prototype = {
    get url()
    {
        return this._url;
    },

    get type()
    {
        return this._type;
    },

    getContent: function(callback)
    {
        function callbackWrapper(response)
        {
            callback(response.content, response.encoding);
        }

        extensionServer.sendRequest({ command: commands.GetResourceContent, url: this._url }, callback && callbackWrapper);
    },

    setContent: function(content, commit, callback)
    {
        extensionServer.sendRequest({ command: commands.SetResourceContent, url: this._url, content: content, commit: commit }, callback);
    }
}

var keyboardEventRequestQueue = [];
var forwardTimer = null;

function forwardKeyboardEvent(event)
{
    const Esc = "U+001B";
    // We only care about global hotkeys, not about random text
    if (!event.ctrlKey && !event.altKey && !event.metaKey && !/^F\d+$/.test(event.keyIdentifier) && event.keyIdentifier !== Esc)
        return;
    var requestPayload = {
        eventType: event.type,
        ctrlKey: event.ctrlKey,
        altKey: event.altKey,
        metaKey: event.metaKey,
        keyIdentifier: event.keyIdentifier,
        location: event.location,
        keyCode: event.keyCode
    };
    keyboardEventRequestQueue.push(requestPayload);
    if (!forwardTimer)
        forwardTimer = setTimeout(forwardEventQueue, 0);
}

function forwardEventQueue()
{
    forwardTimer = null;
    var request = {
        command: commands.ForwardKeyboardEvent,
        entries: keyboardEventRequestQueue
    };
    extensionServer.sendRequest(request);
    keyboardEventRequestQueue = [];
}

document.addEventListener("keydown", forwardKeyboardEvent, false);
document.addEventListener("keypress", forwardKeyboardEvent, false);

/**
 * @constructor
 */
function ExtensionServerClient()
{
    this._callbacks = {};
    this._handlers = {};
    this._lastRequestId = 0;
    this._lastObjectId = 0;

    this.registerHandler("callback", this._onCallback.bind(this));

    var channel = new MessageChannel();
    this._port = channel.port1;
    this._port.addEventListener("message", this._onMessage.bind(this), false);
    this._port.start();

    window.parent.postMessage("registerExtension", [ channel.port2 ], "*");
}

ExtensionServerClient.prototype = {
    /**
     * @param {!Object} message
     * @param {function()=} callback
     */
    sendRequest: function(message, callback)
    {
        if (typeof callback === "function")
            message.requestId = this._registerCallback(callback);
        this._port.postMessage(message);
    },

    /**
     * @return {boolean}
     */
    hasHandler: function(command)
    {
        return !!this._handlers[command];
    },

    registerHandler: function(command, handler)
    {
        this._handlers[command] = handler;
    },

    unregisterHandler: function(command)
    {
        delete this._handlers[command];
    },

    /**
     * @return {string}
     */
    nextObjectId: function()
    {
        return injectedScriptId + "_" + ++this._lastObjectId;
    },

    _registerCallback: function(callback)
    {
        var id = ++this._lastRequestId;
        this._callbacks[id] = callback;
        return id;
    },

    _onCallback: function(request)
    {
        if (request.requestId in this._callbacks) {
            var callback = this._callbacks[request.requestId];
            delete this._callbacks[request.requestId];
            callback(request.result);
        }
    },

    _onMessage: function(event)
    {
        var request = event.data;
        var handler = this._handlers[request.command];
        if (handler)
            handler.call(this, request);
    }
}

function populateInterfaceClass(interfaze, implementation)
{
    for (var member in implementation) {
        if (member.charAt(0) === "_")
            continue;
        var descriptor = null;
        // Traverse prototype chain until we find the owner.
        for (var owner = implementation; owner && !descriptor; owner = owner.__proto__)
            descriptor = Object.getOwnPropertyDescriptor(owner, member);
        if (!descriptor)
            continue;
        if (typeof descriptor.value === "function")
            interfaze[member] = descriptor.value.bind(implementation);
        else if (typeof descriptor.get === "function")
            interfaze.__defineGetter__(member, descriptor.get.bind(implementation));
        else
            Object.defineProperty(interfaze, member, descriptor);
    }
}

// extensionServer is a closure variable defined by the glue below -- make sure we fail if it's not there.
if (!extensionServer)
    extensionServer = new ExtensionServerClient();

return new InspectorExtensionAPI();
}

/**
 * @suppress {checkVars, checkTypes}
 */
function platformExtensionAPI(coreAPI)
{
    function getTabId()
    {
        return tabId;
    }
    chrome = window.chrome || {};
    // Override chrome.devtools as a workaround for a error-throwing getter being exposed
    // in extension pages loaded into a non-extension process (only happens for remote client
    // extensions)
    var devtools_descriptor = Object.getOwnPropertyDescriptor(chrome, "devtools");
    if (!devtools_descriptor || devtools_descriptor.get)
        Object.defineProperty(chrome, "devtools", { value: {}, enumerable: true });
    // Only expose tabId on chrome.devtools.inspectedWindow, not webInspector.inspectedWindow.
    chrome.devtools.inspectedWindow = {};
    chrome.devtools.inspectedWindow.__defineGetter__("tabId", getTabId);
    chrome.devtools.inspectedWindow.__proto__ = coreAPI.inspectedWindow;
    chrome.devtools.network = coreAPI.network;
    chrome.devtools.panels = coreAPI.panels;

    // default to expose experimental APIs for now.
    if (extensionInfo.exposeExperimentalAPIs !== false) {
        chrome.experimental = chrome.experimental || {};
        chrome.experimental.devtools = chrome.experimental.devtools || {};

        var properties = Object.getOwnPropertyNames(coreAPI);
        for (var i = 0; i < properties.length; ++i) {
            var descriptor = Object.getOwnPropertyDescriptor(coreAPI, properties[i]);
            Object.defineProperty(chrome.experimental.devtools, properties[i], descriptor);
        }
        chrome.experimental.devtools.inspectedWindow = chrome.devtools.inspectedWindow;
    }
    if (extensionInfo.exposeWebInspectorNamespace)
        window.webInspector = coreAPI;
}

/**
 * @param {!ExtensionDescriptor} extensionInfo
 * @param {string} inspectedTabId
 * @return {string}
 */
function buildPlatformExtensionAPI(extensionInfo, inspectedTabId)
{
    return "var extensionInfo = " + JSON.stringify(extensionInfo) + ";" +
       "var tabId = " + inspectedTabId + ";" +
       platformExtensionAPI.toString();
}

/**
 * @param {!ExtensionDescriptor} extensionInfo
 * @param {string} inspectedTabId
 * @return {string}
 */
function buildExtensionAPIInjectedScript(extensionInfo, inspectedTabId)
{
    return "(function(injectedScriptId){ " +
        "var extensionServer;" +
        defineCommonExtensionSymbols.toString() + ";" +
        injectedExtensionAPI.toString() + ";" +
        buildPlatformExtensionAPI(extensionInfo, inspectedTabId) + ";" +
        "platformExtensionAPI(injectedExtensionAPI(injectedScriptId));" +
        "return {};" +
        "})";
}
