/** 
 * @fileoverview HTML SYOS is a small JavaScript application that integrates
 * with Tessitura Data to give users the ability to select individual seats
 * of their choice.
 * @version 0.5
 * @requires Prototype 1.5.1+
 * @requires Scriptaculous 1.7.1+
 */

/**
 * Attaches a 'loading' screen to an element on the page
 * @param {string} sElementID The ID of the element to attache to
 */
var LoadingScreen = Class.create();
LoadingScreen.prototype = {
	initialize: function(sElementID) {
		this.loading = false;
		this.loader = Builder.node('div',{id:'loader'});
		this.layout = $(sElementID);
		this.loader.style.width = this.layout.offsetWidth + 'px';
		this.loader.style.height = this.layout.offsetHeight + 'px';
		this.loader.style.marginTop = this.layout.getStyle('marginTop');
		this.layout.parentNode.insertBefore(this.loader,this.layout);
		$(this.loader).setOpacity(0.01);
		this.loader.style.left = '-10001px';
	},
	/**
	 * Updates the height of the screen to match the height of the element that
	 * it is attached to.
	 */
	update: function() {
		this.loader.style.height = this.layout.offsetHeight + 'px';
	},
	/**
	 * Show the loading screen
	 * @param {function} The function to call after the loading screen fades in
	 */
	show: function(fCallback) {
		this.loading = true;
		var ldr = this.loader;
		this.loader.style.left = '';
		fCallback = (typeof fCallback == 'undefined') ? function(){} : fCallback ;
		new Effect.Opacity(ldr,{
			duration: 0.5,
			from: 0.01,
			to: 0.75,
			afterFinish: fCallback
		});
	},
	/**
	 * Hides the loading screen by fading out
	 */
	hide: function() {
		var ldr = this.loader;
		var App = this;
		new Effect.Opacity(ldr,{
			duration: 0.5,
			from: 0.75,
			to: 0.01,
			afterFinish: function(fx) {
				fx.element.style.left = '-10001px';
				App.loading = false;
			}
		});
	}
}

/**
 * Used by HtmlSyos to store Seat data; Holds all necessary data for a Seat;
 * Results of Seat.toString() can be read by .Net for data inspection;
 * @constructor
 * @param {integer} id Tessitura seat id
 * @param {string} priceType Price Type Description
 * @param {int} priceTypeID Price Type ID
 * @param {string} price The price of the seat
 * @param {array} description Array containing the seat Section and seat Row & Number
 * @param {DOMNode} anchor Anchor element associated with the seat
 */
var Seat = Class.create();
Seat.prototype = {
	/** Tessitura seat id
	 * @type {integer} */
	id: null,
	/** Price Type description
	 * @type {string} */
	priceType: null,
	/** Price Type ID
	 * @type {int} */
	priceTypeID: null,
	/** Price
	 * @type {string} */
	price: null,
	/** Section Name
	 * @type {string} */
	section: null,
	/** Row and Number
	 * @type {string} */
	rowAndNumber: null,
	/** Anchor element associated with the seat
	 * @type {DOMNode} */
	anchor: null,
	/** The value of the ID attribute of the Anchor
	 * @type {string} */
	anchorID: null,
	/** Tessitura Zone ID
	 * @type {string} */
	zoneID: null,
	/** @ignore */
	initialize: function(id, priceType, priceTypeID, price, description, anchor, zoneID) {
		this.id = id;
		this.priceType = priceType;
		this.priceTypeID = priceTypeID;
		this.price = price;
		this.section = description[0].innerHTML;
		this.rowAndNumber = description[1].innerHTML;
		this.anchor = anchor;
		this.anchorID = anchor.id;
		this.zoneID = zoneID;
	},
	/**
	 * Overides default Object.toString() method to return a JSON formatted representation of the Seat
	 * @return {string} A JSON formatted string representing the Seat
	 */
	toString: function() {
		return $H(this).toJSON()
	}
}

/**
 * Static class to handle seat collection
 * @class
 * @static
 */
var Seats = {
	/**
	 * Adds a seat to the Seat collection
	 * @param {Seat} oSeat A Seat object
	 */
	add: function(oSeat) {
		this.collection.push(oSeat);
	},
	/**
	 * Removes a seat from the Seat collection based on seat id
	 * @param {integer} iSeatID
	 */
	remove: function(iSeatID) {
		if (this.contains('id',iSeatID)) {
			for (var i=0; i<this.collection.length; i++) {
				var seat = this.collection[i];
				if (seat.id == iSeatID) {
					this.collection.splice(i,1);
				}
			}
		}
	},
	/**
	 * Gets the first seat in the collection based on any property; the results of this
	 * can be misleading as many seats contain the same properties. To be sure, query based
	 * on seat id (not anchor id)
	 * @param {string} sPropertyHook
	 * @param {mixed} mValue
	 * @return {bool}
	 */
	get: function(sPropertyHook, mValue) {
		var found = false;
		this.collection.each(function(item,i){
			if (item[sPropertyHook] == mValue) {
				found = item;
				throw $break;
			}
		}.bind(this));
		return found;
	},
	/**
	 * Tests if a seat is in the collection based on any property; the results of this
	 * can be misleading as many seats contain the same properties. To be sure, query based
	 * on seat id (not anchor id)
	 * @param {string} sPropertyHook
	 * @param {mixed} mValue
	 * @return {bool}
	 */
	contains: function(sPropertyHook, mValue) {
		return (this.get(sPropertyHook,mValue)) ? true : false ;
	},
	/**
	 * Returns the number of seats in the collection
	 * @return {integer}
	 */
	count: function() {
		return this.collection.length;
	},
	/** @ignore */
	collection: [],
	/**
	 * Returns a JSON formatted string array of the colleciton
	 * @return {string}
	 */
	toString: function() {
		return this.collection.toJSON();
	}
}

/**
 * Reusable container for inline content.
 * @constructor
 * @param {string} sElementID The ID of the DIV element to attach to
 * @param {bool} bSticky
 * @param {bool} bHeader
 * @param {bool} bCloseButton
 */
var InlineWindow = Class.create();
InlineWindow.prototype = {
	/** Contains UI elements [window,caption,close,content]
	 * @type {object} */
	UI: {},
	options: {
		sticky: false,
		header: true,
		close_button: true
	},
	/** If the InlineWindow is currently open or not
	 * @type {bool} */
	is_open: false,
	/** @ignore */
	initialize: function(sElementID, bSticky, bHeader, bCloseButton) {
		if (bSticky != undefined) { this.options.sticky = bSticky; }
		if (bHeader != undefined) { this.options.header = bHeader; }
		if (bCloseButton != undefined) { this.options.close_button = bCloseButton; }
		Object.extend(this.UI,{
			window: $(sElementID),
			caption: $$('#'+sElementID+' .win_caption')[0],
			close: $$('#'+sElementID+' a.win_close')[0],
			content: $$('#'+sElementID+' div.win_content')[0]
		});
		if (!bHeader || bHeader == undefined) {
			this.UI.caption.hide();
			this.UI.close.hide();
		}
	},
	/**
	 * Updates the content of the window
	 * 
	 * @param {mixed} mContent The string or DOMNode to populate the win_content div
	 * @param {string} sHeadline The string to populate the win_caption
	 */
	update: function(mContent, sHeadline, bEvenIfStuck) {
		if (this.isStuck()) { return; } // Don't update if the window is stuck open
		if (typeof mContent == 'object' && typeof mContent.nodeName != 'undefined') {
			// When the content is a DOMNode
			this.UI.content.innerHTML = '';
			var content = mContent.cloneNode(true);
			content.style.display = "";
			this.UI.content.appendChild(content);
		} else if (typeof mContent == 'string') {
			// When the content is a string
			this.UI.content.innerHTML = mContent;
		}
		if (sHeadline) {
			// Only update the headline if it was passed
			this.UI.caption.innerHTML = sHeadline;
			this.UI.caption.show();
			this.UI.close.show();
		} else {
			this.UI.caption.hide();
			this.UI.close.hide();
		}
		return this.UI.content.firstChild;
	},
	/**
	 * Display the inline window
	 * 
	 * @param {bool} [bSticky] Keep the window open until closed with this.hide(true)
	 * @return void
	 */
	show: function(bSticky) {
		// don't do anything if STICKY and OPEN
		if (this.isStuck()) { return; }
		// Update Sticky if supplied
		if (bSticky != undefined) { this.options.sticky = bSticky; }
		// Show the window div
		this.UI.window.style.visibility = 'visible';
		Effect.Appear(this.UI.window);
		// Update state
		this.is_open = true;
	},
	/**
	 * Hide the inline window
	 * 
	 * @param {bool} [bCancelStick] Hide the window even if stuck open
	 * @return void
	 */
	hide: function(bCancelStick) {
		// Clear sticky if forcing closed a sticky window
		if (bCancelStick) {
			this.options.sticky = false;
		}
		// Only hide when sticket is FALSE
		if (this.options.sticky == false) {
			// Done hide the div immediately; avoids blinkness when switching between hover elements
			setTimeout(function(){
				// Only hide the DIV if is_open is false
				if (!this.is_open) {
					this.UI.window.style.visibility = 'hidden';
					this.left(-5000);
				}
			}.bind(this),100);
			// Update is_open to false;
			this.is_open = false;
		}
	},
	/**
	 * If the window is currently "stuck"
	 * @return bool
	 */
	isStuck: function() {
		return (this.options.sticky == true && this.is_open);
	},
	width: function(iWidth) {
		if (iWidth != undefined) { this.UI.window.style.width = iWidth - + 'px'; }
		return this.UI.window.offsetWidth;
	},
	height: function(iHeight) {
		if (iHeight != undefined) { this.UI.window.style.height = iHeight + 'px'; }
		return this.UI.window.offsetHeight;
	},
	left: function(iX) {
		if (iX != undefined) { this.UI.window.style.left = iX + 'px'; }
		return this.UI.window.offsetLeft;
	},
	top: function(iY) {
		if (iY != undefined) { this.UI.window.style.top = iY + 'px'; }
		return this.UI.window.offsetTop;
	},
	/**
	 * Moves the inline window to an absolute position if it is not currently "stuck"
	 * 
	 * @param {integer} iX
	 * @param {integer} iY
	 * @return {Array} An array containing the resulting X/Y position of the window
	 */
	position: function(iX,iY,bEvenIfStuck) {
		var positionWhenStuck = (bEvenIfStuck == true) ? true : false ; 
		if ((this.isStuck() && positionWhenStuck) || !this.isStuck())  {
			return [this.left(iX), this.top(iY)];
		} else {
			return [this.left(), this.top()];
		}
	}
}

/**
 * HTML SYOS Application
 * @constructor
 * @todo Add notice that changing screens will dump the current selection
 * @todo Parse saved data in txt_Selection and reload selected seats
 * @todo Handle seat click (single & multiple price types)
 */
var HtmlSyos = Class.create();
HtmlSyos.prototype = {
	_last_seat_with_event: null,
	/** @ignore */
	initialize: function() {
		this.UI = { // Identify all necessary UI elements for initialization
			best: $(POP.Config.BestAvailableDiv),
			syos: $('html_syos')
		}
		// Don't do anything if UI elements not found, or syos is disabled
		if (!POP.Config.SyosEnabled || !this.UI.best || !this.UI.syos) { return; }
		this.UI.best.hide();
		this.UI.syos.show();
		
		//Position the container to fix IE6 disapearing content bug
		Element.makePositioned(this.UI.syos); 
		
		// Find the tabs for switching between "Best Available" and "SYOS"
		(this.UI.liSyos = $('html_syos_tab_syos')).addClassName('on');
		this.UI.liBest = $('html_syos_tab_best');
		this.UI.ulTabs = this.UI.liBest.parentNode;
 		this.UI.best.parentNode.insertBefore(this.UI.ulTabs.parentNode.removeChild(this.UI.ulTabs),this.UI.best.parentNode.firstChild);
 		this.UI.ulTabs.style.display = "block";
 		Event.observe(this.UI.liSyos,'mousedown',this._tabClick.bindAsEventListener(this));
 		Event.observe(this.UI.liBest,'mousedown',this._tabClick.bindAsEventListener(this));
 		
		this.UI.form = this.UI.best.up('form');
		this.UI.submitButton = $$('#html_syos input[type=submit]')[0];
		this.UI.screenSelector = $$('#html_syos select')[0];
		this.UI.dataHolder = $$('#html_syos textarea')[0];
		this.UI.seatsWrapper = $('seats_wrapper');
		this.UI.cart = $('html_syos_cart');
		this.UI.footer = $('html_syos_footer');

		// Setup seatPreview
		this.UI.seatPreview = $('html_syos_seat_preview');
		this.UI.seatImage = $('img_SeatPreview');
		var previewParent = $('mainContent');
		this.UI.seatPreview = $('mainContent').insertBefore(this.UI.seatPreview.parentNode.removeChild(this.UI.seatPreview), previewParent.firstChild);	
		$('lnk_close_seat_preview').observe('click', this._closeSeatPreviewClick.bindAsEventListener(this));
		this.UI.seatPreviewDrag = new Draggable(this.UI.seatPreview, {});// add reference to draggable in case it's needed

		Event.observe(this.UI.submitButton,'click',this._submitClick.bindAsEventListener(this));
		if (this.UI.screenSelector) {
			Event.observe(this.UI.screenSelector,'focus',this._screenSelectorFocus.bindAsEventListener(this));
			Event.observe(this.UI.screenSelector,'change',this._screenSelectorChange.bindAsEventListener(this));
		}
		
		this.UI.popup = new InlineWindow('seat_info');
		Event.observe(this.UI.popup.UI.close,'click',this._closeNotificationClick.bindAsEventListener(this));

		this.UI.spn_ToggleCart = $('html_syos_cart_toggle');
		if (POP.Config.CollapsableCart) {
			// If cart is collapsable, configure it to be so
			if (Prototype.Browser.WebKit) {
				this.UI.cart.style.width = this.UI.cart.parentNode.offsetWidth + 'px';
			}
			this.UI.cart.style.height = this.UI.spn_ToggleCart.offsetHeight + 'px';
			this.UI.cart.style.bottom = '2.5em';
			Event.observe(this.UI.spn_ToggleCart,'click',this._toggleCartClick.bindAsEventListener(this));
		} else {
			// hide the toggle button and don't position the cart
			this.UI.spn_ToggleCart.hide();
			this.UI.cart.style.position = 'relative';
		}
		
		this.updateCartTable();
		
		Event.observe($('seatmap_layout_area'),'scroll',this._mapScroll.bindAsEventListener(this));
		
		this.UI.loader = new LoadingScreen('seatmap_layout_area');
		var screenID = (this.UI.screenSelector) ? this.UI.screenSelector.options[this.UI.screenSelector.selectedIndex].value : 1 ;
		this.getScreen(screenID);
	},
	_mapScroll: function() {
		this.hidePopup(true);
	},
	_bindSeats: function() {
		// Find all of the seats
		var ul = this.UI.seats = $('seatmap_layout_area').getElementsByTagName('ul')[0];
		this.UI.lnks = [];
		var lnks = ul.getElementsByTagName('a');
		var length = lnks.length;
		for (var i=0; i<length; i++) {
			var lnk = lnks[i];
			if (lnk.className.indexOf('lnk') > -1) {
				this.UI.lnks.push(lnk);
				Event.observe(lnk,'mouseover',this._seatHover.bindAsEventListener(this));
				Event.observe(lnk,'mouseout',this._seatOut.bindAsEventListener(this));
				Event.observe(lnk,'mousedown',this._seatClick.bindAsEventListener(this));
				if (lnk.href.indexOf('#') > -1) {
					var seatId = lnk.href.split('#')[1];
					var selected = Seats.get('id',seatId);
					if (selected) {
						if (lnk.id == selected.anchorID) {
							if (lnk.parentNode.className.indexOf('selected') == -1) {
								lnk.parentNode.className += ' selected'
							}
						}
					}
				}
			}
		}
		ul.style.top = Math.floor(((ul.parentNode.offsetHeight/2) - (ul.offsetHeight/2))) + 'px';
		
		var l = $('seatmap_layout_area');
		if (ul.offsetWidth > l.offsetWidth) {
			ul.parentNode.style.width = ul.offsetWidth + 'px';
			l.scrollLeft = (ul.offsetWidth - l.offsetWidth) / 2;
			if (Prototype.Browser.IE) {
				this.UI.seatsWrapper.style.paddingBottom = "10px";
			}
		} else {
			ul.parentNode.style.width = 'auto';
			ul.style.left = Math.floor(((ul.parentNode.offsetWidth/2) - (ul.offsetWidth/2))) + 'px';
			if (Prototype.Browser.IE) {
				this.UI.seatsWrapper.style.paddingBottom = "0px";
			}
		}
	},
	/** @ignore */
	_seatHover: function(e) {
		var e = e || window.event;
		var lnk = Event.element(e);
		var ul = this.UI.seats;
		this.UI.popup.update(lnk.parentNode.getElementsByTagName('ul')[0]);
		// put window below seat when seat is above halfway mark
		var winY = ((lnk.parentNode.offsetTop < lnk.parentNode.parentNode.offsetHeight / 2) ?
				lnk.parentNode.offsetTop + (lnk.offsetHeight*2) :
				lnk.parentNode.offsetTop - this.UI.popup.height() - lnk.offsetHeight) + this.UI.seats.offsetTop ;

		var l = $('seatmap_layout_area'); // 3/10/08 [tw] added l.scrollLeft to the x position calculation
		var winX = (Event.pointerX(e) < Position.cumulativeOffset(ul.parentNode)[0] + ((ul.parentNode.offsetWidth - l.scrollLeft) / 2)) ?
				lnk.parentNode.offsetLeft + ul.offsetLeft :
				lnk.parentNode.offsetLeft + lnk.parentNode.offsetWidth + ul.offsetLeft - this.UI.popup.width() ;
	
		this.UI.popup.position(winX, winY);
		this.UI.popup.show(false);
		this.setLastSeatWithEvent(lnk);
		/* Client requested seat preview images disabled
		if (lnk.className.match(/image/)) {
			var nodes = lnk.parentNode.getElementsByTagName('span');
			for (var i=0; i<nodes.length; i++) {
				if (nodes[i].className.match(/image/)) {
					this.showSeatPreview(nodes[i].innerHTML,lnk.id);
					break;
				}
			}
		}*/
	},
	/** @ignore */
	_closeSeatPreviewClick: function(e){
		this.hideSeatPreview();
	},
	/** @ignore */
	_seatOut: function(e) {
		var e = e || window.event;
		var lnk = Event.element(e);
		this.hidePopup();
		this.setLastSeatWithEvent(lnk);
	},
	/** @ignore */
	_seatClick: function(e) {
		var e = e || window.event;
		Event.stop(e);
		var lnk = Event.element(e);
		if (lnk.parentNode.className.match(/unavailable/i)) { return; }
		
		var dl_priceTypes = Element.extend(lnk.parentNode.getElementsByTagName('dl')[0]);
		var type_count = dl_priceTypes.getElementsByTagName('dt').length; // find the # of price types
		if (lnk.parentNode.className.match(/selected/i)) {
			this.dropSeat(lnk.id,lnk.href.replace(/^(.*)#/i,''));
		} else if (Seats.count()+1 > POP.Config.MaxTicketCount) {
			this.notifyMaximumTicketAllotment();
		} else if (type_count == 1) {
			// if only 1 price type, or seat is already selected
			this.reserveSeat(
				lnk,
				lnk.next(),
				lnk.parentNode.getElementsByTagName('dt')[0],
				lnk.parentNode.getElementsByTagName('dd')[0]
			);
		    Element.addClassName(lnk.parentNode,'selected');
		} else if (type_count > 1) {
			this.showPriceTypes(lnk,dl_priceTypes,e);
		    Element.addClassName(lnk.parentNode,'selected');
		}
		this.setLastSeatWithEvent(lnk);
	},
	
	/**
	 * Associate the last seating interaction event with the supplied seat link
	 * @param {DOMNode} oLnk Seat Anchor
	 */
	setLastSeatWithEvent: function(oLnk) {
		if (!this.UI.popup.isStuck()) {
			this._last_seat_with_event = oLnk;
		}
	},
	
	/**
	 * Perform 'click' actions on the Seat element with the specified ID attribute
	 * @param {string} sSeatID The value of the ID attribute of a Seat element
	 * @param {string} [iPriceTypeID] The price type id to use
	 */
	fakeSeatClick: function(sSeatID, iPriceTypeID) {
		var lnk = $(sSeatID);
		var li = lnk.parentNode;
		var dt, dd;
		var terms = li.getElementsByTagName('dt');
		var defs = li.getElementsByTagName('dd');
		if (iPriceTypeID != 'undefined') {
			for (var i=0; i<terms.length; i++) {
				if (terms[i].getAttribute('rel') == iPriceTypeID) {
					dt = terms[i];
					dd = defs[i];
					break;
				}
			}
		} else {
			dt = terms[0];
			dd = defs[0];
		}
		if (dt && dd) {
			this.reserveSeat(lnk,lnk.next(),dt,dd);
			this.setLastSeatWithEvent(lnk);
		}
	},
	
	/**
	 * TO DO: NEED WEBSERVICE WITH CORRECT DATA
	 * @ignore
	 */
	_priceTypeClick: function(e) {
		var e = e || window.event;
		Event.stop(e);
		var el = Event.element(e);
		this.fakeSeatClick(el.parentNode.getAttribute('rel'),el.getAttribute('rel'));
		this.closeMaximumTicketAllotment();
	},
	
	_priceTypeOver: function(e) {
	    var e = e || window.event;
	    Event.stop(e);
	    var el = Event.element(e);
	    Element.addClassName(el,"hover");
	    if (el.nodeName == "DT")
	        Element.addClassName(el.next(),"hover");
	    else
	        Element.addClassName(el.previous(),"hover") 
	},
	
	_priceTypeOut: function(e) {
	    var e = e || window.event;
	    Event.stop(e);
	    var el = Event.element(e);
	    Element.removeClassName(el,"hover");
	    if (el.nodeName == "DT")
	        Element.removeClassName(el.next(),"hover");
	    else
	        Element.removeClassName(el.previous(),"hover");
	},
	
	/**
	 * When the submit button is clicked, save the seats object to the 
	 * data textfield so C# can access it.
	 * @ignore
	 */
	_submitClick: function(e) {
		var e = e || window.event;
		var btn = Event.element(e);
		this.UI.dataHolder.value = Seats.toString();
	},
	
	_screenSelectorFocus: function(e) {
		var e = e || window.event;
		var el = Event.element(e);
		if (this.UI.loader.loading) {
			el.blur();
			Event.stop(e);
			return false;
		}
	},
	_screenSelectorChange: function(e) {
		var e = e || window.event;
		var el = Event.element(e);
		if (this.UI.loader.loading) { return; }
		el.blur();
		this.getScreen(el.options[el.selectedIndex].value);
	},
	
	hidePopup: function(bEvenIfStuck, bHideHeader) {
		if (bEvenIfStuck) {
			this.UI.popup.hide(true);
			this.UI.popup.UI.window.removeClassName('price_types');
		} else {
			this.UI.popup.hide();
		}
		if (bHideHeader) {
			this.UI.popup.UI.caption.hide();
			this.UI.popup.UI.close.hide();
		}
	},
	
	getScreen: function(iScreenID) {
		this.hidePopup(true,true);
		var App = this;
		App.UI.screenSelector.disable();// disable the select while seatmap is loading

		this.UI.loader.show(
			function() {
				new Ajax.Request(POP.Config.RootVirtual + '/html_syos/seatmap.aspx',{
					method: 'get',
					parameters: {
						show: iScreenID,
						mos: POP.Config.ModeOfSale,
						src: location.href.toString().toQueryParams().src,
						perf: location.href.toString().toQueryParams().id,
						seatwidthheight: POP.Config.SeatWidthHeight,
						seatspacing: POP.Config.SeatSpacing,
						nocache: Math.random()
					},
					onSuccess: function(transport) {
						var f = transport.responseXML.getElementsByTagName('form')[0];
						var cdata = false;
						for (var i=0; i<f.childNodes.length; i++) {
							if (f.childNodes[i].nodeType == 4) {
								cdata = f.childNodes[i];
								break;
							}
						}
						if (cdata) {
							App.UI.seatsWrapper.innerHTML = cdata.nodeValue.strip();
							App.UI.seats = App.UI.seatsWrapper.firstChild;
							App._bindSeats();
							App.UI.loader.update();
							App.UI.loader.hide();
							App.UI.screenSelector.enable();// enable the select
						}
					}
				});
			}
		)
	},
	
	/**
	 * When the 'close' link is clicked.
	 * @ignore
	 */
	_closeNotificationClick: function(e) {
		var e = e || window.event;
		Event.stop(e);
		if (this._last_seat_with_event) {
		    Element.removeClassName(this._last_seat_with_event.parentNode,'selected');
		}
		this.closeMaximumTicketAllotment();
	},
	
	/**
	 * The current state of the syos cart [open|closed|animating]
	 * @type {string}
	 */
	cart_state: 'closed',
	
	/** @ignore */
	_toggleCartClick: function(e) {
		var e = e || window.event;
		this.toggleCartDisplay();
	},
	
	/**
	 * If the cart is open, close it; if it's closed, open it.
	 */
	toggleCartDisplay: function() {
		if (this.cart_state == 'closed') {
			this.openCart();
		} else if (this.cart_state == 'open') {
			this.closeCart();
		}
	},
	
	/**
	 * Start the 'open' animation on the cart
	 */
	openCart: function() {
		if (!POP.Config.CollapsableCart) { return; } // RETURN if cart isn't collapsable
		if (this.cart_state == 'animating') { return; }
		var App = this;
		new Effect.Morph(this.UI.cart,{
			style: { height: $('html_syos_cart_content').offsetHeight + this.UI.spn_ToggleCart.offsetHeight + 'px' },
			beforeStart: function() { App.cart_state = 'animating' },
			afterFinish: function() { App.cart_state = 'open' },
			duration: 0.25
		});
	},
	
	/**
	 * Start the 'close' animation on the cart
	 */
	closeCart: function() {
		if (!POP.Config.CollapsableCart) { return; } // RETURN if cart isn't collapsable
		if (this.cart_state == 'animating') { return; }
		var App = this;
		new Effect.Morph(this.UI.cart,{
			style: { height: this.UI.spn_ToggleCart.offsetHeight + 'px' },
			beforeStart: function() { App.cart_state = 'animating' },
			afterFinish: function() { App.cart_state = 'closed' },
			duration: 0.25
		});
	},
	
	showPriceTypes: function(oLnk,oPriceTypes,oEvent) {
		oPriceTypes.setAttribute('rel',oLnk.id);
		var dl = this.UI.popup.update(oPriceTypes,'Select a Price Option')

		for (var i=0; i<dl.childNodes.length; i++) {
			var node = dl.childNodes[i];
			if (node.nodeName == 'DT' || node.nodeName == 'DD') {
				Event.observe(node,'mousedown',this._priceTypeClick.bindAsEventListener(this));
				Event.observe(node,'mouseover',this._priceTypeOver.bindAsEventListener(this));
				Event.observe(node,'mouseout',this._priceTypeOut.bindAsEventListener(this));
			}
		}

		this.UI.popup.UI.caption.show();
		this.UI.popup.UI.close.show();

		this.UI.popup.UI.window.addClassName('price_types');
		this.UI.popup.show(true);

		var ul = this.UI.seats;
		// put window below seat when seat is above halfway mark
		var winY = ((oLnk.parentNode.offsetTop < oLnk.parentNode.parentNode.offsetHeight / 2) ?
				oLnk.parentNode.offsetTop + (oLnk.offsetHeight*2) :
				oLnk.parentNode.offsetTop - this.UI.popup.height() - oLnk.offsetHeight) + this.UI.seats.offsetTop  ;
		var winX = (Event.pointerX(oEvent) + this.UI.popup.width() < Position.cumulativeOffset(ul.parentNode.parentNode)[0] + ul.parentNode.parentNode.offsetWidth) ?
				oLnk.parentNode.offsetLeft + ul.offsetLeft :
				oLnk.parentNode.offsetLeft + oLnk.parentNode.offsetWidth + ul.offsetLeft - this.UI.popup.width() ;
		
		this.UI.popup.position(winX, winY, true);
	},
	
	/**
	 * Display the notification of max ticket allotment
	 * @return void
	 */
	notifyMaximumTicketAllotment: function() {
		if (Seats.count()+1 > POP.Config.MaxTicketCount) {
			this.UI.popup.update(POP.Config.MaxTicketMessage,'Note:');
			this.UI.popup.UI.caption.show();
			this.UI.popup.UI.close.show();
			
			var lnk = this._last_seat_with_event;
			var winY = ((lnk.parentNode.offsetTop < this.UI.seats.offsetHeight / 2) ?
					lnk.parentNode.offsetTop + (lnk.offsetHeight*2) :
					lnk.parentNode.offsetTop - this.UI.popup.height() - lnk.offsetHeight) + this.UI.seats.offsetTop  ;
			var winX = (lnk.parentNode.offsetLeft + this.UI.seats.offsetLeft < this.UI.seatsWrapper.offsetWidth / 2) ?
					lnk.parentNode.offsetLeft + this.UI.seats.offsetLeft :
					lnk.parentNode.offsetLeft + lnk.parentNode.offsetWidth + this.UI.seats.offsetLeft - this.UI.popup.width() ;

			this.UI.popup.position(winX,winY);
			this.UI.popup.show(true);
		}
	},
	
	/**
	 * Hide the notification of max ticket allotment
	 */
	closeMaximumTicketAllotment: function() {
		this.hidePopup(true,true);
	},
	
	/** Fired when one of the panel tabs are clicked
	 * @ignore */
	_tabClick: function(e) {
		var e = e || window.event;
		var el = Event.findElement(e,'li');
		this.showPanel(el.id.match(/[a-z]+$/)); // match the last word of the id of the clicked element
	},
	
	/**
	 * Show the specified panel
	 * @param {string} sPanel [syos|best] The name of the panel to be shown
	 */
	showPanel: function(sPanel) {
		if (sPanel == 'syos') {
			this.UI.best.hide();
			this.UI.liBest.removeClassName('on');
			this.UI.syos.show();
			this.UI.liSyos.addClassName('on');
		} else if (sPanel == 'best') {
			this.UI.best.show();
			this.UI.liBest.addClassName('on');
			this.UI.syos.hide();
			this.UI.liSyos.removeClassName('on');
		}
	},
	
	/**
	 * Updates the seat preview draggable with a new image and seat number; image display is delayed
	 * for a smooth browsing experience.
	 * 
	 * @param {string} sImageUrl The URL to assign to the image element
	 * @param {string} seatLnkId The id of the link (photo icon) to get seat info from
	 * @return void
	 */
	showSeatPreview: function(sImageUrl, seatLnkId) {
		if($(seatLnkId)){
			var previewInfo = $(seatLnkId).down().innerHTML;
			$('seat_number').innerHTML = previewInfo;
		}		
		this.UI.seatImage.src = sImageUrl;
		this.seatPreviewTimout = setTimeout(function(){
			this.UI.seatPreview.show();
		}.bind(this),400);
	},

	hideSeatPreview: function() {
		if (this.seatPreviewTimout) {
			window.clearTimeout(this.seatPreviewTimout);
		}
		this.UI.seatImage.src = '';
		this.UI.seatPreview.hide();
	},
		
	reserveSeat: function(aLnk, ulInfo, dtPriceType, ddPrice) {
		if (!aLnk.readAttribute('href') || Seats.count()+1 > POP.Config.MaxTicketCount) { return; }
		var s = new Seat(
			aLnk.href.replace(/^(.*)#/,''),				// seat id
			dtPriceType.innerHTML,						// price type
			dtPriceType.getAttribute('rel'),			// price type id
			ddPrice.innerHTML,							// price
			ulInfo.getElementsByTagName('li'),			// info
			aLnk,										// anchor
			aLnk.readAttribute('rel'));					// zoneID

		// Add the seat to the Seats collection
		Seats.add(s);
		// Add a row to the cart
		var tbl = this.UI.cart.getElementsByTagName('table')[0];
		var row, section, seat, type, price, remove, lnk, rel;
		hook = s.anchorID+':'+s.id;
		(row = tbl.insertRow(1)).setAttribute("rel",hook);
		(section = row.insertCell(0)).innerHTML = s.section;
		(seat = row.insertCell(1)).innerHTML = s.rowAndNumber;
		(type = row.insertCell(2)).innerHTML = s.priceType;
		(price = row.insertCell(3)).innerHTML = '$'+parseFloat(s.price.replace('$','')).toFixed(2); // enforce 2-decimal currency format
		price.className = 'price';
		lnk = (remove = row.insertCell(4)).appendChild(Builder.node('a',{rel:hook,title:'Remove This Seat'},Builder.node('span','Remove')));
		Event.observe(lnk,'click',function(e){
			var e = e || window.event;
			Event.stop(e);
			var el = Event.findElement(e,'a');
			this.dropSeat(s.anchorID,s.id);
		}.bindAsEventListener(this));
		this.updateCartTable();
	},
	
	dropSeat: function(sAnchorID, iSeatID) {
		var s = Seats.get('id',iSeatID);
		var lnk = $(s.anchorID);
		var r = new RegExp("#"+iSeatID+"$","i");
		if (r && lnk && r.test(lnk.href)) {
			Element.removeClassName(lnk.parentNode,'selected');
			//resetting the class forces IE6 and IE7 to update the seat icon
			lnk.parentNode.setAttribute('class',lnk.parentNode.getAttribute('class'));
		}
		Seats.remove(iSeatID);
		var tbl = this.UI.cart.getElementsByTagName('table')[0];
		for (var i=0; i<tbl.rows.length; i++) {
			try {
				// non-item rows don't have a 'rel' attribute, and throw an error
				var hook = tbl.rows[i].getAttribute('rel').split(':');
			} catch (e) { continue; }
			if (hook[0] == sAnchorID && hook[1] == iSeatID) {
				tbl.rows[i].parentNode.removeChild(tbl.rows[i]);
				break;
			}
		}
		// If the window is stuck open, close it since we're deleting a seat
		if (this.UI.popup.isStuck()) {
			this.closeMaximumTicketAllotment();
		}
		this.updateCartTable();
	},
	
	/**
	 * Core interface for adding/removing seats for the seat collection;
	 * @param {DOMNode} aLink Anchor associated with the seat
	 * @param {DOMNode} ulInfo UL containing seat info
	 * @param {DOMNode} dtPriceType DT containing price type
	 * @param {DOMNode} ddPrice DD containing price
	 */
	updateCartTable: function() {
		// Total up all rows and display for display in the cart footer.
		var tbl = this.UI.cart.getElementsByTagName('table')[0];
		var total = 0;
		for (var i=1; i<tbl.rows.length; i++) {
			var row = Element.extend(tbl.rows[i]);
			row.removeClassName('odd').removeClassName('even');
			if (i < tbl.rows.length - 2) {
				total += parseFloat(row.cells[3].innerHTML.replace('$',''));
			}
			if (i < tbl.rows.length - 2) {
				if (i % 2 == 0) {
					row.addClassName('odd');
				} else {
					row.addClassName('even');
				}
			}
			if (tbl.rows.length == 3 && i == 1) {
				row.removeClassName('hidden');
			} else if (tbl.rows.length > 3 & i == tbl.rows.length - 2) {
				row.addClassName('hidden');
			}
		}
		if (tbl.rows.length == 3) {
			this.UI.footer.hide();
		} else {
			this.UI.footer.show();
		}
		// Compute total price; maintain currency formatting; update the 'total' cell
		tbl.rows[tbl.rows.length - 1].cells[2].innerHTML = "$"+total.toFixed(2);
		
		var tab = $('html_syos_cart_toggle');
		tab.innerHTML = tab.innerHTML.replace(/\d+/gi,tbl.rows.length-3);
		
		if (tbl.rows.length == 3) {
			this.closeCart();
		} else {
			this.openCart();
		}
	}
}
