// Asynchronous RpsGame class
//
// Usage:
//
// var g = new RpsGame();
// var callback = function() { alert('winner was ' + g.winner()); }
// g.run('http://.../player1.js', 'http://.../player2.js', callback);
//
// Assumes two invisible iframes with ids matching RpsGame.sandbox[0, 1].
//


function RpsGame() {
  this.debug = false;
  this.url = null;
  this.seed = null;
  this.loaded = [false, false];
  this.play = [null, null];
  this.state = 0;
  this.history = [];
  this.arrays = [null, null];
  this.forfeit = [null, null];
  this.score = [0, 0, 0];
  this.callback = null;
  this.limit = 5000;
  this.margin = 100;
  this.pause = 1;
  this.batchsize = 128;
  this.logforward = [];
  this.message = [[], []];
  this.called_log = false;
  this.loghistory = [];
  this.round = 0;
}

RpsGame.prototype = {

  sandbox: ['rps_s0', 'rps_s1'],

  run: function run(n0, n1, s0, s1, callback) {
    this.url = [n0, n1];
    this.seed = [s0, s1];
    this.callback = callback;
    this.steplater();
  },

  step: function step() {
    switch (this.state) {
      case 0: this.load_players(); break;
      case 1: this.start_game(); break;
      case 2: this.continue_game(); break;
      case 3: this.finish_game(); break;
    }
  },

  steplater: function steplater() {
    var esto = this;
    setTimeout(function() { esto.step(); }, this.pause);
  },

  load_players: function load_players() {
    var esto = this;
    this.logforward = [
        forward.register(function(c) { esto.notify_log(0, c); }),
        forward.register(function(c) { esto.notify_log(1, c); })
    ];
    var n0 = this.url[0];
    this.setup_frame(0, frames[this.sandbox[0]], n0, this.seed[0],
        this.logforward[0],
        forward.register(function(c) { esto.notify_loaded(0, n0, c); }));
    var n1 = this.url[1];
    this.setup_frame(1, frames[this.sandbox[1]], n1, this.seed[1],
        this.logforward[1],
        forward.register(function(c) { esto.notify_loaded(1, n1, c); }));
  },

  setup_frame: function setup_frame(c, f, n, s, lf, cb) {
    f.window.document.open();
    var out = 
      '<script src="http://davidbau.com/rps/seedrandom.js"><' + '/script>' +
      '<script>' +
      'Math.seedrandom("' + s + '"); ' +
      'var opponent = []; var self = []; ' +
      'function log(m) { parent.forward.notify(' + lf + ', m); }<' +
      '/script>' +
      '<script src="' + n + '"><' + '/script>' +
      '<script>parent.forward.oneshot(' + cb +
      ', [(typeof(play) == "function") ? play : null, opponent, self]);<' +
      '/script>';
    try {
      f.window.document.write(out);
    } catch (e) {
      var msg = '' + e;
      if (msg.length > 0) this.forfeit[c] = msg;
      forward.oneshot(cb, [null, [], []]);
    }
    f.window.document.close();
  },

  notify_loaded: function notify_loaded(j, n, c) {
    var all_done = true;
    if (!this.loaded[j]) {
      if (this.url[j] == n) {
        this.loaded[j] = true;
        this.play[j] = c[0];
        this.arrays[j] = [c[1], c[2]];
      }
    }
    if (this.loaded[0] && this.loaded[1]) {
      this.state = 1;
      this.steplater();
    }
  },

  notify_log: function notify_log(p, m) {
    this.message[p].push(m);
    this.called_log = true;
  },

  getlog: function getlog() {
    if (!this.called_log) { return null; }
    var result = [this.message[0].join('\n'), this.message[1].join('\n')];
    this.message = [[], []];
    this.called_log = false;
    return result;
  },

  start_game: function start_game() {
    for (var j = 0; j < 2; ++j) {
      if (this.forfeit[j] != null) {
        this.state = 3;
      } else if (this.play[j] == null) {
        this.forfeit[j] = 'No play function loaded.';
        this.state = 3;
      }
    }
    if (this.state == 1) {
      this.state = 2;
    }
    this.step();
  },

  check_play: function check_play(player) {
    var move = -1;
    if (this.debug) {
      move = this.play[player]();
    } else {
      try {
        move = this.play[player]();
      } catch (e) {
        this.forfeit[player] =
          'Error ' + e + ' on turn ' + this.history.length + '.';
        return -1;
      }
    }
    if (move != 0 && move != 1 && move != 2) {
      this.forfeit[player] =
        'Invalid move "' + move + '" on turn ' + this.history.length + '.';
      return -1;
    }
    return move;
  },

  continue_game: function continue_game() {
    for (var j = 0; j < this.batchsize; ++j) {
      var moves = [-1, -1];
      if (this.score[0] < this.score[1]) {
        moves[0] = this.check_play(0);
        moves[1] = this.check_play(1);
      } else {
        moves[1] = this.check_play(1);
        moves[0] = this.check_play(0);
      }
      if (moves[0] != -1 && moves[1] != -1) {
        var winner = (2 + moves[0] - moves[1]) % 3;
        this.score[winner] += 1;
        this.arrays[0][0].push(moves[1]);
        this.arrays[0][1].push(moves[0]);
        this.arrays[1][0].push(moves[0]);
        this.arrays[1][1].push(moves[1]);
        this.history.push(moves);
        this.loghistory.push(this.getlog());
      }
      if (this.game_over()) {
        this.state = 3;
        break;
      }
    }
    this.steplater();
  },

  finish_game: function finish_game() {
    this.state = 4;
    for (var j = 0; j < this.logforward.length; ++j) {
      forward.remove(this.logforward[j]);
    }
    this.logforward = [];
    if (this.callback != null) {
      setTimeout(this.callback, 0);
    }
  },

  game_over: function game_over() {
    frames[this.sandbox[0]].window.location = 'about:blank';
    frames[this.sandbox[1]].window.location = 'about:blank';
    if (this.winner() != 2 || this.history.length >= this.limit) {
      return true;
    }
    return false;
  },

  winner: function winner() {
    if (this.forfeit[1] != null) return 0;
    if (this.forfeit[0] != null) return 1;
    var delta = this.score[0] - this.score[1];
    if (delta >= this.margin) return 0;
    if (-delta >= this.margin) return 1;
    return 2;
  },

  error: function error() {
    if (this.winner() == 2) {
      return null;
    }
    return this.forfeit[1 - this.winner()];
  },

  done: function done() {
    return (this.state == 4);
  },

  serialize: function serialize() {
    var lines = [this.url[0] + ' ' + this.score[0],
                 this.url[1] + ' ' + this.score[1],
                 'tie ' + this.score[2], ''];
    if (this.forfeit[0] != null) lines[0] += ' ' + escape(this.forfeit[0]);
    if (this.forfeit[1] != null) lines[1] += ' ' + escape(this.forfeit[1]);
    for (var j = 0; j < this.history.length; ++j) {
      var line = [this.history[j][0], this.history[j][1]];
      if (this.loghistory[j] != null) {
        line.push(escape(this.loghistory[j][0]));
        line.push(escape(this.loghistory[j][1]));
      }
      lines.push(line.join(':'));
    }
    return lines.join('\n');
  },

  deserialize: function deserialize(s) {
    var lines = s.split('\n');
    this.url = [];
    for (var j = 0; j < 3; ++j) {
      var scores = lines[j].split(' ');
      if (j < 2) {
        this.url.push(scores[0]);
      }
      this.score[j] = parseInt(scores[1]);
      if (scores.length > 2) {
        this.forfeit[j] = unescape(scores[2]);
      }
    }
    lines.splice(0, 4);
    for (var j = 0; j < lines.length; ++j) {
      var move = lines[j].split(':');
      this.history.push([parseInt(move[0]), parseInt(move[1])]);
      if (move.length > 2) {
        this.loghistory.push([unescape(move[2]), unescape(move[3])]);
      } else {
        this.loghistory.push(null);
      }
    }
  },

  htmlEscape: function htmlEscape(s) {
    return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;')
            .replace(/</g, '&lt;').replace(/>/g, '&gt;');
  },

  formatname: function formatname(s, n) {
    var cut = s.lastIndexOf('/') + 1
    s = s.substr(cut);
    if (s.lastIndexOf('.js') == s.length - 3) s = s.substr(0, s.length - 3);
    s = (s.substr(0, 16) + ': ' + n +
         ' -------------------').substr(0, 23) + ' ';
    return s;
  },
  
  formatted: function formatted() {
    var ch = ['r', 'p', 's'];
    result = [];
    for (var k = 0; k < 2; ++k) {
      var score = this.score[k];
      if (this.forfeit[k] != null) score = score + 'F';
      if (this.winner() == k) result.push('<span class=win>');
      result.push(this.formatname(this.url[k], score));
      if (this.winner() == k) result.push('</span>');
      for (var j = 0; j < this.history.length; ++j) {
        var won =
          (((this.history[j][k] - this.history[j][1 - k] + 3) % 3) == 1);
        var msg = (this.loghistory[j] != null && this.loghistory[j][k].length);
        if (won || msg) {
          result.push('<span');
          if (msg) {
            result.push(' title="');
            result.push(this.htmlEscape(this.loghistory[j][k]));
            result.push('"');
          }
          if (won) {
            result.push(' class=win');
          }
          result.push('>');
          result.push(ch[this.history[j][k]]);
          result.push('</span>');
        } else {
          result.push(ch[this.history[j][k]]);
        }
      }
      if (this.forfeit[k] != null) result.push(' Forfeit: ' + this.forfeit[k]);
      result.push('<br>');
    }
    return result.join('');
  }
}

