# Source: ServerV0_01.js

    ﻿/**
     * @file Dieser Server kann Clients in verschiedenen Raeumen innerhalb von mehreren Namensraeumen verwalten.
     * <br>Die Clients koennen Raeumen beitreten, sie verlassen, untereinander Nachrichten austauschen und diverse
     * andere Events an den Server schicken.
     * <br>Die Clients koennen ueber global eindeutige UUIDs angesprochen werden.
     * <br>Die UUID wird dem Server beim Login mitgeteilt. Ohne Login wird die Verbindung getrennt.
     */

    // ######## Website rendering ########

    // Benötigtes Modul für das Einlesen von externen Dateien.
    const fs = require('fs');

    let connectedClients = 0;
    let consoleLog = [];
    let log;

    /**
     * Gibt die aktuelle Situation des Servers als HTML-Seite aus.
     * @param {*} request Der Request der zum Server geschickt wird.
     * @param {*} response Die Response die vom Server zum Client geschickt wird.
     */
    let webseite = function (request, response) {

        // Aktualisierung der Räume und deren Clients.
        roomClientMap = roomClientMapping();

        response.writeHead(200, { 'Content-Type': 'text/html' });

        response.write('<!doctype html>\n');
        response.write('<html lang="de">\n\n');

        response.write('<head>\n');
        response.write('<meta charset="utf-8">\n');
        //        response.write('<meta http-equiv="Refresh" content="1">\n'); // refresh page every secound
        response.write('<title>Serverdetails</title>\n');
        response.write('</head>\n\n');

        response.write('<style>\n');
        response.write(fs.readFileSync('ServerStyles.css', { encoding: 'utf8' }));
        response.write('</style>\n');

        response.write('<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>\n');
        response.write('<script>\n');
        response.write(fs.readFileSync('control.js', { encoding: 'utf8' }));
        response.write('</script>\n');

        response.write('<body>\n');

        response.write('<h1 id="test">Serverdetails</h1>\n');

        response.write('<div class="serverListe">\n');
        response.write('<h2>Liste des Servers</h2>\n');
        response.write('<ul>\n');

        Object.keys(io.nsps).forEach((ns) => {
            response.write('<li><div class="firstListLevel">' + ns + '</div></li>\n'); // namespace

            var resultList = [];
            var rooms = io.nsps[ns].adapter.rooms;

            response.write('<ul>\n');

            var i = 0;
            for (var room in rooms) {
                if (room.charAt(0) !== '/') {
                    resultList[i] = room;
                }
                i++;
            }

            var j = 0;
            resultList.forEach(function (room) {
                if (room !== null) {

                    response.write('<li><div class="secondListLevel">' + room + '</div></li>\n'); // rooom
                    response.write('<ul>\n');
                    if (roomClientMap[room]) {
                        roomClientMap[room].forEach(function (entry) {

                            if (clientSocketIds[entry] !== undefined) {
                                response.write('<li><div class="thirdListLevel">' + clientSocketIds[entry].username + "#" + clientSocketIds[entry].UUID + '</div></li>\n'); // client
                            } else {
                                response.write('<li><div class="thirdListLevel">' + entry + '</div></li>\n'); // client
                            }
                        });
                    }

                    response.write('</ul>\n');
                }
                j++;
            });

            response.write('</ul>\n');
        });

        response.write('</ul>\n');
        response.write('</div>\n');

        response.write('<div class="clientsConnected">\n');
        response.write('<h2>Verbindungen ' + connectedClients + '</h2>\n');
        response.write('</div>\n');

        response.write('<div class="legende">\n');
        response.write('<h2>Legende</h2>\n');
        response.write('<ul>\n');
        response.write('<li><div class="firstListLevel">Namespace</div></li>\n');
        response.write('<ul>\n');
        response.write('<li><div class="secondListLevel">Room</div></li>\n');
        response.write('<ul>\n');
        response.write('<li><div class="thirdListLevel">Client</div></li>\n');
        response.write('</ul>\n');
        response.write('</ul>\n');
        response.write('</ul>\n');
        response.write('</div>\n');

        response.write('<div class="serverControl">\n');
        response.write('<h2>Aktualisieren</h2>\n');
        response.write('<button onclick="startAutoRefresh()">AutoRefresh On</button>\n');
        response.write('<button onclick="stopAutoRefresh()">AutoRefresh Off</button>\n');
        //            response.write('<button onclick="refresh()">Refresh Page</button>\n');
        response.write('<p id="refreshText"></p>\n');
        response.write('</div>\n');

        response.write('<div class="serverConsole">\n');
        response.write('<h2>Serverkonsole</h2>\n');
        response.write('<textarea id="console">');
        var maxEintraege = 25;
        var laengeMinusMaxEintraege = 0;
        if (consoleLog.length > maxEintraege) {
            laengeMinusMaxEintraege = consoleLog.length - maxEintraege;
        }
        for (var i = laengeMinusMaxEintraege; i < consoleLog.length; i++) {
            if (consoleLog[i] instanceof Object) {
                response.write(JSON.stringify(consoleLog[i]) + '\n');
            } else {
                response.write(consoleLog[i] + '\n');
            }
        }
        response.write('</textarea>');
        response.write('</div>\n');

        response.write('</body>\n');
        response.write('</html>\n');

        response.write('\n');

        response.end();
    };
    // ---------------------------------------------------------------------------->

    // ######## Server ########
    // Serverparameter.
    const serverDomain = 'pi-lab2.w-hs.de';
    const serverPort = 65400;

    // Benötigte NPM Module.
    const http = require('http');
    const server = http.createServer(webseite).listen(serverPort);
    const io = require('socket.io')(server, { 'pingTimeout': 5000, 'pingInterval': 700, 'transports': ['websocket', 'htmlfile', 'xhr-polling'] });

    // Server-Startmeldung.
    serverLog('Server laeuft auf http://' + serverDomain + ':' + serverPort);

    //Mapping von id zu client.
    const clientUUIDs = {};
    const clientSocketIds = {};

    //Zeit in Sekunden bis ein Socket, der sich noch nicht erfolgreich eingeloggt hat disconnectet wird.
    const timeOutInSec = 10;

    // Inhalt der JSON-Konfiguraitionsdatei auslesen und abspeichern.
    var configDatei = fs.readFileSync("serverConfig.json");
    var configuration = JSON.parse(configDatei);

    // Speicherung der Räume und dazugehörigen Clients.
    var roomClientMap = {};

    // Festlegung der Funktionen des Servers für jeden Namensraum.
    configuration.forEach(function (app) {

        io.of('/' + app.name).on('connection', function (client) {

            connectedClients++;

            serverLog('Client mit SocketID: ' + client.id + ' hat sich mit Namespace: ' + app.name + ' verbunden!');

            /**
             * "verbunden" wird abgesendet sobald eine Verbindung hergestellt wurde.
             * @event verbunden
             */
            client.emit('verbunden');
            client.isLoggedIn = false;

            client.on('login', (data) => {
                onLogin(client, app.name, app.version, data);
            });

            /**
             * Ueberprueft ob der Client sich nach einer bestimmten Zeit (timeOutInSec) eingeloggt hat
             * und trennt die Verbindung, wenn er es nicht getan hat.
             */
            function checkLogin() {
                if (!client.isLoggedIn && client.connected) {
                    serverLog('Client mit SocketID: ' + client.id + ' hat sich nicht rechtzeitig eingeloggt.');
                    client.disconnect();
                }
            }
            setTimeout(checkLogin, 1000 * timeOutInSec);

            client.on('nachricht', function (nachricht) {
                onNachricht(app.name, client, nachricht);
            });

            client.on('raumBeitreten', function (raumObjekt) {
                onRaumBeitreten(client, raumObjekt);
            });

            client.on('raumVerlassen', function (raumObjekt) {
                onRaumVerlassen(client, raumObjekt);
            });

            client.on('zeigeBenutzerImRaum', function (raumObjekt) {
                onZeigeBenutzerImRaum(app.name, raumObjekt, client);
            });

            /**
             * Entfernt den Client vom Server und loggt die Trennung der Verbindung
             */
            let logDisconnect = function () {
                client.isLoggedIn = false;
                delete clientUUIDs[client.UUID];
                delete clientSocketIds[client.id];
                serverLog('Client with SocketID: ' + client.id + ' disconnected!');

                connectedClients--;
            };
            client.on('disconnect', function (reason) {
                logDisconnect();
                serverLog('Disconnect because: ' + reason);
            });
            client.on('close', logDisconnect);
        });
    });

    // ### Listener Funktionen ###

    /**
     * Das Login Event muss der Client vor Ablauf von "timeOutInSec" absenden,
     * damit die Verbindung aufrecht erhalten wird und die Verbindung vom
     * Server verwaltet wird.
     * <br>Beim Senden des Events muss das JSONObjekt "data" mitgesendet werden,
     * dass die noetigen Felder enthaelt.
     * @param {client} client Client, der sich einloggt
     * @param {string} appName Name des Namensraumes, in den der Client sich einloggen moechte.
     * @param {string} appVersion Version der Anwendung des Clients
     * @param {*} data.UUID Die UUID des Clients
     * @param {string} data.app Name der App mit Version (z.B. "bao0.0.1")
     * @param {string} data.username Der gewaehlte Username des Clients
     *
     * @fires login
     */
    function onLogin(client, appName, appVersion, data) {
        if (appName + appVersion === data.app) {

            if (clientUUIDs[data.UUID]) {
                serverLog('ID ist bereits auf dem Server und wird entfernt.');
                clientUUIDs[data.UUID].disconnect();
            }

            clientUUIDs[data.UUID] = client;
            clientSocketIds[client.id] = client;

            client.username = data.benutzername;
            client.UUID = data.UUID;

            /**
            * true, wenn der Login erfolgreich war, sonst false
            * @event login
            * @type {boolean}
            */
            client.emit('login', true);
            serverLog('Login erfolgreich. Name: ' + client.username + ' ID: ' + client.UUID);

            client.isReadyToPlay = true;
            client.isLoggedIn = true;

        } else {
            client.emit('login', false);
            serverLog('App inkompatibel. Login fehlgeschlagen.');

            client.disconnect();
        }
    }

    /**
     * Leitet eine Nachricht an einen anderen Client oder an alle Clients in einem Raum weiter.
     * <br> Beim Senden dieses Events muss das JSONObjekt "nachricht" mitgesendet werden, das
     * die noetigen Felder enthaelt.<br>
     * Entweder nachricht.UUID oder nachricht.room muss angegeben werden.
     *
     * @param {string} appName Der Namensraum, in dem die Nachricht weitergeleitet wird
     * @param {object} nachricht
     * @property {string} nachricht.UUID optionaler Parameter, falls ein einzelner Client benachrichtigt werden soll
     * @property {string} nachricht.raum optinaler Paramter, falls jeder Client in einem Raum benachrichtigt werden soll
     * @property {string} nachricht.type Beschreibung des Inhalts der Nachricht
     * @property {object} nachricht.inhalt Inhalt der Nachricht
     *
     * @fires nachricht
     */
    function onNachricht(appName, client, nachricht) {

        if (nachricht.UUID) {
            if (clientUUIDs[nachricht["UUID"]]) {
                serverLog("Sende message an " + clientUUIDs[nachricht.UUID].id + " mit Inhalt: " + nachricht.inhalt);

                /**
                 * Wird benutzt um einem Client oder allen Clients in einem Raum eine Nachricht zuzustellen.
                 * @event nachricht
                 * @type {object}
                 * @property {string} nachricht.UUID optionaler Parameter, falls ein einzelner Client benachrichtigt werden soll
                 * @property {string} nachricht.raum optinaler Paramter, falls jeder Client in einem Raum benachrichtigt werden soll
                 * @property {string} nachricht.type Beschreibung des Inhalts der Nachricht
                 * @property {object} nachricht.inhalt Inhalt der Nachricht
                 */
                io.of('/' + appName).to(clientUUIDs[nachricht.UUID].id).emit('nachricht', nachricht);
            }

        } else if (nachricht.raum) {

            client.to(nachricht.raum).emit('nachricht', nachricht);
            serverLog("Sende message an Raum " + nachricht.raum + " mit Inhalt: " + nachricht.inhalt);

        }

    }

    /**
     * Der Client tritt einem Raum bei.
     * <br> der Raum muss im Feld "raum" eines JSONObjektes mitgesendet werden.
     * @param {client} client Client, der dem Raum beitreten will
     * @param {object} raumObjekt
     * @property {string} raumObjekt.raum Der Raum, der betreten werden soll
     *
     * @fires benutzerTrittRaumBei
     */
    function onRaumBeitreten(client, raumObjekt) {
        if (raumObjekt.raum) {
            let raum = raumObjekt.raum;
            client.join(raum);

            let beigetretenerBenutzer = {};
            beigetretenerBenutzer['benutzer'] = { benutzername: client.username, UUID: client.UUID };

            /**
             * Benachrichtigt alle Clients im Raum, welcher Client den Raum betreten hat
             *
             * @event userTrittRaumBei
             * @type {object}
             * @property {string} benutzer.username Der Username des Users, der den Raum betreten hat
             * @property {string} benutzer.UUID Die UUID des Users, der den Raum betreten hat
             */
            client.to(raum).emit("userTrittRaumBei", beigetretenerBenutzer);

            serverLog(client.username + " joined to room: " + raum);
        }

    }

    /**
      * Der Client verlaesst einen Raum.
      * <br>Der Raum muss im Feld "raum" eines JSONObjektes mitgesendet werden.
      * @param {object} raumObjekt
      * @property {string} raumObjekt.raum Der Raum, der verlassen werden soll
      *
      * @fires userVerlaesstRaum
      */
    function onRaumVerlassen(client, raumObjekt) {
        if (raumObjekt.raum) {
            let raum = raumObjekt.raum;
            client.leave(raum);

            /**
             * Benachrichtigt alle Clients im Raum, welcher Client den Raum verlassen hat.
             *
             * @event userVerlaesstRaum
             * @type {object}
             * @property {string} benutzer.username Der Username des Users, der den Raum verlassen hat
             * @property {string} benutzer.UUID Die UUID des Users, der den Raum verlassen hat
             */
            let verlassenderUser = {};
            verlassenderUser['benutzer'] = { benutzername: client.username, UUID: client.UUID };
            client.to(raumObjekt.raum).emit("userVerlaesstRaum", verlassenderUser);
            serverLog(client.username + " left room: " + raumObjekt.raum);
            console.log(raumObjekt);
        }
    }

    /**
     * Gibt alle Clients zurueck, die sich im jeweiligen Raum befinden
     * mit dem Event "zeigeUserImRaum" zurueck.
     * <br>Der Raum muss im Feld "raum" eines JSONObjektes mitgesendet werden.
     * @param {string} appName Der Namespace, in dem sich der Raum befindet
     * @param {client} client Client, der die Benutzerliste angefragt hat
     * @param {object} raumObjekt
     * @property {string} raumObjekt.raum Raum, dessen Benutzer bestimmt werden
     * @fires zeigeUserImRaum
     */
    function onZeigeBenutzerImRaum(appName, raumObjekt, client) {
        if (raumObjekt.raum) {
            let raum = raumObjekt.raum;
            io.of('/' + appName).in(raum).clients((err, users) => {
                if (err) {
                    return error;
                } else {

                    let benutzerImRaum = [];

                    let i = 0;
                    users.forEach(function (entry) {
                        benutzerImRaum[i] = {}
                        benutzerImRaum[i]['benutzername'] = clientSocketIds[entry].username;
                        benutzerImRaum[i]['UUID'] = clientSocketIds[entry].UUID;
                        i++;
                    });

                    serverLog('Sending Clients of ' + raum + ' to ' + client.username);

                    let benutzerListe = {};
                    benutzerListe['inhalt'] = benutzerImRaum;
                    /**
                     * Sendet eine Liste mit Benutzern eines Raumes an den Client zurueck.
                     *
                     * @event zeigeBenutzerImRaum
                     * @type {Object[]}
                     * @property {string} benutzerInRaum[]['username']
                     * @property {*} benutzerInRaum[]['UUID']
                     */
                    client.emit('zeigeBenutzerImRaum', benutzerListe);
                }
            });
        }
    }

    //---------------------------------------------------------------------------------------------------->

    // ######## Functions ########
    /**
     * Gibt eine Map aller Räume und deren Clients zurueck.
     */
    function roomClientMapping() {

        Object.keys(io.nsps).forEach((ns) => {

            var resultList = [];
            var rooms = io.nsps[ns].adapter.rooms;

            var i = 0;
            for (var room in rooms) {
                if (room.charAt(0) !== '/') {
                    resultList[i] = room;
                }
                i++;
            }

            var j = 0;
            resultList.forEach(function (room) {
                if (room !== null) {

                    io.of(ns).in(room).clients((error, user) => {
                        if (error)
                            throw error;

                        //                                        console.log(user + '   NS: ' + ns + '   Room: ' + room);
                        roomClientMap[room] = user;
                    });
                }
                j++;
            });
        });

        return roomClientMap;
    }

    function serverLog(logMessage) {
        log = "[" + new Date().toUTCString() + "] " + logMessage;
        fs.appendFileSync("serverLog.log", log + "\n"), (err) => {
            if (err) throw err;
        };
        consoleLog.push(log);
        console.log(log);
    }

    //---------------------------------------------------------------------------------------------------->