ns.panorama = {};

var Panorama = Class.create(Toolkit,
{
	// Settings
	queue_autoplay:             null,
	queue_load_priority:        null,
	queue_default_payload_type: null,
	queue_use_thumbnails:       null,
	
	transition_delay:    null,
	transition_duration: null,
	transition_path:     null,
	transition_default_rendering_priority: null,
	
	// Assets
	container:  null,
	stack:      null,
	transition: null,
	
	// State
	class_name:        'Panorama',
	full_screen:       null,
	display_initiated: null,
	active_bundle:     null,
	autoplay_timer:    null,
	
	// Event list
	TRANSITION_HAS_LOADED:      'PANORAMA:transition_has_loaded',
	STACK_HAS_RECEIVED_CONTENT: 'PANORAMA:stack_has_received_content',
	READY:                      'PANORAMA:ready_to_display',
	DISPLAY_INITIATED:          'PANORAMA:display_initiated',
	COMPLETE:                   'PANORAMA:loading_complete',
	
	BUNDLE_WILL_TRANSITION_IN: 'PANDORA:bundle_will_transition_in',
	BUNDLE_HAS_TRANSITIONED_IN: 'PANDORA:bundle_has_transitioned_in',
	BUNDLE_WILL_TRANSITION_OUT: 'PANDORA:bundle_will_transition_out',
	BUNDLE_HAS_TRANSITIONED_OUT: 'PANDORA:bundle_has_transitioned_out',
	
	// Constructor
	initialize: function ($super, target, queue, parameters)
	{
		var queue_params, transition_params, container_params, default_transition;
		
		$super(target, queue, parameters);
    
    parameters = $H(parameters);
		
		if (typeof target == 'undefined') throw ('Panorama Exception: Failed to load, no target container provided.');
		else this.container = $(target);
		
		// Optional parameters
		queue_params = parameters.get('queue') || {};
		this.queue_autoplay = (typeof queue_params.autoplay !== 'undefined')? queue_params.autoplay : true;
		this.queue_load_priority = (typeof queue_params.load_priority !== 'undefined')? queue_params.load_priority.toUpperCase() : 'FIRST-FILE';
		this.queue_default_payload_type = (typeof queue_params.default_payload_type !== 'undefined')? queue_params.default_payload_type.toUpperCase() : 'IMAGE';
		this.queue_use_thumbnails = (typeof queue_params.use_thumbnails !== 'undefined')? queue_params.use_thumbnails : false;
    
    if(queue_params.shuffle)
    {
      var array = queue;
			for(var j, x, i = array.length; i; j = parseInt(Math.random() * i), x = array[--i], array[i] = array[j], array[j] = x);	//Fisher-Yates shuffle algorithm (jsfromhell.com/array/shuffle)
			queue = array;
    }
		
		transition_params = parameters.get('transition') || {};
		this.transition_delay = (typeof transition_params.delay !== 'undefined')? transition_params.delay : 10;
		this.transition_duration = (typeof transition_params.duration !== 'undefined')? transition_params.duration : 1;
		this.transition_path = (typeof transition_params.path !== 'undefined')? (transition_params.path.match(/\/$/)? transition_params.path: transition_params.path + '/'): '/js/lib/toolkit/panorama/transitions/';
		this.transition_default_rendering_priority = (typeof transition_params.default_rendering_priority !== 'undefined')? transition_params.default_rendering_priority.toUpperCase(): 'STANDARD';
		
		container_params = parameters.get('container') || {};
		this.full_screen = (typeof container_params.full_screen !== 'undefined')? container_params.full_screen : true;

		// Init listeners
		this.setListener(this.notifyStack.bind(this), 'notifyStack');
    this.setListener(function(event) { this.positionAndScaleBundle(event.memo.bundle); }.bind(this), 'positionAndScaleBundle');
		this.setListener(this.checkInstanceReadiness.bind(this), 'checkInstanceReadiness');
		this.setListener(this.checkAssetAquisitionComplete.bind(this), 'checkAssetAquisitionComplete');
		this.setListener(this.initDisplay.bind(this), 'initDisplay');
		this.setListener(this.initAutoplay.bind(this), 'initAutoplay');
		
		// Init the stack
		this.stack = [];
		
		// Fetch bundles and default transition
    default_transition = ((typeof transition_params.type !== 'undefined')? transition_params.type: 'Crossfade') + '.js';
    switch (this.queue_load_priority)
		{
			case 'FIRST-FILE':
				this.observe(this.TRANSITION_HAS_LOADED, this.getListener('checkInstanceReadiness'));
        
        if (!container_params.disable_bundle_resize) this.observe(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('positionAndScaleBundle'));
        this.observe(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('checkInstanceReadiness'));
				this.observe(this.READY, this.getListener('initDisplay'));
				if (this.queue_autoplay) this.observe(this.COMPLETE, this.getListener('initAutoplay'));
				
				this.stack.push(this.loadBundle(queue.shift()));
				this.stack.first().observe(this.stack.first().READY, this.setListener(function (event, queue)
				{
					this.loadBundlesFromQueue(queue);
				}.bindAsEventListener(this, queue), 'firstFileLoadHandler'));
				break;
			case 'FIRST-LOADED':
				this.observe(this.TRANSITION_HAS_LOADED, this.getListener('checkInstanceReadiness'));
        
        if (!container_params.disable_bundle_resize) this.observe(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('positionAndScaleBundle'));
        this.observe(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('checkInstanceReadiness'));
				this.observe(this.READY, this.getListener('initDisplay'));
				if (this.queue_autoplay) this.observe(this.COMPLETE, this.getListener('initAutoplay'));
				
				this.loadBundlesFromQueue(queue);
				break;
			case 'FULL':
				this.observe(this.TRANSITION_HAS_LOADED, this.getListener('checkAssetAquisitionComplete'));
        
        if (!container_params.disable_bundle_resize) this.observe(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('positionAndScaleBundle'));
        this.observe(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('checkAssetAquisitionComplete'));
				this.observe(this.COMPLETE, this.getListener('initDisplay'));
				if (this.queue_autoplay) this.observe(this.DISPLAY_INITIATED, this.getListener('initAutoplay'));
				
				this.loadBundlesFromQueue(queue);
				break;
			default:
				throw('Panorama | initialize | Invalid parameter: load priority value not recognised, must be one of FIRST-FILE, FIRST-LOADED or FULL.');
		}
		this.loadTransitionFromURI(this.transition_path + default_transition, this.identifier, this.transition_duration);
    
		// Special case listener for window resize event
		Event.observe(window, 'resize', this.positionAndScaleContainer.bindAsEventListener(this));
		if (!container_params.disable_bundle_resize) Event.observe(window, 'resize', this.positionAndScaleStack.bindAsEventListener(this));
		
		return this;
	},
	
	loadBundlesFromQueue: function (queue)
	{
		this.stack = this.stack.concat(queue.map(this.loadBundle.bind(this)));
	},
	
	loadBundle: function (parameters)
	{
		var bundle;
    
		if (typeof parameters.payload_type === 'undefined') parameters.payload_type = this.queue_default_payload_type;
		if (typeof parameters.use_thumbnails === 'undefined') parameters.use_thumbnails = this.queue_use_thumbnails;
		if (typeof parameters.rendering_priority === 'undefined') parameters.rendering_priority = this.transition_default_rendering_priority;
		
		parameters.transition_scope = this.identifier;
		parameters.transition_duration = this.transition_duration;
		
		bundle = new ns.panorama.Bundle(parameters);
		bundle.observe(bundle.READY, this.getListener('notifyStack'));
		return bundle;
	},
	
	loadTransitionFromURI: function (URI, scope, duration)
	{
		// Due to the fact that AJAX loaded js runs within the scope of the AJAX request instance transition must be attached manually by the loaded class,
		// in this case abusing the lack of private properties to set it directly.
		new Ajax.Request (URI, {method:'get', target:this, scope:scope, duration:duration});
	},
	
	notifyStack: function (event)
	{
		this.fire(this.STACK_HAS_RECEIVED_CONTENT, {'panorama':this, 'bundle':event.memo.bundle});
	},
	
	checkInstanceReadiness: function(event)
	{
		if (this.transition !== null && this.stack.find(this.bundleCanDisplay, this))
		{
			this.stopObserving(this.TRANSITION_HAS_LOADED, this.getListener('checkInstanceReadiness'));
			this.stopObserving(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('checkInstanceReadiness'));
			this.observe(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('checkAssetAquisitionComplete'));
			
			this.fire(this.READY, {'panorama':this});
		}
	},
	
	checkAssetAquisitionComplete: function (event)
	{
		if (this.transition !== null && this.stack.length === this.stack.findAll(this.bundleCanDisplay, this).length)
		{
			this.stopObserving(this.STACK_HAS_RECEIVED_CONTENT, this.getListener('checkAssetAquisitionComplete'));
			
			this.fire(this.COMPLETE, {'panorama':this});
		}
	},
	
	bundleCanDisplay: function (bundle)
	{
		return bundle.canDisplay();
	},
	
	positionAndScaleContainer: function (event)
	{
		if (!this.full_screen) return;
		
		this.container.setStyle
		({
			position:'fixed', top:0, left: 0,
			width: document.viewport.getWidth() + 'px',
			height: document.viewport.getHeight() + 'px'
		});
	},
	
	positionAndScaleStack: function (event)
	{
		this.stack.findAll(this.bundleCanDisplay, this).each(function (bundle) {this.positionAndScaleBundle(bundle);}.bind (this));
	},
	
	positionAndScaleBundle: function (bundle)
	{
		var target_dimensions = this.container.getDimensions(), target_offset = this.full_screen === true? this.container.cumulativeOffset(): {top:0, left:0};
		
    if(bundle.payload_type !== 'IMAGE')
    {
      bundle.getElement().setStyle({width:target_dimensions.width + 'px', height:target_dimensions.height + 'px'});
    }
		else
		{
			if (!bundle.aspect_ratio) bundle.aspect_ratio = bundle.getElement().width / bundle.getElement().height;
			if (target_dimensions.width / bundle.aspect_ratio < target_dimensions.height)
			{
				bundle.getElement().setStyle
				({
					'width':  (target_dimensions.height * bundle.aspect_ratio) + 'px',
					'height': target_dimensions.height + 'px'
				});
			}
			// Else, width
			else
			{
				bundle.getElement().setStyle
				({
					'width':  target_dimensions.width + 'px',
					'height': (target_dimensions.width / bundle.aspect_ratio) + 'px'
				});
			}
			
			if (bundle.getAnchorPoint().indexOf ('LEFT') >= 0) bundle.getElement().setStyle ({left:target_offset.left + 'px'});
			else if (bundle.getAnchorPoint().indexOf ('RIGHT') >= 0) bundle.getElement().setStyle ({left:(target_offset.left - (parseInt (bundle.getElement.getStyle ('width')) - target_dimensions.width)) + 'px'});
			else bundle.getElement().setStyle ({left:(target_offset.left - ((parseInt (bundle.getElement().getStyle ('width')) - target_dimensions.width) / 2)) + 'px'});
			if (bundle.getAnchorPoint().indexOf ('TOP') >= 0) bundle.getElement().setStyle ({top:target_offset.top + 'px'});
			else if (bundle.getAnchorPoint().indexOf ('BOTTOM') >= 0) bundle.getElement().setStyle ({top:(target_offset.top - (parseInt (bundle.getElement().getStyle ('height')) - target_dimensions.height)) + 'px'});
			else bundle.getElement().setStyle ({top:(target_offset.top - ((parseInt (bundle.getElement().getStyle ('height')) - target_dimensions.height) / 2)) + 'px'});
		}
	},
	
	initDisplay: function (event)
	{
		var transition;
		
		this.stopObserving(this.READY, this.getListener('initDisplay'));
		
		this.positionAndScaleContainer();
		
		this.display_initiated = new Date().getTime();
		
		this.active_bundle = this.stack.find(this.bundleCanDisplay, this);
		
		transition = (this.getActiveBundle().hasTransition())? this.getActiveBundle().getTransition(): this.transition;
		
		// The below abuses the fact that class events are using the document
		this.observe(transition.WILL_TRANSITION_IN, function (event) {this.fire(this.BUNDLE_WILL_TRANSITION_IN, {'panorama':this, 'bundle':event.memo.bundle});}.bind(this));
		this.observe(transition.HAS_TRANSITIONED_IN, function (event) {this.fire(this.BUNDLE_HAS_TRANSITIONED_IN, {'panorama':this, 'bundle':event.memo.bundle});}.bind(this));
		this.observe(transition.WILL_TRANSITION_OUT, function (event) {this.fire(this.BUNDLE_WILL_TRANSITION_OUT, {'panorama':this, 'bundle':event.memo.bundle});}.bind(this));
		this.observe(transition.HAS_TRANSITIONED_OUT, function (event) {this.fire(this.BUNDLE_HAS_TRANSITIONED_OUT, {'panorama':this, 'bundle':event.memo.bundle});}.bind(this));
		
    if (this.container.childElements().indexOf(this.getActiveBundle().getElement()) < 0)
    {
      this.container.insert({bottom:transition.prepareBundleForTransition(this.getActiveBundle()).getElement()});
      transition._in(this.getActiveBundle());
    }
    
		this.fire(this.DISPLAY_INITIATED, {'panorama':this});
	},
	
	initAutoplay: function (event)
	{
		var delta;
		
		this.stopObserving(this.READY, this.getListener('initAutoplay'));
		this.stopObserving(this.DISPLAY_INITIATED, this.getListener('initAutoplay'));
		
		if (this.stack.length < 2) return;
		
		delta = (new Date().getTime() - this.display_initiated) / 1000;
		
		if (delta >= this.transition_delay) this.play();
		else (function () {this.play();}.bind(this)).delay(this.transition_delay - delta);
	},
	
	canAnimate: function ()
	{
		return (this.container.childElements().length === 1)? true: false;
	},
	
	play: function (inbound, ignore_timeout)
	{
		var transition;
		
		window.clearTimeout (this.autoplay_timer);
		
		if (typeof inbound === 'undefined') inbound = this.getNextBundle();
		
		if (inbound === this.getActiveBundle() || !this.canAnimate()) return;
		
		transition = (inbound.hasTransition())? inbound.getTransition(): this.transition;
		
		this.container.insert({bottom:transition.prepareBundleForTransition(inbound).getElement()});
		transition._out(this.getActiveBundle());
		this.active_bundle = transition._in(inbound);
		
		if (!ignore_timeout) this.autoplay_timer = (function () {this.play();}.bind(this)).delay(this.transition_delay);
	},
	
	pause: function ()
	{
    window.clearTimeout (this.autoplay_timer);
	},
	
	getNextBundle: function ()
	{
		return (this.stack.indexOf (this.active_bundle) + 1 < this.stack.length)? this.stack[this.stack.indexOf (this.active_bundle) + 1]: this.stack.first();
	},
	
	next: function (ignore_timeout)
	{
		window.clearTimeout(this.autoplay_timer);
		this.play(this.getNextPackage(), ignore_timeout);
	},
	
	getPreviousBundle: function ()
	{
		return (this.stack.indexOf (this.active_bundle) - 1 >= 0)? this.stack [this.stack.indexOf (this.active_bundle) - 1]: this.stack.last();
	},
	
	previous: function (ignore_timeout)
	{
		window.clearTimeout(this.autoplay_timer);
		this.play(this.getPreviousPackage(), ignore_timeout);
	},
	
	getActiveBundle: function ()
	{
		return this.active_bundle;
	},
	
	getActiveIndex: function ()
	{
		return this.stack.indexOf (this.active_bundle);
	},
  
  getBundleAtIndex: function (index)
  {
    return this.stack[index];
  },
	
	getStackLength: function ()
	{
		return this.stack.length;
	}
});

// Bundle (payload) class
ns.panorama.Bundle = Class.create(Toolkit,
{
	// Settings / properties
	payload_type:        null,
	rendering_priority:  null,
	selector:            null,
	anchor_point:        null,
	aspect_ratio:        null,
	has_transition:      null,
	has_thumbnail:       null,
	parameters:          null,
	
	// Assets
	element:    null,
	transition: null,
	thumbnail:  null,
	
	// State
	class_name:  'Bundle',
	can_display: null,
	
	// 'Static' event handles
	TRANSITION_HAS_LOADED: 'PANORAMA_BUNDLE:transition_has_loaded',
	ASSET_HAS_LOADED:      'PANORAMA_BUNDLE:asset_has_loaded',
	READY:                 'PANORAMA_BUNDLE:ready',
	
	// Constructor
	initialize: function ($super, parameters)
	{
		$super(parameters);
		
		// Parameters object MUST exist
		if (typeof parameters !== 'object') throw('ns.panorama.Bundle | initialize | Invalid parameter: parameters object was expected but not passed in.');
		else this.parameters = $H(parameters);
		
		// Required parameters
		if (!this.parameters.get('payload')) throw('ns.panorama.Bundle | initialize | Invalid parameter: payload was expected but not passed in.');
		
		// Optional proptery parameters
		this.payload_type = (this.parameters.unset('payload_type') || 'IMAGE').toUpperCase();
		this.rendering_priority = (this.parameters.unset('rendering_priority') || 'STANDARD').toUpperCase();
		this.selector = this.parameters.unset('selector') || '.bundle-element';
		this.anchor_point = this.parameters.unset('anchor_point') || ['CENTRE','CENTRE'];
		
		// Setup event listeners
		this.observe(this.ASSET_HAS_LOADED, this.setListener(this.checkAssetStackStatus.bindAsEventListener(this), 'checkAssetStackStatus'));
		
		// Set state
		this.can_display = false;
		
		// Load assets
		this.fetchPayload(this.parameters.get('payload'));
		if (this.parameters.get('transition'))
		{
			this.has_transition = true;
			this.observe(this.TRANSITION_HAS_LOADED, this.getListener('checkAssetStackStatus'));
			this.fetchTransitionFromURI(this.parameters.get('transition'), this.parameters.unset('transition_scope'), this.parameters.unset('transition_duration'));
		}
		if (this.parameters.get('use_thumbnails'))
		{
			this.has_thumbnail = true;
			this.fetchOrGenerateThumbnail(this.parameters.get('thumbnail'));
		}
	},
	
	fetchPayload: function (payload)
	{
		switch (this.payload_type)
		{
			case 'IMAGE':
				this.fetchImageFromURI(payload);
				break;
			case 'HTML':
				this.fetchHTMLFromURI(payload);
				break;
			case 'DOM':
				this.fetchElementFromDOM(payload);
				break;
			default:
				throw ('ns.panorama.Bundle | fetchPayload | Invalid parameter: payload_type not recognised. If passed in payload_type must be one of IMAGE (default), HTML or DOM.');
				break;
		}
	},
	
	fetchImageFromURI: function (URI, is_thumbnail)
	{
		var image = $(new Image());

		image.src = URI;
		image.addClassName (this.getClassNameFromSelector(this.selector) + (is_thumbnail? '-thumbnail': ''));
		
		switch (this.rendering_priority)
		{
			case 'PERFORMANCE':
				try {image.style.msInterpolationMode = 'nearest-neighbor';} catch (e) {}
				image.style.imageRendering = '-moz-crisp-edges';
				break;
			case 'QUALITY':
				try {image.style.msInterpolationMode = 'bicubic';} catch (e) {}
				image.style.imageRendering = 'optimizeQuality';
				break;
			case 'STANDARD':
				break;
			default:
				throw ('ns.panorama.Bundle | fetchImageFromURI | Invalid parameter: rendering_priority not recognised. If passed in rendering_priority must be one of STANDARD (default), PERFORMANCE or QUALITY.');
				break;
		}
		
		if (image.complete) (function () {this.affixLoadedImageAsset(image, is_thumbnail);}.bind(this)).delay(0.1);
		else image.observe('load', this.affixLoadedImageAsset.bindAsEventListener (this, is_thumbnail));
	},
	
	affixLoadedImageAsset: function (image, is_thumbnail)
	{
		if (typeof image.target !== 'undefined') image = image.target;
		
		if (!is_thumbnail)
		{
			this.element = image;
			this.element.setStyle({'position':'absolute'});
			this.aspect_ratio = this.element.width / this.element.height;
		}
		else
		{
			this.thumbnail = image;
		}
		this.fire(this.ASSET_HAS_LOADED, {bundle:this});
	},
	
	fetchHTMLFromURI: function (URI)
	{
		// This could be modified to use an iframe, relying on the source's dom:loaded/window.onload event which would enable more control
		// over the loaded status of the payload, however it does require that the payload has to have additional 
		new Ajax.Request(URI,
		{
			method: 'get',
			onSuccess: function (transport)
			{
				this.element = new Element('div').insert(transport.responseText).down().remove();
				this.element.setStyle ({position:'absolute'}).addClassName (this.getClassNameFromSelector(this.selector));
				this.fire(this.ASSET_HAS_LOADED, {bundle:this});
			}.bind(this)
		});
	},
	
	fetchElementFromDOM: function(element)
	{
		this.element = $(element);
		if (!this.element.visible()) this.element.remove();
    this.element.setStyle({'position':'absolute'});
		// Requires an artificial delay in order to give external classes the change to observe
		(function () {this.fire(this.ASSET_HAS_LOADED, {bundle:this});}.bind(this)).delay(0.1);
	},
	
	fetchTransitionFromURI: function (URI, scope, duration)
	{
		// Due to the fact that AJAX loaded js runs within the scope of the AJAX request instance transition must be attached manually by the loaded class,
		// in this case abusing the lack of private properties to set it directly.
		new Ajax.Request (URI, {method:'get', target:this, scope:scope, duration:duration});
	},
	
	fetchOrGenerateThumbnail: function (URI_or_object)
	{
		if (typeof path_or_object === 'object') this.thumbnail = URI_or_object;
		else this.thumbnail = this.fetchImageFromURI(URI_or_object);
	},
	
	checkAssetStackStatus: function (event)
	{
		var bundle_has_loaded = true;
		
		// Payload still in transit
		if (this.payload === null) bundle_has_loaded = false;
		if (this.has_transition && this.transition === null) bundle_has_loaded = false;
		if (this.has_thumbnail && this.thumbnail === null) bundle_has_loaded = false;
		
		if (bundle_has_loaded)
		{
			this.can_display = true;
			this.fire(this.READY, {bundle:this});
		}
	},
	
	// Public methods
	canDisplay: function ()
	{
		return this.can_display;
	},
	
	getElement: function()
	{
		return this.element;
	},
	
	getAnchorPoint: function ()
	{
		return this.anchor_point;
	},
	
	hasTransition: function ()
	{
		return this.transition !== null;
	},
	
	getTransition: function ()
	{
		return this.transition;
	},
	
	getThumbnail: function ()
	{
		return this.thumbnail;
	},
	
	getParameter: function(parameter)
	{
		return this.parameters.get(parameter);
	},
	
	setParameter: function(parameter, value)
	{
	  this.parameters.set(parameter, value);
	}
});

// Transition base class
ns.panorama.Transition = Class.create(Toolkit,
{
	identifier: null,
	duration:   null,
	
	WILL_TRANSITION_IN:   'PANORAMA_TRANSITION:will_transition_in',
	HAS_TRANSITIONED_IN:  'PANORAMA_TRANSITION:has_transitioned_in',
	WILL_TRANSITION_OUT:  'PANORAMA_TRANSITION:will_transition_out',
	HAS_TRANSITIONED_OUT: 'PANORAMA_TRANSITION:has_transitioned_out',
	
	initialize: function (scope, duration)
	{
		this.identifier = scope;
		this.duration = duration;
		
		return this;
	},
	
	prepareBundleForTransition: function (bundle)
	{
		bundle.getElement().setStyle({'display':'none'});
		return bundle;
	}
});
