/**
 * @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);
}

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