(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./common/util", "./node/util", "@dojo/core/aspect", "http", "path", "fs", "mime-types", "@dojo/core/lang", "@dojo/shim/Promise", "ws"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var util_1 = require("./common/util");
    var util_2 = require("./node/util");
    var aspect_1 = require("@dojo/core/aspect");
    var http_1 = require("http");
    var path_1 = require("path");
    var fs_1 = require("fs");
    var mime_types_1 = require("mime-types");
    var lang_1 = require("@dojo/core/lang");
    var Promise_1 = require("@dojo/shim/Promise");
    var WebSocket = require("ws");
    var Server = (function () {
        function Server(options) {
            lang_1.mixin(this, {
                basePath: '.',
                runInSync: false
            }, options);
        }
        Server.prototype.start = function () {
            var _this = this;
            return new Promise_1.default(function (resolve) {
                var server = _this._httpServer = http_1.createServer(function (request, response) {
                    return _this._handleHttp(request, response);
                });
                _this._sessions = {};
                _this._codeCache = {};
                var sockets = [];
                _this._wsServer = new WebSocket.Server({ port: _this.port + 1 });
                _this._wsServer.on('connection', function (client) {
                    _this.executor.log('WebSocket connection opened:', client);
                    _this._handleWebSocket(client);
                });
                _this._wsServer.on('error', function (error) {
                    _this.executor.emit('error', error);
                });
                // If sockets are not manually destroyed then Node.js will keep itself running until they all expire
                aspect_1.after(server, 'close', function () {
                    var socket;
                    while ((socket = sockets.pop())) {
                        socket.destroy();
                    }
                });
                server.on('connection', function (socket) {
                    sockets.push(socket);
                    _this.executor.log('HTTP connection opened,', sockets.length, 'open connections');
                    socket.on('close', function () {
                        var index = sockets.indexOf(socket);
                        index !== -1 && sockets.splice(index, 1);
                        _this.executor.log('HTTP connection closed,', sockets.length, 'open connections');
                    });
                });
                server.listen(_this.port, function () {
                    resolve();
                });
            });
        };
        Server.prototype.stop = function () {
            var _this = this;
            this.executor.log('Stopping server...');
            var promises = [];
            if (this._httpServer) {
                promises.push(new Promise_1.default(function (resolve) {
                    _this._httpServer.close(resolve);
                }).then(function () {
                    _this.executor.log('Stopped http server');
                    _this._httpServer = null;
                }));
            }
            if (this._wsServer) {
                promises.push(new Promise_1.default(function (resolve) {
                    _this._wsServer.close(resolve);
                }).then(function () {
                    _this.executor.log('Stopped ws server');
                    _this._wsServer = null;
                }));
            }
            return Promise_1.default.all(promises).then(function () {
                _this._codeCache = null;
            });
        };
        /**
         * Listen for all events for a specific session
         */
        Server.prototype.subscribe = function (sessionId, listener) {
            var listeners = this._getSession(sessionId).listeners;
            listeners.push(listener);
            return {
                destroy: function () {
                    this.destroy = function () { };
                    util_1.pullFromArray(listeners, listener);
                }
            };
        };
        Server.prototype._getSession = function (sessionId) {
            var session = this._sessions[sessionId];
            if (!session) {
                session = this._sessions[sessionId] = { listeners: [] };
            }
            return session;
        };
        Server.prototype._handleHttp = function (request, response) {
            var _this = this;
            if (request.method === 'GET') {
                if (/\.js(?:$|\?)/.test(request.url)) {
                    this._handleFile(request, response, true);
                }
                else {
                    this._handleFile(request, response);
                }
            }
            else if (request.method === 'HEAD') {
                this._handleFile(request, response, false, true);
            }
            else if (request.method === 'POST') {
                request.setEncoding('utf8');
                var data_1 = '';
                request.on('data', function (chunk) {
                    data_1 += chunk;
                });
                request.on('end', function () {
                    try {
                        var rawMessages = JSON.parse(data_1);
                        if (!Array.isArray(rawMessages)) {
                            rawMessages = [rawMessages];
                        }
                        var messages = rawMessages.map(function (messageString) {
                            return JSON.parse(messageString);
                        });
                        _this.executor.log('Received HTTP messages');
                        Promise_1.default.all(messages.map(function (message) { return _this._handleMessage(message); })).then(function () {
                            response.statusCode = 204;
                            response.end();
                        }, function () {
                            response.statusCode = 500;
                            response.end();
                        });
                    }
                    catch (error) {
                        response.statusCode = 500;
                        response.end();
                    }
                });
            }
            else {
                response.statusCode = 501;
                response.end();
            }
        };
        Server.prototype._handleFile = function (request, response, shouldInstrument, omitContent) {
            var _this = this;
            var file = /^\/+([^?]*)/.exec(request.url)[1];
            var wholePath;
            this.executor.log('Request for', file);
            if (/^__intern\//.test(file)) {
                wholePath = path_1.join(this.executor.config.internPath, file.replace(/^__intern\//, ''));
                shouldInstrument = false;
            }
            else {
                wholePath = path_1.resolve(path_1.join(this.basePath, file));
            }
            wholePath = util_2.normalizePath(wholePath);
            if (wholePath.charAt(wholePath.length - 1) === '/') {
                wholePath += 'index.html';
            }
            fs_1.stat(wholePath, function (error, stats) {
                // The server was stopped before this file was served
                if (!_this._httpServer) {
                    return;
                }
                if (error || !stats.isFile()) {
                    _this.executor.log('Unable to serve', wholePath, '(unreadable)');
                    _this._send404(response);
                    return;
                }
                _this.executor.log('Serving', wholePath);
                var send = function (contentType, data) {
                    response.writeHead(200, {
                        'Content-Type': contentType,
                        'Content-Length': Buffer.byteLength(data)
                    });
                    response.end(data, function (error) {
                        if (error) {
                            _this.executor.emit('error', new Error("Error serving " + wholePath + ": " + error.message));
                        }
                        else {
                            _this.executor.log('Served', wholePath);
                        }
                    });
                };
                var contentType = mime_types_1.lookup(path_1.basename(wholePath)) || 'application/octet-stream';
                if (shouldInstrument && _this.executor.shouldInstrumentFile(wholePath)) {
                    var mtime_1 = stats.mtime.getTime();
                    var codeCache_1 = _this._codeCache;
                    if (codeCache_1[wholePath] && codeCache_1[wholePath].mtime === mtime_1) {
                        send(contentType, codeCache_1[wholePath].data);
                    }
                    else {
                        fs_1.readFile(wholePath, 'utf8', function (error, data) {
                            // The server was stopped in the middle of the file read
                            if (!_this._httpServer) {
                                return;
                            }
                            if (error) {
                                _this._send404(response);
                                return;
                            }
                            // providing `wholePath` to the instrumenter instead of a partial filename is necessary because
                            // lcov.info requires full path names as per the lcov spec
                            data = _this.executor.instrumentCode(data, wholePath);
                            codeCache_1[wholePath] = {
                                // strictly speaking mtime could reflect a previous version, assume those race conditions are rare
                                mtime: mtime_1,
                                data: data
                            };
                            send(contentType, data);
                        });
                    }
                }
                else {
                    response.writeHead(200, {
                        'Content-Type': contentType,
                        'Content-Length': stats.size
                    });
                    if (omitContent) {
                        response.end();
                    }
                    else {
                        var stream = fs_1.createReadStream(wholePath);
                        stream.pipe(response);
                        stream.on('end', function () {
                            _this.executor.log('Served', wholePath);
                        });
                        stream.on('error', function (error) {
                            _this.executor.log('Error serving', wholePath, ':', error);
                            // If the read stream errors, the write stream has to be manually closed
                            response.end();
                        });
                    }
                }
            });
        };
        Server.prototype._handleMessage = function (message) {
            var _this = this;
            this.executor.log('Processing message [', message.id, '] for ', message.sessionId, ': ', message.name);
            var promise = this._publish(message);
            var shouldWait = getShouldWait(this.runInSync, message);
            if (shouldWait) {
                promise.catch(function (error) {
                    _this.executor.emit('error', error);
                });
                return resolvedPromise;
            }
            return promise;
        };
        Server.prototype._handleWebSocket = function (client) {
            var _this = this;
            client.on('message', function (data) {
                _this.executor.log('Received WebSocket message');
                var message = JSON.parse(data);
                _this._handleMessage(message)
                    .catch(function (error) { return _this.executor.emit('error', error); })
                    .then(function () {
                    // Don't send acks for runEnd, because by the remote will hev been shut down by the time we get
                    // here.
                    if (message.name !== 'runEnd') {
                        _this.executor.log('Sending ack for [', message.id, ']');
                        client.send(JSON.stringify({ id: message.id }), function (error) {
                            if (error) {
                                _this.executor.emit('error', new Error("Error sending ack for [ " + message.id + " ]: " + error.message));
                            }
                        });
                    }
                });
            });
            client.on('error', function (error) {
                _this.executor.log('WebSocket client error:', error);
                _this.executor.emit('error', error);
            });
        };
        Server.prototype._publish = function (message) {
            var listeners = this._getSession(message.sessionId).listeners;
            return Promise_1.default.all(listeners.map(function (listener) { return listener(message.name, message.data); }));
        };
        Server.prototype._send404 = function (response) {
            response.writeHead(404, {
                'Content-Type': 'text/html;charset=utf-8'
            });
            response.end("<!DOCTYPE html><title>404 Not Found</title><h1>404 Not Found</h1>" +
                ("<!-- " + new Array(512).join('.') + " -->"));
        };
        return Server;
    }());
    exports.default = Server;
    var resolvedPromise = Promise_1.default.resolve();
    /**
     * Indicate whether Server should wait for an event to process before sending an acknowlegement.
     */
    function getShouldWait(waitMode, message) {
        var shouldWait = false;
        var eventName = message.name;
        if (waitMode === 'fail') {
            if ((eventName === 'testEnd' && message.data.error) ||
                (eventName === 'suiteEnd' && message.data.error) ||
                eventName === 'error') {
                shouldWait = true;
            }
        }
        else if (waitMode === true) {
            shouldWait = true;
        }
        else if (Array.isArray(waitMode) && waitMode.indexOf(eventName) !== -1) {
            shouldWait = true;
        }
        return shouldWait;
    }
});
//# sourceMappingURL=Server.js.map