var iolib = require("socket.io"),
  { log, gauge, monitorFunction } = require("./log.js"),
  BoardData = require("./boardData.js").BoardData,
  config = require("./configuration");

/** Map from name to *promises* of BoardData
  @type {Object<string, Promise<BoardData>>}
*/
var boards = {};

/**
 * Prevents a function from throwing errors.
 * If the inner function throws, the outer function just returns undefined
 * and logs the error.
 * @template A
 * @param {A} fn
 * @returns {A}
 */
function noFail(fn) {
  const monitored = monitorFunction(fn);
  return function noFailWrapped(arg) {
    try {
      return monitored(arg);
    } catch (e) {
      console.trace(e);
    }
  };
}

function startIO(app) {
  io = iolib(app);
  io.on("connection", noFail(handleSocketConnection));
  return io;
}

/** Returns a promise to a BoardData with the given name
 * @returns {Promise<BoardData>}
 */
function getBoard(name) {
  if (boards.hasOwnProperty(name)) {
    return boards[name];
  } else {
    var board = BoardData.load(name);
    boards[name] = board;
    gauge("boards in memory", Object.keys(boards).length);
    return board;
  }
}

/**
 * Executes on every new connection
 * @param {iolib.Socket} socket
 */
function handleSocketConnection(socket) {
  /**
   * Function to call when an user joins a board
   * @param {string} name
   */
  async function joinBoard(name) {
    // Default to the public board
    if (!name) name = "anonymous";

    // Join the board
    socket.join(name);

    var board = await getBoard(name);
    board.users.add(socket.id);
    log("board joined", { board: board.name, users: board.users.size });
    gauge("connected." + name, board.users.size);
    return board;
  }

  socket.on(
    "error",
    noFail(function onSocketError(error) {
      log("ERROR", error);
    })
  );

  socket.on("getboard", async function onGetBoard(name) {
    var board = await joinBoard(name);
    //Send all the board's data as soon as it's loaded
    socket.emit("broadcast", { _children: board.getAll() });
  });

  socket.on("joinboard", noFail(joinBoard));

  var lastEmitSecond = (Date.now() / config.MAX_EMIT_COUNT_PERIOD) | 0;
  var emitCount = 0;
  socket.on(
    "broadcast",
    noFail(function onBroadcast(message) {
      var currentSecond = (Date.now() / config.MAX_EMIT_COUNT_PERIOD) | 0;
      if (currentSecond === lastEmitSecond) {
        emitCount++;
        if (emitCount > config.MAX_EMIT_COUNT) {
          var request = socket.client.request;
          if (emitCount % 100 === 0) {
            log("BANNED", {
              user_agent: request.headers["user-agent"],
              original_ip:
                request.headers["x-forwarded-for"] ||
                request.headers["forwarded"],
              emit_count: emitCount,
            });
          }
          return;
        }
      } else {
        emitCount = 0;
        lastEmitSecond = currentSecond;
      }

      var boardName = message.board || "anonymous";
      var data = message.data;

      if (!socket.rooms.has(boardName)) socket.join(boardName);

      if (!data) {
        console.warn("Received invalid message: %s.", JSON.stringify(message));
        return;
      }

      if (
        !message.data.tool ||
        config.BLOCKED_TOOLS.includes(message.data.tool)
      ) {
        log("BLOCKED MESSAGE", message.data);
        return;
      }

      // Save the message in the board
      handleMessage(boardName, data, socket);

      //Send data to all other users connected on the same board
      socket.broadcast.to(boardName).emit("broadcast", data);
    })
  );

  socket.on("disconnecting", function onDisconnecting(reason) {
    socket.rooms.forEach(async function disconnectFrom(room) {
      if (boards.hasOwnProperty(room)) {
        var board = await boards[room];
        board.users.delete(socket.id);
        var userCount = board.users.size;
        log("disconnection", { board: board.name, users: board.users.size, reason });
        gauge("connected." + board.name, userCount);
        if (userCount === 0) {
          board.save();
          delete boards[room];
          gauge("boards in memory", Object.keys(boards).length);
        }
      }
    });
  });
}

function handleMessage(boardName, message, socket) {
  if (message.tool === "Cursor") {
    message.socket = socket.id;
  } else {
    saveHistory(boardName, message);
  }
}

async function saveHistory(boardName, message) {
  if (!message.tool && !message._children) {
    console.error("Received a badly formatted message (no tool). ", message);
  }
  var board = await getBoard(boardName);
  board.processMessage(message);
}

function generateUID(prefix, suffix) {
  var uid = Date.now().toString(36); //Create the uids in chronological order
  uid += Math.round(Math.random() * 36).toString(36); //Add a random character at the end
  if (prefix) uid = prefix + uid;
  if (suffix) uid = uid + suffix;
  return uid;
}

if (exports) {
  exports.start = startIO;
}

@@ -233,22 +233,29 @@
 pe %7B
-Object%3C
+%7B%5BboardName: 
 string
-,
+%5D:
  Pro
@@ -269,17 +269,17 @@
 ardData%3E
-%3E
+%7D
 %7D%0A*/%0Avar
@@ -3920,24 +3920,34 @@
 nnection%22, %7B
+%0A         
  board: boar
@@ -3945,32 +3945,42 @@
 ard: board.name,
+%0A         
  users: board.us
@@ -3992,15 +3992,34 @@
 ize,
- reason
+%0A          reason,%0A       
  %7D);
@@ -4105,40 +4105,344 @@
  0) 
-%7B%0A          board.save();%0A      
+unloadBoard(room);%0A      %7D%0A    %7D);%0A  %7D);%0A%7D%0A%0A/**%0A * Unloads a board from memory.%0A * @param %7Bstring%7D boardName%0A **/%0Aasync function unloadBoard(boardName) %7B%0A  if (boards.hasOwnProperty(boardName)) %7B%0A    const board = await boards%5BboardName%5D;%0A    await board.save();%0A    log(%22unload board%22, %7B board: board.name, users: board.users.size %7D);%0A
     
@@ -4455,29 +4455,28 @@
  boards%5B
-room%5D;%0A      
+boardName%5D;%0A
     gaug
@@ -4532,37 +4532,9 @@
 ;%0A  
-      %7D%0A      %7D%0A    %7D);%0A  %7D);
+%7D
 %0A%7D%0A%0A

split unload function