/**
 * External Libraries used for the WoShamBo Gadget
 * Author: Wei-Hwa Huang
 *
 * This file contains multiple utilities used for calculating
 * Woshambo competition information; it can be considered the
 * "Woshambo API".
 *
 * This file contains code that is very WoShamBo-specific;
 * code that is used for general Javascript manipulation, such
 * as DOM and UI stuff, is in general-lib.js.
 */

// The top-level object, all of our other functions and constants
// are fields of this object.

woshambo = {};

///////////////////////////////////////////////
//  Constants

woshambo.C = {};

// Version number.  Bigger numbers are later versions; ideally,
// when we encounter data in the datastore that is of an earlier
// version, we should be able to handle it gracefully.

woshambo.C.VERSION = 5;

// Values that indicate outcomes of a specific combat.

woshambo.C.WIN = 0;
woshambo.C.TIE = 1;
woshambo.C.LOSS = 2;

// Storing an unlimited number of combats puts too much stress
// on server storage; hence we will occasionally compress
// previous results until something called a "past".  A past
// consists of just raw data recording the number of wins,
// ties, and losses.

woshambo.C.EMPTYPAST = [0,0,0]; // wins, ties, losses

// The possible weapons a player can use in combat.

woshambo.C.ROCK = 0;
woshambo.C.PAPER = 1;
woshambo.C.SCISSORS = 2;
woshambo.C.INVALID = 99;

// Source directory for the combat images.

woshambo.C.IMAGES = "http://opensocial-resources.googlecode.com/"
                    + "svn/samples/woshambo/images/";

// Status of opponents.

woshambo.C.WAITING = 99;  // opponent has not made a move yet.
woshambo.C.DEAD = 100; // opponent hasn't played WoShamBo
woshambo.C.SLOW = 101; // opponent has played but hasn't responded yet

// Robot IDs.

woshambo.C.ROCKMAN = 0;  // "Rockman" robot.
woshambo.C.FLIPPER = 1;  // "Flipper" robot.

// Scores for getting results

woshambo.C.POINTS_HUMAN_WIN = 105;
woshambo.C.POINTS_HUMAN_TIE = 60;
woshambo.C.POINTS_HUMAN_LOSS = 15;
woshambo.C.POINTS_HUMAN_AVERAGE = 60;  // (105 + 15 + 60) / 3
woshambo.C.POINTS_ROBOT_WIN = 4;
woshambo.C.POINTS_ROBOT_TIE = 3;
woshambo.C.POINTS_ROBOT_LOSS = 2;

/////////////////////////////////////////
//  Charsheet
//
// A Charsheet, short for "character sheet", is a list
// of stats and information about the player.  If the player
// levels up or has any personal information not related
// to specific combat, it goes here.
//
// Fields in the charsheet:
//   version
//   spent: How many points the player has spent.
//   score: How many points the player has.
//   earned: How many points the player has earned over a lifetime.
//     Note that score and earned are "estimates" only, and may not be accurate
//     due to other combats.
//   storage: Weapons storage.  A player only has a finite
//       number of weapons and needs to buy more with
//       their score.
//   robot_points: How many points the player has won against robots.
//   log: Text string of logs.
//   news: Alerts and information to the player.

// Constructor.
woshambo.Charsheet = function() {
  this.version = woshambo.C.VERSION;
  this.spent = 0;
  this.score = 0;
  this.earned = 0;
  this.robot_points = 0;
  this.storage = [20,20,20];  // 20 rocks, 20 paper, 20 scissors
  this.log = "";
  this.news = "";
};

/////////////////////////////////////////
//  History
//
// A History is a list of all the Moves a player
// has used in a Match (a Match being a series of Combats
// against a specific player).
//
// Fields:
//  version
//  weapons: an array of previous Moves (usually Weapons).
//  past: When the list of weapons gets too big, it gets
//      compressed into this "past", which is an aggregate
//      list of previous weapons.  (Nothing does the compression
//      yet, though.)
//  past_points:  The number of points earned by the "past".

// Constructor.
woshambo.History = function() {
  this.version = woshambo.C.VERSION;
  this.past = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
  this.weapons = []; // list of Moves
  this.past_points = 0;  // points_earned in the past
};

///////////////////////////////////////////////
///////////////////////////////////////////////
//  Functions for manipulating woshambo objects
//
// All these utility functions are stored under "F".
woshambo.F = {};

/******************************************

This is the old spec for History and Charsheet, mostly used in
upgrading and downgrading.

VERSIONS 1-2:

woshambo.Charsheet = function() {
  this.version = woshambo.C.VERSION;
  this.score = 0;
  this.earned = 0;
  this.storage = [100,100,100];  // 100 rocks, 100 paper, 100 scissors
  this.log = "";
  this.news = "";
};
woshambo.History = function() {
  this.version = woshambo.C.VERSION;
  this.past = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
  this.weapons = []; // list of Moves
};

VERSION 3:

Added "past_points" to History.

VERSION 4:

Added "spent" to Charsheet.

VERSION 5:

Added "robotpoints" to Charsheet.

******************************************/

///////////////////////////////////////////////////////
// Functions for encoding and decoding history objects.
///////////////////////////////////////////////////////

// Given an object that is suspected to be a history object,
// attempt to turn it into a workable history object of the
// current version and return it.
woshambo.F.canonHistory = function(history) {
  if (history == undefined || history.version == undefined) {
    debugLog("Invalid history!  Making new one...<br>");
    history = new woshambo.History();
  } else if (history.version < woshambo.C.VERSION) {
    debugLog("History is an old version!  Upgrading...<br>");
    history = woshambo.F.upgradeHistory(history);
  } else if (history.version > woshambo.C.VERSION) {
    debugLog("History is an newer version!  Downgrading...<br>");
    history = woshambo.F.downgradeHistory(history);
  }
  return history;
}


// Given a history from an older version, return a upgraded version of it.
woshambo.F.upgradeHistory = function(original) {
  // check to make sure we are supposed to be called.
  if (original.version == woshambo.C.VERSION)
    return original;
  if (original.version > woshambo.C.VERSION)
    return woshambo.F.downgradeHistory(original);

  var result = new woshambo.History();

  if (original.version <= 2) {
    result.past = woshambo.F.copyPast(original.past);
    result.weapons = original.weapons.slice();
  } else if (original.version <= 4) {
    // no change needed
    result.past = woshambo.F.copyPast(original.past);
    result.weapons = original.weapons.slice();
    result.past_points = original.past_points;
  } else {
    // we shouldn't be here, but let's just make guesses.
    if (original.past != undefined)
      result.past = woshambo.F.copyPast(original.past);
    if (original.weapons != undefined)
      result.weapons = original.weapons.slice();
    if (original.past_points != undefined)
      result.past_points = original.past_points;
  }

  return result;
}

// Given a history from an newer version, attempt to
// return a current version of it.
woshambo.F.downgradeHistory = function(original) {
  // check to make sure we are supposed to be called.
  if (original.version == woshambo.C.VERSION)
    return original;
  if (original.version < woshambo.C.VERSION)
    return woshambo.F.upgradeHistory(original);

  // Rewrite it to be the current version
  original.version = woshambo.C.VERSION;
  // all we can do is make guesses and hope that the fields are still there.
  // if they aren't, just initialize them.
  if (original.past != undefined)
    original.past = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
  if (original.weapons != undefined) original.weapons = [];
  if (original.past_points != undefined) original.past_points = 0;

  return original;
}

///////////////////////////////////////////////////////
// Functions for encoding and decoding charsheet objects.
///////////////////////////////////////////////////////

// Given an object that is suspected to be a charsheet object,
// attempt to turn it into a workable charsheet object of the
// current version and return it.
woshambo.F.canonCharsheet = function(charsheet) {
  if (charsheet == undefined || charsheet.version == undefined) {
    debugLog("Invalid charsheet!  Making new one...<br>");
    charsheet = new woshambo.Charsheet();
  } else if (charsheet.version < woshambo.C.VERSION) {
    debugLog("Charsheet is an old version!  Upgrading...<br>");
    charsheet = woshambo.F.upgradeCharsheet(charsheet);
  } else if (charsheet.version > woshambo.C.VERSION) {
    debugLog("Charsheet is an newer version!  Downgrading...<br>");
    charsheet = woshambo.F.downgradeCharsheet(charsheet);
  }
  return charsheet;
}

// Given a charsheet from an older version, return a upgraded version of it.
woshambo.F.upgradeCharsheet = function(original) {
  // check to make sure we are supposed to be called.
  if (original.version == woshambo.C.VERSION) return original;
  if (original.version > woshambo.C.VERSION)
    return woshambo.F.downgradeCharsheet(original);

  var result = new woshambo.Charsheet();

  if (original.version <= 4) {
    result.score = original.score;
    result.earned = original.earned;
    result.spent = original.earned - original.score;
    result.storage = original.storage.slice();
    result.log = original.log;
    result.news = original.news;
  } else {
    // we shouldn't be here, but let's just make guesses.
    if (original.score != undefined) result.score = original.score;
    if (original.earned != undefined) result.earned = original.earned;
    if (original.spent != undefined) result.spent = 0;
    if (original.storage != undefined) result.storage = original.storage.slice();
    if (original.log != undefined) result.log = original.log;
    if (original.news != undefined) result.news = original.news;
    if (original.robot_points != undefined)
      result.robot_points = original.robot_points;
  }

  return result;
}

// Given a charsheet from an newer version, attempt to
// return a current version of it.
woshambo.F.downgradeCharsheet = function(original) {
  // check to make sure we are supposed to be called.
  if (original.version == woshambo.C.VERSION) return original;
  if (original.version < woshambo.C.VERSION)
    return woshambo.F.upgradeCharsheet(original);

  original.version = woshambo.C.VERSION;

  // all we can do is make guesses and hope that the fields are still there.
  // if they aren't, just initialize them.
  if (original.score != undefined) original.score = 0;
  if (original.earned != undefined) original.earned = 0;
  if (original.spent != undefined) original.spent = 0;
  if (original.storage != undefined) original.storage = [20, 20, 20];
  if (original.log != undefined) original.log = "";
  if (original.news != undefined) original.news = "";
  if (original.robot_points != undefined) original.robot_points = 0;

  return original;
}

////////////////////////////////////////////////
// functions that deal with combat and weapons
////////////////////////////////////////////////

// Given an old "past" object, outputs a new copy of it.
woshambo.F.copyPast = function(old) {
  var newPast = [];
  newPast[woshambo.C.WIN] = old[woshambo.C.WIN];
  newPast[woshambo.C.TIE] = old[woshambo.C.TIE];
  newPast[woshambo.C.LOSS] = old[woshambo.C.LOSS];
  return newPast;
}

// Converts a combat result to a human-readable string.
woshambo.F.resultToString = function(result) {
  if (result == woshambo.C.WIN) return "Win";
  if (result == woshambo.C.LOSS) return "Loss";
  if (result == woshambo.C.TIE) return "Tie";
  return "?";
}

// Converts a weapon to a single letter.
woshambo.F.weaponToLetter = function(weapon) {
  if (weapon == woshambo.C.ROCK) return "R";
  if (weapon == woshambo.C.PAPER) return "P";
  if (weapon == woshambo.C.SCISSORS) return "S";
  return "?";
}

// Converts a weapon to a DOM object of the image.
// explode should be a boolean value specifying whether
// the weapon should have an "explosion" graphic or not.
woshambo.F.weaponToImage = function(weapon, explode) {
  var result = document.createElement("img");
  if (weapon == woshambo.C.ROCK && explode) {
    result.src = woshambo.C.IMAGES + "weapon-rock-inverse-small.png";
    return result;
  }
  if (weapon == woshambo.C.ROCK && !explode) {
    result.src = woshambo.C.IMAGES + "weapon-rock-normal-small.png";
    return result;
  }
  if (weapon == woshambo.C.PAPER && explode) {
    result.src = woshambo.C.IMAGES + "weapon-paper-inverse-small.png";
    return result;
  }
  if (weapon == woshambo.C.PAPER && !explode) {
    result.src = woshambo.C.IMAGES + "weapon-paper-normal-small.png";
    return result;
  }
  if (weapon == woshambo.C.SCISSORS && explode) {
    result.src = woshambo.C.IMAGES + "weapon-scissors-inverse-small.png";
    return result;
  }
  if (weapon == woshambo.C.SCISSORS && !explode) {
    result.src = woshambo.C.IMAGES + "weapon-scissors-normal-small.png";
    return result;
  }
}

// Calculates the result of a combat (relative to the host)
// based on the weapons used.
woshambo.F.combatResult = function(hostWeapon, guestWeapon) {
  if (hostWeapon == woshambo.C.ROCK) { // rock
    if (guestWeapon == woshambo.C.ROCK) return woshambo.C.TIE;
    if (guestWeapon == woshambo.C.PAPER) return woshambo.C.LOSS;
    if (guestWeapon == woshambo.C.SCISSORS) return woshambo.C.WIN;
  } else if (hostWeapon == woshambo.C.PAPER) { // paper
    if (guestWeapon == woshambo.C.ROCK) return woshambo.C.WIN;
    if (guestWeapon == woshambo.C.PAPER) return woshambo.C.TIE;
    if (guestWeapon == woshambo.C.SCISSORS) return woshambo.C.LOSS;
  } else if (hostWeapon == woshambo.C.SCISSORS) { // scissors
    if (guestWeapon == woshambo.C.ROCK) return woshambo.C.LOSS;
    if (guestWeapon == woshambo.C.PAPER) return woshambo.C.WIN;
    if (guestWeapon == woshambo.C.SCISSORS) return woshambo.C.TIE;
  }
  // if all else fails, tie.
  return woshambo.C.TIE;
}

// Given a history object, output how long the past in this history is.
woshambo.F.pastLength = function(history) {
  var wins = history.past[woshambo.C.WIN];
  var ties = history.past[woshambo.C.TIE];
  var losses = history.past[woshambo.C.LOSS];
  return (wins + ties + losses);
}

// Given a history object, output how many total weapons are in the history.
woshambo.F.weaponCount = function(history) {
  return (woshambo.F.pastLength(history) + history.weapons.length);
}

// Given a history object, return a past that is the inverse of
// what the past in the object is.
woshambo.F.invertPast = function(history) {
  var result = woshambo.F.copyPast(woshambo.C.EMPTYPAST);
  result[woshambo.C.WIN] = history.past[woshambo.C.LOSS];
  result[woshambo.C.TIE] = history.past[woshambo.C.TIE];
  result[woshambo.C.LOSS] = history.past[woshambo.C.WIN];
  return result;
}

// Given a combat result, calculate the number of points won.
woshambo.F.calcPoints = function (is_human, result) {
  if (is_human) {
    if (result == woshambo.C.WIN) return woshambo.C.POINTS_HUMAN_WIN;
    if (result == woshambo.C.TIE) return woshambo.C.POINTS_HUMAN_TIE;
    if (result == woshambo.C.LOSS) return woshambo.C.POINTS_HUMAN_LOSS;
  } else {
    if (result == woshambo.C.WIN) return woshambo.C.POINTS_ROBOT_WIN;
    if (result == woshambo.C.TIE) return woshambo.C.POINTS_ROBOT_TIE;
    if (result == woshambo.C.LOSS) return woshambo.C.POINTS_ROBOT_LOSS;
  }
}

//////////////////////////////////////////////////
// functions that calculate values from charsheets and histories
//////////////////////////////////////////////////

// Remove the points earned and points holding in a charsheet.
woshambo.F.resetCharsheetPoints = function(charsheet) {
  charsheet.score = charsheet.robot_points - charsheet.spent;
  charsheet.earned = charsheet.robot_points;
}

// Given a host charsheet, increment by the specified number of points.
woshambo.F.addCharsheetPoints = function(charsheet, pts) {
  charsheet.score += pts;
  charsheet.earned += pts;
}

// Given a host charsheet and two histories (that are involved in a war),
// increment the charsheet to reflect the points earned in the two histories.
woshambo.F.addCharsheetHistoryPoints
      = function(charsheet, hostHistory, guestHistory) {
  if (hostHistory == undefined) return;
  if (guestHistory == undefined) return;
  var pts = woshambo.F.totalPointsEarned(hostHistory, guestHistory, true);
  charsheet.score += pts;
  charsheet.earned += pts;
}

// Given two histories (that are involved in a war), output
// the total number of points earned by the host.
//
// If the host's past is smaller than the guest's past, we have a
// problem -- we don't know what moves the guest did in our first moves.
// For now, let's assume that the host got an "average" score; in a
// future version we'll attempt to do this the "correct" way,
// maybe by recording the guest's moves as a backup.
//
woshambo.F.totalPointsEarned = function(hostHistory, guestHistory, is_human) {
  var answer = hostHistory.past_points;

  var hostPast = woshambo.F.pastLength(hostHistory);
  var guestPast = woshambo.F.pastLength(guestHistory);

  var result = null;
  if (hostPast < guestPast) {
    var hostPos = guestPast - hostPast;
    var guestPos = 0;
    while (hostPos < hostHistory.weapons.length
           && guestPos < guestHistory.weapons.length) {
      answer += woshambo.F.calcPoints(
                  is_human,
                  woshambo.F.combatResult(hostHistory.weapons[hostPos],
                  guestHistory.weapons[guestPos])
                );
      hostPos++;
      guestPos++;
    }
  } else {
    result = woshambo.F.copyPast(hostHistory.past);
    var guestPos = hostPast - guestPast;
    answer += guestPos * woshambo.C.POINTS_HUMAN_AVERAGE;
    // don't count the stuff that is in the host's past
    var hostPos = 0;
    while (hostPos < hostHistory.weapons.length
           && guestPos < guestHistory.weapons.length) {
      answer += woshambo.F.calcPoints(
                  is_human,
                  woshambo.F.combatResult(hostHistory.weapons[hostPos],
                  guestHistory.weapons[guestPos])
                );
      hostPos++;
      guestPos++;
    }
  }
  return answer;
}

// Given two histories (that are involved in a war), output
// a past object that lists the the aggregate result of the two
// histories facing off against each other.
woshambo.F.netResults = function(hostHistory, guestHistory) {
  var hostPast = woshambo.F.pastLength(hostHistory);
  var guestPast = woshambo.F.pastLength(guestHistory);
  var result = null;
  if (hostPast < guestPast) {
    result = woshambo.F.copyPast(woshambo.F.invertPast(guestHistory));
    var hostPos = guestPast - hostPast;
    var guestPos = 0;
    while (hostPos < hostHistory.weapons.length
           && guestPos < guestHistory.weapons.length) {
      result[woshambo.F.combatResult(hostHistory.weapons[hostPos],
                                     guestHistory.weapons[guestPos])]++;
      hostPos++;
      guestPos++;
    }
  } else {
    result = woshambo.F.copyPast(hostHistory.past);
    var guestPos = hostPast - guestPast;
    var hostPos = 0;
    while (hostPos < hostHistory.weapons.length
           && guestPos < guestHistory.weapons.length) {
      result[woshambo.F.combatResult(hostHistory.weapons[hostPos],
                                     guestHistory.weapons[guestPos])]++;
      hostPos++;
      guestPos++;
    }
  }
  return result;
}

// Add a move by the "guest", of the specified weapon.
// Call the callback function with the combat result of this weapon.
woshambo.F.makeMove = function (hostHistory, guestHistory, weapon, callback) {
  var turn = guestHistory.weapons.length + woshambo.F.pastLength(guestHistory);
  var myPosition = guestHistory.weapons.length;
  var hostPosition = turn - woshambo.F.pastLength(hostHistory);

  guestHistory.weapons.push(weapon);

  if (hostPosition < 0) {
    // host's move has been lost to history -- oops!
    callback(woshambo.C.INVALID);
  } else if (hostPosition >= hostHistory.weapons.length) {
    // host hasn't made a move yet
    callback(woshambo.C.WAITING);
  } else {
    callback(woshambo.F.combatResult(guestHistory.weapons[myPosition],
                                     hostHistory.weapons[hostPosition]));
  }
}

///////////////////////////////////////////////
//  functions for woshambo robots
///////////////////////////////////////////////

// Returns a random weapon.
woshambo.F.randomWeapon = function () {
  var temp = Math.random() * 3;
  if (temp < 1) return woshambo.C.ROCK;
  if (temp < 2) return woshambo.C.PAPER;
  return woshambo.C.SCISSORS;
}

// Adds a move from the robot to robot_history.
woshambo.F.robotMove = function (robotID, robot_history, opponent_history) {
  if (robotID == woshambo.C.ROCKMAN) {
    woshambo.F.robotRockmanMove(robot_history, opponent_history);
  } else if (robotID == woshambo.C.FLIPPER) {
    woshambo.F.robotFlipperMove(robot_history, opponent_history);
  } else {
    // don't recognize the robot; default to rockman.
    woshambo.F.robotRockmanMove(robot_history, opponent_history);
  }
}

woshambo.F.robotRockmanMove = function (robot_history, opponent_history) {
  robot_history.weapons.push(woshambo.C.ROCK);
}

woshambo.F.robotFlipperMove = function (robot_history, opponent_history) {
  if (robot_history.weapons.length == 0) {
    robot_history.weapons.push(woshambo.F.randomWeapon());
  } else {
    var last = robot_history.weapons[robot_history.weapons.length-1];
    var next = woshambo.F.randomWeapon();
    while (next == last) {
      next = woshambo.F.randomWeapon();
    }
    robot_history.weapons.push(next);
  }
}

// Returns a printable display name for that robot.
woshambo.F.robotName = function (robotID) {
  if (robotID == woshambo.C.ROCKMAN) {
    return "Rockman the Robot";
  } else if (robotID == woshambo.C.FLIPPER) {
    return "Flipper the Robot";
  } else {
    // don't recognize the robot; default to rockman.
    return "Rockman the Robot";
  }
}

