// Node Server Script
// server.js
   console.log('server version 2.52');
// 1:28 PM Thu January 28, 2021
// Written by: James D. Miller

var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http, {cookiePath:false, cookie:false});
const port = process.env.PORT || 3000;

app.get('/', function(req, res) {
   // In a browser, if you set the URL to localhost:3000, you'll get this page:
   res.sendfile('links.html');
});

// Put various client data (cD) and maps in a global.
var cD = {};
cD.connectionIndex = 0;
cD.nameIndex = 0;

// Map: userName[ socket.id]
cD.userName = {};
cD.nickName = {};

// Map: id[ userName]
cD.id = {};

// Map: room[ socket.id], i.e. roomName
cD.room = {};

// Map: hostID[ roomName]
cD.hostID = {};

// After restarting the server, send info to all remaining connections.
setTimeout( function() {
   io.emit("chat message", "The server has started, restarted, or has been awakened. <br><br>" +
                           "If this is a restart, it's possible that all prior connections will reconnect automatically, or you may only need to press the connect button. <br><br>" +
                           "If there are problems, clients and hosts should refresh their pages. Hosts should indicate rooms and reconnect. Then clients should reconnect to those rooms.");
   console.log("\n" + "info sent to clients: server has restarted");
}, 5000);

// Miscellaneous support functions...

function setDefault( theValue, theDefault) {
   // Return the default if the value is undefined.
   return (typeof theValue !== "undefined") ? theValue : theDefault;
}

function removeUserFromMaps( clientID) {
   // Do this first, before removing this user from the maps.
   // Check to see if this is the host.
   var hostID = cD.hostID[ cD.room[ clientID]];
   if (hostID == clientID) {
      delete cD.hostID[ cD.room[ clientID]];
   }
   
   // In a similar way, make use of the userName map before removing the user from userName.
   delete cD.id[ cD.userName[ clientID]];
   delete cD.userName[ clientID];
   
   // Not every user will have a nick name.
   if (cD.nickName[ clientID]) delete cD.nickName[ clientID];
   
   // The room map was used above. Now it's ok to remove the user from the room map.
   delete cD.room[ clientID];
}

function setDisplayName( clientID, mode) {
   var displayNameString, userName;
   
   var hostID = cD.hostID[ cD.room[ clientID]];
   if (hostID == clientID) {
      userName = 'host';
   } else {
      userName = cD.userName[ clientID];
   }
   
   if (cD.nickName[ clientID]) {
      if (mode == 'comma') {
         displayNameString = cD.nickName[ clientID] + ', ' + userName;
      } else if (mode == 'prens') {
         displayNameString = cD.nickName[ clientID] + ' (' + userName + ')';
      }
   } else {
      displayNameString = userName;
   }
   return displayNameString;
}

function connectionInfo() {
   return 'sockets=' + io.engine.clientsCount + ', connection acts=' + cD.connectionIndex + ', names=' + Object.keys( cD.userName).length + ', nick names=' + Object.keys( cD.nickName).length;
}

function roomReport() {
   let usersByRoom = connectionInfo();
   
   for (let roomInMap in cD.hostID) {
      usersByRoom += "<br>clients in " + roomInMap + " = ";
      for (let socket_id in cD.userName) {
         let userName = cD.userName[ socket_id];
         let userNickName = cD.nickName[ socket_id];
         if (cD.room[ socket_id] == roomInMap) {
            // if this name is the host's name
            if (userName == cD.userName[ cD.hostID[ roomInMap]]) {
               if (userNickName) {
                  usersByRoom += userName + "(h-" + userNickName + "),";
               } else {
                  usersByRoom += userName + "(h),";
               }
            } else {
               if (userNickName) {
                  usersByRoom += userName + "(" + userNickName + "),";
               } else {
                  usersByRoom += userName + ",";
               }
            }
         }
      }
      // remove the trailing ","
      usersByRoom = usersByRoom.slice(0, -1);
   }
   return usersByRoom;
}

// not using this currently...
function highestNameNumber() {
   let maxNumber = 0;
   if (Object.keys( cD.userName).length > 0) {
      for (let socket_id in cD.userName) {
         let userName = cD.userName[ socket_id];
         // remove the leading "u" in the name
         let numberInName = userName.slice(1);
         maxNumber = Math.max( numberInName, maxNumber);
      }
   }
   return maxNumber;
}

function nameInUse( nameToCheck) {
   let nameInUse = false;
   for (let socket_id in cD.userName) {
      let userName = cD.userName[ socket_id];
      if (userName == nameToCheck) {
         nameInUse = true;
      }
   }
   return nameInUse;
}

function disconnectClientsInOneRoom( roomName) {
   for (let socket_id in cD.userName) {
      let userName = cD.userName[ socket_id];
      // apply to users in the room, but not the room host...
      if ( (cD.room[ socket_id] == roomName) && (userName != cD.userName[ cD.hostID[ roomName]]) ) {
         io.to( socket_id).emit('disconnectByServer', userName);
      }
   }
}

// This one is only used when debugging (see commented call)
function disconnectClientsInAllRooms() {
   for (let roomInMap in cD.hostID) {
      disconnectClientsInOneRoom( roomInMap);
   }
}

// Socket.io stuff...

io.on('connection', function(socket) {
   // Example of how to parse out the query string if it is sent in the connection attempt from the client.
   console.log("");
   console.log("Connection starting...");
   console.log("mode=" + socket.handshake.query['mode'] + ", current name=" + socket.handshake.query['currentName'] + ", nickName=" + socket.handshake.query['nickName']);
   
   cD.connectionIndex += 1;
   
   // Normal initial connection
   // Note that the host is always in normal mode.
   if (socket.handshake.query['mode'] == 'normal') {
      // Increment until find a name that's not in use.
      do {
         cD.nameIndex += 1;
         var user_name = 'u' + cD.nameIndex;
      } while (nameInUse( user_name));
      
   } else if (socket.handshake.query['mode'] == 're-connect') {
      // If re-connecting, re-use the current user name that comes in via the query string.
      // Re-connection happens only when the client is starting a stream or when the P2P connection makes a second attempt.
      var user_name = socket.handshake.query['currentName'];
   }
   var nick_name = socket.handshake.query['nickName'];
   
   // Two maps
   cD.userName[ socket.id] = user_name;
   if (nick_name) cD.nickName[ socket.id] = nick_name;
   cD.id[ user_name] = socket.id;
   
   console.log('');
   console.log( connectionInfo());
   
   console.log('New client: '+ cD.userName[socket.id] +', '+ socket.id + '.');
   
   // Tell the new user their network name. Note there is no listener for this on the host.
   if (socket.id != cD.hostID[ cD.room[ socket.id]]) {
      io.to(socket.id).emit('your name is', JSON.stringify({'name':cD.userName[socket.id], 'nickName':nick_name}));
   }
   
   // Now set up the various listeners. I know this seems a little odd, but these listeners
   // need to be defined each time this connection event fires, i.e. for each socket.
   
   // Echo test...
   socket.on('echo-from-Client-to-Server', function(msg) {
      if (msg == 'server') {
         // This bounces off the SERVER and goes right back to the client.
         io.to(socket.id).emit('echo-from-Server-to-Client', 'server');
         
      } else if (msg == 'host') {
         // Send this first to the host (the scenic route). Include the id of the client so that we know where to send it
         // when it bounces off the host.
         io.to( cD.hostID[ cD.room[ socket.id]]).emit('echo-from-Server-to-Host', socket.id);
      }
      
   });
   socket.on('echo-from-Host-to-Server', function(msg) {
      var socket_id = msg;
      // Now that this has come back from the HOST, complete the trip and send this to the originating client.
      io.to(socket_id).emit('echo-from-Server-to-Client', 'host');
   });
   
   
   // Broadcast the incoming chat message to everyone in the sender's room. Allow some special text strings
   // to trigger actions on the server.
   socket.on('chat message', function(msg) {
      if (msg == "dcir") {
         if (socket.id == cD.hostID[ cD.room[ socket.id]]) {
            disconnectClientsInOneRoom( cD.room[ socket.id]);
         } else {
            io.to( socket.id).emit('chat message', 'Requests to disconnect clients must come from the host.');
         }
         
      } else if (msg == "dac") {
         //disconnectClientsInAllRooms();
         
      } else if (msg == "rr") {
         if (socket.id == cD.hostID[ cD.room[ socket.id]]) {
            io.to( cD.hostID[ cD.room[ socket.id]]).emit('chat message', roomReport());
         } else {
            io.to( socket.id).emit('chat message', 'Requests for room reports must come from the host.');
         }
         
      } else {
         // General emit to the room. Note: io.to and io.in do the same thing.
         io.to( cD.room[ socket.id]).emit('chat message', msg + " (" + setDisplayName(socket.id, 'comma') + ")");
      }
   });
   // Broadcast the incoming chat message to everyone in the sender's room, except the sender.
   socket.on('chat message but not me', function(msg) {
      // Emit to everyone in the sender's room except the sender.
      socket.to( cD.room[ socket.id]).emit('chat message',  msg + " (" + setDisplayName(socket.id, 'comma') + ")");
   });
   
   
   // Signaling in support of WebRTC.
   socket.on('signaling message', function(msg) {
      var signal_message = JSON.parse(msg);
      
      if (signal_message.to == 'host') {
         var target = cD.hostID[ cD.room[ socket.id]];
      } else {
         var target = cD.id[ signal_message.to];
      }
      
      // Relay the message (emit) to the target user.
      io.to( target).emit('signaling message', msg);
   });
   
   // General control message (note: same structure as the above handler for signaling messages)
   socket.on('control message', function(msg) {
      var control_message = JSON.parse( msg);
      
      // to the host only
      if (control_message.to == 'host') {
         var target = cD.hostID[ cD.room[ socket.id]];
      // to everyone in the room   
      } else if (control_message.to == 'room') {
         var target = cD.room[ socket.id];
      // to everyone in the room except the sender   
      } else if (control_message.to == 'roomNoSender') {
         var target = cD.room[ socket.id];
         socket.to( target).emit('control message', msg);
         return;
      // to this particular user
      } else {
         var target = cD.id[ control_message.to];
      }
      
      // Relay the message (emit) to the target user(s).
      io.to( target).emit('control message', msg);
   });
   
   // Send mouse and keyboard states to the host.
   socket.on('client-mK-event', function(msg) {
      // Determine the id of the room-host for this client. Then send data to the host for that room.
      // socket.id --> room --> room host.
      var hostID = cD.hostID[ cD.room[ socket.id]];
      
      // StH: Server to Host
      io.to( hostID).emit('client-mK-StH-event', msg);
   });
   
   // After connecting, the 'connect' listener, on client or host, sends a message to 'roomJoin' listener on the server.
   socket.on('roomJoin', function(msg) {
      var msgParsed = JSON.parse( msg);
      
      var roomName = setDefault( msgParsed.roomName, null);
      var requestStream = setDefault( msgParsed.requestStream, false);
      var player = setDefault( msgParsed.player, null);
      var hostOrClient = setDefault( msgParsed.hostOrClient, 'client');
      
      nickName = cD.nickName[ socket.id];
      var displayName = setDisplayName( socket.id, 'prens');
      
      if (hostOrClient == 'client') {
         // Check to make sure the room has a host.
         if (cD.hostID[ roomName]) {
            socket.join( roomName);
            cD.room[ socket.id] = roomName;
            console.log('Room ' + roomName + ' joined by ' + cD.userName[ socket.id] + '.');
            
            // Send message to the individual client that is joining the room.
            io.to(socket.id).emit('room-joining-message', 
               JSON.stringify({'message':'You have joined room ' + cD.room[socket.id] + ' and your client name is '+ displayName +'.', 
               'userName':cD.userName[ socket.id]}));
            
            // Message to the room host.
            // Give the host the name of the new user so a new game client can be created. This is where "player" and "nickName" info gets
            // sent to the host. Notice this emit to new-game-client is not done, or needed, in the host block below.
            io.to( cD.hostID[ roomName]).emit('new-game-client', 
               JSON.stringify({'clientName':cD.userName[socket.id], 'requestStream':requestStream, 'player':player, 'nickName':nickName}));
            
            // Chat message to the host.
            io.to( cD.hostID[ roomName]).emit('chat message', displayName + ' is a new client in room ' + roomName + '.');
            
         } else {
            io.to(socket.id).emit('room-joining-message', 
               JSON.stringify({'message':'Sorry, there is no host yet for room ' + roomName + '.',
               'userName':cD.userName[ socket.id]}));
         }
      
      } else if (hostOrClient == 'host') {
         // Should check if the room already has a host.
         if (cD.hostID[ roomName]) {
            // Send warning to the client that is attempting to host.
            io.to(socket.id).emit('room-joining-message', 
               JSON.stringify({'message':'Sorry, there is already a host for room ' + roomName + '.',
               'userName':cD.userName[ socket.id]}));
            
         } else {
            socket.join( roomName);
            cD.room[ socket.id] = roomName;
            console.log('Room ' + roomName + ' joined by ' + cD.userName[ socket.id] + '.');
            
            // General you-have-joined-the-room message.
            io.to(socket.id).emit('room-joining-message', 
               JSON.stringify({'message':'You have joined room ' + cD.room[socket.id] + ' and your client name is ' + displayName + '.',
               'userName':cD.userName[ socket.id]}));
            
            // Set this user as the host for this room.
            cD.hostID[ cD.room[ socket.id]] = socket.id;
            console.log('User '+ displayName +' identified as host for room '+ cD.room[ socket.id] + '.');
            
            // And oh-by-the-way "you are the host" message.
            io.to(socket.id).emit('room-joining-message', 
               JSON.stringify({'message':'You are the host of room ' + cD.room[ socket.id] + '.',
               'userName':cD.userName[ socket.id]}));
         }
      }
   });
   
   // This "disconnect" event is fired by the server.
   socket.on('disconnect', function() {
      if (cD.userName[ socket.id]) {
         
         var displayName = setDisplayName( socket.id, 'prens');
         
         // Report at the server.
         console.log(' ');
         var message = displayName + ' has disconnected.';
         console.log( message + ' (by self, ' + socket.id + ').');
         
         // Report to the room host.
         var hostID = cD.hostID[ cD.room[ socket.id]];
         io.to( hostID).emit('chat message', message + '.');
         io.to( hostID).emit('client-disconnected', cD.userName[ socket.id]);
         
         // Remove this user from the maps.
         removeUserFromMaps( socket.id);
      }
   });
   
   socket.on('clientDisconnectByHost', function(msg) {
      var clientName = msg;
      var clientID = cD.id[ clientName];
      
      // Send disconnect message to the client.
      io.to( clientID).emit('disconnectByServer', clientName);
      
      // Don't do the following. It will disconnect the host socket. Not what we want here!
      //socket.disconnect();
   });
   
   socket.on('okDisconnectMe', function(msg) {
      // This event indicates that the non-host client has gotten the clientDisconnectByHost message (see above) and
      // agrees to go peacefully.
      var clientName = msg;
      var clientID = cD.id[ clientName];
      
      // Report this at the server.
      console.log(' ');
      var message = clientName + ' has disconnected';
      console.log( message + ' (by host, '+clientID+').');
      
      // Report to the room host.
      var hostID = cD.hostID[ cD.room[ clientID]];
      io.to( hostID).emit('chat message', message);
      io.to( hostID).emit('client-disconnected', clientName);
      
      // Remove this user from the maps.
      removeUserFromMaps( socket.id);
      
      //Finally, go ahead and disconnect this client's socket.
      socket.disconnect();
   });

   socket.on('shutDown-p2p-deleteClient', function( msg) {
      var clientName = msg;
      var clientID = cD.id[ clientName];
      var hostID = cD.hostID[ cD.room[ clientID]];
      io.to( hostID).emit('shutDown-p2p-deleteClient', clientName);   
   });
   
   socket.on('command-from-host-to-all-clients', function( msg) {
      // General emit to the room.
      io.to( cD.room[ socket.id]).emit('command-from-host-to-all-clients', msg);
   });
   
});

http.listen( port, function() {
   console.log('listening on *:' + port);
});