//
// Utilities
//

//
// from prototype.js
//

Function.prototype.bind = function(object) {
  var __method = this;
  return function() {
    return __method.apply(object, arguments);
  }
}


//
//
//

function Log()
{
}

Log.write = function(s)
{
  Log.element.innerHTML += s + "<br/>";
}
  
//
// Collection
//

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 ( this.data[i] == value )
    {
      this.removeAt(i);
      break;
    }
  }
}

Collection.prototype.removeAt = function(index)
{
  // should throw
  if ((index < 0) || (index > this.data.length - 1))
  {
    throw "Index " + 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 " + index + " out of bounds";
  }

  return this.data[index];
}

Collection.prototype.indexOf = function(value)
{
  for (var i = 0; i < this.data.length; ++i)
  {
    if (data[i] == value)
    {
      return i;
    }
  }
  
  throw "Object not found";
}


//
// String
//

String.prototype.lpad = function(length, padstring)
{
  var s = this;
  while(s.length < length)
    s = padstring + s;
  return s;
}


//
// Length
//

// validate number, unit?
function Length(number, unit)
{
  this.number = number;
  this.unit = unit;
}

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)))))
}


//
// Animation stuff
//

//
// Clock
//

function Clock()
{
  this.stopped = false;
  this.speed = 1;
  this.timeBeforeLastStop = 0;
  this.start();
}

Clock.prototype.getTime = function()
{
  return this.timeBeforeLastStop + new Date().getTime() - lastStartTime;
}

Clock.prototype.stop = function()
{
  this.timeBeforeLastStop = getTime();
  this.stopped = true;
}

Clock.prototype.start = function()
{
  lastStartTime = new Date().getTime();
}

Clock.instance = new Clock();


//
// Task
//

function Task()
{
  this.started = false;
  this.completed = false;
}  

Task.prototype.start = function()
{
  this.started = true;
}
  
Task.prototype.update = function()
{
  if (this.started && !this.completed)
  {
    this.updateTask();
  }
}

Task.prototype.updateTask = function()
{
}

// inheritors call when complete
Task.prototype.onComplete = function()
{
  this.completed = true;
  // call registered event handlers (?)
}


//
// CompoundTask : Task
//

function CompoundTask()
{
  Task.call(this);
  this.tasks = new Collection();
}

CompoundTask.prototype = new Task();

//
// TaskGroup : CompoundTask
//

function TaskGroup()
{
  CompoundTask.call(this);
}

TaskGroup.prototype = new CompoundTask();

TaskGroup.prototype.updateTask = function()
{
  var allTasksCompleted = true;
  
  for(var i = 0; i < this.tasks.length; ++i)
  {
    var task = this.tasks.get(i);
    if(!task.completed)
    {
      allTasksCompleted = false;

      if (!task.started)
      {
        task.start();
      }
      
      task.update();
    }
  }
  
  if(allTasksCompleted)
  {
    this.onComplete();
  }
}


//
// TaskSequence : CompoundTask
//

function TaskSequence()
{
  CompoundTask.call(this);
  this.currentTaskIndex = 0;
}

TaskSequence.prototype = new CompoundTask();

TaskSequence.prototype.updateTask = function()
{
  if(this.currentTaskIndex > this.tasks.length - 1)
  {
    this.onComplete();
  }
  // misses one frame
  else
  {
    var task = this.tasks.get(this.currentTaskIndex);
    if(task.completed)
    {
      this.currentTaskIndex++;
    }
    else
    {
      if(!task.started)
      {
        task.start();
      }
      task.update();
    }
  }
}


//
// TimedTask : Task
//


// todo: add progress function
function TimedTask(durationMilliseconds, clock)
{
  if (clock == null)
  {
    clock = Clock.instance;
  }
  
  Task.call(this);
  this.durationMilliseconds = durationMilliseconds;
  this.clock = clock;
}

TimedTask.prototype = new Task();

TimedTask.prototype.start = function()
{
  Task.prototype.start.call(this);
  this.startTime = this.clock.getTime();
}

TimedTask.prototype.updateTask = function()
{
    var elapsedMilliseconds = this.clock.getTime() - this.startTime;
    var percentComplete = Math.min(1.0, elapsedMilliseconds / this.durationMilliseconds);
    
    this.updateTimedTask(percentComplete);
    if (percentComplete >= 1.0)
    {
      this.onComplete();
    }  
}

TimedTask.prototype.updateTimedTask = function(percentComplete)
{
}


//
// Animation : TimedTask
//

function Animation(target, propertyName, startValue, endValue, durationMilliseconds, clock)
{
  if (clock == null)
  {
    clock = Clock.instance;
  }

  TimedTask.call(this, durationMilliseconds, clock);
  this.target = target;
  this.propertyName = propertyName;
  this.startValue = startValue;
  this.endValue = endValue;
  this.isNumeric = typeof startValue == "number";
}

Animation.prototype = new TimedTask();

// weird but javascriptey!
Animation.prototype.updateTimedTask = function(percentComplete)
{
  if(this.isNumeric)
  {
    this.target[this.propertyName] = this.startValue + (this.endValue - this.startValue) * percentComplete;
  }
  else
  {
    this.target[this.propertyName] = this.startValue.mix(this.endValue, percentComplete).toString();
  }
}



// todo: need an event listener to kill interval
function Animator(animation)
{
  this.animation = animation;
}

Animator.prototype.start = function()
{
  this.animation.start();
  this.interval = window.setInterval(this.animation.update.bind(this.animation), 33);
}



function AnimationManager()
{
  this.animations = new Collection();
  this.interval = window.setInterval( this.update.bind( this ), 33 );
}

AnimationManager.prototype.addAnimation = function( animation )
{
  this.animations.add( animation );
  animation.start();
}

AnimationManager.prototype.update = function()
{
  var completedAnimations = new Collection();

  for ( i = 0; i < this.animations.length; ++i )
  {
    var animation = this.animations.get( i );
    
    if ( animation.completed )
    {
      completedAnimations.add( animation );
    }
    else
    {
      animation.update();
    }
  }
  
  for ( i = 0; i < completedAnimations.length; ++i )
  {
    this.animations.remove( completedAnimations.get( i ) );
  }
}

function Effect(element, duration)
{
  if (duration == null)
  {
    duration = 1000;
  }
  
  new Animator(this.createAnimation(element, duration)).start();
}

// inheritors override
Effect.prototype.createAnimation = function(element, duration)
{
}


function FadeInEffect(element, duration)
{
  Effect.call(this, element, duration);
}

FadeInEffect.prototype.createAnimation = function(element, duration)
{
  return new Animation(element.style, "opacity", 0.0, 1.0, duration);
}


function FadeOutEffect(element, duration)
{
  Effect.call(this, element, duration);
}

FadeOutEffect.prototype.createAnimation = function(element, duration)
{
  return new Animation(element.style, "opacity", 1.0, 0.0, duration);
}


function PuffEffect(element, duration)
{
  Effect.call(this, element, duration);
}

PuffEffect.prototype.createAnimation = function(element, duration)
{
  var tg = new TaskGroup();
  tg.tasks.add(new Animation(element.style, "opacity", 1.0, 0.0, duration));
  
  var width = Length.parse(element.style.width);
  var height = Length.parse(element.style.height);
  
  tg.tasks.add(new Animation(element.style, "width", width, width.multiply(4), duration));
  tg.tasks.add(new Animation(element.style, "height", height, height.multiply(4), duration));
  return tg;
}




//
// Timeline
//

// throw if no clock?
function Timeline(clock)
{
  if (clock == null)
  {
    clock = Clock.instance;
  }
  
  this.clock = clock;
  this.tasks = new Collection();
  this.startTimes = new Collection();
}

Timeline.prototype.addTask = function(task, delayMilliseconds)
{
  this.tasks.add(task);
  
  if (delayMilliseconds == null)
  {
    this.startTimes.add(this.clock.getTime());
  }
  else
  {
    this.startTimes.add(this.clock.getTime() + delayMilliseconds);
  }
}

Timeline.prototype.removeTask = function(task)
{
  var i = this.tasks.indexOf(task);
  this.tasks.removeAt(i);
  this.startTimes.removeAt(i);
}

Timeline.prototype.update = function()
{
  var completedTaskIndices = new Array();
  
  for(var i = 0; i < this.tasks.length; ++i)
  {
    var task = this.tasks.get(i);
    var startTime = this.startTimes.get(i);
    
    if (task.completed)
    {
      completedTaskIndices[completedTaskIndices.length] = i;
    }
    else if (task.started)
    {
      task.update();
    }
    else if (this.clock.getTime() >= startTime)
    {
      task.start();
    }
  }

  for (var i = completedTaskIndices.length - 1; i >= 0; --i)
  {
    //this.tasks.removeAt(completedTaskIndices[i]);
    //this.startTimes.removeAt(completedTaskIndices[i]);
  }
}


//
// WaitTask : TimedTask
//

// just a pretty name
function WaitTask(durationMilliseconds)
{
  TimedTask.call(this, durationMilliseconds);
}

WaitTask.prototype = new TimedTask();
