/* --------------------------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See License.txt in the project root for license information.
 * ------------------------------------------------------------------------------------------ */
/// <reference path="./thenable.ts" />
'use strict';
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
function __export(m) {
    for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
var is = require("./is");
var messages_1 = require("./messages");
exports.RequestType = messages_1.RequestType;
exports.RequestType0 = messages_1.RequestType0;
exports.RequestType1 = messages_1.RequestType1;
exports.RequestType2 = messages_1.RequestType2;
exports.RequestType3 = messages_1.RequestType3;
exports.RequestType4 = messages_1.RequestType4;
exports.RequestType5 = messages_1.RequestType5;
exports.RequestType6 = messages_1.RequestType6;
exports.RequestType7 = messages_1.RequestType7;
exports.RequestType8 = messages_1.RequestType8;
exports.RequestType9 = messages_1.RequestType9;
exports.ResponseError = messages_1.ResponseError;
exports.ErrorCodes = messages_1.ErrorCodes;
exports.NotificationType = messages_1.NotificationType;
exports.NotificationType0 = messages_1.NotificationType0;
exports.NotificationType1 = messages_1.NotificationType1;
exports.NotificationType2 = messages_1.NotificationType2;
exports.NotificationType3 = messages_1.NotificationType3;
exports.NotificationType4 = messages_1.NotificationType4;
exports.NotificationType5 = messages_1.NotificationType5;
exports.NotificationType6 = messages_1.NotificationType6;
exports.NotificationType7 = messages_1.NotificationType7;
exports.NotificationType8 = messages_1.NotificationType8;
exports.NotificationType9 = messages_1.NotificationType9;
var messageReader_1 = require("./messageReader");
exports.StreamMessageReader = messageReader_1.StreamMessageReader;
exports.IPCMessageReader = messageReader_1.IPCMessageReader;
var messageWriter_1 = require("./messageWriter");
exports.StreamMessageWriter = messageWriter_1.StreamMessageWriter;
exports.IPCMessageWriter = messageWriter_1.IPCMessageWriter;
var events_1 = require("./events");
exports.Disposable = events_1.Disposable;
exports.Event = events_1.Event;
exports.Emitter = events_1.Emitter;
var cancellation_1 = require("./cancellation");
exports.CancellationTokenSource = cancellation_1.CancellationTokenSource;
exports.CancellationToken = cancellation_1.CancellationToken;
__export(require("./pipeSupport"));
var CancelNotification;
(function (CancelNotification) {
    CancelNotification.type = new messages_1.NotificationType('$/cancelRequest');
})(CancelNotification || (CancelNotification = {}));
var Trace;
(function (Trace) {
    Trace[Trace["Off"] = 0] = "Off";
    Trace[Trace["Messages"] = 1] = "Messages";
    Trace[Trace["Verbose"] = 2] = "Verbose";
})(Trace = exports.Trace || (exports.Trace = {}));
(function (Trace) {
    function fromString(value) {
        value = value.toLowerCase();
        switch (value) {
            case 'off':
                return Trace.Off;
            case 'messages':
                return Trace.Messages;
            case 'verbose':
                return Trace.Verbose;
            default:
                return Trace.Off;
        }
    }
    Trace.fromString = fromString;
    function toString(value) {
        switch (value) {
            case Trace.Off:
                return 'off';
            case Trace.Messages:
                return 'messages';
            case Trace.Verbose:
                return 'verbose';
            default:
                return 'off';
        }
    }
    Trace.toString = toString;
})(Trace = exports.Trace || (exports.Trace = {}));
var SetTraceNotification;
(function (SetTraceNotification) {
    SetTraceNotification.type = new messages_1.NotificationType('$/setTraceNotification');
})(SetTraceNotification = exports.SetTraceNotification || (exports.SetTraceNotification = {}));
var LogTraceNotification;
(function (LogTraceNotification) {
    LogTraceNotification.type = new messages_1.NotificationType('$/logTraceNotification');
})(LogTraceNotification = exports.LogTraceNotification || (exports.LogTraceNotification = {}));
var ConnectionErrors;
(function (ConnectionErrors) {
    /**
     * The connection is closed.
     */
    ConnectionErrors[ConnectionErrors["Closed"] = 1] = "Closed";
    /**
     * The connection got disposed.
     */
    ConnectionErrors[ConnectionErrors["Disposed"] = 2] = "Disposed";
    /**
     * The connection is already in listening mode.
     */
    ConnectionErrors[ConnectionErrors["AlreadyListening"] = 3] = "AlreadyListening";
})(ConnectionErrors = exports.ConnectionErrors || (exports.ConnectionErrors = {}));
var ConnectionError = (function (_super) {
    __extends(ConnectionError, _super);
    function ConnectionError(code, message) {
        var _this = _super.call(this, message) || this;
        _this.code = code;
        Object.setPrototypeOf(_this, ConnectionError.prototype);
        return _this;
    }
    return ConnectionError;
}(Error));
exports.ConnectionError = ConnectionError;
var ConnectionState;
(function (ConnectionState) {
    ConnectionState[ConnectionState["New"] = 1] = "New";
    ConnectionState[ConnectionState["Listening"] = 2] = "Listening";
    ConnectionState[ConnectionState["Closed"] = 3] = "Closed";
    ConnectionState[ConnectionState["Disposed"] = 4] = "Disposed";
})(ConnectionState || (ConnectionState = {}));
function _createMessageConnection(messageReader, messageWriter, logger) {
    var sequenceNumber = 0;
    var version = '2.0';
    var requestHandlers = Object.create(null);
    var notificationHandlers = Object.create(null);
    var responsePromises = Object.create(null);
    var requestTokens = Object.create(null);
    var trace = Trace.Off;
    var tracer;
    var state = ConnectionState.New;
    var errorEmitter = new events_1.Emitter();
    var closeEmitter = new events_1.Emitter();
    var unhandledNotificationEmitter = new events_1.Emitter();
    var disposeEmitter = new events_1.Emitter();
    function isListening() {
        return state === ConnectionState.Listening;
    }
    function isClosed() {
        return state === ConnectionState.Closed;
    }
    function isDisposed() {
        return state === ConnectionState.Disposed;
    }
    function closeHandler() {
        if (state === ConnectionState.New || state === ConnectionState.Listening) {
            state = ConnectionState.Closed;
            closeEmitter.fire(undefined);
        }
        // If the connection is disposed don't sent close events.
    }
    ;
    function readErrorHandler(error) {
        errorEmitter.fire([error, undefined, undefined]);
    }
    function writeErrorHandler(data) {
        errorEmitter.fire(data);
    }
    messageReader.onClose(closeHandler);
    messageReader.onError(readErrorHandler);
    messageWriter.onClose(closeHandler);
    messageWriter.onError(writeErrorHandler);
    function handleRequest(requestMessage) {
        if (isDisposed()) {
            // we return here silently since we fired an event when the
            // connection got disposed.
            return;
        }
        function reply(resultOrError, method, startTime) {
            var message = {
                jsonrpc: version,
                id: requestMessage.id
            };
            if (resultOrError instanceof messages_1.ResponseError) {
                message.error = resultOrError.toJson();
            }
            else {
                message.result = resultOrError === void 0 ? null : resultOrError;
            }
            traceSendingResponse(message, method, startTime);
            messageWriter.write(message);
        }
        function replyError(error, method, startTime) {
            var message = {
                jsonrpc: version,
                id: requestMessage.id,
                error: error.toJson()
            };
            traceSendingResponse(message, method, startTime);
            messageWriter.write(message);
        }
        function replySuccess(result, method, startTime) {
            // The JSON RPC defines that a response must either have a result or an error
            // So we can't treat undefined as a valid response result.
            if (result === void 0) {
                result = null;
            }
            var message = {
                jsonrpc: version,
                id: requestMessage.id,
                result: result
            };
            traceSendingResponse(message, method, startTime);
            messageWriter.write(message);
        }
        traceReceviedRequest(requestMessage);
        var requestHandler = requestHandlers[requestMessage.method];
        var startTime = Date.now();
        if (requestHandler) {
            var cancellationSource = new cancellation_1.CancellationTokenSource();
            var tokenKey_1 = String(requestMessage.id);
            requestTokens[tokenKey_1] = cancellationSource;
            try {
                var handlerResult = void 0;
                if (!requestMessage.params) {
                    handlerResult = requestHandler(cancellationSource.token);
                }
                else if (is.array(requestMessage.params)) {
                    handlerResult = requestHandler.apply(void 0, requestMessage.params.concat([cancellationSource.token]));
                }
                else {
                    handlerResult = requestHandler(requestMessage.params, cancellationSource.token);
                }
                var promise = handlerResult;
                if (!handlerResult) {
                    delete requestTokens[tokenKey_1];
                    replySuccess(handlerResult, requestMessage.method, startTime);
                }
                else if (promise.then) {
                    promise.then(function (resultOrError) {
                        delete requestTokens[tokenKey_1];
                        reply(resultOrError, requestMessage.method, startTime);
                    }, function (error) {
                        delete requestTokens[tokenKey_1];
                        if (error instanceof messages_1.ResponseError) {
                            replyError(error, requestMessage.method, startTime);
                        }
                        else if (error && is.string(error.message)) {
                            replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, "Request " + requestMessage.method + " failed with message: " + error.message), requestMessage.method, startTime);
                        }
                        else {
                            replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, "Request " + requestMessage.method + " failed unexpectedly without providing any details."), requestMessage.method, startTime);
                        }
                    });
                }
                else {
                    delete requestTokens[tokenKey_1];
                    reply(handlerResult, requestMessage.method, startTime);
                }
            }
            catch (error) {
                delete requestTokens[tokenKey_1];
                if (error instanceof messages_1.ResponseError) {
                    reply(error, requestMessage.method, startTime);
                }
                else if (error && is.string(error.message)) {
                    replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, "Request " + requestMessage.method + " failed with message: " + error.message), requestMessage.method, startTime);
                }
                else {
                    replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, "Request " + requestMessage.method + " failed unexpectedly without providing any details."), requestMessage.method, startTime);
                }
            }
        }
        else {
            replyError(new messages_1.ResponseError(messages_1.ErrorCodes.MethodNotFound, "Unhandled method " + requestMessage.method), requestMessage.method, startTime);
        }
    }
    function handleResponse(responseMessage) {
        if (isDisposed()) {
            // See handle request.
            return;
        }
        var key = String(responseMessage.id);
        var responsePromise = responsePromises[key];
        traceReceviedResponse(responseMessage, responsePromise);
        if (responsePromise) {
            delete responsePromises[key];
            try {
                if (responseMessage.error) {
                    var error = responseMessage.error;
                    responsePromise.reject(new messages_1.ResponseError(error.code, error.message, error.data));
                }
                else if (responseMessage.result !== void 0) {
                    responsePromise.resolve(responseMessage.result);
                }
                else {
                    throw new Error('Should never happen.');
                }
            }
            catch (error) {
                if (error.message) {
                    logger.error("Response handler '" + responsePromise.method + "' failed with message: " + error.message);
                }
                else {
                    logger.error("Response handler '" + responsePromise.method + "' failed unexpectedly.");
                }
            }
        }
    }
    function handleNotification(message) {
        if (isDisposed()) {
            // See handle request.
            return;
        }
        var notificationHandler;
        if (message.method === CancelNotification.type.method) {
            notificationHandler = function (params) {
                var id = params.id;
                var source = requestTokens[String(id)];
                if (source) {
                    source.cancel();
                }
            };
        }
        else {
            notificationHandler = notificationHandlers[message.method];
        }
        if (notificationHandler) {
            try {
                traceReceivedNotification(message);
                if (!message.params) {
                    notificationHandler();
                }
                else if (is.array(message.params)) {
                    notificationHandler.apply(void 0, message.params);
                }
                else {
                    notificationHandler(message.params);
                }
            }
            catch (error) {
                if (error.message) {
                    logger.error("Notification handler '" + message.method + "' failed with message: " + error.message);
                }
                else {
                    logger.error("Notification handler '" + message.method + "' failed unexpectedly.");
                }
            }
        }
        else {
            unhandledNotificationEmitter.fire(message);
        }
    }
    function handleInvalidMessage(message) {
        if (!message) {
            logger.error('Received empty message.');
            return;
        }
        logger.error("Received message which is neither a response nor a notification message:\n" + JSON.stringify(message, null, 4));
        // Test whether we find an id to reject the promise
        var responseMessage = message;
        if (is.string(responseMessage.id) || is.number(responseMessage.id)) {
            var key = String(responseMessage.id);
            var responseHandler = responsePromises[key];
            if (responseHandler) {
                responseHandler.reject(new Error('The received response has neither a result nor an error property.'));
            }
        }
    }
    function traceSendingRequest(message) {
        if (trace === Trace.Off || !tracer) {
            return;
        }
        var data = undefined;
        if (trace === Trace.Verbose && message.params) {
            data = "Params: " + JSON.stringify(message.params, null, 4) + "\n\n";
        }
        tracer.log("Sending request '" + message.method + " - (" + message.id + ")'.", data);
    }
    function traceSendNotification(message) {
        if (trace === Trace.Off || !tracer) {
            return;
        }
        var data = undefined;
        if (trace === Trace.Verbose) {
            if (message.params) {
                data = "Params: " + JSON.stringify(message.params, null, 4) + "\n\n";
            }
            else {
                data = 'No parameters provided.\n\n';
            }
        }
        tracer.log("Sending notification '" + message.method + "'.", data);
    }
    function traceSendingResponse(message, method, startTime) {
        if (trace === Trace.Off || !tracer) {
            return;
        }
        var data = undefined;
        if (trace === Trace.Verbose) {
            if (message.error && message.error.data) {
                data = "Error data: " + JSON.stringify(message.error.data, null, 4) + "\n\n";
            }
            else {
                if (message.result) {
                    data = "Result: " + JSON.stringify(message.result, null, 4) + "\n\n";
                }
                else if (message.error === void 0) {
                    data = 'No result returned.\n\n';
                }
            }
        }
        tracer.log("Sending response '" + method + " - (" + message.id + ")'. Processing request took " + (Date.now() - startTime) + "ms");
    }
    function traceReceviedRequest(message) {
        if (trace === Trace.Off || !tracer) {
            return;
        }
        var data = undefined;
        if (trace === Trace.Verbose && message.params) {
            data = "Params: " + JSON.stringify(message.params, null, 4) + "\n\n";
        }
        tracer.log("Received request '" + message.method + " - (" + message.id + ")'.", data);
    }
    function traceReceivedNotification(message) {
        if (trace === Trace.Off || !tracer || message.method === LogTraceNotification.type.method) {
            return;
        }
        var data = undefined;
        if (trace === Trace.Verbose) {
            if (message.params) {
                data = "Params: " + JSON.stringify(message.params, null, 4) + "\n\n";
            }
            else {
                data = 'No parameters provided.\n\n';
            }
        }
        tracer.log("Received notification '" + message.method + "'.", data);
    }
    function traceReceviedResponse(message, responsePromise) {
        if (trace === Trace.Off || !tracer) {
            return;
        }
        var data = undefined;
        if (trace === Trace.Verbose) {
            if (message.error && message.error.data) {
                data = "Error data: " + JSON.stringify(message.error.data, null, 4) + "\n\n";
            }
            else {
                if (message.result) {
                    data = "Result: " + JSON.stringify(message.result, null, 4) + "\n\n";
                }
                else if (message.error === void 0) {
                    data = 'No result returned.\n\n';
                }
            }
        }
        if (responsePromise) {
            var error = message.error ? " Request failed: " + message.error.message + " (" + message.error.code + ")." : '';
            tracer.log("Received response '" + responsePromise.method + " - (" + message.id + ")' in " + (Date.now() - responsePromise.timerStart) + "ms." + error, data);
        }
        else {
            tracer.log("Received response " + message.id + " without active response promise.", data);
        }
    }
    var callback = function (message) {
        if (messages_1.isRequestMessage(message)) {
            handleRequest(message);
        }
        else if (messages_1.isReponseMessage(message)) {
            handleResponse(message);
        }
        else if (messages_1.isNotificationMessage(message)) {
            handleNotification(message);
        }
        else {
            handleInvalidMessage(message);
        }
    };
    function throwIfClosedOrDisposed() {
        if (isClosed()) {
            throw new ConnectionError(ConnectionErrors.Closed, 'Connection is closed.');
        }
        if (isDisposed()) {
            throw new ConnectionError(ConnectionErrors.Disposed, 'Connection is disposed.');
        }
    }
    function throwIfListening() {
        if (isListening()) {
            throw new ConnectionError(ConnectionErrors.AlreadyListening, 'Connection is already listening');
        }
    }
    function computeMessageParams(type, params) {
        var result;
        var numberOfParams = type.numberOfParams;
        switch (numberOfParams) {
            case 0:
                result = null;
                break;
            case 1:
                result = params[0] || null;
                break;
            default:
                result = [];
                for (var i = 0; i < params.length && i < numberOfParams; i++) {
                    result.push(params[i] || null);
                }
                if (params.length < numberOfParams) {
                    for (var i = params.length; i < numberOfParams; i++) {
                        result.push(null);
                    }
                }
                break;
        }
        return result;
    }
    var connection = {
        sendNotification: function (type) {
            var params = [];
            for (var _i = 1; _i < arguments.length; _i++) {
                params[_i - 1] = arguments[_i];
            }
            throwIfClosedOrDisposed();
            var method;
            var messageParams;
            if (is.string(type)) {
                method = type;
                switch (params.length) {
                    case 0:
                        messageParams = null;
                        break;
                    case 1:
                        messageParams = params[0];
                        break;
                    default:
                        messageParams = params;
                        break;
                }
            }
            else {
                method = type.method;
                messageParams = computeMessageParams(type, params);
            }
            var notificatioMessage = {
                jsonrpc: version,
                method: is.string(type) ? type : type.method,
                params: messageParams
            };
            traceSendNotification(notificatioMessage);
            messageWriter.write(notificatioMessage);
        },
        onNotification: function (type, handler) {
            throwIfClosedOrDisposed();
            notificationHandlers[is.string(type) ? type : type.method] = handler;
        },
        sendRequest: function (type) {
            var params = [];
            for (var _i = 1; _i < arguments.length; _i++) {
                params[_i - 1] = arguments[_i];
            }
            throwIfClosedOrDisposed();
            var method;
            var messageParams;
            var token = undefined;
            if (is.string(type)) {
                method = type;
                switch (params.length) {
                    case 0:
                        messageParams = null;
                        break;
                    case 1:
                        // The cancellation token is optional so it can also be undefined.
                        if (cancellation_1.CancellationToken.is(params[0])) {
                            messageParams = null;
                            token = params[0];
                        }
                        else {
                            messageParams = params[0] || null;
                        }
                        break;
                    default:
                        var last = params.length - 1;
                        if (cancellation_1.CancellationToken.is(params[last])) {
                            token = params[last];
                            if (params.length === 2) {
                                messageParams = params[0] || null;
                            }
                            else {
                                messageParams = params.slice(0, last).map(function (value) { return value || null; });
                            }
                        }
                        else {
                            messageParams = params.map(function (value) { return value || null; });
                        }
                        break;
                }
            }
            else {
                method = type.method;
                messageParams = computeMessageParams(type, params);
                var numberOfParams = type.numberOfParams;
                token = cancellation_1.CancellationToken.is(params[numberOfParams]) ? params[numberOfParams] : undefined;
            }
            var id = sequenceNumber++;
            var result = new Promise(function (resolve, reject) {
                var requestMessage = {
                    jsonrpc: version,
                    id: id,
                    method: method,
                    params: messageParams
                };
                var responsePromise = { method: method, timerStart: Date.now(), resolve: resolve, reject: reject };
                traceSendingRequest(requestMessage);
                try {
                    messageWriter.write(requestMessage);
                }
                catch (e) {
                    // Writing the message failed. So we need to reject the promise.
                    responsePromise.reject(new messages_1.ResponseError(messages_1.ErrorCodes.MessageWriteError, e.message ? e.message : 'Unknown reason'));
                    responsePromise = null;
                }
                if (responsePromise) {
                    responsePromises[String(id)] = responsePromise;
                }
            });
            if (token) {
                token.onCancellationRequested(function () {
                    connection.sendNotification(CancelNotification.type, { id: id });
                });
            }
            return result;
        },
        onRequest: function (type, handler) {
            throwIfClosedOrDisposed();
            requestHandlers[is.string(type) ? type : type.method] = handler;
        },
        trace: function (_value, _tracer, sendNotification) {
            if (sendNotification === void 0) { sendNotification = false; }
            trace = _value;
            if (trace === Trace.Off) {
                tracer = undefined;
            }
            else {
                tracer = _tracer;
            }
            if (sendNotification && !isClosed() && !isDisposed()) {
                connection.sendNotification(SetTraceNotification.type, { value: Trace.toString(_value) });
            }
        },
        onError: errorEmitter.event,
        onClose: closeEmitter.event,
        onUnhandledNotification: unhandledNotificationEmitter.event,
        onDispose: disposeEmitter.event,
        dispose: function () {
            if (isDisposed()) {
                return;
            }
            state = ConnectionState.Disposed;
            disposeEmitter.fire(undefined);
            var error = new Error('Connection got disposed.');
            Object.keys(responsePromises).forEach(function (key) {
                responsePromises[key].reject(error);
            });
            responsePromises = Object.create(null);
            requestTokens = Object.create(null);
        },
        listen: function () {
            throwIfClosedOrDisposed();
            throwIfListening();
            state = ConnectionState.Listening;
            messageReader.listen(callback);
        }
    };
    connection.onNotification(LogTraceNotification.type, function (params) {
        if (trace === Trace.Off || !tracer) {
            return;
        }
        tracer.log(params.message, trace === Trace.Verbose ? params.verbose : undefined);
    });
    return connection;
}
function isMessageReader(value) {
    return value.listen !== void 0 && value.read === void 0;
}
function isMessageWriter(value) {
    return value.write !== void 0 && value.end === void 0;
}
function createMessageConnection(input, output, logger) {
    var reader = isMessageReader(input) ? input : new messageReader_1.StreamMessageReader(input);
    var writer = isMessageWriter(output) ? output : new messageWriter_1.StreamMessageWriter(output);
    return _createMessageConnection(reader, writer, logger);
}
exports.createMessageConnection = createMessageConnection;
