String.prototype.lpad = function(length, padstring)
{
  var s = this;
  while(s.length < length)
    s = padstring + s;
  return s;
}

function Color(r, g, b)
{
  /* test size of r, g, b? */
  this.r = r;
  this.g = g;
  this.b = b;
}

Color.rgbpattern = /^rgb\((\d{1,3}),\s+(\d{1,3}),\s+(\d{1,3})\)$/;
Color.hexpattern = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/;

Color.prototype.toString = function()
{
  var s = "#";
  s += this.r.toString(16).lpad(2, "0");
  s += this.g.toString(16).lpad(2, "0");
  s += this.b.toString(16).lpad(2, "0");
  return s;
}

Color.parse = function(color)
{
  if (Color.rgbpattern.test(color))
  {
    return new Color(
      parseInt(RegExp.$1),
      parseInt(RegExp.$2),
      parseInt(RegExp.$3));
  }

  if (Color.hexpattern.test(color))
  {
    return new Color(
      parseInt(RegExp.$1, 16),
      parseInt(RegExp.$2, 16),
      parseInt(RegExp.$3, 16));
  }

  return null;
}

Color.prototype.mix = function(color, percent)
{
  /* test number of arguments, type of color, type of percent? */
  if ((percent < 0) || (percent > 1))
    return null;

  return new Color(
    Math.round(this.r + (color.r - this.r) * percent),
    Math.round(this.g + (color.g - this.g) * percent),
    Math.round(this.b + (color.b - this.b) * percent));
}

Color.prototype.brighten = function(percent)
{
  return new Color(
    Math.round(Math.min(255, this.r + percent * (255 - this.r))),
    Math.round(Math.min(255, this.g + percent * (255 - this.g))),
    Math.round(Math.min(255, this.b + percent * (255 - this.b))))
}

function ColorAnimation(target, startColor, endColor, durationMilliseconds, delayMilliseconds)
{
  this.target = target;
  this.startColor = startColor;
  this.endColor = endColor;
  this.durationMilliseconds = durationMilliseconds;
  this.started = false;
  this.finished = false;

  if (delayMilliseconds == null)
  {
    this.delayMilliseconds = 0;
  }
  else
  {
    this.delayMilliseconds = delayMilliseconds;
  }
}

ColorAnimation.prototype.start = function()
{
  this.started = true;
  this.startTime = new Date();
}

ColorAnimation.prototype.update = function()
{
  if (!this.started || this.finished)
  {
    return;
  }

  var elapsedMilliseconds = new Date().getTime() - this.startTime.getTime();

  if (elapsedMilliseconds < this.delayMilliseconds)
  {
    return;
  }

  var percentComplete = Math.min(1.0, (elapsedMilliseconds - this.delayMilliseconds) / this.durationMilliseconds);
  this.target.style.color = this.startColor.mix(this.endColor, percentComplete).toString();

  if (percentComplete >= 1.0)
  {
    this.finished = true;
  }
}

function Collection()
{
  this.data = new Array(0)
  this.length = 0;
}

Collection.prototype.add = function(value)
{
  this.data[this.data.length] = value;
  this.length++;
}

Collection.prototype.remove = function(value)
{
  for (var i = 0; i < this.data.length; ++i)
  {
    if (data[i] == value)
    {
      this.removeAt(i);
      break;
    }
  }
}

Collection.prototype.removeAt = function(index)
{
  // should throw
  if ((index < 0) || (index > this.data.length - 1))
  {
    throw "Index out of bounds";
  }

  for(var i = index; i < this.data.length - 1; ++i)
  {
    this.data[i] = this.data[i + 1];
  }

  this.data.length = this.data.length - 1;
  this.length--;
}

Collection.prototype.get = function(index)
{
  if ((index < 0) || (index > this.data.length - 1))
  {
    throw "Index out of bounds";
  }

  return this.data[index];
}

function AnimationManager()
{
  this.animations = new Collection();
}

AnimationManager.prototype.update = function()
{
  var finishedAnimationIndices = new Array();

  for (var i = 0; i < this.animations.length; ++i)
  {
    this.animations.get(i).update();

    if (this.animations.get(i).finished)
    {
      finishedAnimationIndices[finishedAnimationIndices.length] = i;
    }
  }

  for (var i = finishedAnimationIndices.length - 1; i >= 0; --i)
  {
    this.animations.removeAt(finishedAnimationIndices[i]);
  }
}