
/**
 * clone(), a utility function.
 *
 * Unfortunately, Javascript doesn't have it built-in.
 *
 * I took it from:
 *   http://keithdevens.com/weblog/archive/2007/Jun/07/javascript.clone
 * But, check this out:
 *   http://stackoverflow.com/questions/122102/what-is-the-most-efficent-way-to-clone-a-javascript-object
 * Also, investigate why Grey's fails:
 *   http://my.opera.com/GreyWyvern/blog/show.dml/1725165
 */
function clone(obj) {
  if(obj == null || typeof(obj) != 'object')
    return obj;
  var temp = new obj.constructor();
  for(var key in obj)
    temp[key] = clone(obj[key]);
  return temp;
}

/**
 * Constructor.
 */
function Abacus(htmlObj) {
  this.htmlObj = htmlObj;
  this.beads = [];
  this.history = new History(); // A dummy one.
}

Abacus.prototype = {

  htmlObj: null,           // A container HTML element for the abacus.
  beads: null,             // Contains the state of the beads.

  /**
   * The abacus' configuration.
   */
  heaven_size: 1,          // How many beads at heaven?
  earth_size: 4,           // How many beads at earth?
  heaven_factor: 5,        // How many units a heaven bead counts?
  earth_factor: 1,         // How many units an earth bead count?
  decimal_position: 2,     // How many rightmost rods denote the fraction?
  rods_count: 10,          // How many rods shall we have?
  base: 10,                // How to interpret the number in the bacus.
  variation: 'jap',        // Added as a CSS class to the abacus' <TABLE>.
  gap_size: 1,

  /**
   * Creates the bead's HTML element.
   *
   * @param where
   *   Either 'heaven' or 'earth'.
   * @param new_number
   *   The number of beads to set on the rod when this bead is clicked.
   * @param variation
   *   CSS class to add to the HTML.
   */
  createBallElement: function(rod, where, new_number, variation) {
    var abacus = this;
    return $('<div>')
            .addClass('ball')
            .addClass(variation)
            .click(function() {
               abacus.setBeads(where, rod, new_number)
               abacus.draw()
            });
  },

  /**
   * Creates the HTML for the gap. The "gap" separates beads that are 'on' from
   * beads that are 'off'.
   */
  createGapElement: function() {
    var gap = $('<div>');
    var n = this.gap_size;
    while (n-- > 0)
      gap.append($('<div>').attr('class', 'gap'));
    return gap;
  },

  /**
   * Prints out a tabula.
   *
   * @param which
   *   Either 'heaven' or 'earth'.
   * @param direction
   *   Either 'ttb' (top to botom) or 'btt' (bottom to top).
   */
  renderTabula: function(which, direction) {
    var row = $('<tr>')
    var beads_count = (which == 'earth' ? this.earth_size : this.heaven_size);
    var insertion_method = (direction == 'ttb' ? 'append' : 'prepend');

    for (var rod = 0; rod < this.rods_count; rod++) {
      var column = $('<td>');
      column.addClass('position-' + (this.rods_count - rod - this.decimal_position));
 
      var activated = this.getBeads(which, rod)
      var cleared = beads_count - activated;

      if (!cleared) {
        column.append(this.createGapElement())
      }

      for (var bead = 1; bead <= beads_count; bead++) {
        var ball;

        // Draw an 'off' bead
        if (bead <= cleared) {
          var click_count = beads_count - bead + 1;
          ball = this.createBallElement(rod, which, click_count, 'off');
        }
        // Draw an 'on' bead
        if (bead > cleared) {
          var click_count = beads_count - bead;
          ball = this.createBallElement(rod, which, click_count, 'on');
        }

        ball.addClass('bead-' + bead);
        column[insertion_method](ball);

        if (bead == cleared) {
          column[insertion_method](this.createGapElement());
        }
      }
      row.append(column);
    }

    row.addClass(beads_count > 8 ? 'big' : 'small');
    return row;
  },

  /**
   * Prints out the complete abacus.
   */
  draw: function() {
    var table = $('<table>')
                 .attr('border', '1')  // I ought to use CSS instead...
                 .addClass('abacus base-' + this.base)
                 .addClass(this.variation);
    if (this.heaven_size > 0) {
      table.append(this.renderTabula('heaven', 'ttb'))
    }
    if (this.earth_size > 0) {
      table.append(this.renderTabula('earth', 'btt'));
    }
    table.append(this.renderValues());
    this.htmlObj.empty().append(table);
  },

  /**
   * Calculates the value of a rod.
   */
  calculateRod: function(rod) {
    return this.heaven(rod) * this.heaven_factor +
            this.earth(rod) * this.earth_factor;
  },

  /**
   * Gives the index of the 'unit rod'.
   */
  unitRod: function() {
    return this.rods_count - this.decimal_position - 1;
  },

  /**
   * Gives the left-most "interesting" rod.
   */
  getMinimumActiveRod: function() {
    var n = this.rods_count;
    for (var rod = 0; rod < this.rods_count; rod++) {
      if (this.calculateRod(rod) > 0) {
        n = rod;
        break;
      }
    }
    // Ensure the unit rod is included:
    return Math.min(n, this.unitRod());
  },

  /**
   * Gives the right-most "interesting" rod.
   */
  getMaximumActiveRod: function() {
    var n = -1;
    for (var rod = this.rods_count - 1; rod >= 0; rod--) {
      if (this.calculateRod(rod) > 0) {
        n = rod;
        break;
      }
    }
    // Ensure the unit rod is included:
    return Math.max(n, this.unitRod());
  },

  /**
   * Prints out the tallying row.
   */
  renderValues: function() {
    var abacus = this;
    var minimum_active = this.getMinimumActiveRod();
    var maximum_active = this.getMaximumActiveRod();
    var row = $('<tr>').addClass('values');
    for (var rod = 0; rod < this.rods_count; rod++) {
      var column = $('<td>');
      column[0].decimal_position = this.rods_count - rod - 1;
      column.click(function() {
        abacus.decimal_position = this.decimal_position;
        abacus.beedsHaveBeenMoved();
        abacus.draw();
      });
      if (rod >= minimum_active && rod <= maximum_active) {
        column.text(this.formatNumber(this.calculateRod(rod)))
      }
      row.append(column)
    }
    return row;
  },

  /**
   * Clears the abacus.
   */
  clear: function() {
    this.beads = [];
    this.draw();
  },

  /**
   * Reads the number of beads on a rod.
   * This is a low-level function. Call heaven() or earth() instead.
   *
   * @param where
   *   Either 'heaven' or 'earth'.
   *
   */
  getBeads: function (where, rod) {
    if (this.beads[rod]) {
      return this.beads[rod][where];
    } else {
      return 0;
    }
  },

  /**
   * Schedule some code to occur in the future.
   *
   * @param id
   *   If the previous code registered under this ID hasn't been executed yet, this
   *   previous code is discarded.
   */
  schedule: function(id, seconds, func) {
    if (!this.timeouts) {
      this.timeouts = []
    }
    if (this.timeouts[id]) {
      window.clearTimeout(this.timeouts[id]);
      delete this.timeouts[id];
    }
    this.timeouts[id] = window.setTimeout(func, seconds);
  },
  
  unscheduleAll: function() {
    if (this.timeouts) {
      for (id in this.timeouts) {
        window.clearTimeout(this.timeouts[id]);
      }
      this.timeouts = [];
    }
  },

  /**
   * Sets the number of beads on a rod.
   * This is a low-level function. Call heaven() or earth() instead.
   */
  setBeads: function (where, rod, count) {
    if (!this.beads[rod]) {
      this.beads[rod] = { heaven: 0, earth: 0 }
    }
    this.beads[rod][where] = count;
    this.beedsHaveBeenMoved();
  },

  beedsHaveBeenMoved: function() {
    var abacus = this;
    this.schedule('a bead has been moved', 2000, function() {abacus.recordInHistory()});
    this.schedule('user has gone', 7000, function() {abacus.history.appendSeparator()});
  },

  recordInHistory: function() {
    this.history.append(this.formatNumber(this.readNumber(), true), {
      beads: clone(this.beads),
      decimal_position: this.decimal_position
    });
  },

  formatNumber: function(n, long) {
    s = n.toString(this.base).toUpperCase();
    if (long && this.base != 10) {
      s += ' (Decimal: ' + n + ')';
    }
    return s;
  },

  /**
   * Figures out the number shown on the abacus.
   */
  readNumber: function() {
    var n = 0;
    for (var rod = 0; rod < this.rods_count; rod++) {
      n = n * this.base + this.calculateRod(rod)
    }
    n /= Math.pow(this.base, this.decimal_position);
    return n;
  },

  /**
   * Sets the number shown on the abacus. More accurately,
   * it sets the beads to a previously saved configuration.
   */
  setNumberByBeads: function(data) {
    if (data) {
      this.beads = clone(data.beads);
      this.decimal_position = data.decimal_position;
      this.draw();
    }
  },

  /**
   * Sets the number shown on the abacus.
   */
  setNumber: function(_n) {
    var n = _n;
    this.beads = [];

    var rod = this.unitRod();
    while (n > 0) {
      var digit = n % this.base;
      n = (n - digit) / this.base;

      // First, exhaust heaven.
      if (this.heaven_size != 0 && this.heaven_factor != 0) {
        while (digit >= this.heaven_factor) {
          this.setHeaven(rod, this.heaven(rod) + 1);
          digit -= this.heaven_factor;
        }
      }
      // Next, drop the rest on earth.
      this.setEarth(rod, digit);

      rod--;
    }

    if (rod < 0) {
      // My, my, my! We need to enlage the abacus.
      this.rods_count += 1;
      this.setNumber(_n);
    }

    this.draw();
  },

  /**
   * The following are shorthands for getBeads() and setBeads().
   */
  heaven: function(rod) {
    return this.getBeads('heaven', rod);
  },

  setHeaven: function(rod, count) {
    this.setBeads('heaven', rod, count)
  },

  earth: function (rod, set) {
    return this.getBeads('earth', rod);
  },

  setEarth: function(rod, count) {
    this.setBeads('earth', rod, count)
  }
}

/**
 * Constructor.
 *
 * A History object manages the history list box.
 */
function History(htmlObj) {
  this.data = []
  if (htmlObj && htmlObj.size()) {
    this.htmlObj = htmlObj;
    var history = this;
    htmlObj.click(function() {
      history.copyToAbacus();
    });
    htmlObj.dblclick(function() {
      history.highlight();
    });
  }
}

History.prototype = {
  htmlObj: null,
  abacus: null,
  data: null,

  appendSeparator: function(text) {
    if (this.htmlObj) {
      var old_sel = this.htmlObj[0].selectedIndex;
      this.append(text || '------');
      // We don't want to disorient the user, so we restore
      // his selection.
      this.htmlObj[0].selectedIndex = old_sel;
    }
  },

  clear: function() {
    if (this.htmlObj) {
      this.htmlObj.empty();
    }
    this.data = [];
  },

  highlight: function() {
    if (this.htmlObj) {
      var sel = this.htmlObj[0].selectedIndex;
      if (sel != -1) {
         $(this.htmlObj[0].options[sel]).addClass('highlighted');
      }
    }
  },

  copyToAbacus: function() {
    if (this.htmlObj && this.abacus) {
      var sel = this.htmlObj[0].selectedIndex;
      if (sel != -1) {
         this.abacus.setNumberByBeads(this.data[sel]);
      }
    }
  },

  append: function(text, data) {
    if (this.htmlObj) {
      var opt = $('<option>').text(text);
      this.data[this.data.length] = data;
      this.htmlObj.append(opt);
      this.htmlObj[0].selectedIndex = $('option', this.htmlObj).size() - 1;
    }
  }
}

// Build an abacus object (possibly with an associated history object)
// for each .abacus-wrapper element on the page.
$(function() {
  $('.abacus-wrapper').each(function(i, wrapper) {
    var abacus  = new Abacus($('.abacus', wrapper));
    var history = new History($('select.history', wrapper));
    abacus.history = history;
    history.abacus = abacus;
    abacus.draw();
    var variable_name = $('input[name=abacus-variable]', wrapper).attr('value');
    if (variable_name) {
      window[variable_name] = abacus;
    }
  });
})
