var Panorama = Class.create
(Toolkit, {
  version:2.0,

	// 'Static' event properties (shown in most likely event sequence)
	TRANSITION_HAS_LOADED:        'PANORAMA:transition_has_loaded',
	PACKAGE_PAYLOAD_HAS_LOADED:   'PANORAMA:package_payload_has_loaded',
	PACKAGE_HAS_LOADED:           'PANORAMA:package_has_loaded',
	STACK_HAS_CONTENT:            'PANORAMA:stack_has_content',
	STACK_HAS_LOADED:             'PANORAMA:stack_has_loaded',
	INSTANCE_IS_READY:            'PANORAMA:instance_is_ready',
	ELEMENT_WILL_TRANSITION_IN:   'PANORAMA:element_will_transition_in',
	ELEMENT_HAS_TRANSITIONED_IN:  'PANORAMA:element_has_transitioned_in',
	ELEMENT_WILL_TRANSITION_OUT:  'PANORAMA:element_will_transition_out',
	ELEMENT_HAS_TRANSITIONED_OUT: 'PANORAMA:element_has_transitioned_out',

	// Required parameters
	container: null,

	// Queue
	queue_autoplay:               true,
	queue_shuffle:                false,
	queue_quick_start_method:     'FIRST_FILE',
	queue_payload_is_html:  false,
	queue_use_thumbnails:         false,
	queue_use_package_transitions:false,

	// Transition
	transitions_path:              '/js/lib/toolkit/panorama/transitions/',
	transitions_default:           'Crossfade',
	transitions_delay:             10.00,
	transitions_duration:          1.00,
	transitions_rendering_priority:'STANDARD',

	// Container
	container_full_screen:         true,
	container_default_anchor:      ['top', 'centre'],
	container_element_indentifier: 'panorama-element',

	// 'Private' properties
	loading_display:         null,
	stack:                   null,
	stack_complete:          null,
	default_transition:      null,
	transition_timer:        null,
	first_element_displayed: null,
	active_package:          null,

  // Constructor
	initialize: function ($super, container, package_queue, parameters)
	{
		$super(container, package_queue, parameters);

		// Alias the target container (with explicit extension of element)
		if (typeof container == 'undefined') throw ('Panorama Exception: Failed to load, no target container provided.');
		else this.container = $(container);

		// Capture the file queue
		if (typeof package_queue == 'undefined') throw('Panorama Exception: Failed to load, package queue not provided.');
		else this.stack = package_queue;

		// Treat the first element found in the container as a 'loading...' display, remove everything else
		this.container.childElements().each(function(element, i) { if(i == 0) { this.loading_display = element } else element.remove(); }.bind(this));

		// Parse optional parameters
		if (typeof parameters != 'undefined')
		{
			if (typeof parameters.queue != 'undefined')
			{
				if (typeof parameters.queue.autoplay != 'undefined')
				{
					this.queue_autoplay = parameters.queue.autoplay;
				}
				if (typeof parameters.queue.shuffle != 'undefined' && parameters.queue.shuffle)
				{
					var array = this.stack;
					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)
			    this.file_stack = array;
				}
				if (typeof parameters.queue.quick_start != 'undefined')
				{
					if (['FIRST_FILE','FIRST_LOADED','NONE'].indexOf (parameters.queue.quick_start.toUpperCase ()) >= 0) this.queue_quick_start_method = parameters.queue.quick_start.toUpperCase ();
					else throw ('Panorama Exception: Invalid parameter, if provided queue.quick_start must be one of first_file, first_loaded or none.');
				}
        if (typeof parameters.queue.payload_is_html != 'undefined') this.queue_payload_is_html = parameters.queue.payload_is_html;
        if (typeof parameters.queue.use_thumbnails != 'undefined') this.queue_use_thumbnails = parameters.queue.use_thumbnails;
        if (typeof parameters.queue.use_package_transitions != 'undefined') this.queue_use_package_transitions = parameters.queue.use_package_transitions;
			}

			if (typeof parameters.transitions != 'undefined')
			{
				if (typeof parameters.transitions.path != 'undefined') this.transitions_path = parameters.transitions.path.match(/\/$/)? parameters.transitions.path: parameters.transitions.path + '/';
				if (typeof parameters.transitions.default_transition != 'undefined') this.transitions_default = parameters.transitions.default_transition.toLowerCase().replace(/^([a-z])/, function(s, m) { return m.toUpperCase(); });
				if (typeof parameters.transitions.rendering_priority != 'undefined' && parameters.transitions.rendering_priority.toUpperCase() != 'STANDARD')
				{
					if (['QUALITY','PERFORMANCE'].indexOf(parameters.transitions.rendering_priority.toUpperCase ()) >= 0) this.transitions_rendering_priority = parameters.transitions.rendering_priority.toUpperCase ();
					else throw ('Panorama Exception: Invalid parameter, if provided rendering priority must be one of either Standard, Quality or Performance.');
				}
				if (typeof parameters.transitions.delay != 'undefined')
				{
					if (isNaN(parameters.transitions.delay)) throw('Panorama Exception: Invalid parameter, transitions.delay must be a valid number in seconds.');
					else this.transitions_delay = parameters.transitions.delay;
				}
				if (typeof parameters.transitions.duration != 'undefined')
				{
					if (isNaN(parameters.transitions.duration)) throw('Panorama Exception: Invalid parameter, transitions.duration must be a valid number in seconds.');
					else this.transitions_duration = parameters.transitions.duration;
				}
			}

			if (typeof parameters.container != 'undefined')
			{
				if (typeof parameters.container.full_screen != 'undefined' && !parameters.container.full_screen) this.container_full_screen = false;
				// if (!this.container_full_screen && ['fixed', 'relative'].indexOf(this.container.getStyle ('position').toLowerCase()) < 0) throw ('Panorama Exception: Failed to load, when not full screen target container must have position fixed or relative set.');
				this.container.setStyle ({ overflow:'hidden' });
				if (typeof parameters.container.default_anchor != 'undefined')
				{
					try
					{
						this.container_default_anchor.clear();
						parameters.container.default_anchor.each(function (anchor_point) { this.container_default_anchor.push(anchor_point); }.bind(this));
					}
					catch (e) { throw ('Panorama Exception: Invalid parameter, container.default_anchor must be an array with a length of 1 or 2.'); }
				}
			}
		}

		// Load the default transition file
		this.loadDefaultTransition();

		// Register bound event listeners
		this.setListener(this.canDisplay.bind(this), 'canDisplay');
		this.setListener(this.packageHasLoaded.bind(this), 'packageHasLoaded');
		this.setListener(this.notifyStack.bind(this), 'notifyStack');
		this.setListener(this.stackHasLoaded.bind(this), 'stackHasLoaded');
		this.setListener(this.playFirst.bindAsEventListener(this), 'playFirst');
		this.setListener(this.playEnable.bindAsEventListener(this), 'playEnable');
		this.setListener(this.positionAndScaleContainer.bindAsEventListener(this), 'positionAndScaleContainer');
		this.setListener(this.positionAndScaleStack.bindAsEventListener(this), 'positionAndScaleStack');
		this.setListener(this.positionAndScalePackage.bindAsEventListener(this), 'positionAndScalePackage');
		this.setListener(this.removeLoadingDisplay.bindAsEventListener(this), 'removeLoadingDisplay');

		// Setup event listeners
		this.observe(this.TRANSITION_HAS_LOADED, this.getListener('canDisplay'));
		this.observe(this.PACKAGE_PAYLOAD_HAS_LOADED, this.getListener('packageHasLoaded'));
    this.observe(this.PACKAGE_HAS_LOADED, this.getListener('positionAndScalePackage'));
    this.observe(this.PACKAGE_HAS_LOADED, this.getListener('notifyStack'));
		this.observe(this.PACKAGE_HAS_LOADED, this.getListener('stackHasLoaded'));

		if(this.queue_quick_start_method == 'FIRST_FILE') this.observe(this.STACK_HAS_CONTENT, this.getListener('canDisplay'));
		else this.observe(this.STACK_HAS_LOADED, this.getListener('canDisplay'));

    this.observe(this.INSTANCE_IS_READY, this.getListener('playFirst'));

		// Special case listener for window resize event
		if (this.container_full_screen) {
			Event.observe(window, 'resize', this.getListener('positionAndScaleContainer'));
			this.positionAndScaleContainer(null);
		}
		Event.observe(window, 'resize', this.getListener('positionAndScaleStack'));

		// Load queue
		if (this.queue_quick_start_method == 'FIRST_FILE') this.loadPackage(this.stack.first());
		else this.loadStack();
		
		return this;
	},

	loadDefaultTransition: function ()
	{
		new Ajax.Request (this.transitions_path + this.transitions_default + '.js',
		{
			method: 'get',
			panorama: this
		});
	},

	loadStack: function ()
	{
		this.stack.reject(this.packageCanDisplay, this).each(this.loadPackage.bind(this));
	},

	loadPackage: function (package)
	{
		if (!this.queue_payload_is_html) package.element = this.loadImage(package, false);
		else this.loadHTML(package);

		if (this.queue_use_thumbnails)
		{
			if (typeof package.thumbnail_source == 'undefined' && this.queue_payload_is_html) throw('Panorama exception: Cannot auto generate a thumbnail for an html payload.');
			else if (typeof package.thumbnail_source != 'undefined') package.thumbnail = this.loadImage(package, true);
			else package.thumbnail = package.element.cloneNode(true);
		}
		if (this.queue_use_package_transitions && typeof package.transition != 'undefined') this.loadPackageTransition(package);
	},

	loadPackageTransition: function (package)
	{
		new Ajax.Request (this.transitions_path + package.transition.toLowerCase().replace(/^([a-z])/, function(s, m) { return m.toUpperCase(); }) + '.js',
		{
			method: 'get',
			panorama: this,
			package: package
		});
	},

	loadImage: function (package, is_thumbnail)
	{
		var image = $(new Image());

		image.src = is_thumbnail? package.thumbnail_source: package.source;
		image.addClassName (this.container_element_indentifier + (is_thumbnail? '-thumbnail': ''));

		if(!is_thumbnail)
		{
			image.setStyle ({ position:'absolute' });
			if (this.transitions_rendering_priority != 'STANDARD')
			{
				if(this.transitions_rendering_priority == 'PERFORMANCE')
				{
					try { image.style.msInterpolationMode = 'nearest-neighbor'; } catch (e) {}
					image.style.imageRendering = '-moz-crisp-edges';
				}
				else
				{
					try { image.style.msInterpolationMode = 'bicubic'; } catch (e) {}
					image.style.imageRendering = 'optimizeQuality';
				}
			}
		}

		if(image.completed)
		{
			image.has_completed = true;
			this.fire(this.PACKAGE_PAYLOAD_HAS_LOADED, { panorama:this, package:package });
		}
		else
		{
			image.observe('load', function (event)
			{
				if (/MSIE/i.test (navigator.userAgent) && !/MSIE 9/i.test (navigator.userAgent)) event.target.has_completed = true;
				this.fire(this.PACKAGE_PAYLOAD_HAS_LOADED, { panorama:this, package:package });
			}.bind (this));
		}

    return image;
	},

	loadHTML: function (package)
	{
		new Ajax.Request(package.source,
		{
			method: 'get',
			onSuccess: function (package, transport)
			{
				package.element = new Element('div').update(transport.responseText).down().remove();
				package.element.setStyle ({ position:'absolute' }).addClassName (this.container_element_indentifier);
				this.fire(this.PACKAGE_PAYLOAD_HAS_LOADED, { panorama:this, package:package });
			}.bind(this, package)
		});
	},

	positionAndScaleContainer: function (event)
	{
		this.container.setStyle
		({
			position:'fixed', zIndex:0,
			width: document.viewport.getWidth() + 'px',
			height: document.viewport.getHeight() + 'px'
		});
	},

	positionAndScaleStack: function (event)
	{
		this.stack.findAll(this.packageCanDisplay, this).each(function (package) { this.positionAndScalePackage ({ memo:{ package:package }}); }.bind (this));
	},

	positionAndScalePackage: function (event)
	{
		var package = event.memo.package, target_dimensions = this.container.getDimensions(), target_offset = this.container.cumulativeOffset(), anchorage = (typeof package.anchor != 'undefined')? package.anchor: this.container_default_anchor;
		
		if (!this.queue_payload_is_html && typeof package.aspect_ratio == 'undefined') package.aspect_ratio = package.element.width / package.element.height;

		if(this.queue_payload_is_html) package.element.setStyle({ width:target_dimensions.width + 'px', height:target_dimensions.height + 'px' });
		else
		{
			if (target_dimensions.width / package.aspect_ratio < target_dimensions.height)
			{
				package.element.setStyle
				({
					'width':  (target_dimensions.height * package.aspect_ratio) + 'px',
					'height': target_dimensions.height + 'px'
				});
			}
			// Else, width
			else
			{
				package.element.setStyle
				({
					'width':  target_dimensions.width + 'px',
					'height': (target_dimensions.width / package.aspect_ratio) + 'px'
				});
			}

			if (anchorage.indexOf ('left') >= 0) package.element.setStyle ({ left:target_offset.left + 'px' });
			else if (anchorage.indexOf ('right') >= 0) package.element.setStyle ({ left:(target_offset.left - (parseInt (package.element.getStyle ('width')) - target_dimensions.width)) + 'px' });
			else package.element.setStyle ({ left:(target_offset.left - ((parseInt (package.element.getStyle ('width')) - target_dimensions.width) / 2)) + 'px' });
			if (anchorage.indexOf ('top') >= 0) package.element.setStyle ({ top:target_offset.top + 'px' });
			else if (anchorage.indexOf ('bottom') >= 0) package.element.setStyle ({ top:(target_offset.top - (parseInt (package.element.getStyle ('height')) - target_dimensions.height)) + 'px' });
			else package.element.setStyle ({ top:(target_offset.top - ((parseInt (package.element.getStyle ('height')) - target_dimensions.height) / 2)) + 'px' });
		}
	},

	packageHasLoaded: function (event)
	{
		var package = event.memo.package;

		if(this.packageCanDisplay(package) && !package.complete)
		{
			this.fire(this.PACKAGE_HAS_LOADED, { panorama:this, package:package });
			package.complete = true;
		}
	},

	packageCanDisplay: function (package)
	{
		if(typeof package.element == 'undefined') return false;
		if(this.queue_payload_is_html) return true;
		if((/MSIE/i.test (navigator.userAgent) && !/MSIE 9/i.test (navigator.userAgent)))
		{
			if(!package.element.has_completed) return false;
			if(this.queue_use_thumbnails)
			{
				if(typeof package.thumbnail === 'undefined') return false;
				if(!package.thumbnail.has_completed) return false;
			}
		}
		else
		{
			if(!package.element.complete) return false;
			if(this.queue_use_thumbnails && (typeof package.thumbnail === 'undefined' || !package.thumbnail.complete)) return false;
		}
		if(this.queue_use_package_transitions && typeof package.transition == 'string') return false;
		return true;
	},

	notifyStack: function (event)
	{
		this.fire(this.STACK_HAS_CONTENT, { panorama:this });
		this.stopObserving(this.PACKAGE_HAS_LOADED, this.getListener('notifyStack'));
	},

	stackHasContent: function ()
	{
		return typeof this.stack.find(this.packageCanDisplay, this) !== 'undefined';
	},

	stackHasLoaded: function (event)
	{
		if (this.stack.findAll(this.packageCanDisplay, this).length != this.stack.length) return false;
		this.stack_complete = true;
    this.fire(this.STACK_HAS_LOADED, { panorama:this });
    this.stopObserving(this.PACKAGE_HAS_LOADED, this.getListener('stackHasLoaded'));
		return this.stack_complete;
	},

	canDisplay: function (event)
	{
		// Default transition must have loaded and the stack must have content. If not using quick start, the whole stack must have loaded
		if (this.transition == null || !this.stackHasContent() || (['FIRST_FILE', 'FIRST_LOADED'].indexOf(this.queue_quick_start_method) < 0 && !this.stack_complete)) return false;

		// Remove related listeners
		this.stopObserving(this.TRANSITION_HAS_LOADED, this.getListener('canDisplay'));
		this.stopObserving(this.STACK_HAS_CONTENT, this.getListener('canDisplay'));
		this.stopObserving(this.STACK_HAS_LOADED, this.getListener('canDisplay'));

		// Fire instance ready event
		this.fire(this.INSTANCE_IS_READY, { panorama:this });
	},

	canAnimate:function ()
	{
		return (this.container.select ('.' + this.container_element_indentifier).length == 1)? true: false;
	},

	removeLoadingDisplay: function (event)
	{
		this.loading_display.remove();
		this.stopObserving(this.ELEMENT_HAS_TRANSITIONED_IN, this.getListener('removeLoadingDisplay'));
	},

	playFirst: function (event)
	{
		if(this.loading_display) this.observe(this.ELEMENT_HAS_TRANSITIONED_IN, this.getListener('removeLoadingDisplay'));

		this.first_element_displayed = new Date().getTime();

		this.active_package = this.stack.find(this.packageCanDisplay, this);

		if(this.active_package.transition) this.active_package.transition._in(null, true);
		else this.transition._in(this.active_package, true);

		if (this.queue_quick_start_method == 'FIRST_FILE')
		{
			this.observe(this.STACK_HAS_LOADED, this.getListener('playEnable'));
			this.loadStack();
		}
		else this.playEnable();
	},

	playEnable: function (event)
	{
		this.stopObserving(this.STACK_HAS_LOADED, this.getListener('playEnable'));
		if (!this.queue_autoplay) return;
		if (this.first_element_displayed != null && this.stack.length > 1)
		{
			var delta = (new Date ().getTime () - this.first_element_displayed) / 1000;
			this.first_element_displayed = null;
			if (delta >= this.transitions_delay) this.play ();
			else
			{
				var delayedPlay = this.play.bind (this);
				this.transition_timer = delayedPlay.delay (this.transitions_delay - delta);
			}
		}
	},

  // 'Public' methods
	play: function (inbound_package, ignore_timeout)
	{
		window.clearTimeout (this.transition_timer);
		
		if (typeof inbound_package == 'undefined') inbound_package = this.getNextPackage();

    if (inbound_package == this.active_package || !this.canAnimate()) return;

		var transition = inbound_package.transition? inbound_package.transition: this.transition;

    transition._out (this.active_package);
		this.active_package = transition._in (inbound_package.transition? null: inbound_package);

		if (typeof ignore_timeout != 'undefined' && ignore_timeout) return;

		var delayedPlay = this.play.bind (this);
		this.transition_timer = delayedPlay.delay (this.transitions_delay);
	},

	pause: function()
	{
		window.clearTimeout (this.transition_timer);
	},

	getNextPackage: function ()
	{
		return (this.stack.indexOf (this.active_package) + 1 < this.stack.length)? this.stack[this.stack.indexOf (this.active_package) + 1]: this.stack.first();
	},

	next: function (ignore_timeout)
	{
		window.clearTimeout(this.transition_timer);
		this.play(this.getNextPackage(), ignore_timeout);
	},

	getPreviousPackage: function ()
	{
		return (this.stack.indexOf (this.active_package) - 1 >= 0)? this.stack [this.stack.indexOf (this.active_package) - 1]: this.stack.last();
	},

	previous: function (ignore_timeout)
	{
		window.clearTimeout(this.transition_timer);
		this.play(this.getPreviousPackage(), ignore_timeout);
	},

  getStackLength: function ()
	{
		return this.stack.length;
	},

	getActivePackage: function ()
	{
		return this.active_package;
	},

	getActivePackageIndex: function ()
	{
		return this.stack.indexOf (this.active_package);
	}
});

toolkit.Transition = Class.create(
{
	version: 1.0, //

	// Instance properties
	animation_scope: null, //
	panorama: null, //
	package: null, //

	// Constructor
	initialize: function (panorama, package)
	{
		this.animation_scope = this.generateScopeIdentifier();

		if(typeof panorama == 'undefined') throw('');
		else this.panorama = panorama;

		if(typeof package != 'undefined') this.package = package;

		return this;
	},

	generateScopeIdentifier: function ()
	{
		var s = [], itoh = '0123456789ABCDEF';
		for (var i = 0; i <36; i++) s[i] = Math.floor(Math.random()*0x10);
		s[14] = 4; s[19] = (s[19] & 0x3) | 0x8;
		for (var i = 0; i <36; i++) s[i] = itoh[s[i]];
		s[8] = s[13] = s[18] = s[23] = '-';
		return s.join('');
	},

	prepareForTransition: function (inbound_package)
	{

	},

	_in: function (inbound_package, without_delay)
	{
		return inbound_package;
	},

	_out: function (outbound_package)
	{
		return outbound_package;
	}
});
