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

WebInspector.ExtensionServer = function()
{
    this._clientObjects = {};
    this._handlers = {};
    this._subscribers = {};
    this._status = new WebInspector.ExtensionStatus();

    this._registerHandler("subscribe", this._onSubscribe.bind(this));
    this._registerHandler("unsubscribe", this._onUnsubscribe.bind(this));
    this._registerHandler("getResources", this._onGetResources.bind(this));
    this._registerHandler("getResourceContent", this._onGetResourceContent.bind(this));
    this._registerHandler("getPageTimings", this._onGetPageTimings.bind(this));
    this._registerHandler("createPanel", this._onCreatePanel.bind(this));
    this._registerHandler("createSidebarPane", this._onCreateSidebar.bind(this));
    this._registerHandler("log", this._onLog.bind(this));
    this._registerHandler("evaluateOnInspectedPage", this._onEvaluateOnInspectedPage.bind(this));
    this._registerHandler("setSidebarHeight", this._onSetSidebarHeight.bind(this));
    this._registerHandler("setSidebarExpanded", this._onSetSidebarExpansion.bind(this));

    this._registerHandler("addAuditCategory", this._onAddAuditCategory.bind(this));
    this._registerHandler("addAuditResult", this._onAddAuditResult.bind(this));
    this._registerHandler("stopAuditCategoryRun", this._onStopAuditCategoryRun.bind(this));

    window.addEventListener("message", this._onWindowMessage.bind(this), false);
}

WebInspector.ExtensionServer.prototype = {
    notifyPanelShown: function(panelName)
    {
        this._postNotification("panel-shown-" + panelName);
    },

    notifyObjectSelected: function(panelId, objectType, objectId)
    {
        this._postNotification("panel-objectSelected-" + panelId, objectType, objectId);
    },

    notifyResourceFinished: function(resource)
    {
        this._postNotification("resource-finished", this._convertResource(resource));
    },

    notifySearchAction: function(panelId, action, searchString)
    {
        this._postNotification("panel-search-" + panelId, action, searchString);
    },

    notifyInspectedPageLoaded: function()
    {
        this._postNotification("inspectedPageLoaded");
    },

    notifyInspectedURLChanged: function()
    {
        this._postNotification("inspectedURLChanged");
    },

    notifyInspectorReset: function()
    {
        this._postNotification("reset");
    },

    startAuditRun: function(category, auditRun)
    {
        this._clientObjects[auditRun.id] = auditRun;
        this._postNotification("audit-started-" + category.id, auditRun.id);
    },

    stopAuditRun: function(auditRun)
    {
        delete this._clientObjects[auditRun.id];
    },

    _convertResource: function(resource)
    {
        return {
            id: resource.identifier,
            type: WebInspector.Resource.Type.toString(resource.type),
            har: (new WebInspector.HAREntry(resource)).build(),
        };
    },

    _postNotification: function(type, details)
    {
        var subscribers = this._subscribers[type];
        if (!subscribers)
            return;
        var message = {
            command: "notify-" + type,
            arguments: Array.prototype.slice.call(arguments, 1)
        };
        for (var i = 0; i < subscribers.length; ++i)
            subscribers[i].postMessage(message);
    },

    _onSubscribe: function(message, port)
    {
        var subscribers = this._subscribers[message.type];
        if (subscribers)
            subscribers.push(port);
        else
            this._subscribers[message.type] = [ port ];
    },

    _onUnsubscribe: function(message, port)
    {
        var subscribers = this._subscribers[message.type];
        if (!subscribers)
            return;
        subscribers.remove(port);
        if (!subscribers.length)
            delete this._subscribers[message.type];
    },

    _onCreatePanel: function(message, port)
    {
        var id = message.id;
        // The ids are generated on the client API side and must be unique, so the check below
        // shouldn't be hit unless someone is bypassing the API.
        if (id in this._clientObjects || id in WebInspector.panels)
            return this._status.E_EXISTS(id);
        var panel = new WebInspector.ExtensionPanel(id, message.label, message.icon);
        this._clientObjects[id] = panel;

        var toolbarElement = document.getElementById("toolbar");
        var lastToolbarItem = WebInspector.panelOrder[WebInspector.panelOrder.length - 1].toolbarItem;
        WebInspector.addPanelToolbarIcon(toolbarElement, panel, lastToolbarItem);
        WebInspector.panels[id] = panel;
        var iframe = this._createClientIframe(panel.element, message.url);
        iframe.style.height = "100%";
        return this._status.OK();
    },

    _onCreateSidebar: function(message, port)
    {
        var panel = WebInspector.panels[message.panel];
        if (!panel)
            return this._status.E_NOTFOUND(message.panel);
        if (!panel.sidebarElement || !panel.sidebarPanes)
            return this._status.E_NOTSUPPORTED();
        var id = message.id;
        var sidebar = new WebInspector.SidebarPane(message.title);
        this._clientObjects[id] = sidebar;
        panel.sidebarPanes[id] = sidebar;
        panel.sidebarElement.appendChild(sidebar.element);
        this._createClientIframe(sidebar.bodyElement, message.url);
        return this._status.OK();
    },

    _createClientIframe: function(parent, url, requestId, port)
    {
        var iframe = document.createElement("iframe");
        iframe.src = url;
        iframe.style.width = "100%";
        parent.appendChild(iframe);
        return iframe;
    },

    _onSetSidebarHeight: function(message)
    {
        var sidebar = this._clientObjects[message.id];
        if (!sidebar)
            return this._status.E_NOTFOUND(message.id);
        sidebar.bodyElement.firstChild.style.height = message.height;
    },

    _onSetSidebarExpansion: function(message)
    {
        var sidebar = this._clientObjects[message.id];
        if (!sidebar)
            return this._status.E_NOTFOUND(message.id);
        if (message.expanded)
            sidebar.expand();
        else
            sidebar.collapse();
    },

    _onLog: function(message)
    {
        WebInspector.log(message.message);
    },

    _onEvaluateOnInspectedPage: function(message, port)
    {
        function callback(resultPayload)
        {
            var resultObject = WebInspector.RemoteObject.fromPayload(resultPayload);
            var result = {};
            if (resultObject.isError())
                result.isException = true;
            result.value = resultObject.description;
            this._dispatchCallback(message.requestId, port, result);
        }
        var evalExpression = "JSON.stringify(eval('" +
            "with (window.console._commandLineAPI) with (window) {' + unescape('" + escape(message.expression) +
            "') + '}'));";
        InjectedScriptAccess.getDefault().evaluate(evalExpression, callback.bind(this));
    },

    _onRevealAndSelect: function(message)
    {
        if (message.panelId === "resources" && type === "resource")
            return this._onRevealAndSelectResource(message);
        else
            return this._status.E_NOTSUPPORTED(message.panelId, message.type);
    },

    _onRevealAndSelectResource: function(message)
    {
        var id = message.id;
        var resource = null;

        resource = WebInspector.resources[id] || WebInspector.resourceForURL(id);
        if (!resource)
            return this._status.E_NOTFOUND(typeof id + ": " + id);
        WebInspector.panels.resources.showResource(resource, message.line);
        WebInspector.showPanel("resources");
    },

    _dispatchCallback: function(requestId, port, result)
    {
        port.postMessage({ command: "callback", requestId: requestId, result: result });
    },

    _onGetResources: function(request)
    {
        function resourceWrapper(id)
        {
            return WebInspector.extensionServer._convertResource(WebInspector.resources[id]);
        }

        var response;
        if (request.id)
            response = WebInspector.resources[request.id] ? resourceWrapper(request.id) : this._status.E_NOTFOUND(request.id);
        else
            response = Object.keys(WebInspector.resources).map(resourceWrapper);
        return response;
    },

    _onGetResourceContent: function(message, port)
    {
        var ids;
        var response = [];

        function onContentAvailable(id, encoded, content)
        {
            var resourceContent = {
                id: id,
                encoding: encoded ? "base64" : "",
                content: content
            };
            response.push(resourceContent);
            if (response.length === ids.length)
                this._dispatchCallback(message.requestId, port, response);
        }

        if (typeof message.ids === "number")
            ids = [ message.ids ];
        else if (message.ids instanceof Array)
            ids = message.ids;
        else
            return this._status.E_BADARGTYPE("message.ids", "Array", typeof message.ids);

        for (var i = 0; i < ids.length; ++i) {
            var id = ids[i];
            var resource = WebInspector.resources[id];
            if (!resource)
                response.push(this._status.E_NOTFOUND(id));
            else {
                var encode = !WebInspector.Resource.Type.isTextType(resource.type);
                WebInspector.getEncodedResourceContent(id, encode, onContentAvailable.bind(this, id, encode));
            }
        }
        if (response.length === ids.length)
            this._dispatchCallback(message.requestId, port, response);
    },

    _onGetPageTimings: function()
    {
        return (new WebInspector.HARLog()).buildMainResourceTimings();
    },

    _onAddAuditCategory: function(request)
    {
        var category = new WebInspector.ExtensionAuditCategory(request.id, request.displayName, request.ruleCount);
        if (WebInspector.panels.audits.getCategory(category.id))
            return this._status.E_EXISTS(category.id);
        this._clientObjects[request.id] = category;
        WebInspector.panels.audits.addCategory(category);
    },

    _onAddAuditResult: function(request)
    {
        var auditResult = this._clientObjects[request.resultId];
        if (!auditResult)
            return this._status.E_NOTFOUND(request.resultId);
        try {
            auditResult.addResult(request.displayName, request.description, request.severity, request.details);
        } catch (e) {
            return e;
        }
        return this._status.OK();
    },

    _onStopAuditCategoryRun: function(request)
    {
        var auditRun = this._clientObjects[request.resultId];
        if (!auditRun)
            return this._status.E_NOTFOUND(request.resultId);
        auditRun.cancel();
    },

    initExtensions: function()
    {
        InspectorExtensionRegistry.getExtensionsAsync();
    },

    _addExtensions: function(extensions)
    {
        // See ExtensionAPI.js and ExtensionCommon.js for details.
        InspectorFrontendHost.setExtensionAPI(this._buildExtensionAPIInjectedScript());
        for (var i = 0; i < extensions.length; ++i) {
            var extension = extensions[i];
            try {
                if (!extension.startPage)
                    return;
                var iframe = document.createElement("iframe");
                iframe.src = extension.startPage;
                iframe.style.display = "none";
                document.body.appendChild(iframe);
            } catch (e) {
                console.error("Failed to initialize extension " + extension.startPage + ":" + e);
            }
        }
    },

    _buildExtensionAPIInjectedScript: function()
    {
        var resourceTypes = {};
        var resourceTypeProperties = Object.getOwnPropertyNames(WebInspector.Resource.Type);
        for (var i = 0; i < resourceTypeProperties.length; ++i) {
             var propName = resourceTypeProperties[i];
             var propValue = WebInspector.Resource.Type[propName];
             if (typeof propValue === "number")
                 resourceTypes[propName] = WebInspector.Resource.Type.toString(propValue);
        }
        var platformAPI = WebInspector.buildPlatformExtensionAPI ? WebInspector.buildPlatformExtensionAPI() : "";
        return "(function(){ " +
            "var private = {};" +
            "(" + WebInspector.commonExtensionSymbols.toString() + ")(private);" +
            "(" + WebInspector.injectedExtensionAPI.toString() + ").apply(this, arguments);" +
            "webInspector.resources.Types = " + JSON.stringify(resourceTypes) + ";" +
            platformAPI +
            "})";
    },

    _onWindowMessage: function(event)
    {
        if (event.data !== "registerExtension")
            return;
        var port = event.ports[0];
        port.addEventListener("message", this._onmessage.bind(this), false);
        port.start();
    },

    _onmessage: function(event)
    {
        var request = event.data;
        var result;

        if (request.command in this._handlers)
            result = this._handlers[request.command](request, event.target);
        else
            result = this._status.E_NOTSUPPORTED(request.command);

        if (result && request.requestId)
            this._dispatchCallback(request.requestId, event.target, result);
    },

    _registerHandler: function(command, callback)
    {
        this._handlers[command] = callback;
    }
}

WebInspector.ExtensionServer._statuses =
{
    OK: "",
    E_EXISTS: "Object already exists: %s",
    E_BADARG: "Invalid argument %s: %s",
    E_BADARGTYPE: "Invalid type for argument %s: got %s, expected %s",
    E_NOTFOUND: "Object not found: %s",
    E_NOTSUPPORTED: "Object does not support requested operation: %s",
}

WebInspector.ExtensionStatus = function()
{
    function makeStatus(code)
    {
        var description = WebInspector.ExtensionServer._statuses[code] || code;
        var details = Array.prototype.slice.call(arguments, 1);
        var status = { code: code, description: description, details: details };
        if (code !== "OK") {
            status.isError = true;
            console.log("Extension server error: " + String.vsprintf(description, details));
        }
        return status;
    }
    for (status in WebInspector.ExtensionServer._statuses)
        this[status] = makeStatus.bind(null, status);
}

WebInspector.addExtensions = function(extensions)
{
    WebInspector.extensionServer._addExtensions(extensions);
}

WebInspector.extensionServer = new WebInspector.ExtensionServer();

WebInspector.getEncodedResourceContent = function(identifier, encode, callback)
{
    InspectorBackend.getResourceContent(identifier, encode, callback);
}
