var UltraCart = new Class({
	Implements: [Events, Options],
	options: {
		secureHostName: 'secure.ultracart.com',
		callbackUrl: 'https://secure.ultracart.com/cgi-bin/UCCheckoutAPIJSON',
		onError: $empty,
		onLoad: $empty,
		onLogError: $empty,
		onUpdate: $empty,
		onRequest: $empty,
		onReceive: $empty,
		onAddItem: $empty,
		onRemoveItem: $empty,
		onUpdateItem: $empty,
		onUpdateCart: $empty
	},
	initialize: function(mid, options) {
		this.setOptions(options);
		this.version = "1.0";
		this.merchantId = mid;
		this.callbackUrl = this.options.callbackUrl;
		this.secureHostName = this.options.securehostName;
		this.session = null;
		this.getCartInstance();
		
		// to avoid unnecessary server requests, store the last methods returned by estimateShipping()
		this.shippingEstimates = null;
	},

	genericCall: function(functionParameters, args) {
		var result = null;
	
		// Do we want async?
		var async = false;
		var onComplete;
	
		if ($defined(args) && $defined(args.async)) async = args.async;
		if ($defined(args) && $defined(args.onComplete)) onComplete = args.onComplete;
		
		// Send the request
		new Request.JSON({
			url: this.callbackUrl, 
			async: async, 
			onComplete: function(jsonResult) {
				result = jsonResult
				if (async && $defined(onComplete)) onComplete(result);
			}
		}).post(functionParameters);
		return result;
	},
	
	createCart: function(args) {
		return this.genericCall({'functionName': 'createCart', 'merchantId': this.merchantId, 'version': this.version}, args);
	},

	getCart: function(cartId, args) {
		return this.genericCall({'functionName': 'getCart', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(cartId)}, args);
	},

	// Create, or get cart instance
	getCartInstance: function() {
		// Return the cart we already have
		if (this.session != null) return this.session;
	
		if (Cookie.read('UltraCartShoppingCartID')) {
			this.session = this.getCart(Cookie.read('UltraCartShoppingCartID'));
			if (this.session == null) {
				Cookie.dispose('UltraCartShoppingCartID');
				this.session = this.createCart();
				Cookie.write('UltraCartShoppingCartID', this.session.cartId, {path: '/'});
			}
		} else {
			this.session = this.createCart();
			Cookie.write('UltraCartShoppingCartID', this.session.cartId, {path: '/'});
		}
		return this.session;
	},

	updateCart: function(args) {
		var result = this.genericCall({'functionName': 'updateCart', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
		if (result != null) this.session = result;
		if (result == null) {
			this.fireEvent('onError');
			return ["removeCoupon result was null"];
		}
		this.fireEvent('onUpdateCart');
		return this.session;
	},

	addItems: function(items, args) {
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'addItems', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(items)}, args);
		if (result != null && result.cart != null) this.session = result.cart;
		if (result == null) {
			this.fireEvent('onError');
			return ["addItems result was null"];
		}
		this.fireEvent('onAddItem');
		return result.errors;
	},

	removeItems: function(itemIds, args) {
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'removeItems', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(itemIds)}, args);
		if (result != null && result.cart != null) this.session = result.cart;
		if (result == null) {
			this.fireEvent('onError');
			return ["removeItems result was null"];
		}
		this.fireEvent('onRemoveItem');
		return result.errors;
	},

	removeItem: function(itemId, args) {
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'removeItem', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(itemId)}, args);
		if (result != null && result.cart != null) this.session = result.cart;
		if (result == null) {
			this.fireEvent('onError');
			return ["removeItem result was null"];
		}
		this.fireEvent('onRemoveItem');
		return result.errors;
	},

	clearItems: function(args) {
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'clearItems', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
		if (result != null && result.cart != null) this.session = result.cart;
		if (result == null) {
			this.fireEvent('onError');
			return ["clearItems result was null"];
		}
		this.fireEvent('onRemoveItem');
		return result.errors;
	},

	updateItems: function(items, args) {
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'updateItems', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(items)}, args);
		if (result != null && result.cart != null) this.session = result.cart;
		if (result == null) {
			this.fireEvent('onError');
			return ["updateItems result was null"];
		}
		this.fireEvent('onUpdateItem');
		return result.errors;
	},

	getAdvertisingSources: function(args) {
		return this.genericCall({'functionName': 'getAdvertisingSources', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	logError: function(error, args) {
		// Automatically include browser information when the error is logged
		error = "Browser Engine Name: " + Browser.Engine.name + "\nBrowser Engine Version: " + Browser.Engine.version + "\nBrowser Platform Name: " + Browser.Platform.name + "\n" + error;
		var result = this.genericCall({'functionName': 'logError', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(error)}, args);
		this.fireEvent('onLogError');
		return result;
	},

	getReturnPolicy: function(args) {
		return this.genericCall({'functionName': 'getReturnPolicy', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	getAllowedCountries: function(args) {
		return this.genericCall({'functionName': 'getAllowedCountries', 'merchantId': this.merchantId, 'version': this.version}, args);
	},

	getStateProvinces: function(country, args) {
		// They really shouldn't use the async call since the local call is instanteous, but we'll support it for consistency's sake
		if (args != null) {
			return this.genericCall({'functionName': 'getStateProvinces', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(country)}, args);
		}
	
		var i;
		for (i = 0; i < this.stateProvinces.length; i++) {
			if (this.stateProvinces[i].country == country) {
				return this.stateProvinces[i].stateProvinces;
			}
		}
		return new Array();
	},

	unabbreviateStateProvinceCode: function(country, code) {
		var i;
		var j;
	
		for (i = 0; i < this.stateProvinces.length; i++) {
			if (this.stateProvinces[i].country == country) {
				for (j = 0; j < this.stateProvinces[i].codes.length; j++) {
					if (this.stateProvinces[i].codes[j] == code) {
						return this.stateProvinces[i].stateProvinces[j];
					}
				}
			}
		}
		return code;
	},

	getStateProvinceCodes: function(country, args) {
		// They really shouldn't use the async call since the local call is instanteous, but for consistency sake we'll support it
		if (args != null) {
			return this.genericCall({'functionName': 'getStateProvinceCodes', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(country)}, args);
		}
	
		var i;
		for (i = 0; i < this.stateProvinces.length; i++) {
			if (this.stateProvinces[i].country == country) {
				return this.stateProvinces[i].codes;
			}
		}
	
		return new Array();
	},

	estimateShipping: function(args) {
		// to avoid unnecessary server requests, store the last methods returned by estimateShipping()
		this.shippingEstimates = this.genericCall({'functionName': 'estimateShipping', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
		return this.shippingEstimates;
	},

	getRelatedItems: function(args) {
		return this.genericCall({'functionName': 'getRelatedItems', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	getGiftSettings: function(args) {
		return this.genericCall({'functionName': 'getGiftSettings', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	getItems: function(itemIds, args) {
		return this.genericCall({'functionName': 'getItems', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(itemIds), 'parameter2': JSON.encode(this.session)}, args);
	},

	validate: function(checks, args) {
		return this.genericCall({'functionName': 'validate', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(checks)}, args);
	},

	validateAll: function(args) {
		return this.genericCall({'functionName': 'validate', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	getTaxCounties: function(args) {
		return this.genericCall({'functionName': 'getTaxCounties', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	establishCustomerProfile: function(email, password, args) {
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'establishCustomerProfile', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(email), 'parameter3': JSON.encode(password)}, args);
		if (result != null && result.cart != null) this.session = result;
		if (result == null) {
			this.fireEvent('onError');
			return ["establishCustomerProfile result was null"];
		}
		return result.errors;
	},

	getCustomerProfile: function(args) {
		return this.genericCall({'functionName': 'getCustomerProfile', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	loginCustomerProfile: function(email, password, args) {
		return this.genericCall({'functionName': 'loginCustomerProfile', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(email), 'parameter3': JSON.encode(password)}, args);
	},

	logoutCustomerProfile: function(args) {
		return this.genericCall({'functionName': 'logoutCustomerProfile', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session)}, args);
	},

	updateCustomerProfile: function(customerProfile,args) {
		return this.genericCall({'functionName': 'updateCustomerProfile', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session),'parameter2': JSON.encode(customerProfile)}, args);
	},

	checkoutHandoff: function(returnOnErrorUrl, errorMessageParameterName, args) {
		return this.genericCall({'functionName': 'checkoutHandoff', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(returnOnErrorUrl), 'parameter3': JSON.encode(errorMessageParameterName)}, args);
	},

	checkoutHandoffOnCustomSSL: function(secureHostName, returnOnErrorUrl, errorMessageParameterName, args) {
		return this.genericCall({'functionName': 'checkoutHandoff', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(secureHostName), 'parameter3': JSON.encode(returnOnErrorUrl), 'parameter4': JSON.encode(errorMessageParameterName)}, args);
	},

	googleCheckoutHandoff: function(returnOnErrorUrl, errorMessageParameterName, args) {
		return this.genericCall({'functionName': 'googleCheckoutHandoff', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(returnOnErrorUrl), 'parameter3': JSON.encode(errorMessageParameterName)}, args);
	},

	googleCheckoutHandoffOnCustomSSL: function(secureHostName, returnOnErrorUrl, errorMessageParameterName, args) {
		return this.genericCall({'functionName': 'googleCheckoutHandoff', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(secureHostName), 'parameter3': JSON.encode(returnOnErrorUrl), 'parameter4': JSON.encode(errorMessageParameterName)}, args);
	},

	paypalHandoff: function(returnOnErrorUrl, errorMessageParameterName, args) {
		return this.genericCall({'functionName': 'paypalHandoff', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(returnOnErrorUrl), 'parameter3': JSON.encode(errorMessageParameterName)}, args);
	},

	paypalHandoffOnCustomSSL: function(secureHostName, returnOnErrorUrl, errorMessageParameterName, args) {
		return this.genericCall({'functionName': 'paypalHandoff', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(secureHostName), 'parameter3': JSON.encode(returnOnErrorUrl), 'parameter4': JSON.encode(errorMessageParameterName)}, args);
	},

	validateGiftCertificate: function(giftCertificateCode, args) {
		return this.genericCall({'functionName': 'validateGiftCertificate', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(giftCertificateCode)}, args);
	},

	applyGiftCertificate: function(giftCertificateCode, args) {
		var result = this.genericCall({'functionName': 'applyGiftCertificate', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(giftCertificateCode)}, args);
		if (result != null && result.cart != null) this.session = result;
		if (result == null) {
			this.fireEvent('onError');
			return ["applyGiftCertificate result was null"];
		}
		return result.errors;
	},

	applyCoupon: function(couponCode, args) {
		// the coupon might affect shipping costs, so force updateSession() to recalculate shipping methods
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'applyCoupon', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(couponCode)}, args);
		if (result != null && result.cart != null) this.session = result.cart;
		if (result == null) {
			this.fireEvent('onError');
			return ["applyCoupon result was null"];
		}
		return result.errors;
	},

	removeCoupon: function(couponCode, args) {
		this.shippingEstimates = null;
		var result = this.genericCall({'functionName': 'removeCoupon', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(couponCode)}, args);
		if (result != null) this.session = result;
		if (result == null) {
			this.fireEvent('onError');
			return ["removeCoupon result was null"];
		}
		return result.errors;
	},

	// ip address geo-location used to estimate shipping costs
	getIpAddress: function(args) {
		var result = null;
	
		// Do we want async?
		var async = false;
		var onComplete;
	
		if (args != undefined && args.async != undefined) {
			async = args.async;
		}
		if (args != undefined && args.onComplete != undefined) {
			onComplete = args.onComplete;
		}
	
		// Send the request
		new Request({
			url: this.callbackUrl, 
			async: async, 
			onSuccess: function(responseText) {
				// Store the result into our variable.
				result = responseText;
			
				// Call their function
				if (async && onComplete != undefined) {
					onComplete(responseText);
				}
			}
		}).post({'functionName': 'getIpAddress', 'merchantId': this.merchantId, 'version': this.version});
	
		return result;
	},

	subscribeToAutoResponder: function(autoResponderName, listIds) {
		var result = genericCall({'functionName': 'subscribeToAutoResponder', 'merchantId': this.merchantId, 'version': this.version, 'parameter1': JSON.encode(this.session), 'parameter2': JSON.encode(autoResponderName), 'parameter3': JSON.encode(listIds)}, args);
		if (result != null && result.cart != null) this.session = result;
		if (result == null) {
			this.fireEvent('onError');
			return ["subscribeToAutoResponder result was null"];
		}
		return result.errors;
	}, 

	javaUrlDecode: function(str) {
		return _utf8_decode(unescape(str)).replace(/\+/g, ' ');
	}, 

	// Private helper method for javaUrlDecode
	_utf8_decode: function(utftext) {
		var s = '';
		var i = 0;
		var c = c1 = c2 = 0;
	
		while (i < utftext.length) {
			c = utftext.charCodeAt(i);
	
			if (c < 128) {
				s += String.fromCharCode(c);
				i++;
			}
			else if ((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i + 1);
				s += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i + 1);
				c3 = utftext.charCodeAt(i + 2);
				s += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}
		}
	
		return s;
	},
	
	constant: {
		// payment methods
		paymentMethod: {
			creditCard: 'Credit Card',
			purchaseOrder: 'Purchase Order',
			payPal: 'PayPal'
		},
		
		// credit card types
		creditCard: {
			amex: "AMEX",
			discover: 'Discover',
			masterCard: 'MasterCard',
			jcb: 'JCB',
			dinersClub: 'Diners Club',
			visa: 'Visa'
		},
		
		// item option input types
		inputType: {
			single: 'single',
			multiline: 'multiline',
			dropDown: 'dropdown',
			hidden: 'hidden',
			radio: 'radio',
			fixed: 'fixed'
		},
		
		// units of measurement
		measure: {
			weight: {
				imperial:'IN',
				metric: 'CM'
			},
			length: {
				imperial: 'LB',
				metric: 'KG'
			}
		},
		
		// multimedia types
		multimedia: {
			image: 'Image',
			video: 'Video',
			unknown: 'Unknown',
			pdf: 'PDF',
			text: 'Text'
		},
		
		// integrated auto response and email marketing service providers
		emailProvider: {
			iContact: 'icontact',
			silverPop: 'silverpop',
			mailChimp: 'mailchimp',
			lyris: 'lyris',
			campaignMonitor: 'campaignMonitor',
			getResponse: 'getResponse'
		},
		
		// validation options
		validate: {
			// item
			itemQuantity: 'Item Quantity Valid',
			itemsRequired: 'Items Present',
			itemLimit: 'One per customer violations',
			itemRelationship: 'Merchant Specific Item Relationships',
			quantity: 'Quantity requirements met',
			options: 'Options Provided',
			priceLimit: 'Pricing Tier Limits',
			
			// billing address
			billToAddress: 'Billing Address Provided',
			billToStateCode: 'Billing State Abbreviation Valid',
			billToPhone: 'Billing Phone Numbers Provided',
			billToCityStateZip: 'Billing Validate City State Zip',
			email: 'Email provided if required',
			
			// shipping address
			shipToAddress: 'Shipping Addres Provided',
			shipToStateCode: 'Shipping State Abbreviation Valid',
			shipToCityStateZip: 'Shipping Validate City State Zip',
			shipToRestriction: 'Shipping Destination Restriction',

			// shipping method and gift wrap
			shippingMethod: 'Shipping Method Provided',
			shippingPaymentConflict: 'Credit Card Shipping Method Conflict',
			shipDate: 'Valid Ship On Date',
			shipping: 'Shipping Needs Recalculation',
			giftMessage: 'Gift Message Length',
			taxCounty: 'Tax County Specified',

			// payment
			payment: 'Payment Information Validate',
			paymentMethod: 'Payment Method Provided',
			cvv2: 'CVV2 Not Required',
			eCheck: 'Electronic Check Confirm Account Number',

			
			// referrer
			advertisingSource: 'Advertising Source Provided',
			referralCode: 'Referral Code Provided',

			// customer profile
			profileExists: 'Customer Profile Does Not Exist.',
			all: 'All'
		}
	},

	shipTo: {
		address1: null,
		address2: null,
		city: null,
		state: null,
		postalCode: null,
		country: null
	},

	// Static data used by the getStateProvinces() and getStateProvinceCodes()
	stateProvinces: [
		{
		'country': 'United States',
		'stateProvinces' : ["Alabama","Alaska","American Samoa","Arizona","Arkansas","Armed Forces Africa","Armed Forces Americas","Armed Forces Canada","Armed Forces Europe","Armed Forces Middle East","Armed Forces Pacific","California","Colorado","Connecticut","Delaware","District of Columbia","Federated States of Micronesia","Florida","Georgia","Guam","Hawaii","Idaho","Illinois","Indiana","Iowa","Kansas","Kentucky","Louisiana","Maine","Marshall Islands","Maryland","Massachusetts","Michigan","Minnesota","Mississippi","Missouri","Montana","Nebraska","Nevada","New Hampshire","New Jersey","New Mexico","New York","North Carolina","North Dakota","Northern Mariana Islands","Ohio","Oklahoma","Oregon","Palau","Pennsylvania","Puerto Rico","Rhode Island","South Carolina","South Dakota","Tennessee","Texas","Utah","Vermont","Virgin Islands","Virginia","Washington","West Virginia","Wisconsin","Wyoming"],
		'codes': ["AL","AK","AS","AZ","AR","CA","CO","CT","DE","DC","FM","FL","GA","GU","HI","ID","IL","IN","IA","KS","KY","LA","ME","MH","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND","MP","OH","OK","OR","PW","PA","PR","RI","SC","SD","TN","TX","UT","VT","VI","VA","WA","WV","WI","WY","AE","AA","AE","AE","AE","AP"]
		},
		{
		'country': 'Canada',
		'stateProvinces' : ["Alberta","British Columbia","Manitoba","New Brunswick","Newfoundland","Northwest Territories","Nova Scotia","Nunavut","Ontario","Prince Edward Island","Quebec","Saskatchewan","Yukon Territory"],
		'codes' : ["AB","BC","MB","NB","NF","NT","NS","NU","ON","PE","QC","SK","YT"]
		}
	]
});