var uri = location.href.parseUri();
var URL_DOMAIN = uri.protocol + "://" + uri.host;
var URL_DOMAIN_HTTP = "http://" + uri.host;
var URL_DOMAIN_HTTPS = "https://" + uri.host;

var SpinnerHtml = '<span class="spinnerText"><img src="/images/global/ajax-loading.gif" width="14" height="14"> Loading...</span>';

var BehaviorRollover = 'def_rollover'; // why not just Rollover.classIndicator ?? CM

// Create an image object for the livechat button.
// We want to do this asap because this image sometimes takes a while to load
// due to the logic and roundtrip time to query the liveperson server.
// I put this in this file due to its dependence on the above uri object.
var liveChatButtonImageObj = new Image();
liveChatButtonImageObj.src = uri.protocol+'://service.liveperson.net/hc/24631554/?cmd=repstate&site=24631554&channel=web&&ver=1&imageUrl='+URL_DOMAIN+'/images/global/livepersonSm&skill=Clinique';
liveChatButtonImageObj.name="hcIcon";
liveChatButtonImageObj.style.border="0";

// Load any livechat links with the button above
var loadLiveChatButtons = function() {
	$$('a.livechat-image-tag').each(function(el){
		if ( el.empty() ) {
			// Image width/height needs to match actual image.
			// Specifying dimensions seems to make IE8 a little happier.
			var img = new Image(179,25);
			img.src = liveChatButtonImageObj.src;
			img.name = liveChatButtonImageObj.name;
			img.style.border = liveChatButtonImageObj.style.border;
			el.update(img);
		}
	});
}


var el = $;

Element.addMethods({
   addBehavior: function(e,b) {
		$(e).className = b;
   }   
});

// like old whenEl()

var onElement = function (element,callback,duration,attempts) {
	var count = 0;
	var duration = duration || .5;
	var attempts = attempts || 100;
	new PeriodicalExecuter( function (pe) {
		count++;
		if ( count >= attempts ) { pe.stop() }
		if ($(element)) {
			pe.stop();
			callback();
			callback = function () {};
		}
	}, duration);	
};

var onObject = function (object,callback,duration,attempts) {
	var count = 0;
	var duration = duration || .5;
	var attempts = attempts || 100;
	var obj = undefined;
	new PeriodicalExecuter( function (pe) {
		count++;
		if ( count >= attempts ) { pe.stop() }
		try {
			obj = eval(object);
		} catch (e) {
			// try again!
		}
		if (obj) {
			pe.stop();
			callback();
			callback = function () {};
		}
	}, duration);	
};

function generateGuid () {
	var result, i, j;
	result = '';
	// Note: JS spec requires id's to start with char.
	i = Math.floor(10+(Math.random()*6)).toString(16).toUpperCase();
	result = result + i;
	for(j=1; j<32; j++)
	{
	if( j == 8 || j == 12|| j == 16|| j == 20)
	result = result + '-';
	i = Math.floor(Math.random()*16).toString(16).toUpperCase();
	result = result + i;
	}
	return result
}

// Added this so we can check for boolean settings in html-template files.
// That's cuz this gives an error if no value is set: obj.checked = ( #{VAL} ? true : false )

// Why not toBool() method off the extended String object??? Think OO!!! '#{VAL}'.toBool() - CM

function CheckBooleanVar(val) {
	if ( val ) return true;
	return false;	
}

var jsonRPC = Class.create({
	initialize: function () {
		var PRODUCT_JSON_URL = "/jsonrpc.json";
		this.id = 0;
		this.url = URL_DOMAIN + PRODUCT_JSON_URL;
	},
	fetch: function ( method, params, options ) {
		this.id++;
		this.options = Object.extend({
			onSuccess: function (r) {
				alert('success');
				alert(r.responseText);
			}, 
			onFailure: function (r) {
				alert('failure');
				alert(r.responseText);
			}	
		}, options || {});		
	 	var postobj = {
	 		method: method,
	 		id: this.id
	 	};
		if (typeof params != 'object') {
			postobj.params = params.evalJSON();
		} else {
			postobj.params = params;
		}
		this.options.method = 'post';
		poststring = (Object.toJSON(postobj))
		this.options.parameters = 'JSONRPC=[' + poststring + ']';
		
		// WDR: adding the method to the query string so we can see it in the apache logs.
		var targetUrl = this.url + '?dbgmethod=' + method;
		new Ajax.Request( targetUrl, this.options );
		//new Ajax.Request( this.url, this.options );
		
		return this.id;	
	}, 
	test : function () {
		alert('test');
	}
});

jsonrpc = new jsonRPC();

Template.prototype.evaluate = Template.prototype.evaluate.wrap(
function (fnEval,obj) {
	if ( Object.isArray(obj) 
		&& (typeof obj[0] == 'object') )
    {
		var array = $A(obj);
		var results = '';
		array.each( function (obj) {
			results += fnEval(obj);
		});
		return results;		
	} else {
		return fnEval(obj);
	}
});

var RediTemplate = Class.create( Template, {
	initialize: function ( template, pattern ) {
		this.template = template?template:'';
		this.readyState = template?1:0;
		this.pattern = pattern?pattern:Template.Pattern;
		
		this.queue = new Array();		
		return;
	},
	load: function(template) {
	    this.template = template.toString();
		this.readyState = 1;
		this.onReadyState();
	},
	_preprocess: function (object) {
		return object;
	},
	evaluateCallback: function (options) {
	    this.options = {
	      object:       {},
	      callback: 	function () {}
	    };
	    Object.extend(this.options, options || { });
		object = this._preprocess(this.options.object);
		if (this.readyState) {
			this.options.callback(this.evaluate(this.options.object));
		} else {
			this.queue.push({
				qtype: 'callback',
				obj: this.options.object,
				fnc: this.options.callback
			});
		}
		return;		
	},
	evaluateElement: function (element, options) {
		var element = $(element);
	    this.options = {
	      object:       {},
		  position: 'bottom',
	      callback: 	function () {}
	    };
		var elm;
		Object.extend(this.options, options || { });
		object = this._preprocess(this.options.object);
		if (this.readyState) {
			if (this.options.position == 'replace') {
				elm = element.removeAllContents().insert( {top: this.evaluate(this.options.object)} );
			} else {
				var position = {};
				position[this.options.position] = this.evaluate(this.options.object);
				elm = element.insert( position );
			}
			this.options.callback.delay(.01, elm);
		} else {
			this.queue.push({
				qtype: 'insert',
				elm: element,
				pos: this.options.position,
				obj: this.options.object,
				fnc: this.options.callback
			});
		}
		return;
	},
	onReadyState: function () {
		var q;
		while (q = this.queue.shift()) {
			var object = q.obj;
			var qtype = q.qtype;
			var callback = q.fnc;
			var elm;
			switch (qtype) {
			case 'insert':
				var element = q.elm;
				var position = {};
				if (q.pos == "replace") {
					position['top'] = this.evaluate(q.obj);
					elm = element.removeAllContents().insert( position );					
				} else {
					position[q.pos] = this.evaluate(q.obj);
					elm = element.insert( position );					
				}
				callback.delay(.01, elm);
				break;    
			case 'callback':
				if ( object ) {
					callback(this.evaluate(object));
				}
				break;
			}		
		}
	}
});	

var TemplateFactory = Class.create( Hash, {
    dir: "/js/html-templates/",

	get: function (key, bRefresh) {
		if (typeof this._object[key] != "undefined" && !bRefresh) {
			return this._object[key];
		}
		this._object[key] = new RediTemplate();
		var filename = key + '.tmpl'	
		var url = this.dir + filename;
		var tAjax = new Ajax.Request(url, {
		    method: 'get',
		    onSuccess: function(transport) {
		        this._object[key].load(transport.responseText);
		    }.bind(this)
		});
		return this._object[key];	
	}	
});
templatefactory = new TemplateFactory();

Element.addMethods({	
	fillin : function (element, options){
	    this.options = {
		  template: null,
	      object: {},
	      position: 'bottom',
	      callback: function () {}
	    };
	    Object.extend(this.options, options || { });		
		this.options.template.evaluateElement(element, this.options);
	},
	removeAllContents: function(element) {
		element = $(element);
		if (element.childElements().length) {
			element.childElements().each(function(el){
				el.removeAllContents();
				try{
					el.remove();
				} catch (e) { /* ignore this - 'too few args' error */ }
				
			});
		}
		if (element.innerHTML) {
			element.innerHTML = '';
		}
		return element;
	},
	// This function converts multiple <br>'s to a single one
	// (thus collapsing blank lines).
	collapseBlankLines: function(element) {
		var element = $(element);
		var html = element.innerHTML;
		var re = /<br[^>]*>\s*<br[^>]*>/g;
		while ( html.match(re) ) {
			html = html.replace(re,'<br>');
		}
		element.innerHTML = html;
	},
	
	// Return the larger of the styled height or prototype's computed height.
	// Under quirky circumstances, they don't always match.
	getComputedHeight: function(element) {
		var element = $(element);
		var styleHeight = parseInt(element.style.height);
		var protoHeight = element.getHeight();
		return ( styleHeight > protoHeight ? styleHeight : protoHeight );
	},
	
	// Return the height of the element plus the offset.
	// This will represent the "bottom" value of this element,
	// which is useful for deriving the actual height of the parent object.
	getOffsetHeight: function(element) {
		var element = $(element);
		var ht = element.getComputedHeight();
		var off = element.positionedOffset();
		return ht + off["top"];
	},
	
	// Set the height of the current element to the cumulative offset of all children.
	// This is to simplify fixing divs containing positioned content (e.g. from CMS...)
	fixContentHeight: function(element) {
		var element = $(element);
        var h = 0;
        
        element.descendants().each( function(elem) {
        	var el = $(elem);
        	// Apparently script tags have a height in IE....
        	if ( el.tagName != 'SCRIPT' ) {
	        	var ch = el.getOffsetHeight();
	        	if ( ch > h ) {
	        		h = ch;
	        	}
	        }
        });
        
        element.style.height = h+'px';
	}
	
});

var UIMessage = Class.create({
	REGEX_SYNTAX: /(^|.|\r|\n)(::(\w+)::)/,
	initialize: function(options) {
		this.options = Object.extend({
			LIMIT: [],
			CALLBACK: function () {}
		}, options);		
		var id = jsonrpc.fetch( 'javascript.uimessages', 
			this.options.LIMIT,
			{ 
				onSuccess: this._load_data.bind(this),
				onFailure: function () { console.log( 'UIMessage JSON failed to load ' ) }
			}
		);
		return id;
	},
	_load_data: function (t) {
		var o = t.responseText.evalJSON()[0];
		var data = o.result;
		if (o.error == null) {
			this.keys = $H(data);
		}
		this.options.CALLBACK(this);
		document.fire("messages:loaded");
	}
});

var Resource = Class.create({
	REGEX_SYNTAX: /(^|.|\r|\n)(::(\w+)::)/,
	initialize: function(options) {
		this.options = Object.extend({
			CONFIG_LIST: [],
			LIMIT: [],
			CALLBACK: function () {}
		}, options);
		var id = jsonrpc.fetch( 'javascript.resource', 
			[this.options],
			{ 
				onSuccess: this._load_data.bind(this),
				onFailure: function () { console.log( 'Resource JSON failed to load ' ) }
			}
		);
		return id;
	},
	_load_data: function (t) {
		var o = t.responseText.evalJSON()[0];
		var data = o.result;
		if (o.error == null) {
			this.keys = $H(data);
		}
		this.options.CALLBACK(this);
		document.fire("resources:loaded");
	}
});

// used later in notifyUser
NO_USER_NOTIFY = 0;

var User = Class.create({
	first_name: '',
	last_name:  '',
	signed_in: false,
	signed_in_insecure_user: false,
	recognized_user: false,
	skin_typed: 0,
	email_optin: false,
	gwp_optin: false,
	took_skin_quiz: false,
	email_address: '',
		
	initialize: function() {
		var id = jsonrpc.fetch( 'user.current', 
			[],
			{ 
				onSuccess: this._load_data.bind(this),
				onFailure: function () { console.log( 'User JSON failed to load ' ) }
			}
		);
		return id;
	},
	_load_data: function (t) {
		var o = t.responseText.evalJSON()[0];
		var data = o.result;
		if (o.error == null) {
			/*
			 * WDR: changed this to just use object-extend.
			 * We can remove this stuff when we're sure I didn't break anything else...
			this.first_name = (data.first_name || '');
			this.last_name = (data.last_name || '');
			this.signed_in = data.signed_in_user==1?true:false;
			this.recognized_user = data.recognized_user==1?true:false;
			this.registered_user = data.registered_user==1?true:false;
			this.signed_in_insecure_user = data.signed_in_insecure_user==1?true:false;
			this.skin_typed = 0;
			this.email_optin = data.email_optin==1?true:false;			
			this.gwp_optin = data.gwp_optin==1?true:false;
			this.took_skin_quiz = data.took_skin_quiz==1?true:false;
			this.email_address = (data.email_address || '');
			*/
			Object.extend ( this, data );
		}
		document.fire("user:loaded");
	},
	// Return true if EITHER signed in flag is set.
	// (Yes, i'm too lazy to test both vars in the code.)
	isSignedIn: function() {
		return ( this.signed_in || this.signed_in_insecure_user ? true : false );
	}
});

var Cart = Class.create({
	history: new Hash(),
	contents: new Hash(),
	pwp: new Hash(),
	options: {},	
	emptyMessage: '<p>Your cart is currently empty.</p>',
	addMessage:	'<p>The following items have been added to your cart:</p>',
	removeMessage: '<p>The following items have been removed from your cart:</p>',

	initialize: function(options) {
		this.options = Object.extend({
			cart_box: 'cart-box',
			cart_items: 'cart-items',
			cart_link: 'cart-link',
			cart_qty_class: '.cart-qty',
			cart_message: 'cart-message'
		}, options || {});
		var self = this;
		var id = this._contents ({ 
				success: this._load_data.bind(this),
				failure: function () { console.log( 'Cart JSON failed to load ' ) }
		});
		return id;	
	},
	_load_data: function (t) {
		var o = t.responseText.evalJSON()[0];
		var data = o.result;
		if (o.error == null) {
			this.contents = $H(data.contents);
			if ( data.pwp ) {
				this.pwp = $H(data.pwp);
			}
			if ( data.gwp ) {
				this.gwp = $H(data.gwp);
			}
		}
		this.resetDropCart(0);
		document.fire("cart:loaded");
	},
	qty: function () {
		var ttl = 0;
		this.contents.each( function (item) {
			ttl = ttl + item.value;
		});
		return ttl;
	},	
	
	// Manually add a sku qty (used by egifts)
	addSku: function(skuid,qtyToAdd) {
		var qty = this.contents.get(skuid) || 0;
		qty += qtyToAdd;
		this.contents.set(skuid,qty);
		this.updateGlobalQty();
		$(this.options.cart_message).innerHTML = this.addMessage;
	},
	
	checkSkuCat: function(sku) {
		if (sku.simple) return sku;
		if ( sku && sku.sku_id ) {
			var catid = GlobalCatProdData.getCategoryIdBySku(sku.sku_id);
			if ( catid )
				sku.category_id = catid;
		}
		return sku;
	},
	
	// addNotify does the cart add AND triggers the drop down in one step.
	// convenience method, especially for adding to cart from flash apps.
	addNotify: function() {
	    console.log('addNotify');
	    var self = this;
		var args = Array.from(arguments);
		args[args.length] = { success  : function (t) { self.userNotify(addid); } };
		var addid = this.__add(args);
	},
	
	// most of the site still calls this add function, which now just derives
	// the array of args and calls the internal __add function.
	add: function () {
		var args = Array.from(arguments);
		this.__add(args);
	},
	
	// WDR 11/6/09:
	// Split "add" into the above two functions which pull the array of skus
	// from "arguments" and pass the array here.
	__add: function (args) {
		// 'cart.setqty'
		var skus = [];
		var options = {};
		if (args.length == 1) {
			arg = args[0];
			if ( typeof arg == 'object' ) {
				sku = Object.extend({
					sku_id: '',
					shopping_id: '',
					qty: 1,
					increment: 0
				}, arg);
			} else {
				sku = {
					sku_id: arg,
					shopping_id: '',
					qty: 1,
					increment: 0,
					simple: true
				};
			}
			sku = this.checkSkuCat(sku);
			skus.push(sku);
		} else {	
			args.each( function (arg) {
				if ( typeof arg == 'object' ) {
					if (arg.success || arg.failure) {
						options = arg;
					} else {
						sku = Object.extend({
							sku_id: '',
							shopping_id: '',
							qty: 1,
							increment: 0
						}, arg);
						sku = this.checkSkuCat(sku);
						skus.push(sku);						
					}
				} else {
					sku = {
						sku_id: arg,
						shopping_id: '',
						qty: 1,
						increment: 0,
						simple: true
					};
					sku = this.checkSkuCat(sku);
					skus.push(sku);
				}						
			}, this);
		}

		var options = Object.extend({
			success: function () {},
			failure: function () {}
		}, options || {});		
		
		var self = this;
		var id = jsonrpc.fetch( 'cart.setqty', 
			skus,
			{ 
				onSuccess: function (t) {
					self.modify(t);
					options.success(t);					
				},
				onFailure: function (t) {
					options.failure(t);
				}
			}
		);
		skuids = skus.pluck('sku_id');
		this.history.set(id,{type: 'add', status: 'pending', skus: skuids});
		return id;
	},
	sub: function ( skuid, options ) {
		var options = Object.extend({
			success: function () {},
			failure: function () {}
		}, options || {});		

		var qty = this.contents.get(skuid) || 0;
		if (--qty<0) return (-1);

		var self = this;
		var id = jsonrpc.fetch( 'cart.setqty', 
			[{ sku_id: skuid, qty: qty}],
			{ 
				onSuccess: function (t) {
					self.modify(t);
					options.success(t);
				},
				onFailure: function (t) {
					options.failure(t);
				}
			}
		);
		this.history.set(id,{type: 'sub', status: 'pending', skus: [skuid]});
		return id;	
	},
	remove: function ( skuid, options ) {
		//'cart.remove'
		var self = this;
		var options = Object.extend({
			success: function () {},
			failure: function () {}
		}, options || {});		
		var id = jsonrpc.fetch( 'cart.remove', 
			[{ 
				sku_id: skuid	
			}],
			{ 
				onSuccess: function (t) {
					self.modify(t);
					options.success(t);
				},
				onFailure: function (t) {
					options.failure(t);
				}
			}
		);
		this.history.set(id,{type: 'remove', status: 'pending', skus: [skuid]});
		return id;
	},
	modify: function (t) {
		var obj=t.responseText.evalJSON()[0];
		var id = obj.id;
		var err = obj.error;
		var result = obj.result;
		if (err==null){
			var thisHistory = this.history.get(id);
			var type = thisHistory.type;
			if (type == 'remove'){
				result.detail = {};
				result.detail[thisHistory.skus[0]] = 0;
			}
			var detail = $H(result.detail);
			this.history.set(id,{type: type, status: 'complete', skus: detail});
			var self = this;
			detail.each( function (item) {
				self.contents.set(item.key, item.value);
			});
			this.userNotify(id);
			document.fire("cart:modify", obj);
		}		
	},
	_contents: function ( options ) {
		//'cart.contents'
		var options = Object.extend({
			success: function () {},
			failure: function () {}
		}, options || {});		
		var id = jsonrpc.fetch( 'cart.contents',
			[],
			{ 
				onSuccess: options.success.bind(this),
				onFailure: options.failure.bind(this) 
			}
		);
		return id;		
	},
	resetDropCart: function (id) {
		// Clear the previous items
		if ($(this.options.cart_message) && $(this.options.cart_items)) {
			$(this.options.cart_message).innerHTML = this.emptyMessage;
			$(this.options.cart_items).fillin({ template: templatefactory.get('dropcart-items-empty'), position: 'replace' });
		}
	},
	fillinDropCart: function(args) {
		$(this.options.cart_items).fillin(args);
	},
	refreshDropCart: function (id) {

		var skus = this.history.get(id).skus;
		var type = this.history.get(id).type;
		var self = this;

		// Clear the previous items
		if ($(this.options.cart_message)) {
			$(this.options.cart_message).innerHTML = (
				type=='add' ? this.addMessage : this.removeMessage
				);			
		}
		
		skus.each( function (item,index) {
			var skuid = item.key;
			var qty = item.value;
			var skuObj = GlobalCatProdData.lookupSku(skuid);
			if ( !skuObj ) return;
			var prodObj = GlobalCatProdData.lookupProduct(skuObj.PRODUCT_ID);
			if ( !prodObj ) return; // bail
			var ttl = Number(qty*skuObj.PRICE).toCurrency('$');
			var calObj={
				QTY: qty,
				TTL: ttl				
			};
			var sendObj={
				PRODUCT: prodObj,
				SKU: skuObj,
				CALC: calObj
			};
			if ($(self.options.cart_items)) {
				$(self.options.cart_items).fillin({
					template: templatefactory.get('dropcart-items'),
					position: ( index ? 'bottom' : 'replace' ),
					object: sendObj
				});
				self.showDropCart();					
			}
		});	
		
	},
	
	userNotify: function (id) {
		// set NO_USER_NOTIFY to prevent drop cart on a page
		if (!NO_USER_NOTIFY) {
			if (id) {
				this.refreshDropCart(id);
			} else {
				// notify with 0 = tell them there is nothing in the cart
				this.resetDropCart(id);
				this.showDropCart();
			}			
		}
		// update global nav quantity
		this.updateGlobalQty();
	},
	showDropCart: function () {
		$(this.options.cart_link).addClassName('active').scrollTo();
		$(this.options.cart_box).show();

		var reset = function () {
			$(this.options.cart_link).removeClassName('active');
			$(this.options.cart_box).hide();				
		};

		if( this.timeout ){
			clearTimeout(this.timeout);
		}
		this.timeout = setTimeout(reset.bind(this),8000);		
	},
	updateGlobalQty: function () {
		var ttl = this.qty();
		var stl = (ttl == 1) ? ttl + ' item' : ttl + ' items';
		$$(this.options.cart_qty_class).each( function (elem) 
		{ 
			elem.innerHTML = stl 
		});		
	}	
});

var JsonRPCForm = Class.create({
	initialize: function (form, options) {
		this.options = Object.extend({
			method: null,
			onSuccess: function () {},
			onFailure: function () {},
			msgLoading: 'Saving...',
			msgError: 'An unexpected error occurred. Please try again.',
			msgFailure: '', //'Failed!', qa didn't like this msg
			msgComplete: 'Complete.'
		}, options || {})
		form = $(form);	
		form.observe(
			'submit',
			function (evt) {
				// dont bubble this
				evt.stop();
				
				// see if we are valid
				var form = evt.element();
				var handler = form.findDefinition('FormHandler');
				if (!handler) return;
				var validator = handler.validation;				
				var valid = validator.validate();						
				if (!valid) return;
				var progress = form.down('.progress');
				progress.innerHTML = this.options.msgLoading;
				
				var id = jsonrpc.fetch( this.options.method , 
					[evt.element().serialize(true)],
					{ 
						onSuccess: function (t) {
							o = t.responseText.evalJSON()[0];
							errors = o.error;
							var messages = form.down('.error_messages') || $(document.body).down('.error_messages');
							var center = form.down('.error_messages') ? false : true;
							var progress = form.down('.progress') || null;
							var ul = messages.down('ul');	
							ul.select('li').each(function(elm){elm.remove()});							
							if (errors && errors.length) {
								// set up messages
								ul.insert({ bottom: '<li>' + errors[0] + '</li>' });
								if (center) {
									messages.show().center();
								} else {
									messages.show();
								}
								if (progress) {
									progress.innerHTML = this.options.msgFailure;
								}
								this.options.onFailure(o);								
							} else {
								messages.hide();
								if (progress) {
									progress.innerHTML = this.options.msgComplete;
								}											
								this.options.onSuccess(o)								
							}
						}.bind(this),
						onFailure: function (t) {
							var progress = form.down('.progress') || null;
							if (progress) {
								progress.innerHTML = this.options.msgError;
							}
							this.options.onFailure(o);
						}.bind(this)
					}
				);	
		
			}.bind(this)
		);	
	}
})


// ModRPCForm:
// You can turn (almost) any form into a ModRPCForm by doing this:
// - Make sure the backend handler (if any) is in the "validHandlers" list below.
// - Define your form with a unique "id" (that matches it's "name"),
//   and include the class "validation" in the form tag.
// - Somewhere on the form page, define these two div's (for progress/error messages):
//		<div class="progress"></div>
//		<div id="(some id)" class="error_messages" style="display: none"><ul class="error"></ul></div>
// - Immediately after the form definition, include this script:
//		<script type="text/javascript">
//			Definition.applyDefinition(FormHandler,'(your form id)');
//		</script>
// - Required input fields on your form should include the class "required"
//   to enable the pre-post form validation.
var ModRPCForm = Class.create({
	initialize: function (form, options) {
		this.options = Object.extend({
			onSuccess: function () {},
			onFailure: function () {},
			onValidateFailure: function () {},
			msgLoading: 'Saving...',
			msgError: 'An unexpected error occurred. Please try again.',
			msgFailure: '', //'Failed!',
			msgComplete: 'Complete.'
		}, options || {})
		form = $(form);
		
		form.observe(
			'keydown',
			function (evt) {
				var keyCode = evt.keyCode;
				console.dir(evt);
			});
		
		form.observe(
			'submit',
			function (evt) {
				// dont bubble this
				evt.stop();

				// see if we are valid
				var form = evt.element();
				var handler = form.findDefinition('FormHandler');
				if (!handler) return;
				var validator = handler.validation;				
				var valid = validator.validate();						
				if (!valid) 
				{
					this.options.onValidateFailure();
					return;
				}

				// set up url
				var urlBits = new Array('');
				var actions = form.getAttribute('action').split('/');
				var validHandlers = $w('quickreg checkout altercart signupemail lostpw signin register modaddressbook modwallet signout giftcard giftcard_balance');
				actions.each(function (action) {
					if (validHandlers.indexOf(action)>0) urlBits.push(action);
				});
				urlBits.push('modrpc.tmpl');
				var url = urlBits.join('/');
				
				// set up messages
				var messages = form.down('.error_messages') || $(document.body).down('.error_messages');
				var progress = form.down('.progress') || null;
				var ul = messages.down('ul');

				// Override the error url, add INTERNAL_REDIRECT_TARGET
				var params = Object.extend(
					evt.element().serialize(true),
					{
						ERROR_URL: '/modrpc.tmpl'
					});
				
				if ( urlBits.length==3 ) {
					/* 
						this only works with one handler, multiple handlers blow this up:
						urlBits = '','handler','modrpc.tmpl'
					*/
					params['INTERNAL_REDIRECT_TARGET'] = '/modrpc.tmpl';
				}
					
				// loose succes and return, we dont need them	
				delete params['SUCCESS_URL'];
				delete params['RETURN_URL'];
				// Post data	
				new Ajax.Request(
					url,
					{
						method: 'post',
						parameters: params,
						onLoading: function () {
							// clean up errors
							messages.hide();
							ul.select('li').each(function(elm){elm.remove()});
							
							// update stauts	
							if (progress != null) {
								progress.unsetErrorState();
								progress.innerHTML = this.options.msgLoading;
							}					
						}.bind(this),
						onFailure: function () {
							if (progress != null) {
								progress.setErrorState();
								progress.innerHTML = this.options.msgError;
							}							
						}.bind(this),
						onComplete: function(transport){
							var result = transport.responseText.evalJSON();
							var errMsg = '';
							if (result.REDIRECT && !result.REDIRECT.match(/modrpc/)) {
								location.href = result.REDIRECT;
								
							} else if (result.ERRORS.length) {	
																						
								result.ERRORS.each(function(err){
									errMsg = errMsg || err;
									ul.insert({ bottom: '<li>' + err + '</li>' });
								});

								if (progress != null) {
									progress.setErrorState();
									progress.innerHTML = this.options.msgFailure;
								}

								messages.show();
								messages.scrollTo();
								this.options.onFailure(result);
																		
							} else {
								if (progress != null) {
									progress.unsetErrorState();
									progress.innerHTML = this.options.msgComplete;
								}								
								// clean up errors
								messages.hide();
								ul.select('li').each(function(elm){elm.remove()});								
								this.options.onSuccess(result);
							}
						}.bind(this)
					}
				);
				return false;		
			}.bind(this)
		);	
	}
})

	
/**
* Connects a link element to a pane element, enabling the link to toggle
* the visibility of the pane. Mulitiple links can be connected to the same pane.
*
* Usage:
* panelink_args = { active_state: 'highlighted' }
* var nav_panelink = new FloatingPaneLink(["email-link"], "email-box", panelink_args),
*/
	
var FloatingPaneLink = Class.create({

    link_nodes: [],
    pane_node: null,
    active_state: "",
    
    initialize: function (link_ids, pane_id, args) {
        var self = this;
	    Object.extend(this, args);
        this.pane_node = $(pane_id);
        if (this.pane_node) {
            //
            // there can be multiple links that open the same pane.
            link_ids.each(function (link_id) {
                var lnk = $(link_id);
				var watch_event = link_id=='cart-link'?'click':'mouseover';
                if (lnk) {
                    self.link_nodes.push(lnk);
                    //
                    // Toggle pane view state if link is clicked.    
                    lnk.observe(watch_event, function(evt) {
						
						// this link takes care of itself
						if (lnk.id=="cart-link") return;
						
						// reposition boxes	
						lnk_left = lnk.positionedOffset().left+lnk.getWidth();
						pane_width = self.pane_node.getWidth();
						pane_left = ((pane_width-lnk_left)*-1)+'px';
						self.pane_node.setStyle({right: ''});
						self.pane_node.setStyle({left: pane_left});

                        self.pane_node.toggle();
						var frm = self.pane_node.down('form');
						if (frm) {
							frm.focusFirstElement();
						}
						$('server-errors').hide();
                        lnk.toggleClassName(self.active_state);
                    });
                    //
                    // add event listener to body. Close pane if anything
                    // unrelated to the link or the pane is clicked.    
                    Event.observe(document.body, watch_event, function(evt){
                        var clicked = Event.element(evt);
                        if (!(clicked.descendantOf(self.pane_node))
                           && (clicked !== lnk)
                           && (clicked !== self.pane_node) ) {
                            self.pane_node.hide();
							var frm = self.pane_node.down('form');
							if (frm) {
								$('server-errors').hide();
								frm.findDefinition('FormHandler').validation.reset();
							}
                            lnk.removeClassName(self.active_state);
                        }
                    });
                }
            });
        }
    }
});


var Progress = Class.create({

    progressNode: null,
    containerNode: null,
	
    initialize: function(args) {
        Object.extend(this,args);
    },
    start: function() {
    	if ((typeof this.containerNode !== "undefined") &&
            (typeof this.progressNode !== "undefined")) {
    	    this.containerNode.hide();
        	this.progressNode.show();
        }
    },
    revert: function() {
    	if ((typeof this.containerNode !== "undefined") &&
            (typeof this.progressNode !== "undefined")) {
        	this.containerNode.show();
        	this.progressNode.hide();
        }
    },
    onComplete: function() {
        this.clear();
    },
    onException: function() {
        this.clear();
    },
    onFailure: function() {
        this.clear();
    },
    onTimeout: function() {
        this.clear();
    }
});


// This is a basic message box class.
// Simplest usage is just to pass in a text and title string.
// This will automatically give an "ok" button.
// To specify custom text or more than one button, just pass in
// an array of text, one element per button, as the buttonText option.
// Use the buttonCallbacks option to specify the list of callbacks corresponding
// to the text buttons.
// Use the buttonStyles option to specify a list of styles to be used with each button.
// buttonStyles and buttonCallbacks parameters are optional, but if given, you must supply ALL
// the styles/callbacks to use in the order corresponding to the buttonText list.
// (You can use "null" for a list element to use the default.)
//
// The simplest example is:
//	new MessageBox ( "Greetings Earthlings" );
// This pops up a message box with an "Ok" button.
//
// A more interesting example:
//	new MessageBox ( "Help! The sky is falling!", "Caution",
//					{ buttonText: ["Ok", "Ignore", "Fire Missles"],
//					  buttonCallbacks: [handleOk, null, handleFire],
//					  buttonStyles: ['okStyle', 'cancelStyle'] } );
// This will popup a message with the given text/title and three buttons.
// Note that since only two buttonStyles are given, the third is assumed to be null.
//

var MessageBox = Class.create({

	
    initialize: function ( messagetext, messagetitle, args ) {
    	
		this.buttonText = [];
		this.buttonStyles = [];
		this.buttonCallbacks = [];
		this.messageText = messagetext;
		this.messageTitle = messagetitle || "";
		
		this.options = Object.extend({
			templateFile: '',
			useLighterbox: false,
			hideTitle: false,
			ID: generateGuid(),
			WIDTH: 350,
			HEIGHT: 350,
			TITLE_TEXT: this.messageTitle,
			MESSAGE_TEXT: this.messageText
			},args);
		
		if ( !this.options.templateFile ) {
			this.options.templateFile = ( this.options.useLighterbox ? 'message-box-overlay' : 'message-box' );
		}
	
    	if ( args ) {
    		// Figure out how many buttons were given
    		var cnt = 0;
    		if ( args.buttonText && args.buttonText.length > cnt ) cnt = args.buttonText.length;
    		if ( args.buttonStyles && args.buttonStyles.length > cnt ) cnt = args.buttonStyles.length;
    		if ( args.buttonCallbacks && args.buttonCallbacks.length > cnt ) cnt = args.buttonCallbacks.length;
    		
    		// Check each arg.  If not present, use default.
    		for ( var i=0; i<cnt; i++ ) {
    			this.buttonText[i] = ( args.buttonText && args.buttonText[i] ? args.buttonText[i] : i == 0 ? 'Ok' : 'Cancel' );
    			this.buttonStyles[i] = ( args.buttonStyles && args.buttonStyles[i] ? args.buttonStyles[i] : 'buttonDefault' );
    			this.buttonCallbacks[i] = ( args.buttonCallbacks && args.buttonCallbacks[i] ? args.buttonCallbacks[i] : null );
    		}
    		
    	} else {
    		// set up default button:
    		this.buttonText.push ( "Ok" );
    		this.buttonStyles.push ( "buttonDefault" );
    	}
    	
		// make sure we have the prerequestite background
		if ( this.options.useLighterbox && !$('lighterwindow_background') ) {
			$(document.body).insert(
				{
					top : '<div id="lighterwindow_background" class="lighterwindow_background" style="display: none"></div>'
				});
		}		
    	
    	this.createMB();
    },
    
    // All clicks start here.  This function determines the index
    // that will be used to find the targetted callback.
    // We also destroy the MB here before doing the callback.
    onClick: function(evt) {
    	var ix = this.buttonIndex;
    	var cb = this.mb.buttonCallbacks[ix] || function() {};
    	evt.stop();
    	this.mb.destroyMB();
    	cb();
    },
    
    // Create a templated message box.
    createMB: function() {
		if ( this.options.useLighterbox ) $('lighterwindow_background').show();
		$(document.body).fillin({
			template: templatefactory.get(this.options.templateFile),
			position: 'bottom',
			object: this.options,
			callback: this.loadMB.bind(this)
		});
    },
    
    // After template is created, load it up with the content.
    loadMB: function() {
    	if ( this.options.hideTitle ) $(this.options.ID+'-message-box-titlebar-text').hide();
    	
    	// Set up a close button, which is by default a cancel.
    	// (This could probably be an optional button... maybe...)
    	Event.observe( $(this.options.ID+'-message-box-titlebar-close'), 'click', this.destroyMB.bind(this) );
    	
    	// Set up each button requested.  We always base
    	// each button on the text that was given.
    	this.buttonText.each( function(el,ix) {
    		var btndiv = new Element("div");
    		btndiv.id = this.options.ID+'-message-box-buttons-id-'+ix;
    		btndiv.buttonIndex = ix;
    		btndiv.mb = this;
    		btndiv.className = this.buttonStyles[ix] || 'buttonDefault';
    		btndiv.innerHTML = el;
    		$(this.options.ID+'-message-box-buttons').insert( btndiv );
			Event.observe( btndiv, 'click', this.onClick );
    	}, this);
    	
    	// Show the ol' message
    	$(this.options.ID+'-message-box').center( { update: true, zIndex: 9001 } );
    	$(this.options.ID+'-message-box').show();
    },
    
    // Hide and remove the button from our sight!
    destroyMB: function() {
		if ( this.options.useLighterbox ) $('lighterwindow_background').hide();
    	$(this.options.ID+'-message-box').hide();
    	$(this.options.ID+'-message-box').remove();
	}
    
    
});

// General purpose status message object.
// Prints to "progess" area (e.g. "Loading...") and
// prints errors to an error area.
// Requires the include file '/templates/includes/content/status.tmpl'
// so it can write to the progress/error areas.
// You MUST pass to it the id of the parent container element that contains
// the above include file.  (This allows you to have more than one status/progress
// area on a page.)
var StatusMessage = Class.create({
	initialize: function (element, options) {
		this.parentElement = null;
		this.elMessages = null;
		this.elProgress = null;
		this.ulMessages = null;

		this.options = Object.extend({
			msgLoading: 'Saving...',
			msgError: 'An unexpected error occurred. Please try again.',
			msgFailure: '', //'Failed!',
			msgComplete: 'Complete.'
		}, options || {});
			
		this.parentElement = $(element);
			
		// set up messages
		this.elMessages = this.parentElement.down('.error_messages') || $(document.body).down('.error_messages');
		this.ulMessages = ( this.elMessages ? this.elMessages.down('ul') : null );
		this.elProgress = this.parentElement.down('.progress') || null;
	},

	onLoading: function ( msgLoading ) {
		// clean up errors
		if ( this.elMessages ) this.elMessages.hide();
		if ( this.ulMessages ) this.ulMessages.select('li').each(function(elm){elm.remove()});
		
		if ( msgLoading ) 
			this.options.msgLoading = msgLoading;
			
		// update stauts	
		if ( this.elProgress != null ) {
			 this.elProgress.unsetErrorState();
			 this.elProgress.innerHTML = this.options.msgLoading;
		}
	},
	
	onFailure: function ( msgError ) {
		if ( msgError )
			this.options.msgError = msgError;
			
		if ( this.elProgress != null ) {
			 this.elProgress.setErrorState();
			 this.elProgress.innerHTML = this.options.msgError;
		}
	},
	
	onComplete: function(errors){
		var errMsg = '';
		if ( errors && errors.length ) {
			
			errors.each(function(err){
				errMsg = errMsg || err;
				if ( this.ulMessages ) this.ulMessages.insert({ bottom: '<li>' + err + '</li>' });
			}, this);

			if ( this.elProgress != null ) {
				 this.elProgress.setErrorState();
				 this.elProgress.innerHTML = this.options.msgFailure;
			}

			if ( this.elMessages ) this.elMessages.show();

		} else {
			
			if ( this.elProgress != null ) {
				 this.elProgress.unsetErrorState();
				 this.elProgress.innerHTML = this.options.msgComplete;
			}
										
			// clean up errors
			if ( this.elMessages ) this.elMessages.hide();
			if ( this.ulMessages ) this.ulMessages.select('li').each(function(elm){elm.remove()});
		}
	}
});

flashHandlerCallbacks = {};

function flashHandlerNamer ( flash_id, event_name ) {
	return flash_id.toLowerCase() + ':' + event_name.toLowerCase();
}

function addFlashHandlerCallback ( id, evt, func ) {
	flashHandlerCallbacks[ flashHandlerNamer ( id, evt  ) ] = func ( id, evt );
}

flashToJavascript = function ( flash_id, event_name ) {
	var idEvt = flashHandlerNamer ( flash_id, event_name );
	var handler = flashHandlerCallbacks[ idEvt ];
	if ( typeof handler == 'function' ) {
		handler( flash_id, event_name );
		return true;
	} else { return false; }
};

/* FLASH TEST FUNCTIONS

	addFlashHandlerCallback ( 'test', 'test', 
		function ( id, evt ) { 
			alert ( flashHandlerNamer ( id, evt ) + ' is found.' ) });
	flashToJavascript ('test', 'test');

END TEST FUNCTIONS */
