///////////////////////////////////////////////////////////////////////////////

//

//  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" ) + ")"

  }

};