// RpsBackground
//
// Usage:
//
// b = new RpsBackground(
//    'http://davidbau.com/meta/rps/code/',  // produces a list of players
//    'http://davidbau.com/list/rps/game/',  // a list of game names
//    'http://davidbau.com/data/rps/code/',  // base url for player files
//    'http://davidbau.com/save/rps/game/',  // where to save games
//    'http://davidbau.com/meta/rps/game/',  // where to load games
//    onstatechange,
// );
//
// b.run();
// ...
// b.players();         // returns [[name, w, l, t]]
// b.games(name);       // returns [name, w, l, t] for each opponent
// b.focus(name1, name2);   // prioritizes games w/ given player
// b.focused_game();    // returns the focused game, or null if not yet done

// states:
//
//    start refresh
//    loading code/games
//    compute needed games
//    select next game
//    loading game
//    playing game
//    

function RpsBackground(
    meta_url, game_url, code_url, save_url, load_url, onchange) {
  this.sleeptime = 1000;
  this.sleeptimer = null;
  this.meta_url = meta_url;
  this.game_url = game_url;
  this.code_url = code_url;
  this.save_url = save_url;
  this.load_url = load_url;
  this.onchange = onchange;
  this.requested_gen = 0;
  this.requested_code = null;
  this.requested_game = null;
  this.current_gen = 0;
  this.player_count = 0;
  this.name_to_md5 = {};
  this.md5_to_name = {};
  this.md5_to_record = {};
  this.md5_to_md5_to_score = {};
  this.needed_game_count = 0;
  this.md5_sorted = [];
  this.requested_focus = [];
  this.next_pair = [];
  this.focus_game = null;
  this.current_game = null;
}

RpsBackground.prototype = {

  run: function run() {
    this.start_refresh();
  },
  
  start_refresh: function start_refresh() {
    console.log('start_refresh');
    if (this.current_game != null) return;
    if (this.sleeptimer != null) {
      clearTimeout(this.sleeptimer);
      this.sleeptimer = null;
    }
    this.requested_gen += 1;
    this.requested_code = null;
    this.requested_game = null;
    var local_gen = this.requested_gen;
    var esto = this;
    $.ajax({
      url: this.game_url,
      dataType: 'json',
      cache: false,
      success: function(data, status) {
        if (esto.requested_gen != local_gen) return;
        esto.requested_game = data;
        esto.finish_refresh();
      },
      error: function() {
        if (esto.requested_gen != local_gen) return;
        esto.requested_game = 'error';
        esto.finish_refresh();
      }
    });
    $.ajax({
      url: this.meta_url,
      dataType: 'json',
      cache: false,
      success: function(data, status) {
        if (esto.requested_gen != local_gen) return;
        esto.requested_code = data;
        esto.finish_refresh();
      },
      error: function() {
        if (esto.requested_gen != local_gen) return;
        esto.requested_code = 'error';
        esto.finish_refresh();
      }
    });
  },

  parse_game_name: function parse_game_name(n) {
    var field = n.split('-');
    if (field.length == 5) {
      try {
        var result = [[field[0], field[1]],
                [parseInt(field[2]), parseInt(field[3]), parseInt(field[4])]];
        return result;
      } catch (e) {
        // do nothing
      }
    }
    return [['', ''], [0, 0, 0]];
  },

  finish_refresh: function finish_refresh() {
    if (this.requested_game == null || this.requested_code == null) return;
    if (this.requested_game == 'error' || this.requested_code == 'error') {
      this.wait_and_refresh();
      return;
    }
    this.current_gen = this.requested_gen;
    this.player_count = this.requested_code.length;
    for (var p = 0; p < this.requested_code.length; p++) {
      var md5 = this.requested_code[p].md5;
      var name = this.requested_code[p].name;
      this.md5_to_name[md5] = name;
      this.name_to_md5[name] = md5;
      this.md5_to_md5_to_score[md5] = { };
      this.md5_to_record[md5] = [0, 0, 0];
    }
    for (var g = 0; g < this.requested_game.length; g++) {
      var parsed = this.parse_game_name(this.requested_game[g].name);
      var player = parsed[0];
      if (!(player[0] in this.md5_to_name && player[1] in this.md5_to_name)) {
        continue;
      }
      var wlt = parsed[1];
      for (var k = 0; k < 2; ++k) {
        var p1 = player[k];
        var p2 = player[1 - k];
        var score = [wlt[k], wlt[1 - k], wlt[2]];
        this.md5_to_md5_to_score[p1, p2] = score;
        this.md5_to_record[p1][this.winner(score)] += 1;
      }
    }
    var esto = this;
    setTimeout(function() { esto.onchange(); }, 0);
    this.continue_background_play();
  },

  winner: function winner(wlt) {
    if (wlt[0] - wlt[1] >= 100) return 0;
    if (wlt[1] - wlt[0] >= 100) return 1;
    return 2;
  },

  continue_background_play: function continue_background_play() {
    if (this.current_game != null) return;
    if (this.sleeptimer != null) {
      clearTimeout(this.sleeptimer);
      this.sleeptimer = null;
    }
    var selected = this.select_next_game();
    if (selected == null) {
      this.wait_and_refresh();
      return;
    }
    selected = [selected[0], selected[1]].sort();
    if (selected[0] in this.md5_to_md5_to_score &&
        selected[1] in this.md5_to_md5_to_score[selected[0]]) {
      this.load_game(selected);
      return;
    }
    this.start_game(selected);
  },

  load_game: function load_game(pair) {
    var esto = this;
    var wlt = this.md5_to_md5_to_score[pair[0]][pair[1]];
    var name = (pair.concat(wlt)).join('-');
    this.current_game = new RpsGame();
    $.ajax({
      url: this.load_url + name,
      dataType: 'json',
      cache: false,
      success: function(data, status) {
        var player = esto.parse_game_name(data.name)[0];
        if ((player[0] == esto.requested_focus[0] &&
             player[1] == esto.requested_focus[1]) ||
            (player[0] == esto.requested_focus[1] &&
             player[1] == esto.requested_focus[0])) {
          esto.current_game.deserialize(data.data);
          esto.focus_game = esto.current_game;
        }
        esto.current_game = null;
        setTimeout(function() { esto.onchange(); }, 0);
        esto.continue_background_play();
      },
      error: function() {
        esto.start_game(pair);
      }
    });
  },

  total_record: function total_record(r) {
    return r[0] + r[1] + r[2];
  },

  select_next_game: function select_next_game() {
    var focus = this.requested_focus;
    if (focus.length == 2 && this.focus_game == null) {
      return focus;
    }
    if (focus.length == 0 ||
        (this.player_count - 1) ==
        this.total_record(this.md5_to_record[focus[0]])) {
      var needed = 0;
      for (var m in this.md5_to_record) {
        var record = this.md5_to_record[m];
        needed += this.player_count - 1 - this.total_record(record);
      }
      if (needed > 0) {
        var select = Math.floor(Math.random() * needed);
        for (var m in this.md5_to_record) {
          var record = this.md5_to_record[m];
          select -= this.player_count - 1 - this.total_record(record);
          if (select < 0) {
            focus = [m];
          }
        }
      }
    }
    if (focus.length > 0) {
      var scores = {};
      if (focus[0] in this.md5_to_md5_to_score) {
        scores = this.md5_to_md5_to_score[focus[0]];
      }
      var candidates = [];
      for (var m in this.md5_to_name) {
        if (m in scores || m == focus[0]) continue;
        candidates.push(m);
      }
      if (candidates.length > 0) {
        return [focus[0],
          candidates[Math.floor(Math.random() * candidates.length)]];
      }
    }
    return null;
  },

  start_game: function start_game(m) {
    var esto = this;
    var game = new RpsGame();
    this.current_game = game;
    var n = [this.code_url + this.md5_to_name[m[0]],
             this.code_url + this.md5_to_name[m[1]]];
    game.run(n[0], n[1], m[1], m[0], function finish_game() {
      var score = game.score;
      esto.md5_to_md5_to_score[m[0]][m[1]] = score;
      var revscore = [score[1], score[0], score[2]];
      esto.md5_to_md5_to_score[m[1]][m[0]] = revscore;
      var winner = game.winner();
      esto.md5_to_record[m[0]][winner] += 1;
      if (winner < 2) winner = 1 - winner;
      esto.md5_to_record[m[1]][winner] += 1;
      if (game.error() == null) {
        var url = esto.save_url + (m.concat(score)).join('-');
        $.ajax({
          type: 'POST',
          url: url,
          data: { data: game.serialize() }
        });
        esto.sleeptime = 1000;
      }
      if ((m[0] == esto.requested_focus[0] &&
           m[1] == esto.requested_focus[1]) ||
          (m[0] == esto.requested_focus[1] &&
           m[1] == esto.requested_focus[0])) {
        esto.focus_game = esto.current_game;
      }
      esto.current_game = null;
      setTimeout(function() { esto.onchange(); }, 0);
      esto.continue_background_play();
    });
  },

  wait_and_refresh: function wait_and_refresh() {
    var esto = this;
    if (this.sleeptime < 3600000) this.sleeptime *= 2;
    console.log('waiting ' + this.sleeptime);
    this.sleeptimer = setTimeout(
      function() { esto.start_refresh(); }, this.sleeptime);
  },

  players: function players() {
    return this.summary(this.md5_to_record);
  },

  games: function games(name) {
    var result = [];
    if (!(name in this.name_to_md5)) return result;
    return this.summary(this.md5_to_md5_to_record[this.name_to_md5[name]]);
  },

  summary: function summary(map) {
    var result = [];
    for (var m in map) {
      result.push([this.md5_to_name[m]].concat(map[m]));
    }
    result.sort(function(a, b) {
      if (a[1] > b[1]) return -1;
      if (a[1] < b[1]) return 1;
      if (a[2] > b[2]) return 1;
      if (a[2] < b[2]) return -1;
      if (a[3] > b[3]) return 1;
      if (a[3] < b[3]) return -1;
      if (a[0] > b[0]) return 1;
      if (a[0] < b[0]) return -1;
      return 0;
    });
    return result;
  },

  equivalent_array: function equivalent_array(a, b) {
    if (a.length != b.length) return false;
    for (var j = 0; j < a.length; ++j) {
      if (a[j] != b[j]) return false;
    }
    return true;
  },

  focus: function focus(tuple) {
    if (this.equivalent_array(tuple, this.requested_focus)) return;
    this.requested_focus = tuple;
    this.focus_game = null;
    if (this.sleeptimer != null) {
      this.continue_background_play();
    }
  },

  focused_game: function focused_game() {
    return this.focus_game;
  }
};
