/**
 * @copyright Copyright (C) 2018, struktur AG
 *
 * @author Joachim Bauch <mail@joachim-bauch.de>
 *
 * @license AGPL-3.0
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
"use strict";

var process = require('process');

var config;
try {
  config = require('./config');
} catch (e) {
  if (e.code !== 'MODULE_NOT_FOUND') {
    throw e;
  }

  console.log('Could not load "config.js":', e.code);
  console.log('Please make sure to copy "config.js.in" to "config.js" and adjust to your environment.');
  process.exit(1);
}

var port = config.port || 8080;
var secret = config.secret || '';

var child_process = require('child_process');
var fs = require('fs');
var path = require('path');
var https = require('https');
var http = require('http');
var jwt = require('jsonwebtoken');
var os = require('os');
var socketio = require('socket.io');
var querystring = require('querystring');
var url = require('url');

if (config.use_auto_ecdh_curve) {
  // See https://github.com/nodejs/node/issues/21513#issuecomment-399790415
  require("tls").DEFAULT_ECDH_CURVE = "auto";
}

if (config.allow_invalid_certificates) {
  console.log('WARNING: Invalid certificates are allowed!');
  process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
}

var CMD_PDFTK = config.cmd_pdftk || 'pdftk';
console.log('Using', CMD_PDFTK, 'to run as pdftk');
var CMD_SVG2PDF = config.cmd_svg2pdf || 'svg2pdf';
console.log('Using', CMD_SVG2PDF, 'to run as svg2pdf');

var PERMISSION_CREATE = 4;
var PERMISSION_READ = 1;
var PERMISSION_UPDATE = 2;
var PERMISSION_DELETE = 8;
var PERMISSION_SHARE = 16;
var PERMISSION_ALL = 31;

function isEmpty(obj) {
  return Object.keys(obj).length === 0 && obj.constructor === Object;
}

function parse_url(s) {
  if (url.hasOwnProperty('URL')) {
    return new url.URL(s);
  } else {
    return url.parse(s);
  }
}

function performOcsRequest(token, request_url, body, method) {
  return new Promise(function(resolve, reject) {
    var u = parse_url(request_url);
    var headers = {
      "Accept": "application/json",
      "Authorization": "Bearer " + token,
      "OCS-APIRequest": "true"
    };
    var options = {
      "hostname": u.hostname,
      "port": u.port,
      "path": u.pathname,
      "headers": headers
    };
    if (body) {
      options.method = method ? method : "POST";
      if (typeof(body) !== "string") {
        body = querystring.stringify(body);
      }
      headers["Content-Type"] = "application/x-www-form-urlencoded";
      // "Transfer-Encoding: chunked" doesn't seem to work.
      headers["Content-Length"] = body.length;
    } else {
      options.method = method ? method : "GET";
    }

    var req = https.request(options, function(response) {
      var data = [];
      response.on('data', function(chunk) {
        data.push(chunk);
      });

      response.on('end', function() {
        if (data.length) {
          data = Buffer.concat(data).toString("utf-8");
        }

        var ct = response.headers['content-type'];
        if (ct && ct.indexOf("application/json") === 0) {
          data = JSON.parse(data);
        }

        if (response.statusCode !== 200) {
          return reject({
            "code": response.statusCode,
            "data": data,
            "headers": response.headers
          });
        }

        if (!data || typeof(data) === "string" || !data.hasOwnProperty("ocs") ||
            !data.ocs.hasOwnProperty("meta") || !data.ocs.hasOwnProperty("data")) {
          return reject({
            "code": response.statusCode,
            "data": data,
            "headers": response.headers,
            "msgid": "invalid_response"
          });
        }

        var meta = data.ocs.meta;
        if (meta.status !== "ok" || meta.statuscode !== 200) {
          return reject({
            "code": meta.statusCode,
            "data": data,
            "headers": response.headers
          });
        }

        resolve(data.ocs.data);
      });
    });

    req.on("error", function(error) {
      reject({
        "error": error
      });
    });
    if (body) {
      req.write(body);
    }
    req.end();
  });
}

var Room = function(io, id, base_url) {
  this.io = io;
  this.room_io = io.to(id);
  this.id = id;
  this.base_url = base_url;
  this.ocs_url = base_url + '/ocs/v2.php/apps/pdfdraw';
  this.users = {};
  this.items = [];
  this.currentPage = null;
  this.loadItems();
};

Room.prototype.isEmpty = function() {
  return isEmpty(this.users);
};

Room.prototype.createToken = function() {
  var token = jwt.sign({
    iss: 'backend',
    exp: Math.floor(Date.now() / 1000) + 300,  // 5 minutes
    file: this.id,
  }, secret, {'algorithm': 'HS256'});
  return token;
};

Room.prototype.loadItems = function() {
  var request_url = this.ocs_url + "/api/v1/item/" + this.id;
  var token = this.createToken();
  performOcsRequest(token, request_url).then(function(items) {
    console.log("Received items", items);
    for (var i = 0; i < items.length; i++) {
      var item = items[i];
      this._addItem(item.name, item.page, item.data);
    }
    this.sendItems();
  }.bind(this), function(error) {
    console.log("Error receiving items", error);
  }.bind(this));
};

Room.prototype._storeItem = function(page, name, data) {
  var request_url = this.ocs_url + "/api/v1/item/" + this.id + "/" + page + "/" + encodeURIComponent(name);
  var body = {
    "data": data
  };
  var token = this.createToken();
  performOcsRequest(token, request_url, body).then(function() {
    console.log("Saved item", name);
  }.bind(this), function(error) {
    console.log("Error saving item", name, error);
  }.bind(this));
};

Room.prototype._removeItem = function(page, name) {
  var request_url = this.ocs_url + "/api/v1/item/" + this.id + "/" + page + "/" + encodeURIComponent(name);
  var token = this.createToken();
  performOcsRequest(token, request_url, null, "DELETE").then(function() {
    console.log("Removed item", name);
  }.bind(this), function(error) {
    console.log("Error removing item", name, error);
  }.bind(this));
};

Room.prototype.send = function(socket, type, message) {
  socket.to(this.id).emit(type, message);
};

Room.prototype.sendMessage = function(socket, message) {
  this.send(socket, 'message', message);
};

Room.prototype._addItem = function(name, page, data) {
  var items;
  if (!this.items.hasOwnProperty(page)) {
    items = this.items[page] = {};
  } else {
    items = this.items[page];
  }
  items[name] = data;
};

Room.prototype.addItem = function(socket, name, page, data) {
  if (!this.userMayModify(socket)) {
    return false;
  }

  this._addItem(name, page, data);
  this._storeItem(page, name, data);
  return true;
};

Room.prototype.removeItem = function(socket, name, page) {
  if (!this.userMayModify(socket)) {
    return false;
  }

  var items;
  if (!this.items.hasOwnProperty(page)) {
    return true;
  }

  items = this.items[page];
  delete items[name];
  this._removeItem(page, name);
  if (isEmpty(items)) {
    delete this.items[page];
  }
  return true;
};

Room.prototype.sendItem = function(socket, name, page, data) {
  var message = {
    'type': 'item',
    'item': {
      'page': page,
      'name': name,
      'data': data
    }
  };
  socket.emit("message", message);
};

Room.prototype.sendItems = function(socket) {
  for (var page in this.items) {
    if (!this.items.hasOwnProperty(page)) {
      continue;
    }

    var items = this.items[page];
    for (var name in items) {
      if (!items.hasOwnProperty(name)) {
        continue;
      }

      if (!socket) {
        for (var userid in this.users) {
          if (!this.users.hasOwnProperty(userid)) {
            continue;
          }
          this.sendItem(this.io.to(userid), name, page, items[name]);
        }
      } else {
        this.sendItem(socket, name, page, items[name]);
      }
    }
  }
};

Room.prototype.handleControl = function(socket, message) {
  if (!this.userMayModify(socket)) {
    return false;
  }

  var control = message.control;
  if (!control || !control.type) {
    console.log('ignore invalid control message', message);
    return false;
  }

  switch (control.type) {
    case 'page':
      var page = control.page;
      console.log('room switched page', this.id, page);
      this.currentPage = page;
      break;
    default:
      console.log('ignore unsupported control message', message);
      return false;
  }

  return true;
};

Room.prototype.addUser = function(socket, data) {
  data.userid = socket.id;
  console.log('user joined room', this.id, data);
  this.send(socket, 'user.joined', [data]);
  this.users[socket.id] = data;
  var all_users = [];
  for (var userid in this.users) {
    if (!this.users.hasOwnProperty(userid)) {
      continue;
    }
    all_users.push(this.users[userid]);
  }
  socket.emit('user.joined', all_users);
  this.sendItems(socket);
  if (this.currentPage) {
    socket.emit("message", {
      'type': 'control',
      'control': {
        'type': 'page',
        'page': this.currentPage
      }
    });
  }
};

Room.prototype.removeUser = function(socket) {
  if (!this.users.hasOwnProperty(socket.id)) {
    return;
  }

  var data = this.users[socket.id];
  console.log('user left room', this.id, data);
  delete this.users[socket.id];
  this.send(socket, 'user.left', data);
};

Room.prototype.userMayModify = function(socket) {
  if (!this.users.hasOwnProperty(socket.id)) {
    return false;
  }

  var userdata = this.users[socket.id];
  if (userdata.permissions == null) {
    // Old-style user with an old token.
    return false;
  }

  return (userdata.permissions & PERMISSION_UPDATE) === PERMISSION_UPDATE;
};

Room.prototype.downloadFile = function(token) {
  return new Promise(function(resolve, reject) {
    var u = parse_url(this.base_url + '/apps/pdfdraw/download/' + this.id);
    var options = {
      "hostname": u.hostname,
      "port": u.port,
      "method": "GET",
      "path": u.pathname,
      "headers": {
        "Authorization": "Bearer " + token,
      }
    };

    var req = https.request(options, function(response) {
      var data = [];
      response.on('data', function(chunk) {
        data.push(chunk);
      });

      response.on('end', function() {
        if (response.statusCode !== 200) {
          return reject({
            "code": response.statusCode,
            "data": data,
            "headers": response.headers
          });
        }

        resolve(Buffer.concat(data));
      });
    });

    req.on("error", function(error) {
      reject({
        "error": error
      });
    });
    req.end();
  }.bind(this));
};

var rooms = {};

function svg2pdf(svg_filename) {
  return new Promise(function(resolve, reject) {
    var cmd = CMD_SVG2PDF;
    console.log("Converting", svg_filename, "to pdf ...");
    var options = {};
    child_process.execFile(cmd, [svg_filename], options, function(error, stdout, stderr) {
      if (error) {
        console.log("Converting", svg_filename, "returned an error", error);
        if (stderr) {
          console.log(CMD_SVG2PDF, "returned an error", error.message, stderr.toString());
        } else {
          console.log(CMD_SVG2PDF, "returned an error", error.message);
        }
        reject(error);
        return;
      }

      var pdf_filename = svg_filename.substr(0, svg_filename.length - 4) + ".pdf";
      if (!fs.existsSync(pdf_filename)) {
        reject(new Error("Converting " + svg_filename + " didn't produce a file"));
        return;
      }

      console.log("Finished converting", svg_filename);
      resolve(pdf_filename);
    });
  });
}

function spawn(cmd, args, options) {
  return new Promise((resolve, reject) => {
    let stdin;
    if (options && options.input) {
      stdin = options.input;
      delete options.input;
    }

    const proc = child_process.spawn(cmd, args, options);
    if (stdin) {
      proc.stdin.write(stdin, (error) => {
        proc.stdin.end();
      });
    }

    let data;
    let errorData;

    proc.on("message", console.log);

    proc.stdout.on("data", chunk => {
      if (!data) {
        data = chunk;
      } else {
        data = Buffer.concat([data, chunk]);
      }
    });

    proc.stderr.on("data", chunk => {
      if (!errorData) {
        errorData = chunk;
      } else {
        errorData = Buffer.concat([errorData, chunk]);
      }
    });

    proc.on("close", function(code) {
      resolve({
        "status": code,
        "stdout": data,
        "stderr": errorData,
      });
    });

    proc.on("error", function(err) {
      reject(err);
    });
  });
}

async function combine_pdfs(pages, filename) {
  if (!pages.length) {
    return false;
  }

  var cmd = CMD_PDFTK;
  console.log("Combining pages ...");
  var args = pages.splice(0);
  args.push("cat");
  args.push("output");
  args.push(filename);
  var status = await spawn(cmd, args);
  console.log("Done", status.status);
  if (status.status !== 0) {
    if (status.stderr) {
      console.log(CMD_PDFTK, "returned an error when combining", status.stderr.toString());
    } else {
      console.log(CMD_PDFTK, "returned an error when combining", status);
    }
    return false;
  }

  // TODO(jojo): Check if file has been created and is non-empty.
  return true;
}

async function overlay_pdf(source_pdf, overlay_filename) {
  if (!overlay_filename) {
    return source_pdf;
  }

  var cmd = CMD_PDFTK;
  console.log("Merging combined with overlay ...");
  var args = ["-", "multistamp", overlay_filename, "output", "-"];
  var status = await spawn(cmd, args, {
    "input": source_pdf,
  });
  console.log("Done", status.status);
  if (status.status !== 0) {
    if (status.stderr) {
      console.log(CMD_PDFTK, "returned an error when merging", status.stderr.toString());
    } else {
      console.log(CMD_PDFTK, "returned an error when merging", status);
    }
    return null;
  }

  var merged = status.stdout;
  if (!merged.length) {
    console.log(CMD_PDFTK, "didn't return a PDF when merging");
    return null;
  }
  return merged;
}

async function add_text_annotations(source_pdf, text, tempdir) {
  if (!text || !text.length) {
    return source_pdf;
  }

  var cmd = __dirname + "/pdfannotate";
  console.log("Adding " + text.length + " text annotations ...");

  var text_filename = tempdir + "/text-annotations.json";
  fs.writeFileSync(text_filename, JSON.stringify(text));

  var args = ["--text", text_filename, "-", "-"];
  var status = await spawn(cmd, args, {
    "input": source_pdf,
  });
  console.log("Done", status.status);
  if (status.status !== 0) {
    if (status.stderr) {
      console.log("Adding text annotations returned an error", status.stderr.toString());
    } else {
      console.log("Adding text annotations returned an error", status);
    }
    return source_pdf;
  }

  var annotated = status.stdout;
  if (!annotated.length) {
    console.log("Adding text annotations didn't return a PDF");
    return source_pdf;
  }

  return annotated;
}

// mktemp code taken from https://github.com/sasaplus1/mktemp/ (MIT license)
/**
 * random table string and table length.
 */
var TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
var TABLE_LEN = TABLE.length;

/**
 * generate random string from template.
 *
 * replace for placeholder "X" in template.
 * return template if not has placeholder.
 *
 * @param {String} template template string.
 * @throws {TypeError} if template is not a String.
 * @return {String} replaced string.
 */
function generate_randomstring(template) {
  var match, i, len, result;

  if (typeof template !== 'string') {
    throw new TypeError('template must be a String: ' + template);
  }

  match = template.match(/(X+)[^X]*$/);

  // return template if not has placeholder
  if (match === null) {
    return template;
  }

  // generate random string
  for (result = '', i = 0, len = match[1].length; i < len; ++i) {
    result += TABLE[Math.floor(Math.random() * TABLE_LEN)];
  }

  // concat template and random string
  return template.slice(0, match.index) + result +
      template.slice(match.index + result.length);
}

/**
 * sync version createDir.
 *
 * @param {String} template template string for dirname.
 * @return {String} created dirname.
 */
function createDirSync(template) {
  var isExist, dirname;

  // FIXME: infinite loop
  do {
    isExist = false;
    dirname = generate_randomstring(template);
    try {
      fs.mkdirSync(dirname, 448 /*=0700*/);
    } catch (e) {
      if (e.code === 'EEXIST') {
        isExist = true;
      } else {
        throw e;
      }
    }
  } while (isExist);

  return dirname;
}

// From https://stackoverflow.com/a/32197381
var deleteFolderRecursive = function(path) {
  if (fs.existsSync(path)) {
    fs.readdirSync(path).forEach(function(file, index) {
      var curPath = path + "/" + file;
      if (fs.lstatSync(curPath).isDirectory()) { // recurse
        deleteFolderRecursive(curPath);
      } else { // delete file
        fs.unlinkSync(curPath);
      }
    });
    fs.rmdirSync(path);
  }
};

var runSvg2PdfCombiner = function(tempdir, data) {
  return new Promise(function(resolve, reject) {
    var promises = [];
    var svg_cache = {};
    for (var i = 0; i < data.svg.length; i++) {
      var svg = data.svg[i];
      if (svg_cache.hasOwnProperty(svg)) {
        promises.push(svg_cache[svg]);
        continue;
      }

      var svg_filename = tempdir + "/page-" + i + ".svg";
      fs.writeFileSync(svg_filename, svg);
      var promise = svg2pdf(svg_filename);
      svg_cache[svg] = promise;
      promises.push(promise);
    }

    Promise.all(promises)
    .then(function(pdfs) {
      var combined_filename = tempdir + "/combined.pdf";
      combine_pdfs(pdfs, combined_filename)
      .then((status) => {
        if (!status) {
          reject();
          return;
        }
        resolve(combined_filename);
      });
    })
    .catch(function(error) {
      reject(error);
    });
  });
};

function pad(n) {
  return n < 10 ? '0' + n : n;
}

function formatTimestamp(d) {
  return '' +
      d.getFullYear() +
      pad(d.getMonth() + 1) +
      pad(d.getDate()) + '-' +
      pad(d.getHours()) +
      pad(d.getMinutes()) +
      pad(d.getSeconds());
}

function getDownloadFilename(filename, now) {
  var ext = path.extname(filename);
  filename = path.basename(filename, ext);
  filename = filename.replace('"', '');
  filename += '-Annotated-';
  filename += formatTimestamp(now);
  filename += ext || '.pdf';
  return filename;
}

var server = http.createServer(function(request, response) {
  if (request.method === "OPTIONS") {
    // Pre-flight request
    response.writeHead(200, {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "POST",
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Expose-Headers": "Content-Disposition"
    });
    response.end();
    return;
  }
  if (request.method !== "POST") {
    response.writeHead(400, {
      "Content-Type": "text/plain"
    });
    response.end('Bad Request');
    return;
  }

  var u = url.parse(request.url);
  if (u.pathname.indexOf("/download/") !== 0) {
    response.writeHead(404, {
      "Content-Type": "text/plain"
    });
    response.end('Not Found');
    return;
  }

  var room_id = u.pathname.substr(10);
  if (!rooms.hasOwnProperty(room_id)) {
    console.log("Unknown room", room_id);
    response.writeHead(404, {
      "Content-Type": "text/plain"
    });
    response.end('Not Found');
    return;
  }

  var room = rooms[room_id];

  var body = null;
  request.on("data", function(chunk) {
    if (body === null) {
      body = chunk;
    } else {
      body += chunk;
    }
  });

  request.on('end', () => {
    if (!body || !body.length) {
      console.log("Client sent an empty request");
      response.writeHead(400, {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "POST",
        "Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
        "Access-Control-Expose-Headers": "Content-Disposition",
        "Content-Type": "text/plain"
      });
      response.end('Bad Request');
      return;
    }

    var data = JSON.parse(body.toString("utf-8"));
    var token = data.token;
    var decoded;
    try {
      decoded = jwt.verify(token, secret, {"algorithms": ['HS256']});
    } catch (err) {
      console.log("Client sent an invalid token", token);
      response.writeHead(401, {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "POST",
        "Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
        "Access-Control-Expose-Headers": "Content-Disposition",
        "Content-Type": "text/plain"
      });
      response.end('Unauthorized');
      return;
    }

    if (room_id !== decoded.file) {
      console.log("Client sent a token for a different file", decoded);
      response.writeHead(401, {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "POST",
        "Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
        "Access-Control-Expose-Headers": "Content-Disposition",
        "Content-Type": "text/plain"
      });
      response.end('Unauthorized');
      return;
    }

    var downloader = room.downloadFile(token);

    var tempdir = createDirSync(os.tmpdir() + "/svg2pdf-XXXXXX");
    var combiner = runSvg2PdfCombiner(tempdir, data);

    Promise.all([downloader, combiner]).then(function(results) {
      var source_pdf = results[0];
      var combined_filename = results[1];
      if (!source_pdf.length) {
        deleteFolderRecursive(tempdir);
        console.log("Empty file received", room.id, token);
        response.writeHead(500, {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Methods": "POST",
          "Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
          "Access-Control-Expose-Headers": "Content-Disposition",
          "Content-Type": "text/plain"
        });
        response.end('Internal Server Error');
        return;
      }

      overlay_pdf(source_pdf, combined_filename)
      .then((merged) => {
        add_text_annotations(merged, data.text, tempdir)
        .then((merged) => {
          deleteFolderRecursive(tempdir);
          if (!merged) {
            response.writeHead(500, {
              "Access-Control-Allow-Origin": "*",
              "Access-Control-Allow-Methods": "POST",
              "Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
              "Access-Control-Expose-Headers": "Content-Disposition",
              "Content-Type": "text/plain"
            });
            response.end('Internal Server Error');
            return;
          }

          var now = new Date();
          var filename = getDownloadFilename(decoded.filename || room_id, now);
          response.writeHead(200, {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "POST",
            "Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
            "Access-Control-Expose-Headers": "Content-Disposition",
            "Content-Type": "application/pdf",
            "Content-Disposition": "attachment; filename=\"" + filename + "\"",
            "Content-Length": merged.length
          });
          response.end(merged);
        });
      });
    })
    .catch(function(error) {
      deleteFolderRecursive(tempdir);
      if (error) {
        console.log("Error", error);
      }
      response.writeHead(500, {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "POST",
        "Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
        "Access-Control-Expose-Headers": "Content-Disposition",
        "Content-Type": "text/plain"
      });
      response.end('Internal Server Error');
    });
  });
});

var io = socketio(server, {
  'cors': {
    'origin': '*',
  },
  'allowEIO3': true
});

io.on('connection', function(socket) {
  var query = socket.handshake.query;
  console.log('a user connected', query);

  var token = query.token;
  var decoded;
  try {
    decoded = jwt.verify(token, secret, {"algorithms": ['HS256']});
  } catch(err) {
    console.log("Invalid token received", err);
    socket.disconnect(true);
    return;
  }

  var baseurl = decoded.iss;
  if (!baseurl) {
    console.log('token did not contain a issuer', decoded);
    socket.disconnect(true);
    return;
  }

  var room_id = decoded.file;
  if (!room_id) {
    console.log("token did not contain a file id", decoded);
    socket.disconnect(true);
    return;
  }

  console.log("Token", decoded);

  var room;

  socket.on('disconnecting', function(reason) {
    if (!room) {
      return;
    }
    room.removeUser(socket);
    if (room.isEmpty()) {
      console.log("deleting empty room", room.id);
      delete rooms[room.id];
    }
  });

  socket.on('disconnect', function(reason) {
    console.log('a user disconnected');
  });

  if (!rooms.hasOwnProperty(room_id)) {
    room = rooms[room_id] = new Room(io, room_id, baseurl);
    console.log("created room", room_id);
  } else {
    room = rooms[room_id];
    console.log('joined room', room_id);
  }

  socket.on('message', function(message) {
    message.userid = socket.id;
    var page, name;
    if (message.type === "item") {
      page = message.item.page;
      name = message.item.name;
      var data = message.item.data;
      if (!page || !name || !data) {
        // Ignore invalid entries.
        return;
      }
      try {
        JSON.parse(data);
      } catch (e) {
        // Ignore items with invalid data.
        return;
      }
      if (!room.addItem(socket, name, page, data)) {
        return;
      }
    } else if (message.type === "delete") {
      page = message.delete.page;
      name = message.delete.name;
      if (!page || !name) {
        // Ignore invalid entries.
        return;
      }
      if (!room.removeItem(socket, name, page)) {
        return;
      }
    } else if (message.type === "control") {
      if (!room.handleControl(socket, message)) {
        return;
      }
    }
    room.sendMessage(socket, message);
  });

  socket.join(room_id);

  var displayname = decoded.displayname || null;
  var permissions = decoded.permissions || null;
  room.addUser(socket, {
    'displayname': displayname,
    'permissions': permissions
  });
});

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