/**
 * PSLib.js is built on top of Prototype and Scriptaculous. It aims to provide wrappers to simplify
 * the use of the scriptaculous queueing system and improve syntax, semantics, and general ease of use.
 * 
 * @author Peter Swan
 * @copyright 2008
 * @license MIT
 */

/* TODO: 
 * add a preventIf callback which prevents an event from firing if it returns true/false
 * determine how to leverage position: with-last to provide multiple effects on single element
 * 
 * add default open, close, move functionality to every animatable element
 * 
 * must do bind as event listener and stop event if it is present
 *  
 * can probably make a collection without parallel by using a single queue for all events on objects.
 * could NOT set a limit on the queue and would have to use with-last.
 * 
 * use identify instead of readAttribute('id') to get around no id provided errors!
 * 
 * modify _action_open/_action_close/etc. to allow for event specification as well; e.g. elmid_action_open_click
 * (id of element)_(type)_(function)_(event)
 * 
 * / 

/**
 * @namespace contains all library functionality
 */
var PSLib = 
{
	/** Version of the Library 
	 * @type String
	 */
	Version: '0.1.0',
	/** Required version of Scriptaculous 
	 * @type String
	 */
	REQUIRED_SCRIPTACULOUS: '1.8.0',
	/**
	 * Checks dependencies of library
	 * @return {Void}
	 */
	load: function(){
		function convertVersionString(versionString){
			var r = versionString.split('.');
			return parseInt(r[0]) * 100000 + parseInt(r[1]) * 1000 + parseInt(r[2]);
		}
		
		if ((typeof Scriptaculous == 'undefined') ||
			(convertVersionString(Scriptaculous.Version) <
			convertVersionString(PSLib.REQUIRED_SCRIPTACULOUS)) ||
			typeof(Effect) == 'undefined' ) //|| typeof(Builder) == 'undefined'	) 
			throw ("pslib requires the Scriptaculous JavaScript framework >= " +
			PSLib.REQUIRED_SCRIPTACULOUS + ' with Effects and Builder');
	}
};
PSLib.load();

/* TODO: make this deep copy arrays as well */
Object.extend( Object, {
	recursiveExtend: function( destination, source){
		for( var k in source){
			if( typeof( destination[k]) == 'object' && typeof( source[k]) == 'object'){
				Object.recursiveExtend( destination[k], source[k]);
			}else{
				destination[k] = source[k];
			}
		}
		return destination;
	}
});

Element.addMethods({
		selectChildren: function( element, c){
			return $(element).childElements().findAll( function( elm ){
					return elm.match(c);
			});
		}
});

Effect.Base.addMethods(
{
	start: function(options) {
	    function codeForEvent(options,eventName){
	      return (
	        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
	        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
	      );
	    }
	    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
	    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
	    this.currentFrame = 0;
	    this.state        = 'idle';
	    this.startOn      = this.options.delay*1000;
	    this.finishOn     = this.startOn+(this.options.duration*1000);
	    this.fromToDelta  = this.options.to-this.options.from;
	    this.totalTime    = this.finishOn-this.startOn;
	    this.totalFrames  = this.options.fps*this.options.duration;
	    
	    eval('this.render = function(pos){ '+
	      'if (this.state=="idle"){' +
		  'this.state="running";'+
	      codeForEvent(this.options,'beforeSetup')+
	      (this.setup ? 'this.setup();':'')+ 
	      codeForEvent(this.options,'afterSetup')+
	      '};if (this.state=="running"){'+
	      'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
	      'this.position=pos;'+
	      codeForEvent(this.options,'beforeUpdate')+
	      (this.update ? 'this.update(pos);':'')+
	      codeForEvent(this.options,'afterUpdate')+
	      '}}');
	    
		if (!this.options.sync) {
			// if the effect was successfully added to the queue, execute beforeStart
			if( Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).add(this))
				this.event('beforeStart');
		}
		// if this is a synched event it's definitely happening, so execute beforeStart
		else{
			this.event('beforeStart');
		}
  }
});

Effect.ScopedQueue.addMethods(
{
	add: function(effect){
		var timestamp = new Date().getTime();
		
		var position = Object.isString(effect.options.queue) ? effect.options.queue : effect.options.queue.position;
		
		switch (position) {
			case 'front':
				// move unstarted effects after this effect  
				this.effects.findAll(function(e){
					return e.state == 'idle'
				}).each(function(e){
					e.startOn += effect.finishOn;
					e.finishOn += effect.finishOn;
				});
				break;
			case 'with-last':
				timestamp = this.effects.pluck('startOn').max() || timestamp;
				break;
			case 'end':
				// start effect after last queued effect has finished
				timestamp = this.effects.pluck('finishOn').max() || timestamp;
				break;
		}
		
		effect.startOn += timestamp;
		effect.finishOn += timestamp;
		
		/* return true if effect was inserted into the queue false otherwise */
		var ret = false;
		if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)){
			ret = true;
			this.effects.push(effect);
		}
		
		if (!this.interval) 
			this.interval = setInterval(this.loop.bind(this), 15);
		
		return ret;
	}
});

Effect.Scroll = Class.create( Effect.Base, {
	initialize: function( element ){
		this.element = $(element);
    	if (!this.element) throw(Effect._elementDoesNotExistError);
		var options = Object.extend({
			x: 0,
			y: 0,
			mode: 'absolute'
		}, arguments[1] || {});
		this.start(options);
	},
	setup: function(){
		if( this.element === window){
			this.originalTop = this.element.scrollY;
			this.originalLeft = this.element.scrollX;
		}else{
			this.originalTop = this.element.scrollTop;
			this.originalLeft = this.element.scrollLeft;
		}
		
		if( this.options.mode == 'absolute'){
			this.options.x -= this.originalLeft;
			this.options.y -= this.originalTop;
		}
	},
	update: function( pos ){
		var new_x = this.originalLeft + this.options.x * pos;
		var new_y = this.originalTop + this.options.y * pos;
		this._setScroll( new_x, new_y);
	},
	_setScroll: function( x, y){
		if( this.element === window){
			window.scrollTo(x, y);
		}else{
			this.element.scrollTop = x;
			this.element.scrollLeft = y;
		}
	}
});

/**
 * @namespace contains all debug functionality
 */
PSLib.Debug = 
{
	debug: false,
	/** toggle debugging 
	 * @param {Boolean} d
	 */
	setDebug: function( d ){
		if( d ){
			this.debug = true;
		}else{
			this.debug = false;
		}
	},
	/**
	 * print to the console if it is defined
	 * @param {String} msg
	 */
	printToConsole: function(msg){
		if( PSLib.Debug.debug ){
			try{
				console.log(msg);
			}catch(e){}
		}
	},
	
	printObjectToConsole: function(obj, spaces){
		if (PSLib.Debug.debug) {
			spaces = spaces || '';
			for (k in obj) {
				if (typeof(obj[k]) == 'object') {
					//PSLib.Debug.printToConsole(spaces + k + ": ")
				}
				else {
					//PSLib.Debug.printToConsole(spaces + k + ": " + obj[k]);
				}
			}
		}
	},
	/**
	 * print to alert box
	 * @param {String} msg
	 */
	printAlert: function(msg){
		if (PSLib.Debug.debug) {
			alert(msg);
		}
	}
};

/**
 * @namespace Contains utility functions
 */
PSLib.Util = {
	/**
	 * Does deep copies of objects to create a new combined object.  Properties of each object in the array
	 * will overwrite the properties of the previous object.
	 * 
	 * @param {Mixed} options... one or more objects containing options
	 * @return {Object} An object containing the 
	 */
	assembleOptions: function(){
		var options = new Object();
		$A(arguments).each(function(o){
			Object.recursiveExtend(options, o);
		});
		return options;
	},
	
	removeOptions: function(obj, arr){
		$A(arr).each( function(e){
			if(obj[e]){
				obj[e] = null;
				delete obj[e];	
			}
		});
	},
	
	deepCopy: function( dest, source){
		for( k in source){
			if( typeof(source[k]) == 'object'){
				dest[k] = new Object();
				deepCopy( dest[k], source[k]);
			}else if( typeof(source[k]) == 'array'){
				dest[k] = $A(source[k]).clone();
			}else{
				dest[k]	= source[k];
			}
		}
	}
};

/**
 * @namespace Contains error/exception handling functions
 */
PSLib.Error = 
{
	/**
	 * @param {String} type
	 * @param {String} required_type
	 */
	TypeError: function(type, required_type){
		throw { name: 'PSLib.TypeError', message: type + ' found where ' + required_type + ' required.'};
	},
	
	/**
	 * @param {Mixed} id
	 */
	ElementNotFoundError: function(){
		throw { name: 'PSLib.ElementNotFoundError', message: 'element not found'};
	},
	
	/**
	 * @param {Mixed} elm
	 */
	ScopeWarning: function( elm ){
		throw { name: 'PSLib.ScopeWarning', message: 'no scope was provided for ' + elm + ', this may result in unexpected behavior'};
	}
};

PSLib.State = {
	state_string: '',
	objects: {},
	anchor: null,
	recordState: function(obj){
		PSLib.Debug.printToConsole('recording state: ' + PSLib.State.getStateString());
		/* probably want to remove state from state string if state is '' */
		PSLib.State.addObject(obj);
		var r = new RegExp(obj.getId() + '=[^&]+');
		var s = obj.getState();
		PSLib.Debug.printToConsole('state: ' + s);
		if( r.test(PSLib.State.state_string)){
			PSLib.Debug.printToConsole('replacing: ' + obj.getId());
			PSLib.State.state_string = PSLib.State.state_string.replace(r, s);
		}else{
			if( PSLib.State.state_string != ''){
				PSLib.State.state_string += '&';
			}
			PSLib.State.state_string += s;
		}
	},
	
	addObject: function(obj){
		if( Object.isUndefined(PSLib.State.objects[obj.getId()]) ){
			PSLib.State.objects[obj.getId()] = obj;
		}
	},
	
	getStateString: function(){
		return PSLib.State.state_string;
	},
	
	applyState: function(){
		var s = window.location.hash;
		PSLib.Debug.printToConsole(s);
		if(s){
			s = s.split('#')[1];
			PSLib.Debug.printToConsole(s);
			var a = s.split('&');
			if (Object.isArray(a)) {
				a.each(function(e){
					e = e.split('=');
					if (!Object.isUndefined(PSLib.State.objects[e[0]])) {
						PSLib.Debug.printToConsole('setting state ' + e[0] + ', ' + e[1]);
						PSLib.State.objects[e[0]].setState(e[1]);
					}
				});
				//PSLib.State.state_string = s;
			}
		}
	},
	
	getUrl: function(){
		var href = window.location.href.split('?')[0].split('#')[0];
		var query = window.location.search;
		/* want to append to query instead of using hash (like google maps) */
		PSLib.Debug.printToConsole(href + query + '#' + PSLib.State.getStateString());
		return href + query + this.getHash();
	},
	
	getHash: function(){
		return  '#' + PSLib.State.getStateString();
	}
}

/**
 * @namespace Contains functions and classes for easily animating elements
 */
PSLib.Animation = {
	HOOKS: ['beforeStart', 'afterFinish', 'beforeUpdate', 'afterUpdate'],
	PARALLEL: 1,
	SEQUENTIAL: 2
};

PSLib.Animation.Elements = {
	_elements: $H(),
	addElement: function(elm){
		var id = elm.getElement().readAttribute('id');
		if( id ){
			PSLib.Animation.Elements._elements.set(id, elm);
		}
	},
	getElement: function(id){
		if( id ){
			return PSLib.Animation.Elements._elements.get(id);
		}
	}
}

PSLib.DynamicLoader = Class.create({
	initialize: function(element, options){
		options = options || {};
		this._after = null;
		this._before = null;
		this._parent = null;
		this._first = this;
		this._loaded = false;
		this._loading = false;
		this._element_list = $A();
		this._attempts = 0;
		this._max_attempts = options['max_attempts'] || 3;
		this._attempt_interval = options['attempt_interval'] || 0.25;
		options = options || {};
		
		this._containing_element = $(element);
		if( this._containing_element ){
			PSLib.Debug.printToConsole('found containing element: ' + this._containing_element.innerHTML);
			var content_path = this._containing_element.down('a');
			if( content_path && content_path.hasClassName('dynamic_content_path')){
				PSLib.Debug.printToConsole('found link: ' + content_path);
				this._url = content_path.readAttribute('href');
				if( this._url){
					PSLib.Debug.printToConsole('found href: ' + this._url);
					var beforeLoad = this._containing_element.down('.beforeLoad');
					if( beforeLoad ){
						beforeLoad = eval( beforeLoad.innerHTML.stripTags().stripScripts());
					}
					var afterLoad = this._containing_element.down('.afterLoad');
					if( afterLoad ){
						afterLoad = eval( afterLoad.innerHTML.stripTags().stripScripts());
					}
					
					this._options = {
						beforeLoad: Object.isFunction(options['beforeLoad'])?options['beforeLoad']:Object.isFunction(beforeLoad)?beforeLoad:Prototype.emptyFunction,
						afterLoad: Object.isFunction(options['afterLoad'])?options['afterLoad']:Object.isFunction(afterLoad)?afterLoad:Prototype.emptyFunction
					}
					
					this._element_list.push(this._containing_element);
					var dependencies = this._containing_element.select('ul.dependencies > li');
					PSLib.Debug.printToConsole('found ' + dependencies.length + ' dependencies');
					this._containing_element.select('ul.dependencies > li').each( function(e){
						if( e.innerHTML ){
							var elm = $( e.innerHTML.stripScripts().stripTags().strip() );
							if( elm ){
								var after = 0;
								this._addElement(elm);
								if( e.hasClassName('after') ){
									after = 1;
								}
								this._addDependency(new PSLib.DynamicLoader(elm), after);
							}else{
								PSLib.Debug.printToConsole('no element by that id!');
							}
						}else{
							PSLib.Debug.printToConsole('no inner html!');
						}
					}, this);
				}else{
					return false;
				}
			}else{
				return false;
			}
		}else{
			return false;
		}
	},
	
	getElements: function(){
		return this._element_list;
	},
	
	_addElement: function(elm){
		this._element_list.push(elm);
	},
	
	_addDependency: function( dl, after){
		if( after ){
			PSLib.Debug.printToConsole('adding after dependency: ' + dl._containing_element.identify());
			if( !this._after ){
				this._setParent(dl);
			}else{
				this._after._setParent(dl);
			}
			this._after = dl;
		}else{
			PSLib.Debug.printToConsole('adding before dependency: ' + dl._containing_element.identify());
			if( !this._before){
				this._first = dl;
			}else{
				this._before._setParent(dl);
			}
			dl._setParent(this);
			this._before = dl;
		}
	},
	
	_setParent: function(dl){
		this._parent = dl;
	},
	
	load: function(){
		if( this._loaded == false && this._loading == false){
			this._loading = true;
			this._first._loadSelf();
		}
	},
	
	_loadSelf: function(){
		PSLib.Debug.printToConsole('_loadSelf: ' + this._containing_element.identify() + ', ' + this._url);
		this._options.beforeLoad(this);
		new Ajax.Request( this._url, {
			method: 'get',
			onSuccess: this._afterLoad.bind(this),
			onFailure: this._onFailure.bind(this)
		});
	},
	
	_loadParent: function(){
		if( this._parent ){
			this._parent._loadSelf();
		}
	},
	
	_afterLoad: function(r){
		this._loading = false;
		this._loaded = true;
		this._containing_element.update(r.responseText);
		this._options.afterLoad.defer(this);
		this._loadParent();
	},
	
	_onFailure: function(r){
		this._loading = false;
		PSLib.Debug.printToConsole(r.status + ':' + r.statusText);
		if( r.status == 503 ){
			this._attempts++;
			if( this._attempts < this._max_attempts){
				this._loadSelf.bind(this).delay(this._attempt_interval);
			}else{
				PSLib.Debug.printToConsole('maximum attempts have been reached');
				alert('The application is currently unable to process your request, please try again later.');
			}
		}else{
			alert("Sorry! We can't find the data you requested.");
		}
	}
});

PSLib.DynamicLoader.Util = {
	afterLoadFuncs: $A(),
	beforeLoadFuncs: $A(),
	isLoadable: function(elm){
		elm = $(elm);
		if( elm ){
			var link = elm.down('a');
			if( link && link.hasClassName('dynamic_content_path')){
				var url = link.readAttribute('href');
				if( url ){
					return true;
				}
			}
		}
		return false;
	},
	addBeforeLoad: function( f ){
		if( Object.isFunction(f)){
			PSLib.DynamicLoader.Util.beforeLoadFuncs.push(f);
		}
	},
	addAfterLoad: function(f){
		if( Object.isFunction(f) ){
			PSLib.DynamicLoader.Util.afterLoadFuncs.push(f);
		}
	},
	beforeLoad: function( dl ){
		PSLib.DynamicLoader.Util.onEvent(PSLib.DynamicLoader.Util.afterLoadFuncs, dl);
	},
	afterLoad: function( dl ){
		PSLib.DynamicLoader.Util.onEvent(PSLib.DynamicLoader.Util.afterLoadFuncs, dl);
	},
	onEvent: function( fl, dl ){
		var l = fl.length;
		for( var i=0; i<l; i++){
			fl[i](dl);
		}
	}
}

/**
 * @namespace Contains transitions which supplement Effect.Transitions
 */
PSLib.Animation.Transitions = 
{
	none: function( pos ){
		return Math.floor(pos);
	}
};

PSLib.Animation.Directions = {
	FORWARD: 1,
	BACKWARD: -1,
	HORIZONTAL: 1,
	VERTICAL: 2
};

PSLib.Animation.AnimatableElement = Class.create(
{
	/**
	 * @param {Mixed} element string or DOM element
	 * @param {Object} types types of events to add at creation
	 * @param {Object} options
	 */
	initialize: function( element, options, types){
		/* get the element */
		this._element = $(element);
		if( this._element){
			this._animations = $H();
			this._callbacks = $H();
			options = options || {};
			
			/* set default transition, queue, and before after functions */
			this._options = new Object({
				transition: Effect.Transitions.linear,
				queue: {
					scope: this._element.readAttribute('id'),
					limit: 0,
					position: 'end'
				}
			});
			Object.recursiveExtend( this._options, options);
			if( !this.getOption('queue').scope){
				PSLib.Error.ScopeWarning(this._element);
				this.setOptions({queue: {scope: 'pslib_animatable_element'}});
			}
			
			////PSLib.Debug.printObjectToConsole(this._options);
			this._processCallbacks(this._options);
			
			types = $H(types || {});
			types.each( function( p){
				this.registerAnimationType( p.key, p.value);
			}, this);
			PSLib.Animation.Elements.addElement(this);
		}else{
			PSLib.Error.ElementNotFoundError(element);
		}
	},
	
	/**
	 * Returns the element the element on which the AnimatableElement was created 
	 * @return DOM Element
	 */
	getElement: function(){
		return this._element;
	},
	
	/**
	 * Returns the value of the option specified by option.  If context is specified
	 * returns the option's value for the animation specified by context
	 * @param {String} option
	 * @param {String} [context]
	 */
	getOption: function(option, context){
		return this._getOptionsFromContext(context)[option];
	},
	
	/**
	 * @param {String} option the option to set
	 * @param {Mixed} value the value of the option
	 * @param {string} [context] optional context, defaul is global
	 */
	setOption: function(option, value, context){
		var o = this._getOptionsFromContext(context);
		o[option] = value;
	},
	
	/** @ignore */
	_getOptionsFromContext: function(context){
		var a = this._getAnimationType(context);
		if( a){
			return a.options;
		}else{
			return this._options;
		}
	},
	
	/**
	 * @param {Object} options the options to set
	 * @param {String} [context] optional context
	 */
	setOptions: function(options, context){
		Object.recursiveExtend(this._getOptionsFromContext(context), options);
	},
	
	/**
	 * Animates using the effect bound to 'name'
	 * @param {String} name
	 * @param {Object} [override] options specific to the animation which will override default and animation level options
	 * @return {Mixed}
	 */
	doAnimation: function(name, override, ret_effect){
		//PSLib.Debug.printToConsole('parent doAnimation');
		var a = this._getAnimationType(name);
		if( a ){
			this._processOneTimeCallbacks(name, override);
			//PSLib.Debug.printToConsole('processedCallbacks');
			//PSLib.Debug.printToConsole(override);
			var options = PSLib.Util.assembleOptions( this._options, a.options, override);
			//PSLib.Debug.printToConsole('doing animation ' + name);
			//PSLib.Debug.printObjectToConsole(this._options);
			//PSLib.Debug.printToConsole('--------');
			//PSLib.Debug.printObjectToConsole(a.options);
			//PSLib.Debug.printToConsole('--------');
			//PSLib.Debug.printObjectToConsole(override);
			//PSLib.Debug.printObjectToConsole(options);*/
			
			/* can force the return of the effect to allow for Effect.Parallel */
			//PSLib.Debug.printToConsole(a.effect);
			if( ret_effect){
				return new a.effect(this._element, options);;
			}
			/* otherwise return the AnimatableElement to allow for chaining */
			else{
				new a.effect(this._element, options);
				return this;
			}
		}else{
			PSLib.Debug.printToConsole('no effect');
		}
	},
	
	_processOneTimeCallbacks: function(name, options){
		options = options || {};
		PSLib.Animation.HOOKS.each( function(h){
			if( Object.isFunction(options[h])){
				options[h] = options[h].wrap( function(orig, s_obj){
					this._executeCallbacks(h, s_obj, name, orig);
				}.bind(this));
			}
		});
	},
	
	stopCurrent: function(name){
		this._getQueue(name).entries()[0].cancel();
	},
	
	stopAll: function(name){
		this._getQueue(name).each(function(e){
			e.cancel();
		});
	},
	
	/** @ignore */
	_getQueue: function(name){
		if( name ){
			var a = this._getAnimationType(name);
			if( a ){
				if( a.options.queue && a.options.queue.scope ){
					return Effect.Queues.get(a.options.queue.scope);
				}
			}
			return null;
		}else{
			return Effect.Queues.get(this._options.queue.scope);
		}
	},
	
	/**
	 * bind an animation to occur when a certain event is observed on an element or set of elements
	 * 
	 * @param {Object} name the name of the animation to trigger
	 * @param {Object} elements the element or set of elements to bind the animation to
	 * @param {Object} event the name of the event to observe
	 * @param {Object} [override] options specific to the animation which will override default and animation level options
	 */
	bindElements: function( name, elements, event, override, ret_effect){
		if( Object.isArray(elements) ){
			elements.each( function(e){
				this._bindElement( name, e, event, override, ret_effect);
			}, this);		
		}else{
			this._bindElement( name, elements, event, override, ret_effect);
		}
	},
	
	/** @ignore */
	_bindElement: function( name, element, event, override, ret_effect){
		var a = this._getAnimationType(name);
		if( a ){
			try{
				var e = $(element);
				/*e.observe(event, function(){
					this[name](override, ret_effect);
				}.bind(this));*/
				e.observe( event, this._eventWrapper.bindAsEventListener(this, name, override, ret_effect));
				a.elements.push(e);
			}catch( e ){
				debug_message('_bindElement failure: ' + name + ' ' + element + ', ' + event);
			}
		}
	},
	
	_eventWrapper: function( evt, name, override, ret_effect){
		try{
			evt.stop();
		}catch(ex){}
		this[name](override, ret_effect);
	},
	
	/**
	 * Allows users to manually specify elements which perform a certain action
	 * @param {String} name
	 * @param {Object} elms
	 */
	addElements: function(name, elms){
		var a = this._getAnimationType(name);
		if( a ){
			a.elements = a.elements.concat(elms);
		}
	},
	
	/**
	 * Returns the elements bound to the animation specified by name
	 * @param {String} name
	 * @return {Object}
	 */
	getElements: function( name){
		var a = this._getAnimationType(name);
		if( a ){
			return a.elements;
		}else{
			return [];
		}
	},
	
	/**
	 * determine whether an animation type exists on an element
	 * @param {Object} name
	 * @return {Boolean}
	 */
	animationTypeExists: function (name){
		var a;
		if( (a = this._getAnimationType(name)) ){
			return a;
		}else{
			return false;
		}
	},
	
	/** @ignore */
	_getAnimationType: function( name ){
		return this._animations.get(name);
	},
	
	/**
	 * add an animation type to the object
	 * @param {String} name
	 * @param {Object} options
	 */
	registerAnimationType: function(name, options){
		//PSLib.Debug.printToConsole('registering animation ' + name);
		////PSLib.Debug.printObjectToConsole(options);
		if( !Object.isUndefined(this._animations[name])){
				this.removeAnimationType(name);
		}
		
		var obj = {
				effect: Object.isFunction(options['effect'])?options['effect']:Effect.Appear,
			   options: {},
		      elements: $A(),
			 callbacks: $H()
		}
		Object.recursiveExtend(obj.options, options.options || {});

		this._animations.set( name, obj);
		this._processCallbacks( obj.options, name);
		
		/* may want to make a list of reserved words that functions can not be named */
		/*if( !this[name]){*/
			this[name] = function(o, r){
				//PSLib.Debug.printToConsole('calling doAnimation: ' + name);
				return this.doAnimation(name, o, r);
			}.bind(this);	
		/*}*/
	},
	
	_executeCallbacks: function(name, s_obj, context, func){
		if( Object.isFunction(func)	){
			func(s_obj, this);
		}
		var callbacks = $A();
		if( context ){
			c = this._getCallbacksFromContext(context);
			if (c && Object.isArray(c.get(name))){
				callbacks = callbacks.concat(c.get(name));
			}
		}
		c = this._getCallbacksFromContext();
		if( c && Object.isArray(c.get(name))){
			callbacks = callbacks.concat(c.get(name));
		}
		callbacks.each( function(f){
			PSLib.Debug.printToConsole( 'callback ' + name + ': ' + f);
			f( s_obj, this);
		}, this);
	},
	
	addCallback: function( name, callback, context){
		//PSLib.Debug.printToConsole('adding callback ' + name + ' to context ' + context + ': ' + callback);
		var c = this._getCallbacksFromContext(context);
		if( !Object.isArray(c.get(name))){
			c.set(name, $A());
		}
		c.get(name).push(callback);
	},
	
	removeCallback: function( name, callback, context){
		var c = this._getCallbacksFromContext(context);
		/* need to make sure this actually does what it's supposed to */
		if( c && Object.isArray(c[name])){
			c[name] = c[name].without(callback);
		}
	},
	
	_processCallbacks: function(options, context){
		var c = this._getCallbacksFromContext(context);
		if (c) {
			PSLib.Animation.HOOKS.each(function(h){
				if (Object.isFunction(options[h])){
					//PSLib.Debug.printToConsole('found callback: ' + h + ' in context ' + (context||'global'));
					this.addCallback(h, options[h], context);
				}
				options[h] = function(s_obj){
					this._executeCallbacks(h, s_obj, context);
				}.bind(this);
			}, this);
		}
	},
	
	_getCallbacksFromContext: function(context){
		var a = this._getAnimationType(context);
		if( a){
			return a.callbacks;
		}else{
			return this._callbacks;
		}
	},
	
	/**
	 * removes the animation type form the element
	 * @param {String} name
	 */
	removeAnimationType: function( name){
		this._animations.remove(name);
		this[name] = null;
	},
	
	/**
	 * executes a parallel animation returning the effect or this element
	 * @param {Array} elements an array of objects of the form {element, method, options} 
	 * @param {Object} [options]
	 * @param {Integer} [ret_effect] if 1 returns the effect
	 */
	parallel: function(elements, options, ret_effect){
		var e = PSLib.Animation.Parallel(elements, options);
		if( ret_effect ){
			return e;
		}else{
			return this;
		}
	}
});

PSLib.Animation.DynamicLoadAnimatableElement = Class.create( PSLib.Animation.AnimatableElement, {
	initialize: function( $super, element, options, types){
		if( PSLib.DynamicLoader.Util.isLoadable(element)){
			this._dl = new PSLib.DynamicLoader(element, {afterLoad: this.afterLoad.bind(this)});
			if( this._dl ){
				this._loaded = false;
				this._loading = false;
				this._load_queue = $A();
				this._load_queue_max = options['load_queue_max'] || 1;
				this._beforeLoad = options['beforeLoad'] || Prototype.emptyFunction;
				this._afterLoad = options['afterLoad'] || Prototype.emptyFunction;
			}else{
				return false;
			}
		}
		else{
			this._dl = null;
			this._loaded = true;
			this._loading = false;
		}
		$super( element, options, types);
	},
	
	isDynamicLoad: function(){
		return (this._dl?true:false);
	},
	
	getDynamicLoader: function(){
		return this._dl;
	},
	
	doAnimation: function($super, name, override, ret_effect){
		if( !this._loaded ){
			if( this._load_queue_max == 0 || this._load_queue.length < this._load_queue_max){
				this._load_queue.push({'name': name, 'override': override, 'ret_effect': ret_effect});	
			}
			if( !this._loading){
				this.loadContent();
			}
		}
		else{
			return $super(name, override, ret_effect);
		}
	},
	
	loadContent: function(){
		this._beforeLoad();
		this._dl.load();
	},
	
	afterLoad: function( dl ){
		this._afterLoad(this);
		this._loaded = true;
		this._loading = false;
		this._processLoadQueue();
	},
	
	_processLoadQueue: function(){
		this._load_queue.each( function(e){
			this.doAnimation( e.name, e.override, e.ret_effect);
		}, this);
		this._load_queue.clear();
	}
});

/* TODO - Parallel should check the queue of each participating element and grab the greatest finishOn,
 * this start time of the effect should be the finishOn value.  Each participating element should also
 * be locked so other animations can not start on it while it's in a parallel effect
 * 
 * need to assemble all of the beforeStart functions for each animation and put them on beforeStart
 * for parallel effect.
 */
PSLib.Animation.Parallel = function( elements, options){
	var parallel = $A( elements || []);
	var animations = [];
	var options = PSLib.Util.assembleOptions({
		queue: {scope: 'pslib_parallel', position: 'end'}
	}, options || {});
	var each_options = {
		sync: 1
	}
	parallel.each( function(e){
		if( e ){
			if( e.object.animationTypeExists(e.method)){
				var options = PSLib.Util.assembleOptions( each_options, e.options || {});
				if( (ret = e.object[e.method](options, 1)) ){
					animations.push( ret );
				}else{
					//PSLib.Debug.printToConsole('PSLib.Animation.Parallel: animation failed');
				}
			}else{
				//PSLib.Debug.printToConsole('PSLib.Animation.AnimatableElement.parallel: invalid method - ' + method);
			}
		}else{
			//PSLib.Debug.printToConsole('PSLib.Animation.AnimatableElement.parallel: no object');
		}
	});
	if( animations.length ){
		var ep = new Effect.Parallel(
						animations,
		  				options
				 );
		return ep;
	}else{
		return null;
	}
};

//DynamicLoad
PSLib.Animation.OpenCloseElement = Class.create( PSLib.Animation.DynamicLoadAnimatableElement,
{
	initialize: function($super, elm, options, open, close){
		/* set up the defaults so we at least have an effect */
		var types = {
			'open': {
				effect: Effect.BlindDown
			},
			'close': {
				effect: Effect.BlindUp	
			}
		};
		/* copy any passed options */
		/* TODO - this is a mess! could be taking a long time */
		types.open = Object.recursiveExtend(types.open, open);
		types.close = Object.recursiveExtend(types.close, close);
		Object.recursiveExtend( types.open, Object.clone(open) || {});
		Object.recursiveExtend( types.close, Object.clone(close) || {});
		
		/* options is being continuously overwritten! */
		this._options = Object.clone(options || {});
		if( Object.isUndefined(this._options.stateful) ){
			this._options.stateful = true;
		}
		
		$super( elm, this._options, types);
		
		this.addCallback('beforeStart', this._toggleOpen.bind(this));
		this._is_open = this.getElement().getStyle('display') == 'none'?false:true;
		if (this._options.stateful) {
			this.addCallback('beforeStart', function(s_obj, obj){
				PSLib.State.recordState(obj);
			});
			PSLib.State.recordState(this);
		}
		
		
		/* override the open/close methods 
		 * could probably make this cleaner by passing these function as the effects.
		 * DUH, use inheritance and super! see below */
		this._open = this.open;
		this.open = function(myoptions, ret_effect){
			if( !this._is_open ){
				return this._open(myoptions, ret_effect);
			}else{
				if( ret_effect){
					return null;
				}else{
					return this;
				}
			}
		};
		this._close = this.close;
		this.close = function(options, ret_effect){
			if( this._is_open){
				return this._close(options, ret_effect);
			}else{
				return this;
			}
		}
	},
	
	/*
	 * can't use these because methods are added dynamically? maybe use stubs?
	 * 
	 open: function($super, opts, ret_effect){
		PSLib.Debug('got to overriden open');
		if( !this._is_open ){
			//PSLib.Debug.printToConsole('not open, opening: ' + this._element.readAttribute('id'));
			return $super( opts, ret_effect);
		}else{
			//PSLib.Debug.printToConsole('open, not opening' + this._element.readAttribute('id'));
			if(ret_effect){
				return null;
			}else{
				return this;
			}
		}
	},
	
	close: function($super, opts, ret_effect){
		PSLib.Debug('got t');
		if( this._is_open){
			//PSLib.Debug.printToConsole('not closed, closing: ' + this._element.readAttribute('id'));
			return $super( opts, ret_effect)
		}else{
			//PSLib.Debug.printToConsole('closed, not closing: ' + this._element.readAttribute('id'));
			if(ret_effect){
				return null;
			}else{
				return this;
			}
		}
	},*/
	
	_toggleOpen: function(){
		this._is_open = !this._is_open;
		PSLib.Debug.printToConsole('toggling open ' + this._is_open + ' ' + this.getElement().readAttribute('id'));
	},
	
	getOpenElements: function(){
		return this.getElements('open');
	},
	
	getCloseElements: function(){
		return this.getElements('close');
	},
	
	bindOpenElements: function( elements, event){
		this.bindElements('open', elements, event);
	},
	
	bindCloseElements: function( elements, event){
		this.bindElements('close', elements, event);
	},
	
	isOpen: function(){
		return this._is_open;
	},
	
	getState: function(){
		return this.getElement().readAttribute('id') + '=' + this.isOpen();
	},
	
	setState: function(s){
		PSLib.Debug.printToConsole('open close element: ' + s);
		if(s == 'true'){
			this._simpleOpen();
		}else{
			this._simpleClose();
		}
	},
	
	getId: function(){
		return this.getElement().readAttribute('id');
	},
	
	_simpleOpen: function(){
		if( !this._is_open){
			this.getOption('beforeStart', 'open')(this);
			this.getElement().show();
			this.getOption('afterFinish', 'open')(this);
		}
	},
	
	_simpleClose: function(){
		if (this._is_open) {
			this.getOption('beforeStart', 'close')(this);
			this.getElement().hide();
			this.getOption('afterFinish', 'close')(this);
		}
	}
});

/*PSLib.Animation.OpenCloseIDBound = function( elm, options, open, close){
	var ret = new PSLib.Animation.OpenCloseElement( elm, options, open, close);
	var open_class = ret.getElement().readAttribute('id') + '_action_open';
	var close_class = ret.getElement().readAttribute('id') + '_action_close';
	ret.bindElements('open', $$('.' + open_class), 'click');
	ret.bindElements('close', $$('.' + close_class), 'click');
	return ret;
}*/

PSLib.Animation.OpenCloseIDBound = Class.create( PSLib.Animation.OpenCloseElement, {
	initialize: function($super, elm, options, open, close){
		$super(elm, options, open, close);
		this._open_class = this.getElement().readAttribute('id') + '_action_open';
		this._close_class = this.getElement().readAttribute('id') + '_action_close';
		var open_elements = $$('.' + this._open_class);
		var close_elements = $$('.' + this._close_class);
		this.bindElements('open', open_elements, 'click');
		this.bindElements('close', close_elements, 'click');
		open_elements.invoke('removeClassName', this._open_class);
		close_elements.invoke('removeClassName', this._close_class);
	}
});

/* want to hide all elements that are not current here? */
PSLib.Animation.OpenCloseCollection = Class.create({
	initialize: function(container, collection_options, options, open_options, close_options){
		/* collection options can include whether this should be parallel, each queue, this queue */
		this._collection_options = collection_options || {};
		this._options = PSLib.Util.assembleOptions( options, {stateful: false});
		this._elements = [];
		this._current_item = null;
		this._scope = 'parallel';
		this._animation_type = this._collection_options['animation_type'] || PSLib.Animation.PARALLEL;
		this._position_items = true;
		if( !Object.isUndefined(this._collection_options['position_items'])){
			this._position_items = this._collection_options['position_items'];
		}
		if( Object.isUndefined(this._collection_options['stateful'])){
			this._collection_options['stateful'] = true;
		}
		
		open_options = open_options || {};
		close_options = close_options || {};
		
		this._container = $(container);
		if( this._container && this._container.readAttribute('id')){
			var p = this._container.getStyle('position');
			if (p != 'relative' && p != 'absolute') {
				this._container.makePositioned();
			}
			this._scope = this._container.readAttribute('id');
			this._scope_options = {queue: {scope: this._scope}};
			this._container.selectChildren('.item').each( function(e){
				var oce = new PSLib.Animation.OpenCloseElement(e, this._options, open_options, close_options);
				//PSLib.Debug.printToConsole('open function: ' + oce.open);
				if( oce ){
					oce.addCallback('beforeStart', function(){
						this._setCurrent(oce);
						if( this._position_items){
							this._changeContainerHeight(oce);
							oce.getElement().setStyle({position: 'absolute', top: '0px', left: '0px'});
						}
					}.bind(this), 'open');
					
					oce.addCallback('afterFinish', function(){
						if( this._position_items ){
							oce.getElement().setStyle({position: ''});
							this._changeContainerHeight();
						}
					}.bind(this), 'open');
					
					if( this._collection_options['stateful']){
						oce.addCallback('beforeStart', function(){
							PSLib.State.recordState(this);
						}.bind(this), 'open');
					}
					
					var id = oce.getElement().readAttribute('id');
					if( id ){
						var c = id + '_action_open';
						$$('.' + c).each( function(ee){
							ee.observe('click', function(e){
								this._doOpen(e, oce);
							}.bindAsEventListener(this));
							oce.addElements('open', ee);
							ee.removeClassName(c);
						}, this);
					}
					if( oce.isOpen()){
						if (!this.getCurrent()) {
							this._setCurrent(oce);
						}else{
							/* may want to do hide() here instead */
							oce._simpleClose(this._scope_options);
						}
					}
					this._elements.push(oce);
				}
			}, this);
			var c = this._container.readAttribute('id') + '_action_close';
			this._close_elements = $$('.' + c);
			this._close_elements.invoke('observe', 'click', this.close.bindAsEventListener(this));
			this._close_elements.invoke('removeClassName', c);
			if( this._collection_options['stateful'] ){
				PSLib.State.recordState(this);
			}
		}else{
			//PSLib.Debug.printToConsole('PSLib.Animation.OpenCloseCollection no container or container does not have id!');
		}
	},
	
	_doOpen: function(e, elm){
		try{
			e.stop();
		}catch(ex){}
		//PSLib.Debug.printToConsole('doopen called by ' + this._doOpen.caller);
		var c = null;
		if( (c = this.getCurrent()) && elm != c){
			if (this._animation_type == PSLib.Animation.SEQUENTIAL) {
				/* if it's sequential, just overwrite the queue options to put them in the queue
				 * for the collection.
				 */
				var options = {queue: {scope: this._scope}};
				c.close(options);
				elm.open(options);
			}
			else {
				/* if this is a parallel animation, use the global options for the animation, be sure to overwrite the scope
	 			 */
				var options = PSLib.Util.assembleOptions(this._options, {
					queue: {
						scope: this._scope
					}
				});
				/* since these options have been used on each OpenCloseElement, we don't want more event handlers declared
	 			 * for the parallel. remove them here.
	 			 */
				PSLib.Util.removeOptions(this._options, ['beforeStart', 'afterFinish', 'beforeUpdate', 'afterUpdate'])
				PSLib.Animation.Parallel([{
					object: elm,
					method: 'open'
				}, {
					object: c,
					method: 'close'
				}], options);
			}
		}else{
			elm.open({
				queue: {
					scope: this._scope
				}
			});
		}
	},
	
	_simpleOpen: function(elm){
		var c = null;
		if( (c = this.getCurrent()) && elm != c ){
			c._simpleClose();
		}
		if( elm ){
			this._setCurrent(elm);
			elm._simpleOpen();
		}
	},
	
	_setCurrent: function( elm ){
		if( elm ){
			//PSLib.Debug.printToConsole('setting current! ' + elm.getElement().readAttribute('id'));
			/* changed here */
			this._current_item = elm;
		}
	},
	
	getCurrent: function(){
		return this._current_item;
	},
	
	_changeContainerHeight: function(elm){
		if( elm ){
			current_height = this._container.getHeight();
			new_height = elm.getElement().getHeight();
			PSLib.Debug.printToConsole('got to changeContainerHeight!' + new_height);
			this._container.setStyle({height: new_height + 'px'});
		}else{
			/* CHANGED HERE */
			this._container.setStyle({height: 'auto'});
		}
	},
	
	close: function(e){
		try{
			e.stop();
		}catch(ex){}
		if( this.getCurrent() ){
			this.getCurrent().close(this._scope_options);
			this._setCurrent(null);
		}
	},
	
	getState: function(){
		if( this.getCurrent()){
			return this._container.readAttribute('id') + '=' + this._elements.indexOf(this.getCurrent());
		}else{
			return '';
		}
	},
	
	setState: function(i){
		if( i > 0 && i < this._elements.length){
			/* could potentially add options at the end of _doOpen to pass one time callbacks */
			this._simpleOpen(this._elements[i]);
		}
	},
	
	getId: function(){
		return this._container.readAttribute('id');
	}
});

PSLib.Animation.SequentialOpenCloseCollection = Class.create( PSLib.Animation.OpenCloseCollection, {
	initialize: function( $super, container, collection_options, options, open_options, close_options){
		$super( container, collection_options, options, open_options, close_options);
		this._current_index = this._collection_options['startindex'] || 0;
		this._wrap = this._collection_options['wrap'] || 1;
		
		this._elements.invoke('addCallback', 'beforeStart', this._updateDisplayCurrentElements.bind(this), 'open');
		
		var c = this._container.readAttribute('id') + '_action_next';
		this._next_elements = $$( '.' + c);
		this._next_elements.invoke('removeClassName', c);
		
		c = this._container.readAttribute('id') + '_action_previous';
		this._prev_elements = $$( '.' + c);
		this._prev_elements.invoke('removeClassName', c);
		
		c = this._container.readAttribute('id') + '_display_current';
		this._display_current_elements = $$( '.' + c);
		this._display_current_elements.invoke('removeClassName', c);
		
		c = this._container.readAttribute('id') + '_display_total';
		this._display_total_elements = $$( '.' + c);
		this._display_total_elements.invoke('removeClassName', c);
		
		//PSLib.Debug.printToConsole( this._display_current_elements);
		
		if( this._current_index != this._elements.indexOf(this.getCurrent())){
			this._doOpen(null, this._elements[this._current_index]);
		}
		
		this._next_elements.each( function(e){
			e.observe('click', this.next.bindAsEventListener(this));
		}, this);
		this._prev_elements.each( function(e){
			e.observe('click', this.previous.bindAsEventListener(this));
		}, this);
		
		this._updateDisplayTotalElements();
		this._updateDisplayCurrentElements();
	},
	
	previous: function(e){
		try{
			e.stop();
		}catch(ex){}
		var new_index = this._current_index - 1;
		new_index = new_index<0? this._wrap?this._elements.length-1:0 : new_index;
		//PSLib.Debug.printToConsole(new_index);
		if( new_index != this._current_index ){
			this._current_index = new_index;
			this._doOpen(null, this._elements[this._current_index]);	
		}
	},
	
	next: function(e){
		try{
			e.stop();
		}catch(ex){}
		var new_index = this._current_index + 1;
		new_index = new_index>this._elements.length - 1? !this._wrap?this._elements.length-1:0 : new_index;
		//PSLib.Debug.printToConsole('new_index: ' + new_index);
		if( new_index != this._current_index ){
			this._current_index = new_index;
			this._doOpen(null, this._elements[this._current_index]);	
		}
	},
	
	_updateDisplayCurrentElements: function(s_obj, oc_obj){
		var i = this._current_index + 1;
		if( oc_obj ){
			i = this._elements.indexOf(oc_obj) + 1;
		}
		this._display_current_elements.invoke('update', i);
	},
	
	_updateDisplayTotalElements: function(){
		this._display_total_elements.invoke('update', this._elements.length);
	}
});

/*
 * TODO: may want to use duration option along with time option to come up with some well-defined behavior
 * this is actually a version of sequential open close, should merge the two.
 */
PSLib.Animation.AutoOpenCloseCollection = Class.create( PSLib.Animation.OpenCloseCollection, {
	initialize: function($super, container, collection_options, options, open_options, close_options){
		$super(container, collection_options, options, open_options, close_options);
		this._time = this._collection_options['time'] || 3;
		this._stopafter = this._collection_options['stopafter'] || 0;
		this._interval = null;
		this._iterations = 0;
		this._direction = this._collection_options['direction'] || PSLib.Animation.Directions.FORWARD;
		this._running = false;
		if( this._current_item ){
			this._current_index = this._elements.indexOf(this._current_item);
		}else{
			this._current_index = 0;
		}
		if( this._collection_options['autostart']){
			this.start();
		}
	},
	
	start: function(){
		this.stop();
		this._iterations = 0;
		/*
		 * Firefox has a bug in setInterval which causes erratic behavior
		 * when multiple intervals are set. for now use setTimeout
		 * 
		 * this._interval = new PeriodicalExecuter(this.swap.bind(this), this._time);*/
		this._running = true;
		this._interval = setTimeout( this.swap.bind(this), this._time * 1000);
	},
	
	stop: function(){
		try{
			/* this._interval.stop();
			 * delete this._interval;*/
			this._running = false;
		}catch(ex){}
	},
	
	/* TODO: map this to do open? */
	swap: function(){
		this._current_index += this._direction;
		if( this._current_index < 0){
			this._current_index = this._elements.length - 1;
		}else if( this._current_index > this._elements.length - 1){
			this._current_index = 0;
		}
		this._doOpen(null, this._elements[this._current_index]);
		this._iterations++;
		if( this._iterations == this._stopafter){
			this.stop();
		}else if( this._running ){
			this._interval = setTimeout( this.swap.bind(this), this._time * 1000);
		}
	}
});

/* 
 * This is a HORIZONTAL ONLY carousel
 * TODO - make this into a general purpose carousel
 * 
 * figure out best way to enforce style standards. external css?
 */
PSLib.Animation.MovableCollection = Class.create({
	initialize: function(container, options){
		this._container = $(container);
		this._options = Object.extend({
				'loop': true
			},options || {});
		
		if( Object.isUndefined(this._options.direction) ){
			this._options.direction = PSLib.Animation.Directions.VERTICAL;
		}
		if( this._container && this._container.readAttribute('id')){
			this._container.makePositioned();
			this._original_offset = this._container.positionedOffset();
			this._content = this._container.selectChildren('#' + this._container.readAttribute('id') + '_content')[0];
			if( this._content ){
				this._content.makePositioned();
				this._children = this._content.selectChildren('.item');
				var temp = this._content.getStyle('overflow');
				this._content.setStyle({'overflow': 'auto'});
				this._offsets = this._children.invoke('positionedOffset');
				this._content.setStyle({'overflow': temp});
				
				if (this._options.direction == PSLib.Animation.Directions.HORIZONTAL) {
					this._children.invoke('setStyle', {'float': 'left'});
					this._initWidth();
				}
				
				this._current_index = 0;
				//PSLib.Debug.printToConsole(this._offsets);
				this._element = new PSLib.Animation.AnimatableElement( this._content, {}, {move: {effect: Effect.Move, options: {mode: 'absolute', transition: Effect.Transitions.sinoidal}}});
				
				var c = this._container.readAttribute('id') + '_action_previous';
				this._prev_elements = $$('.' + c);
				this._prev_elements.invoke('observe', 'click', this.prev.bindAsEventListener(this));
				this._prev_elements.invoke('removeClassName', c);
				
				c = this._container.readAttribute('id') + '_action_next';
				this._next_elements = $$('.' + c);
				this._next_elements.invoke('observe', 'click', this.next.bindAsEventListener(this));
				this._next_elements.invoke('removeClassName', c);
				
			}else{
				return null;
			}
		}else{
			return null;
		}
	},
	
	_initWidth: function(){
		if (this._content.getWidth() == 0) {
			var temp = new Element('div', {
				style: 'float: left; width: 20px; border: 1px solid white;'
			});
			this._content.setStyle({
				width: '100000px'
			});
			this._children.last().insert({
				after: temp
			});
			console.log('temp offset: ' + temp.positionedOffset()[0]);
			var width = temp.positionedOffset()[0];
			temp.remove();
			this._content.setStyle({
				width: width + 'px'
			});
		}
	},
	
	next: function(e){
		try{
			e.stop();
		}catch(ex){}
		this._initWidth();
		var new_index = this._current_index + 1;
		if( new_index >= this._offsets.length){
			if( this._options.loop ){
				new_index = 0;
			}else{
				new_index = this._offsets.length - 1;
			}
		}
		
		if( new_index != this._current_index){
			if( this._options.direction == PSLib.Animation.Directions.HORIZONTAL){
				//PSLib.Debug.printToConsole('new index: ' + new_index);
				var new_x = this._offsets[new_index][0];
				if( new_x == 0 && new_index != 0){
					this._offsets[new_index] = this._children[new_index].positionedOffset();
					new_x = this._offsets[new_index][0];
				}
				//PSLib.Debug.printToConsole('new x: ' + new_x + ', ' + this._container.getWidth());
				if( this._content.getWidth() - new_x >= this._container.getWidth()){
					var options = Object.extend( this._options, {x: -new_x});
					this._element.move(options);
					this._current_index = new_index;
				}
			}else{
				var new_y = this._offsets[new_index][1];
				if( new_y == 0 && new_index!= 0){
					this._offsets[new_index] = this._children[new_index].positionedOffset();
					new_y = this._offsets[new_index][1];
				}
				if( this._content.getHeight() - new_y >= this._container.getHeight() ){
					var options = Object.extend( this._options, {y: -new_y});
					this._element.move(options);
					this._current_index = new_index;
				}
			}
		}
	},
	
	prev: function(e){
		try{
			e.stop();
		}catch(ex){}
		this._initWidth();
		var new_index = this._current_index - 1;
		if( new_index < 0){
			if( this._options.loop ){
				new_index = this._offsets.length - 1;
			}else{
				new_index = 0;
			}
		}
		if( new_index != this._current_index){
			if (this._options.direction == PSLib.Animation.Directions.HORIZONTAL) {
				//PSLib.Debug.printToConsole('new index: ' + new_index);
				var new_x = this._offsets[new_index][0];
				//PSLib.Debug.printToConsole('new x: ' + new_x + ', ' + this._container.getWidth());
				if (this._content.getWidth() - new_x >= this._container.getWidth()) {
					var options = Object.extend( this._options, {x: -new_x});
					this._element.move(options);
					this._current_index = new_index;
				}
			}else{
				var new_y = this._offsets[new_index][1];
				if( this._content.getHeight() - new_y >= this._container.getHeight()){
					var options = Object.extend( this._options, {y: -new_y});
					this._element.move(options);
					this._current_index = new_index;
				}
			}
		}
	}
});

/* based on code @ http://www.webtoolkit.info/ajax-file-upload.html */
PSLib.AjaxUpload = Class.create({
	initialize: function(elm, opts){
		this._input_element = $(elm);
		this._element = this._input_element.up('form');
		if( this._element && this._input_element.readAttribute('id')){
			opts = opts || {};
			this.onStart = Object.isFunction(opts['onStart'])?opts['onStart']:Prototype.emptyFunction;
			this.onComplete = Object.isFunction(opts['onComplete'])?opts['onComplete']:Prototype.emptyFunction;
			this._frame_action = !Object.isUndefined(opts['action'])?opts['action']:'';
			this._frame_id = this._input_element.readAttribute('id') + '_iframe';
			if ($(this._frame_id)) {
				var i = 1;
				var id = this._frame_id + i;
				while( $(id) ){
					i++;
					id = this._frame_id + i;
				}
				this._frame_id = id;
			}
			this._frame = new Element('iframe', {
					id: this._frame_id,
					name: this._frame_id,
					src: 'about:blank',
					style: 'display: none'
				});
			$$('body')[0].appendChild(this._frame);
			this._frame.observe('load', this.load.bindAsEventListener(this));
			/* no longer override submit in case the upload input is inside a larger form */
			//this._element.observe('submit', this.submit.bindAsEventListener(this));
			//this._element.setAttribute('target', frame_id);
		}else{
			return false;
		}
	},
	
	submit: function(e){
		var old_action, old_target;
		// grab current action
		old_action = this._element.readAttribute('action');
		if( this._frame_action){
			// if we have an alternate action, set it
			this._element.setAttribute('action', this._frame_action);
		}
		// grab old target
		old_target = this._element.readAttribute('target');
		// set hidden iframe target
		this._element.setAttribute('target', this._frame_id);
		// execute before start
		this.onStart();
		// submit the form
		this._element.submit();
		this._input_element.setAttribute('disabled', 'disabled');
		// reset originals
		this._element.setAttribute('action', old_action);
		this._element.setAttribute('target', old_target);
	},
	
	load: function(e){
		if (this._frame.contentDocument) {
            var d = this._frame.contentDocument;
        } else if (this._frame.contentWindow) {
            var d = this._frame.contentWindow.document;
        } else {
            var d = window.frames[this._frame.readAttribute('id')].document;
        }
		if (d.location.href == "about:blank") {
            return;
        }
		this._input_element.value = '';
		this._input_element.writeAttribute('disabled', null);
        this.onComplete(d.body.innerHTML);
	}
});

PSLib.WindowScrollManager = {
	effect: null,
	scrollToElement: function( e, offx, offy ){
		var elm = $(e);
		if( elm ){
			var p = elm.cumulativeOffset();
			var offx = offx || 0;
			var offy = offy || 0;
			PSLib.WindowScrollManager.scrollTo( p[0] + offx, p[1] + offy);
		}
	},
	scrollTo: function( x, y){
		PSLib.WindowScrollManager.effect = new Effect.Scroll(window, {'x': x, 'y': y});
	},
	cancelScroll: function(){
		var e = PSLib.WindowScrollManager.effect;
		if( e ){
			e.cancel();
			e = null;
		}
	}
};

