/*
 * Copyright (C) 2011 Google Inc. All rights reserved.
 * Copyright (C) 2013, 2015, 2016 Apple Inc. All rights reserved.
 * Copyright (C) 2014 University of Washington.
 *
 * 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.
 */

InspectorBackendClass = class InspectorBackendClass
{
    constructor()
    {
        this._registeredDomains = {};
        this._activeDomains = {};

        this._customTracer = null;
        this._defaultTracer = new WI.LoggingProtocolTracer;
        this._activeTracers = [this._defaultTracer];

        // FIXME: <https://webkit.org/b/213632> Web Inspector: release unused backend domains/events/commands once the debuggable type is known
        this._supportedDomainsForTargetType = new Multimap;
        this._supportedCommandParameters = new Map;
        this._supportedEventParameters = new Map;

        WI.settings.protocolAutoLogMessages.addEventListener(WI.Setting.Event.Changed, this._startOrStopAutomaticTracing, this);
        WI.settings.protocolAutoLogTimeStats.addEventListener(WI.Setting.Event.Changed, this._startOrStopAutomaticTracing, this);
        this._startOrStopAutomaticTracing();

        this.currentDispatchState = {
            event: null,
            request: null,
            response: null,
        };
    }

    // Public

    // This should only be used for getting enum values (`InspectorBackend.Enum.Domain.Type.Item`).
    // Domain/Command/Event feature checking should use one of the `has*` functions below.
    get Enum()
    {
        return this._activeDomains;
    }

    // It's still possible to set this flag on InspectorBackend to just
    // dump protocol traffic as it happens. For more complex uses of
    // protocol data, install a subclass of WI.ProtocolTracer.
    set dumpInspectorProtocolMessages(value)
    {
        // Implicitly cause automatic logging to start if it's allowed.
        WI.settings.protocolAutoLogMessages.value = value;

        this._defaultTracer.dumpMessagesToConsole = value;
    }

    get dumpInspectorProtocolMessages()
    {
        return WI.settings.protocolAutoLogMessages.value;
    }

    set dumpInspectorTimeStats(value)
    {
        WI.settings.protocolAutoLogTimeStats.value = value;

        if (!this.dumpInspectorProtocolMessages)
            this.dumpInspectorProtocolMessages = true;

        this._defaultTracer.dumpTimingDataToConsole = value;
    }

    get dumpInspectorTimeStats()
    {
        return WI.settings.protocolAutoLogTimeStats.value;
    }

    set filterMultiplexingBackendInspectorProtocolMessages(value)
    {
        WI.settings.protocolFilterMultiplexingBackendMessages.value = value;

        this._defaultTracer.filterMultiplexingBackend = value;
    }

    get filterMultiplexingBackendInspectorProtocolMessages()
    {
        return WI.settings.protocolFilterMultiplexingBackendMessages.value;
    }

    set customTracer(tracer)
    {
        console.assert(!tracer || tracer instanceof WI.ProtocolTracer, tracer);
        console.assert(!tracer || tracer !== this._defaultTracer, tracer);

        // Bail early if no state change is to be made.
        if (!tracer && !this._customTracer)
            return;

        if (tracer === this._customTracer)
            return;

        if (tracer === this._defaultTracer)
            return;

        if (this._customTracer)
            this._customTracer.logFinished();

        this._customTracer = tracer;
        this._activeTracers = [this._defaultTracer];

        if (this._customTracer) {
            this._customTracer.logStarted();
            this._activeTracers.push(this._customTracer);
        }
    }

    get activeTracers()
    {
        return this._activeTracers;
    }

    registerDomain(domainName, targetTypes)
    {
        targetTypes = targetTypes || WI.TargetType.all;
        for (let targetType of targetTypes)
            this._supportedDomainsForTargetType.add(targetType, domainName);

        this._registeredDomains[domainName] = new InspectorBackend.Domain(domainName);
    }

    registerVersion(domainName, version)
    {
        let domain = this._registeredDomains[domainName];
        domain.VERSION = version;
    }

    registerEnum(qualifiedName, enumValues)
    {
        let [domainName, enumName] = qualifiedName.split(".");
        let domain = this._registeredDomains[domainName];
        domain._addEnum(enumName, enumValues);
    }

    registerCommand(qualifiedName, targetTypes, callSignature, replySignature)
    {
        let [domainName, commandName] = qualifiedName.split(".");
        let domain = this._registeredDomains[domainName];
        domain._addCommand(targetTypes, new InspectorBackend.Command(qualifiedName, commandName, callSignature, replySignature));
    }

    registerEvent(qualifiedName, targetTypes, signature)
    {
        let [domainName, eventName] = qualifiedName.split(".");
        let domain = this._registeredDomains[domainName];
        domain._addEvent(targetTypes, new InspectorBackend.Event(qualifiedName, eventName, signature));
    }

    registerDispatcher(domainName, dispatcher)
    {
        let domain = this._registeredDomains[domainName];
        domain._addDispatcher(dispatcher);
    }

    activateDomain(domainName, debuggableTypes)
    {
        // FIXME: <https://webkit.org/b/201150> Web Inspector: remove "extra domains" concept now that domains can be added based on the debuggable type

        // Ask `WI.sharedApp` (if it exists) as it may have a different debuggable type if extra
        // domains were activated, which is the only other time this will be called.
        let currentDebuggableType = WI.sharedApp?.debuggableType || InspectorFrontendHost.debuggableInfo.debuggableType;

        if (debuggableTypes && !debuggableTypes.includes(currentDebuggableType))
            return;

        console.assert(domainName in this._registeredDomains);
        console.assert(!(domainName in this._activeDomains));

        let domain = this._registeredDomains[domainName];
        this._activeDomains[domainName] = domain;

        let supportedTargetTypes = WI.DebuggableType.supportedTargetTypes(currentDebuggableType);

        for (let [targetType, command] of domain._supportedCommandsForTargetType) {
            if (!supportedTargetTypes.has(targetType))
                continue;

            let parameters = this._supportedCommandParameters.get(command._qualifiedName);
            if (!parameters) {
                parameters = new Set;
                this._supportedCommandParameters.set(command._qualifiedName, parameters);
            }
            parameters.addAll(command._callSignature.map((item) => item.name));
        }

        for (let [targetType, event] of domain._supportedEventsForTargetType) {
            if (!supportedTargetTypes.has(targetType))
                continue;

            let parameters = this._supportedEventParameters.get(event._qualifiedName);
            if (!parameters) {
                parameters = new Set;
                this._supportedEventParameters.set(event._qualifiedName, parameters);
            }
            parameters.addAll(event._parameterNames);
        }
    }

    dispatch(message)
    {
        InspectorBackend.backendConnection.dispatch(message);
    }

    runAfterPendingDispatches(callback)
    {
        if (!WI.mainTarget) {
            callback();
            return;
        }

        // FIXME: Should this respect pending dispatches in all connections?
        WI.mainTarget.connection.runAfterPendingDispatches(callback);
    }

    supportedDomainsForTargetType(type)
    {
        console.assert(WI.TargetType.all.includes(type), "Unknown target type", type);

        return this._supportedDomainsForTargetType.get(type) || new Set;
    }

    hasDomain(domainName)
    {
        console.assert(!domainName.includes(".") && !domainName.endsWith("Agent"));

        return domainName in this._activeDomains;
    }

    hasCommand(qualifiedName, parameterName)
    {
        console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent."));

        let parameters = this._supportedCommandParameters.get(qualifiedName);
        if (!parameters)
            return false;

        return parameterName === undefined || parameters.has(parameterName);
    }

    hasEvent(qualifiedName, parameterName)
    {
        console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent."));

        let parameters = this._supportedEventParameters.get(qualifiedName);
        if (!parameters)
            return false;

        return parameterName === undefined || parameters.has(parameterName);
    }

    getVersion(domainName)
    {
        console.assert(!domainName.includes(".") && !domainName.endsWith("Agent"));

        let domain = this._activeDomains[domainName];
        if (domain && "VERSION" in domain)
            return domain.VERSION;

        return -Infinity;
    }

    invokeCommand(qualifiedName, targetType, connection, commandArguments, callback)
    {
        let [domainName, commandName] = qualifiedName.split(".");

        let domain = this._activeDomains[domainName];
        return domain._invokeCommand(commandName, targetType, connection, commandArguments, callback);
    }

    // Private

    _makeAgent(domainName, target)
    {
        let domain = this._activeDomains[domainName];
        return domain._makeAgent(target);
    }

    _startOrStopAutomaticTracing()
    {
        this._defaultTracer.dumpMessagesToConsole = this.dumpInspectorProtocolMessages;
        this._defaultTracer.dumpTimingDataToConsole = this.dumpTimingDataToConsole;
        this._defaultTracer.filterMultiplexingBackend = this.filterMultiplexingBackendInspectorProtocolMessages;
    }
};

InspectorBackend = new InspectorBackendClass;

InspectorBackend.Domain = class InspectorBackendDomain
{
    constructor(domainName)
    {
        this._domainName = domainName;

        // Enums are stored directly on the Domain instance using their unqualified
        // type name as the property. Thus, callers can write: Domain.EnumType.

        this._dispatcher = null;

        // FIXME: <https://webkit.org/b/213632> Web Inspector: release unused backend domains/events/commands once the debuggable type is known
        this._supportedCommandsForTargetType = new Multimap;
        this._supportedEventsForTargetType = new Multimap;
    }

    // Private

    _addEnum(enumName, enumValues)
    {
        console.assert(!(enumName in this));
        this[enumName] = enumValues;
    }

    _addCommand(targetTypes, command)
    {
        targetTypes = targetTypes || WI.TargetType.all;
        for (let type of targetTypes)
            this._supportedCommandsForTargetType.add(type, command);
    }

    _addEvent(targetTypes, event)
    {
        targetTypes = targetTypes || WI.TargetType.all;
        for (let type of targetTypes)
            this._supportedEventsForTargetType.add(type, event);
    }

    _addDispatcher(dispatcher)
    {
        console.assert(!this._dispatcher);
        this._dispatcher = dispatcher;
    }

    _makeAgent(target)
    {
        let commands = this._supportedCommandsForTargetType.get(target.type) || new Set;
        let events = this._supportedEventsForTargetType.get(target.type) || new Set;
        return new InspectorBackend.Agent(target, commands, events, this._dispatcher);
    }

    _invokeCommand(commandName, targetType, connection, commandArguments, callback)
    {
        let commands = this._supportedCommandsForTargetType.get(targetType);
        for (let command of commands) {
            if (command._commandName === commandName)
                return command._makeCallable(connection).invoke(commandArguments, callback);
        }

        console.assert();
    }
};

InspectorBackend.Agent = class InspectorBackendAgent
{
    constructor(target, commands, events, dispatcher)
    {
        // Commands are stored directly on the Agent instance using their unqualified
        // method name as the property. Thus, callers can write: DomainAgent.commandName().
        for (let command of commands) {
            this[command._commandName] = command._makeCallable(target.connection);
            target._supportedCommandParameters.set(command._qualifiedName, command);
        }

        this._events = {};
        for (let event of events) {
            this._events[event._eventName] = event;
            target._supportedEventParameters.set(event._qualifiedName, event);
        }

        this._dispatcher = dispatcher ? new dispatcher(target) : null;
    }
};

InspectorBackend.Dispatcher = class InspectorBackendDispatcher
{
    constructor(target)
    {
        console.assert(target instanceof WI.Target);

        this._target = target;
    }
};

InspectorBackend.Command = class InspectorBackendCommand
{
    constructor(qualifiedName, commandName, callSignature, replySignature)
    {
        this._qualifiedName = qualifiedName;
        this._commandName = commandName;
        this._callSignature = callSignature || [];
        this._replySignature = replySignature || [];
    }

    // Private

    _hasParameter(parameterName)
    {
        return this._callSignature.some((item) => item.name === parameterName);
    }

    _makeCallable(connection)
    {
        let instance = new InspectorBackend.Callable(this, connection);

        function callable() {
            console.assert(this instanceof InspectorBackend.Agent);
            return instance._invokeWithArguments.call(instance, Array.from(arguments));
        }
        callable._instance = instance;
        Object.setPrototypeOf(callable, InspectorBackend.Callable.prototype);
        return callable;
    }
};

InspectorBackend.Event = class InspectorBackendEvent
{
    constructor(qualifiedName, eventName, parameterNames)
    {
        this._qualifiedName = qualifiedName;
        this._eventName = eventName;
        this._parameterNames = parameterNames || [];
    }

    // Private

    _hasParameter(parameterName)
    {
        return this._parameterNames.includes(parameterName);
    }
};

// InspectorBackend.Callable can't use ES6 classes because of its trampoline nature.
// But we can use strict mode to get stricter handling of the code inside its functions.
InspectorBackend.Callable = function(command, connection)
{
    "use strict";

    this._command = command;
    this._connection = connection;

    this._instance = this;
};

// As part of the workaround to make commands callable, these functions use `this._instance`.
// `this` could refer to the callable trampoline, or the InspectorBackend.Callable instance.
InspectorBackend.Callable.prototype = {
    __proto__: Function.prototype,

    // Public

    invoke(commandArguments, callback)
    {
        "use strict";

        let command = this._instance._command;
        let connection = this._instance._connection;

        function deliverFailure(message) {
            console.error(`Protocol Error: ${message}`);
            if (callback)
                setTimeout(callback.bind(null, message), 0);
            else
                return Promise.reject(new Error(message));
        }

        if (typeof commandArguments !== "object")
            return deliverFailure(`invoke expects an object for command arguments but its type is '${typeof commandArguments}'.`);

        let parameters = {};
        for (let {name, type, optional} of command._callSignature) {
            if (!(name in commandArguments) && !optional)
                return deliverFailure(`Missing argument '${name}' for command '${command._qualifiedName}'.`);

            let value = commandArguments[name];
            if (optional && value === undefined)
                continue;

            if (typeof value !== type)
                return deliverFailure(`Invalid type of argument '${name}' for command '${command._qualifiedName}' call. It must be '${type}' but it is '${typeof value}'.`);

            parameters[name] = value;
        }

        if (typeof callback === "function")
            connection._sendCommandToBackendWithCallback(command, parameters, callback);
        else
            return connection._sendCommandToBackendExpectingPromise(command, parameters);
    },

    // Private

    _invokeWithArguments(commandArguments)
    {
        "use strict";

        let command = this._instance._command;
        let connection = this._instance._connection;
        let callback = typeof commandArguments.lastValue === "function" ? commandArguments.pop() : null;

        function deliverFailure(message) {
            console.error(`Protocol Error: ${message}`);
            if (callback)
                setTimeout(callback.bind(null, message), 0);
            else
                return Promise.reject(new Error(message));
        }

        let parameters = {};
        for (let {name, type, optional} of command._callSignature) {
            if (!commandArguments.length && !optional)
                return deliverFailure(`Invalid number of arguments for command '${command._qualifiedName}'.`);

            let value = commandArguments.shift();
            if (optional && value === undefined)
                continue;

            if (typeof value !== type)
                return deliverFailure(`Invalid type of argument '${name}' for command '${command._qualifiedName}' call. It must be '${type}' but it is '${typeof value}'.`);

            parameters[name] = value;
        }

        if (!callback && commandArguments.length === 1 && commandArguments[0] !== undefined)
            return deliverFailure(`Protocol Error: Optional callback argument for command '${command._qualifiedName}' call must be a function but its type is '${typeof commandArguments[0]}'.`);

        if (callback)
            connection._sendCommandToBackendWithCallback(command, parameters, callback);
        else
            return connection._sendCommandToBackendExpectingPromise(command, parameters);
    }
};
