Controller = {};

Controller.Base = function() {};
Controller.Base.prototype = {

	/*** properties ***/

	locked: false,
	state: 'idle',
	values: [], // values array
	loopnum: 0, // number of iterated loops
	playdir: null, // play direction
	current: { index: 0, value: false },
	
	timeout: null,

	/*** setup ***/

	setValues: function(values) {
		if(Var.is_array(values)) { 			
			this.values = values; 
			if(arguments[1]) { this.reset(); }	
			this.current.value = this.values[this.current.index];
		}
	},

	setOptions: function(options) {
		this.options = Object.extend({
			duration: 1.0, 		// controller time in seconds (usually pause)
			delay: 0.0, 			// delay start in seconds
			loop: -1, 				// times to loop (-1 infinite)
			playmode: 'fw', 	// play forward
			frontback: false	// full front to back loops
		}, options || {});
	},
	
	setCurrent: function(index) {
		this.current.index = index;
		this.current.value = this.values[this.current.index];
	},
	
	forceFirst: function(index) {
		this.setCurrent(this.values.length - 1);
	},
	
	/*** playback ***/
	
	begin: function() {
		setTimeout(this.action.update.bind(this.action, this.current.index, this.current.value), ((arguments[0] || 0) * 1000));
	},
	
	begin_play: function() {
		setTimeout(this.play.bind(this, this.values.length - 1), ((arguments[0] || 0) * 1000));
	},
		
	display: function() {
		this.first(false, true);
	},
	
	start: function() {
		this.event('beforeStart');
		this.cancel();
		if(this.options.frontback) { this.first(false, true); }
		this.timeout = setTimeout(this.loop.bind(this), (this.options.delay * 1000));
	},
	
	auto_play: function() {
		this.play(this.values.length - 1);
	},
		
	play: function() {
		this.event('beforePlay');
		this.setCurrent(arguments[0] || this.current.index);
		this.start();
	},
	
	pause: function() {
		this.event('beforePause');
		this.cancel('paused');
	},
	
	stop: function() {
		this.event('beforeStop');
		this.cancel();
	},
	
	toggle_playback: function() {
		if(this.state != 'running') {
			this.play();
		} else {
			this.stop();
		}
	},
	
	forward: function() {
		this.options.playmode = 'fw';
		this.start();
	},
	
	backward: function() {
		this.options.playmode = 'bw';
		this.start();
	},
	
	/*** static ***/
	
	currentIndex: function() {
		return this.current.index;
	},
	
	first: function() {
		return this.show(0, arguments[0] || false, arguments[1] || false);
	},
	
	valueOfIndex: function(index) {
		return this.values[index];
	},
	
	prevIndex: function() {
		var index = arguments[0] || this.current.index;
		if(index < 1 && this.options.loop != 0) {
			index = this.values.length - 1;
		} else {
			index = index - 1;
		}
		return index;
	},
	
	prevValue: function() {
		return this.values[this.prevIndex(arguments[0] || this.current.index)];
	},
	
	prev: function() {		
		return this.show(this.prevIndex(), arguments[0] || false, false, 'bw');
	},
	
	nextIndex: function() {
		var index = arguments[0] || this.current.index;
		if(index >= this.values.length - 1 && this.options.loop != 0) {
			index = 0;
		} else {
			index = index + 1;
		}
		return index;
	},
	
	nextValue: function() {
		return this.values[this.nextIndex(arguments[0] || this.current.index)];
	},
	
	next: function() {		
		return this.show(this.nextIndex(), arguments[0] || false, false, 'fw');
	},
	
	last: function() {
		return this.show(this.values.length - 1, arguments[0] || false, arguments[1] || false);
	},
	
	show: function(index) { // (index, dynamic, force) bool to force display
		if(this.locked && this.state != 'running') { this.cancel(); return false; }
		if(!arguments[1]) { this.cancel(); } // if not in dynamic mode, cancel dynamics		
		
		if(!Var.is_undef(this.values[index]) && (index != this.current.index || arguments[2])) { 
			this.locked = true;
			
			var isfirst = (index == 0);
			var islast = (index == this.values.length - 1);
			var dir = (arguments[3] || null);
			
			// before update event callbacks
			if(isfirst) { this.event('beforeFirst'); } else if(islast) { this.event('beforeLast'); }
			
			// direction
			if(dir == 'fw' 
				|| (dir == null && index > this.current.index)) {
				this.playdir = 'fw';
			} else if(dir == 'bw' 
				|| (dir == null && index < this.current.index)) { 
				this.playdir = 'bw';
			} else {
				this.playdir = null;
			}
						
			// keep current state
			this.current.index = index;
			this.current.value = this.values[index];
	
			// call update and handle value
			this.event('beforeUpdate');
			this.update(this.current.index, this.current.value);
			this.event('afterUpdate');
			
			// after update event callbacks			
			if(isfirst) { 
				this.event('afterFirst'); 
			} else if(islast) { 
				if(this.options.loop > 0) { this.loopnum++; }
				this.event('afterLast'); 
			}
			
			return true;
		}
		return false;
	},
	
	/*** controlled playback ***/

	isReady: function() {
		// use event binding to prevent auto-play
		return true; 
	},

	ready: function() {
		this._loop(); // call when ready
		this.locked = false;
		this.event('whenReady');
	},

	/*** auto playback ***/

	loop: function() {					
		// apply incremental changes	
		this.render();	
		// finish if end of loop reached
		if(this.options.loop > 0 && this.loopnum >= this.options.loop) {
			if(this.options.frontback) { // go one step further, once
				this.timeout = setTimeout(this.render.bind(this), (this.options.duration * 1000));
			} else { // cancel all loops
				this.cancel();
			}
			this.event('beforeFinish');
			if(this.finish) this.finish(); 
			this.event('afterFinish');
			return;
		}
		// continue loop		
		if(this.isReady()) { this._loop(); }
	},
	
	_loop: function() {
		if(this.state == 'running') this.timeout = setTimeout(this.loop.bind(this), (this.options.duration * 1000));
	},

	render: function() {
		if(this.state == 'idle') {
			this.state = 'running';
			this.event('beforeSetup');
			if(this.setup) this.setup();
			this.event('afterSetup');
		}
		if(this.options.playmode == 'fw') { this.next(true); } else { this.prev(true); }
	},
	
	reset: function() {
		this.cancel();
		this.current = { index: 0, value: false };
	},
	
	cancel: function() {	
		if(this.timeout) clearTimeout(this.timeout);
		this.state = arguments[0] || 'idle';
		this.locked = false;
		this.loopnum = 0;
		this.event('afterCancel');
	},
	
	isFirst: function() {
		return !(this.current.index > 0);
	},
	
	isLast: function() {
		return !(this.current.index < this.values.length - 1);
	},
	
	event: function(eventName) {
		if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
		if(this.options[eventName]) this.options[eventName](this);
	},
	
	/*** interfaces to implement ***/
	
	update: function(index, value) {}
	
};

Controller.Basic = Class.create();
Object.extend(Object.extend(Controller.Basic.prototype, Controller.Base.prototype), {
	
	action: false, // the action to perform
	
	initialize: function(values, action) {
		this.setAction(action);
		this.setValues(values);
		this.setOptions(arguments[2]);
		this.event('initialized');
	},
		
	setAction: function(action) {
		this.action = action;
		this.action.setController(this); // reference
	},
		
	event: function(eventName) {
		// propagate event on action
		this.action.event(eventName);
		// controller events
		if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
		if(this.options[eventName]) this.options[eventName](this);		
	},
			
	/*** controlled playback ***/

	waitsForAction: function() {
		return Var.is_function(this.action.ready);
	},

	isReady: function() {
		// the 'ready' method is mandatory for non-auto-playback actions
		// examples: waiting for images to load or effects to complete
		return !this.waitsForAction();
	},
	
	/*** implemented interface ***/
	
	update: function(index, value) {
		this.action.update(index, value);
	}
	
});

Controller.PositionedImageGroups = Class.create();
Object.extend(Object.extend(Controller.PositionedImageGroups.prototype, Controller.Basic.prototype), {

	// only works for actions having a getPosition method

	setValues: function(values) {
		if(Var.is_array(values)) {			
			var self = this;
			if(Var.is_function(self.action.getPosition)) {
				this.values = $A(values).inject([], function(groups, images, index) {					
					groups[index] = $A(self.action.elements).inject([], function(srcs, elem, i) {
						if(images[i]) {
							srcs[i] = Object.extend({ idx: i, position: self.action.getPosition(images[i].type) }, images[i]);
						} else {
							srcs[i] = { idx: i, src: images[0].src, clear: true };
						}
						return srcs;
					});
					return groups;
				});
			} else {
				this.values = values;
			}
			if(arguments[1]) { this.reset(); }		
			this.current.value = this.values[this.current.index];
		}
	}

});

Controller.Action = {};

Controller.Action.Base = Class.create();
Controller.Action.Base.prototype = {
	
	controller: false,
	parent: false,
	options: {},
	
	initialize: function() {
		this.setOptions(arguments[0] || {});
	},

	setOptions: function(options) {
		Object.extend(this.options, options || {});
	},
	
	setController: function(controller) {
		this.controller = controller;
	},
	
	event: function(eventName) {
		if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
		if(this.options[eventName]) this.options[eventName](this);
	},
	
	/*** interfaces to implement ***/
	
	update: function(index, value) { }
	
};

Controller.Action.Null = Class.create();
Object.extend(Object.extend(Controller.Action.Null.prototype, Controller.Action.Base.prototype), {
	
	update: function(index, value) { }

});

Controller.Action.Alert = Class.create();
Object.extend(Object.extend(Controller.Action.Alert.prototype, Controller.Action.Base.prototype), {
	
	update: function(index, value) { alert(value); }

});

Controller.Action.Console = Class.create();
Object.extend(Object.extend(Controller.Action.Console.prototype, Controller.Action.Base.prototype), {
	
	update: function(index, value) { window.console.log(Var.serialize(value)); }

});

Controller.Action.innerHTML = Class.create();
Object.extend(Object.extend(Controller.Action.innerHTML.prototype, Controller.Action.Base.prototype), {
	
	element: null,
	
	initialize: function(elem) { this.element = $(elem); this.event('initialized'); },
	
	update: function(index, value) { this.element.innerHTML = value; }

});

Controller.Action.Stack = Class.create();
Object.extend(Object.extend(Controller.Action.Stack.prototype, Controller.Action.Base.prototype), {
	
	actions: false, // the action to perform
	
	initialize: function(actions) {	
		this.setOptions(arguments[1] || {});
		this.actions = actions;
		// attach reference to stack holder (parent)
		var ref = this;
		this.actions.each( function(action) { action.parent = ref; });
	},
	
	setController: function(controller) {
		this.controller = controller;
		var ref = this;
		this.actions.each( function(action) { action.controller = ref.controller; } );
	},
	
	event: function(eventName) {
		// action stack events
		this.actions.each( function(action) { action.event(eventName); } );
		// action events
		if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
		if(this.options[eventName]) this.options[eventName](this);		
	},
	
	/*** implemented interfaces ***/
	
	update: function(index, value) {
		this.actions.each( function(action) { action.update(index, value); } );
	}
	
});


Controller.Action.ImageBase = Class.create();
Object.extend(Object.extend(Controller.Action.ImageBase.prototype, Controller.Action.Base.prototype), {
	
	image: null,
	
	setOptions: function(options) {
		this.options = Object.extend({
			direction: 'n',
			duration: 1.0,
			delay: 0.0,
			transition: Effect.Transitions.sinoidal,
			overflow: false
		}, options || {});
		if(this.base_element) { this.element = this.base_element; } // reference
		if(!this.options.overflow && this.element) this.element.style.overflow = 'hidden';
		if(!this.options.overflow && this.layer_element) this.layer_element.style.overflow = 'hidden';
		this.effect_options = this._prepareEffectOptions();		
	},
	
	update: function(index, value) { 
		// wait until the image has loaded, use callback
		// to trigger controller step
		this.image = new Image();
		this.image.onload = this.ready.bind(this);
		this.image.onerror = this.event.bind(this, 'loadError');
		if(Var.is_string(value)) {
			// simple array value passed
			this.image.src = value;
		} else if(Var.is_object(value)
			&& Var.is_string(value.src)) {
			// object (literal) passed, use src property
			this.image.src = value.src;
		} else if(Var.is_object(value)
			&& Var.is_string(value.path)) {
			// object literal passed, use path property
			this.image.src = value.path;
		} else {
			this.controller.cancel();
		}
	},
		
	get_direction: function() {
		// handle horizontal/vertical movement (prev/next)
		if(this.options.direction == 'horizontal') { 
			if(this.controller.playdir == 'bw') {
				var direction = 'left';
			} else {
				var direction = 'right';
			}
		} else if(this.options.direction == 'vertical') {
			if(this.controller.playdir == 'bw') {
				var direction = 'up';
			} else {
				var direction = 'down';
			}
		} else if(this.options.direction == 'random') {
			var dirs = ['n', 'e', 's', 'w'];
			var direction = dirs[Math.floor(Math.random() * 4)];
		} else {
			var direction = this.options.direction || 'horizontal';
		}	
		return direction;
	},
		
	_prepareEffectOptions: function() {		
		// use a clone for options
		var options = Var.clone(this.options);
		
		// two-step effect: duration in half
		options.duration = (options.duration / 2);
		
		// don't pass eventhandlers
		options.beforeStart = null;			
		options.beforeSetup = null;
		options.afterSetup = null;
		options.beforeUpdate = null;
		options.afterUpdate = null;
		options.beforeFinish = null;	
		options.afterFinish = null;
		
		return options;
	}

});


Controller.Action.SlideCssBackground = Class.create();
Object.extend(Object.extend(Controller.Action.SlideCssBackground.prototype, Controller.Action.ImageBase.prototype), {

	element: null,
	
	initialize: function(elem) {
		this.element = $(elem);
		this.setOptions(arguments[1] || {});
		this.event('initialized');
	},
	
	ready: function() {	
		// bring vars into lexical scope
		var controller = this.controller; 
		var imgsrc = this.image.src;
		var dir = this.get_direction();
		
		// move image out of view
		var eff = new Effect.SlideBackgroundImage(this.element, Object.extend(this.effect_options, {
			fade: 'out', direction: dir,
			afterFinish: function(effect) {
				// assign new source
				effect.element.style.backgroundImage = 'url(' + imgsrc + ')';
				// move image back in
				new Effect.SlideBackgroundImage(effect.element, Object.extend(effect.options, { 
					fade: 'in',	delay: 0.0,	direction: dir,				
					afterFinish: function(effect) {
						// call controller to iterate next
						controller.ready();
					}
				}));						
			}
		}));
	}
	
});

Controller.Action.SlideCssBackgroundReveal = Class.create();
Object.extend(Object.extend(Controller.Action.SlideCssBackgroundReveal.prototype, Controller.Action.SlideCssBackground.prototype), {
		
	initialize: function(elem) {
		this.base_element = $(elem);						
		if(arguments[3]) {
			var layer_id = arguments[3];
		} else if(this.base_element['id']) {
			var layer_id = this.base_element['id'] + '_layer';
		} else {
			var layer_id = '__reveal_layer__';
		}
		this.layer_element = $(layer_id);
		if(this.layer_element == undefined) {
			var layer = document.createElement(arguments[2] || 'div');
			layer.id = layer_id;
			layer.style.width = '100%'; layer.style.height = '100%';
			layer.style.backgroundPosition = this.base_element.backgroundPosition || '0 0';
			layer.style.backgroundRepeat = this.base_element.backgroundRepeat || 'no-repeat';
			this.layer_element = this.base_element.appendChild(layer);
		}				
		this.setOptions(arguments[1] || {});
		this.event('initialized');
	},
	
	ready: function() {	
		// bring vars into lexical scope
		var controller = this.controller; 
		var imgsrc = this.image.src;
		var layer_element = this.layer_element;
		var base_element = this.base_element;
		var options = this.effect_options;
		var dir = this.get_direction();
	
		var eff = new Effect.SlideBackgroundImage(layer_element, { direction: dir,
			fade: 'in', duration: 0.0, delay: 0.0, transition: Effect.Transitions.full,			
			afterFinish: function(effect) {
				base_element.style.backgroundImage = 'url(' + imgsrc + ')';				
				new Effect.SlideBackgroundImage(layer_element, Object.extend(options, {
					fade: 'out', direction: dir,
					afterFinish: function(effect) {
						// assign new source
						layer_element.style.backgroundPosition = base_element.style.backgroundPosition; // reset
						layer_element.style.backgroundImage = 'url(' + imgsrc + ')';
						controller.ready();
					}
				}));
		}});
	}
	
});

Controller.Action.SlideCssBackgroundCover = Class.create();
Object.extend(Object.extend(Controller.Action.SlideCssBackgroundCover.prototype, Controller.Action.SlideCssBackground.prototype), {
		
	initialize: function(elem) {
		this.base_element = $(elem);				
		if(arguments[3]) {
			var layer_id = arguments[3];
		} else if(this.base_element['id']) {
			var layer_id = this.base_element['id'] + '_layer';
		} else {
			var layer_id = 'cover_layer';
		}
		this.layer_element = $(layer_id);
		if(this.layer_element == undefined) {
			var layer = document.createElement(arguments[2] || 'div');
			layer.id = layer_id;
			layer.style.width = '100%'; layer.style.height = '100%';
			layer.style.backgroundPosition = this.base_element.backgroundPosition || '0 0';
			layer.style.backgroundRepeat = this.base_element.backgroundRepeat || 'no-repeat';
			this.layer_element = this.base_element.appendChild(layer);
		}		
		this.setOptions(arguments[1] || {});
		this.event('initialized');
	},
	
	ready: function() {	
		// bring vars into lexical scope
		var controller = this.controller; 
		var imgsrc = this.image.src;
		var layer_element = this.layer_element;
		var base_element = this.base_element;
		var options = this.effect_options;
		var dir = this.get_direction();
		
		var eff = new Effect.SlideBackgroundImage(layer_element, { direction: dir,
			fade: 'out', duration: 0.0, delay: 0.0, transition: Effect.Transitions.full,			
			afterFinish: function(effect) {
				layer_element.style.backgroundImage = 'url(' + imgsrc + ')';				
				new Effect.SlideBackgroundImage(layer_element, Object.extend(options, {
					fade: 'in', direction: dir,
					afterFinish: function(effect) {
						// assign new source
						layer_element.style.backgroundPosition = base_element.style.backgroundPosition; // reset
						base_element.style.backgroundImage = 'url(' + imgsrc + ')';
						controller.ready();
					}
				}));
		}});
	}
	
});

Controller.Action.CrossFade = Class.create();
Object.extend(Object.extend(Controller.Action.CrossFade.prototype, Controller.Action.ImageBase.prototype), {
	
	layer_element: null, base_element: null,
		
	initialize: function(elem) {
		this.base_element = $(elem);				
		if(arguments[3]) {
			var layer_id = arguments[3];
		} else if(this.base_element['id']) {
			var layer_id = this.base_element['id'] + '_layer';
		} else {
			var layer_id = 'cover_layer';
		}
		this.layer_element = $(layer_id);
		if(this.layer_element == undefined) {
			var layer = document.createElement(arguments[2] || 'div');
			layer.id = layer_id;
			layer.style.width = '100%'; layer.style.height = '100%';
			layer.style.backgroundPosition = this.base_element.backgroundPosition || '0 0';
			layer.style.backgroundRepeat = this.base_element.backgroundRepeat || 'no-repeat';
			this.layer_element = this.base_element.appendChild(layer);
		}	
		this.setOptions(arguments[1] || {});
		this.event('initialized');
	},
	
	ready: function() {	
		// bring vars into lexical scope
		var controller = this.controller; 
		var imgsrc = this.image.src;
		var layer_element = this.layer_element;
		
		// swap background/static image
		this.base_element.style.backgroundImage = 'url(' + imgsrc + ')';		
		
		// change opacity
		var eff = new Effect.Opacity(this.layer_element, Object.extend(this.effect_options, {
			from: 1.0, to: 0.0,						
			afterFinish: function(effect) {				
				// assign new source
				layer_element.style.backgroundImage = 'url(' + imgsrc + ')';
				Element.setOpacity(layer_element, 1.0); // reset
				controller.ready();						
			}
		}));
	}
	
});

