///////////////////////////////////////////////////////////////////////////////
//
//  Utilities/Extensions
//

Function.prototype.bind = function(object) {
  var __method = this;
  return function() {
    return __method.apply(object, arguments);
  }
};

String.prototype.lpad = function(length, padstring)
{
  var s = this;
  while(s.length < length)
    s = padstring + s;
  return s;
};

Array.prototype.copy = function()
{
  var i;
  var copy = []
  
  for ( i = 0; i < this.length; ++i )
  {
    copy[ i ] = this[ i ];
  }
  
  return copy;
};

function isDescendant( child, parent )
{
  while ( child !== null )
  {
    if ( child == parent )
    {
      return true;
    }

    child = child.parentNode;
  }

  return false;
}


///////////////////////////////////////////////////////////////////////////////
//
//  Event
//

function Event()
{
  this.handlers = new ArrayList();
}

Event.prototype =
{
  attach: function( handler )
  {
    this.handlers.add( handler );
  },

  detach: function( handler )
  {
    this.handlers.remove( handler );
  },

  fire: function()
  {
    var i;
    
    for ( i = 0; i < this.handlers.getLength(); ++i )
    {
      this.handlers.get( i ).apply( null, arguments );
    }
  }
};

//
//
//

function Log()
{
}

Log.write = function(s)
{
  Log.element.innerHTML += s + "<br/>";
}
  

///////////////////////////////////////////////////////////////////////////////
//
//  ArrayList
//

function ArrayList()
{
  this.data = [];
}

ArrayList.prototype.add = function(value)
{
  this.data[this.data.length] = value;
}

ArrayList.prototype.remove = function(value)
{
  for (var i = 0; i < this.data.length; ++i)
  {
    if ( this.data[i] === value )
    {
      this.removeAt(i);
      break;
    }
  }
}

ArrayList.prototype.removeAt = function(index)
{
  var i;
  
  if ((index < 0) || (index > this.data.length - 1))
  {
    throw "Index out of bounds";
  }

  for(i = index; i < this.data.length - 1; ++i)
  {
    this.data[i] = this.data[i + 1];
  }

  this.data.length = this.data.length - 1;
}

ArrayList.prototype.get = function(index)
{
  if ((index < 0) || (index > this.data.length - 1))
  {
    throw "Index out of bounds";
  }

  return this.data[index];
}

ArrayList.prototype.indexOf = function(value)
{
  var i;
  
  for (i = 0; i < this.data.length; ++i)
  {
    if (data[i] === value)
    {
      return i;
    }
  }
  
  throw "Object not found";
}

ArrayList.prototype.toArray = function()
{
  return this.data.copy();
}

ArrayList.prototype.getLength = function()
{
  return this.data.length;
}


///////////////////////////////////////////////////////////////////////////////
//
//  Length
//

// validate number, unit?
function Length(number, unit)
{
  this.number = number || 0;
  this.unit = unit || "px";
}

Length.pattern = /^(-?[0-9]+|-?[0-9]*\.[0-9]+)(em|ex|px|cm|mm|in|pt|pc)$/;

Length.parse = function(length)
{
  if (Length.pattern.test(length))
  {
    return new Length(
      parseFloat(RegExp.$1),
      RegExp.$2 );
  }
  
  throw "Not a valid length: " + length;
}

Length.prototype.toString = function()
{
  return this.number + this.unit;
}

Length.prototype.multiply = function(factor)
{
  return new Length(
    this.number * factor,
    this.unit );
}

Length.prototype.mix = function(length, percent)
{
  if ((percent < 0) || (percent > 1))
  {
    throw "Percent " + percent + " out of range";
  }

  if (this.unit != length.unit)
  {
    throw "Can not mix lengths of different units: " + this.unit + ", " + length.unit;
  }
  
  return new Length(
    this.number + (length.number - this.number) * percent,
    this.unit );
}


///////////////////////////////////////////////////////////////////////////////
//
//  Color
//

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));
  }

  throw "Not a valid color: " + color;
}

Color.prototype.mix = function(color, percent)
{
  /* test number of arguments, type of color, type of percent? */
  if ((percent < 0) || (percent > 1))
  {
    throw "Percent " + percent + " out of range";
  }
  
  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.max(0, Math.min(255, this.r + percent * (255 - this.r)))),
    Math.round(Math.max(0, Math.min(255, this.g + percent * (255 - this.g)))),
    Math.round(Math.max(0, Math.min(255, this.b + percent * (255 - this.b)))))
}


///////////////////////////////////////////////////////////////////////////////
//
//  Timer
//

// optional: interval: milliseconds between ticks
// optional: context: an object supporting setInterval (with same semantics as window.setInterval)

function Timer( interval, context )
{
  this.interval = interval || 33;
  this.context = context || window;
  this.tick = new Event();
  this.started = false;
  this.intervalId = null;
}

Timer.prototype =
{
  start: function()
  {
    if ( !this.started )
    {
      this.started = true;
      this.intervalId =
        this.context.setInterval( this.update.bind( this ), this.interval );
    }
  },
  
  stop: function()
  {
    if ( this.started )
    {
      this.started = false;
      this.context.clearInterval( this.intervalId );
    }
  },
  
  update: function()
  {
    this.tick.fire();
  },
  
  toString: function()
  {
    return "Timer (" + ( this.started ? "started" : "stopped" ) + ")"
  }
};