///////////////////////////////////////////////////////////////////////////////
//
//  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;
}

function isNode(o){
  return (
    typeof Node === "object" ?
			o instanceof Node : 
    	typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
  );
}

///////////////////////////////////////////////////////////////////////////////
//
//  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, 10),
      parseInt(RegExp.$2, 10),
      parseInt(RegExp.$3, 10));
  }

  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" ) + ")";
  }
};

// A throbber shows an animation while an element loads, then shows the element

//
// Throbber
//

function ThrobberBase( options )
{
	options = options || {};
	
	this.context = options.context || document;
	this.throbberImageUrl = options.throbberImageUrl || null;
	
	this.createControls();
}

ThrobberBase.prototype =
{
	getElement: function()
	{
		return this.div;
	},

	render: function( parent )
	{
		parent.appendChild( this.div );
	},
	
	createControls: function()
	{
		var throbberImg;
		
		this.div = this.context.createElement( "div" );
		this.div.className = "throbber";

		this.contentDiv = this.context.createElement( "div" );
		this.contentDiv.className = "throbberContent";
		$( this.contentDiv ).hide();
		this.div.appendChild( this.contentDiv );
		
		this.loadingDiv = this.context.createElement( "div" );
		this.loadingDiv.className = "throbberLoading";
		this.div.appendChild( this.loadingDiv );
		
		if ( this.throbberImageUrl === null )
		{
			this.loadingDiv.innerHTML = "loading";
		}
		else
		{
			throbberImg = this.context.createElement( "img" );
			throbberImg.src = this.throbberImageUrl;
			this.loadingDiv.appendChild( throbberImg );
		}
	},
	
	showContent: function( control )
	{
		if ( isNode( control ) )
		{
			this.contentDiv.appendChild( control );
		}
		else
		{
			control.render( this.contentDiv );
		}
		
		$( this.loadingDiv ).hide();
		$( this.contentDiv ).show();
	}
};

function Throbber( loader, control, options )
{
	ThrobberBase.call( this, options );
	
	this.loader = loader;
	this.control = control;
	this.loader.loaded.attach( this.loaderLoaded.bind( this ) );
}

Throbber.prototype = new ThrobberBase();

Throbber.prototype.loaderLoaded = function()
{
	this.showContent( this.control );
};

//
// ElementThrobber? (takes an ElementLoader)
//

//
// ImageThrobber
//

function ImageThrobber( imageUrl, options )
{
	ThrobberBase.call( this, options );
	
	this.imageUrl = imageUrl;
	
	var loader = new ImageLoader( imageUrl );
	loader.loaded.attach( this.loaderLoaded.bind( this ) );
	loader.load();
}

ImageThrobber.prototype = new ThrobberBase();

ImageThrobber.prototype.loaderLoaded = function()
{
	var img = this.context.createElement( "img" );
	img.src = this.imageUrl;
	
	this.showContent( img );
};



//
// Tab
//

function Tab( labelElement, contentElement )
{
  this.labelClick = new Event();
  this.contentClick = new Event();

  this.labelElement = labelElement;
  this.contentElement = contentElement;

	if ( this.labelElement !== null )
	{
  	this.labelElement.onclick = this.labelElementOnClick.bind( this );
	}
	
	this.contentElement.onclick = this.contentElementOnClick.bind( this );
}

Tab.prototype =
{
  select: function()
  {
    if ( !this.selected )
    {
      this.selected = true;
			$( this.contentElement ).show();
      
			if ( this.labelElement !== null )
			{
				$( this.labelElement ).addClass( "selected" );
 			}
 		}
	},

  deselect: function()
  {
    if ( this.selected )
    {
      this.selected = false;
			$( this.contentElement ).hide();
			
			if ( this.labelElement !== null )
			{
				$( this.labelElement ).removeClass( "selected" );
			}
    }
  },

	contentElementOnClick: function()
	{
		this.contentClick.fire( this );
	},

  labelElementOnClick: function()
  {
    this.labelClick.fire( this );
  }
};

//
// TabGroup
//

function TabGroup( tabs )
{
	var i;
	
  this.tabs = tabs || [];
  this.selectedTab = null;

	for ( i = 0; i < this.tabs.length; ++i )
	{
		this.attachTab( this.tabs[ i ] );
	}
	
	this.updateTabClasses();
	this.updateTabIndices();
	
	if ( this.tabs.length > 0 )
	{
		this.selectTab( this.tabs[ 0 ] );
	}
}

TabGroup.prototype =
{
  addTab: function( tab )
  {
    this.tabs.push( tab );
		this.attachTab( tab );
  	this.updateTabClasses();
		this.updateTabIndices();
	},
  
  removeTab: function( tab )
  {
    // tbi
  },

	updateTabIndices: function()
	{
		var i;
		
		for ( i = 0; i < this.tabs.length; ++i )
		{
			this.tabs[ i ].index = i;
		}
	},

	updateTabClasses: function()
	{
		var i;
		var tab;
		
		for ( i = 0; i < this.tabs.length; ++i )
		{
			tab = $( this.tabs[ i ].contentElement );
			tab.addClass( "tab" );
			
			if ( this.tabs.length > 1 )
			{
				tab.addClass( "siblingTab" );
			}
			else
			{
				tab.removeClass( "siblingTab" );
			}
		}
	},

	attachTab: function( tab )
	{
    tab.labelClick.attach( this.tabLabelClick.bind( this ) );
		tab.contentClick.attach( this.tabContentClick.bind( this ) );
	},
  
  selectTab: function( tab )
  {
    if ( this.selectedTab !== tab ) 
    {
      if ( this.selectedTab !== null )
      {
        this.selectedTab.deselect();
      }

      this.selectedTab = tab;
      this.selectedTab.select();
    }
  },
  
  selectNextTab: function()
  {
    var nextIndex; 
    
    if ( this.selectedTab !== null )
    {
      nextIndex = ( this.selectedTab.index + 1 ) % this.tabs.length;
      this.selectTab( this.tabs[ nextIndex ] ); 
    }
  },

  tabLabelClick: function( tab )
  {
    this.selectTab( tab );
  },

	tabContentClick: function( tab )
	{
		this.selectNextTab();
	}
};




// A Loader must implement
//   load: function() : void
//   loaded: Event

//
// ImageLoader
//

function ImageLoader( url, options )
{
  this.url = url;
  this.image = null;
  this.loaded = new Event();
  this.pending = false;
  this.img = null;
  
  options = options || {};
  this.context = options.context || document;
}  

ImageLoader.prototype =
{
  load: function()
  {
    if ( !this.pending )
    {
      this.pending = true;
      
      var image = this.image = new Image();
      image.onload = this.onImageLoaded.bind( this );
      
      image.src = this.url;
    }
  },
  
  onImageLoaded: function()
  {
    this.pending = false;
    this.loaded.fire( this );
  }
};

//
// MultipleImageLoader
//

function MultipleImageLoader( urls, options )
{
	var i;
	var imageLoaders = [];
	
	options = options || {};
	this.context = options.context || document;
	
	this.loaded = new Event();
	
	for ( i = 0; i < urls.length; ++i )
	{
		imageLoaders.push( new ImageLoader( urls[ i ], options ) );
	}
	
	this.loader = new CompositeLoader( imageLoaders );
	this.loader.loaded.attach( this.loaderLoaded.bind( this ) );
}

MultipleImageLoader.prototype =
{
	load: function()
	{
		this.loader.load();
	},
	
	loaderLoaded: function()
	{
		this.loaded.fire( this );
	}
};

//
// CompositeLoader
//

function CompositeLoader( loaders )
{
  this.loaders    = loaders;
  this.incomplete = 0;
  this.pending    = false;
  this.loaded     = new Event();
}

CompositeLoader.prototype = 
{
  load: function()
  {
    var i;
    var loader;
    
    if ( !this.pending )
    {
      if ( this.loaders.length < 1 )
      {
        this.onLoaded();
      }
      else
      {
        this.pending = true;
        this.incomplete = this.loaders.length;

        for ( i = 0; i < this.loaders.length; ++i )
        {
          loader = this.loaders[ i ];
          loader.loaded.attach( this.loaderLoaded.bind( this ) );
          loader.load();
        }
      }
    }
  },

  loaderLoaded: function()
  {
		--this.incomplete;
		
    if ( this.incomplete === 0 )
    {
      this.onLoaded();
    }
  },
  
  onLoaded: function()
  {
    this.pending = false;
    this.loaded.fire( this );
  }
};

function Fader( elements, options )
{
  this.parent    				= parent;
  this.elements    			= elements;
  
  options               = options || {};
  this.context          = options.context || document;
  this.timer            = options.timer || new Timer();
  this.width            = options.width || 800;
  this.height           = options.height || 600;
  this.duration         = options.duration || 1000;
  this.fadeSpeed        = options.fadeSpeed || 1000;

  this.pages                  = [];
  this.lastShowTime           = 0;
  this.currentPageIndex       = null;
  
  this.initializeFrame();
  this.createPages();
}

Fader.prototype =
{
	render: function( parent )
	{
		parent.appendChild( this.frameDiv );
		this.startTimer();
	},
	
	getElement: function()
	{
		return this.frameDiv;
	},
	
  initializeFrame: function()
  {
    this.frameDiv = this.context.createElement( "div" );
    this.frameDiv.className = "fader";
  },

	startTimer: function()
	{
		this.timer.tick.attach( this.timerTick.bind( this ) );
    this.timer.start();
	},

	createPages: function()
	{
		var page;
		
    for ( i = 0; i < this.elements.length; ++i )
    {
      page = this.createPage( this.elements[ i ] );
      this.frameDiv.appendChild( page );
      this.pages.push( page );
      $( page ).hide();
    }
	},

  createPage: function( element )
  {
    var pageDiv = this.context.createElement( "div" );
    pageDiv.className = "fader_page";
    pageDiv.style.position = "absolute";
    pageDiv.appendChild( element );
    return pageDiv;
  },

  showFirstPage: function()
  {
    if ( this.pages.length > 0 )
    {
      this.showPage( 0, false );
    }
  },
  
  showNextPage: function()
  {
    if ( this.pages.length > 0 )
    {
      var j = ( this.currentPageIndex + 1 ) % this.pages.length;
      this.showPage( j, true );
    }
  },
  
  showPage: function( pageIndex, fade )
  {
    var currentPage;
    var nextPage;

    this.lastShowTime = new Date().getTime();
    
    if ( this.currentPageIndex !== null )
    {
      currentPage = this.pages[ this.currentPageIndex ];
      
      if ( fade ) $( currentPage ).fadeOut( this.fadeSpeed );
      else $( currentPage ).hide();
    }
    
    nextPage = this.pages[ pageIndex ];
    
    if ( fade ) $( nextPage ).fadeIn( this.fadeSpeed );
    else $( nextPage ).show();
    
    this.currentPageIndex = pageIndex;
  },
  
  timerTick: function()
  {
    this.update();
  },

  update: function()
  {
    if ( new Date().getTime() - this.lastShowTime > this.duration )
    {
      this.showNextPage();  
    }
  }
};
