// **BEFORE RUNNING THIS SCRIPT:**
//   1. The server portion is best run on non-Windows systems because they have
//      terminfo databases which are needed to properly work with different
//      terminal types of client connections
//   2. Install `blessed`: `npm install blessed`
//   3. Create a server host key in this same directory and name it `host.key`

var fs = require('fs');

var blessed = require('blessed');
var Server = require('ssh2').Server;

var RE_SPECIAL = /[\x00-\x1F\x7F]+|(?:\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K])/g;
var MAX_MSG_LEN = 128;
var MAX_NAME_LEN = 10;
var PROMPT_NAME = 'Enter a nickname to use (max ' + MAX_NAME_LEN + ' chars): ';

var users = [];

function formatMessage(msg, output) {
  var output = output;
  output.parseTags = true;
  msg = output._parseTags(msg);
  output.parseTags = false;
  return msg;
}

function userBroadcast(msg, source) {
  var sourceMsg = '> ' + msg;
  var name = '{cyan-fg}{bold}' + source.name + '{/}';
  msg = ': ' + msg;
  for (var i = 0; i < users.length; ++i) {
    var user = users[i];
    var output = user.output;
    if (source === user)
      output.add(sourceMsg);
    else
      output.add(formatMessage(name, output) + msg);
  }
}

function localMessage(msg, source) {
  var output = source.output;
  output.add(formatMessage(msg, output));
}

function noop(v) {}

new Server({
  hostKeys: [fs.readFileSync('host.key')],
}, function(client) {
  var stream;
  var name;

  client.on('authentication', function(ctx) {
    var nick = ctx.username;
    var prompt = PROMPT_NAME;
    var lowered;
    // Try to use username as nickname
    if (nick.length > 0 && nick.length <= MAX_NAME_LEN) {
      lowered = nick.toLowerCase();
      var ok = true;
      for (var i = 0; i < users.length; ++i) {
        if (users[i].name.toLowerCase() === lowered) {
          ok = false;
          prompt = 'That nickname is already in use.\n' + PROMPT_NAME;
          break;
        }
      }
      if (ok) {
        name = nick;
        return ctx.accept();
      }
    } else if (nick.length === 0)
      prompt = 'A nickname is required.\n' + PROMPT_NAME;
    else
      prompt = 'That nickname is too long.\n' + PROMPT_NAME;

    if (ctx.method !== 'keyboard-interactive')
      return ctx.reject(['keyboard-interactive']);

    ctx.prompt(prompt, function retryPrompt(answers) {
      if (answers.length === 0)
        return ctx.reject(['keyboard-interactive']);
      nick = answers[0];
      if (nick.length > MAX_NAME_LEN) {
        return ctx.prompt('That nickname is too long.\n' + PROMPT_NAME,
                          retryPrompt);
      } else if (nick.length === 0) {
        return ctx.prompt('A nickname is required.\n' + PROMPT_NAME,
                          retryPrompt);
      }
      lowered = nick.toLowerCase();
      for (var i = 0; i < users.length; ++i) {
        if (users[i].name.toLowerCase() === lowered) {
          return ctx.prompt('That nickname is already in use.\n' + PROMPT_NAME,
                            retryPrompt);
        }
      }
      name = nick;
      ctx.accept();
    });
  }).on('ready', function() {
    var rows;
    var cols;
    var term;
    client.once('session', function(accept, reject) {
      accept().once('pty', function(accept, reject, info) {
        rows = info.rows;
        cols = info.cols;
        term = info.term;
        accept && accept();
      }).on('window-change', function(accept, reject, info) {
        rows = info.rows;
        cols = info.cols;
        if (stream) {
          stream.rows = rows;
          stream.columns = cols;
          stream.emit('resize');
        }
        accept && accept();
      }).once('shell', function(accept, reject) {
        stream = accept();
        users.push(stream);

        stream.name = name;
        stream.rows = rows || 24;
        stream.columns = cols || 80;
        stream.isTTY = true;
        stream.setRawMode = noop;
        stream.on('error', noop);

        var screen = new blessed.screen({
          autoPadding: true,
          smartCSR: true,
          program: new blessed.program({
            input: stream,
            output: stream
          }),
          terminal: term || 'ansi'
        });

        screen.title = 'SSH Chatting as ' + name;
        // Disable local echo
        screen.program.attr('invisible', true);

        var output = stream.output = new blessed.log({
          screen: screen,
          top: 0,
          left: 0,
          width: '100%',
          bottom: 2,
          scrollOnInput: true
        })
        screen.append(output);

        screen.append(new blessed.box({
          screen: screen,
          height: 1,
          bottom: 1,
          left: 0,
          width: '100%',
          type: 'line',
          ch: '='
        }));

        var input = new blessed.textbox({
          screen: screen,
          bottom: 0,
          height: 1,
          width: '100%',
          inputOnFocus: true
        });
        screen.append(input);

        input.focus();

        // Local greetings
        localMessage('{blue-bg}{white-fg}{bold}Welcome to SSH Chat!{/}\n'
                     + 'There are {bold}'
                     + (users.length - 1)
                     + '{/} other user(s) connected.\n'
                     + 'Type /quit or /exit to exit the chat.',
                     stream);

        // Let everyone else know that this user just joined
        for (var i = 0; i < users.length; ++i) {
          var user = users[i];
          var output = user.output;
          if (user === stream)
            continue;
          output.add(formatMessage('{green-fg}*** {bold}', output)
                     + name
                     + formatMessage('{/bold} has joined the chat{/}', output));
        }

        screen.render();
        // XXX This fake resize event is needed for some terminals in order to
        // have everything display correctly
        screen.program.emit('resize');

        // Read a line of input from the user
        input.on('submit', function(line) {
          input.clearValue();
          screen.render();
          if (!input.focused)
            input.focus();
          line = line.replace(RE_SPECIAL, '').trim();
          if (line.length > MAX_MSG_LEN)
            line = line.substring(0, MAX_MSG_LEN);
          if (line.length > 0) {
            if (line === '/quit' || line === '/exit')
              stream.end();
            else
              userBroadcast(line, stream);
          }
        });
      });
    });
  }).on('end', function() {
    if (stream !== undefined) {
      spliceOne(users, users.indexOf(stream));
      // Let everyone else know that this user just left
      for (var i = 0; i < users.length; ++i) {
        var user = users[i];
        var output = user.output;
        output.add(formatMessage('{magenta-fg}*** {bold}', output)
                   + name
                   + formatMessage('{/bold} has left the chat{/}', output));
      }
    }
  }).on('error', function(err) {
    // Ignore errors
  });
}).listen(0, function() {
  console.log('Listening on port ' + this.address().port);
});

function spliceOne(list, index) {
  for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
    list[i] = list[k];
  list.pop();
}
