/**
 * @author Christian Bradley bradleyc@gsicommerce.com
 * @fileoverview
 */

/**
 * Overrides default "ajaxAddToCart" once Minicart2 is initialized.
 * @type Function
 */
var ajaxAddToCart;
/**
 * Overrides default "hideCart" once Minicart2 is initialized.
 * @type Function
 */
var hideCart;

/**
 * Minicart v2: AJAX/JSON Enabled Quick MiniCart Implementation
 *
 * @constructor
 * @requires zParse.js + implementation.js, prototype.js, animator.js - also: GSI, GSI.LightBox, GSI.Browser, GSI.Event
 *  
 * @param {object} 	  options		  The options for the cart. Items with an asterisk (*) are optional.
 * @config {multiple}	container   * The element or element id to use for the minicart container. Default is "minicart"
 * @config {multiple} cartCountEl The element or element id to use to update the cart count (usually in the header)
 * @config {string}   actionUrl   * The url to call for asynchronous addition to the cart. Default is "/cartHandler/index.jsp"
 * @config {string}   jsonUrl     * The url to call to retrieve the JSON for this minicart. Default is "/minicart/index.jsp"
 * @config {string}   templateUrl * The url to call to retrieve the zParse template. Default is "/minicart/template.html"
 * @config {number}   timeout     * Timeout in ms for each phase of the minicart. Default is 10 seconds.
 * @config {boolean}  debugMode   * True to display debug messages etc. Default is false.
 * @config {number}   retries     * Number of times to retry after ajax Failure. Default is 3.
 * @config {number}		closeTimeout * Time in ms to wait before hiding the cart after item has been added.
 *
 * @event onStart						Fires when the process has begun
 * @event onError						Fires when an exception has occured within the minicart process
 * @event onAjaxException		Fires on any Ajax Exception 
 * @event onAjaxFailure			Fires on any Ajax Failure
 * @event onTimeout					Fires on any Ajax Timeout
 * @event onRetry						Fires when a retry has occured
 * @event onRedirect				Fires when the page is being redirected. This is usually because item is out of stock.
 * @event onTemplateLoaded	Fires when the zParse template source has loaded.
 * @event onDataReady				Fires when data is ready
 * @event onItemAdded				Fires when an item has been added
 * @event onComplete				Fires when the process has completed
 */
GSI.Minicart2 = function( options )
{
	/**
	 * Options for the minicart (see options in constructor params)
	 * @type Object
	 */
	this.options = Object.extend({
		debugMode: false,
		container: "minicart",
		actionUrl: "/cartHandler/index.jsp",
		jsonUrl: "/minicart/index.jsp",
		templateUrl: "/minicart/template.html",
		timeout: 10000,
		closeTimeout: 10000,
		retries: 3
	}, options);
	
	/**
	 * Hash of incompatible browsers.
	 * @type Object
	 */
	this.incompatibleBrowsers =
	{
		IE:
		{
			"5.2": true
		}
	};
	
  /**
   * @private
   * @type Number
   */
	this.currentTries = 0;
  /**
   * The current or last request 
   * @type Ajax.Request
   */
	this.currentRequest = null;
  /**
   * The "setTimeout" id for checking Ajax timeouts
   * @private
   * @type Number
   */
	this.timeout = null;
  /**
	 * The "closeTimeout" id for closing the minicart
	 * @private
	 * @type Number
	 */
	this.closeTimeout = null;
	/**
   * The JSON source. Populated after data has returned from server.
   * @type String
   */
	this.jsonSource = "";
  /**
   * The JSON parsed data. Populated after data has returned from server.
   * @type Object
   */
	this.jsonData = {};
  /**
   * The cart queue is used to check on multiple requests.
   * @type Array
   * @private
   */
	this.cartQueue = [];
  /**
   * Boolean flag to determine whether or not the item has been added to the cart.
   * * Note that this can happen before the content is ready, after the call to the XML service.
   * @type Boolean
   */
	this.itemAdded = false;
  /**
   * The internal flag that determines whether or not we are using asynchronous requests.
   * @private
   * @type boolean
   */
	this.useAsync = false;
	/**
	 * The last used form object from ajaxAddToCart. Default or unavailable will return null.
	 * @type Element or null
	 */
	this.formObject = null;
	/**
	 * Boolean value to determine if the form is currently submitting.
	 * @type Boolean
	 */
	this.formSubmitting = false;
	/**
	 * See options.debugMode
	 * @type boolean
	 * @private
   */
	this.debugMode = this.options.debugMode;
	/**
   * The zParse implementation to use to parse the template
   * @type ZParse
   * @private
   */
	this.zParse = new ZParse(Implementation);
	/**
   * ReadyState: There has been an exception in the process
   * @type String
   * @see GSI.Minicart2.ERRORS
   */
	this.READYSTATE_ERROR = "Exception";
  /**
   * ReadyState: Instance is initializing
   * @type String
   */
	this.READYSTATE_INITIALIZING = "Initializing";
  /**
   * ReadyState: Waiting for an asynchronous call
   * @type String
   */
	this.READYSTATE_WAITING = "Waiting for asynchronous call to return";
  /**
   * ReadyState: Ready for requests
   * @type String
   */
	this.READYSTATE_READY = "Ready";
  /**
   * An array of any errors that have occurred during the previous calls
   * @type Array
   */
	this.ERRORS = [];
	
	/**
	 * The current state of the minicart. This can be one of any READYSTATE_* properties.
	 * @type String
	 * @example
	 *  // Checking for errors in the cart
   *  if(myCart.readyState == myCart.READYSTATE_ERROR)
	 *  {
	 *    alert( "An error has occurred!" + myCart.ERRORS.join("\n") );
	 *  }
	 * 
	 * @see GSI.Minicart2.READYSTATE_ERROR
	 * @see GSI.Minicart2.READYSTATE_INITIALIZING
	 * @see GSI.Minicart2.READYSTATE_WAITING
	 * @see GSI.Minicart2.READYSTATE_READY
	 */
	this.readyState = this.READYSTATE_INITIALIZING;

  /**
   * Event dispatched when the template file has been loaded
   * @type GSI.Event
   */
	this.onTemplateLoaded = new GSI.Event();
  /**
   * Event dispatched when a timeout has occured
   * @type GSI.Event
   */
	this.onTimeout = new GSI.Event();
  /**
   * Event dispatched when an error has occured
   * @type GSI.Event
   */
	this.onError = new GSI.Event();
  /**
   * Event dispatched on any AJAX Failure
   * @type GSI.Event
   */
	this.onAjaxFailure = new GSI.Event();
  /**
   * Event dispatched on any AJAX Exception
   * @type GSI.Event
   */
	this.onAjaxException = new GSI.Event();
  /**
   * Event dispatched when data is returned from the async store call
   * @type GSI.Event
   */
	this.onDataReady = new GSI.Event();
  /**
   * Event dispatched when item has successfully been added to the cart.
   * @type GSI.Event
   * @example
   *  function myCart_itemAdded( d )
   *  {
   *  //  Alert all available properties returned from the event
   *    alert( [d.skusAdded,d.qtyAdded,d.itemsInCart].join(",") );
   *  }
   */
  this.onItemAdded = new GSI.Event();
  /**
   * Event dispatched when a retry on an AJAX request has occured
   * @type GSI.Event
   */
	this.onRetry = new GSI.Event();
  /**
   * Event dispatched when the data has been parsed with the current template and
   * the container has been updated with the resulting content.
   * @type GSI.Event
   */
	this.onContentReady = new GSI.Event();
  /**
   * Event dispatched when minicart must redirect.
   * This usually occurs when an item is not available.
   * @type GSI.Event
   */
	this.onRedirect = new GSI.Event();
  /**
   * Event dispatched when the entire minicart process is completed.
   * @type GSI.Event
   */
	this.onComplete = new GSI.Event();
  /**
   * Event dispatched when the minicart process has started.
   * @type GSI.Event
   */
	this.onStart = new GSI.Event();

  // Show errors if debugmode is on
	if( this.debugMode === true ) { this.onError.observe("self",this.showErrors,this); }
	
	// Observe failure-type methods to submit the form as a fallback
	this.onAjaxFailure.observe("self",this.submitForm,this );
	this.onTimeout.observe("self",this.submitForm,this);
	this.onError.observe("self",this.submitForm,this);
	
	// Create the container element if none exists
	if( this.getContainer() === null ) { GSI.$e("div", this.options.container, "minicart", false, document.body); }

	/**
	 * Animation for showing and hiding the container
	 * @type Animator
	 * @private
	 */
	this.animation = new Animator({
		duration: 200,
		interval: 40
	});
	
	this.animation.addSubject( new NumericalStyleSubject(this.getContainer(),'opacity',0,1) );
		
	// Overwrite default functions
	ajaxAddToCart = this.ajaxAddToCart.bind(this);
	hideCart = this.hide.bind(this);
	
  // Add the current instance
	GSI.Minicart2.instances.push(this);
	
	// Get the template
	this.getTemplate();
};

/**
 * An array of Minicart2 instances
 * @type Array
 * @static
 */
GSI.Minicart2.instances = [];

/**
 * Get the container element for the minicart display
 * @returns {element or null} The container element or null
 */
GSI.Minicart2.prototype.getContainer = function()
{
	return $( this.options.container );
};

/**
 * Fallback method to submit form. Checks to see if item has been added first.
 * If itemAdded = true, it simply refreshes the current window. Otherwise,
 * it will automatically submit the form manually.
 * @private
 */
GSI.Minicart2.prototype.submitForm = function()
{
	if( this.itemAdded )
	{
		window.location = window.location;
		return;
	}

	if( this.formObject && !this.formSubmitting )
	{	
		this.formSubmitting = true;
		this.formObject.submit();
		return;
	}
};

/**
 * Get the zParse template source file via AJAX
 * @returns {GSI.Minicart2} the current instance
 */
GSI.Minicart2.prototype.getTemplate = function()
{
	this.readyState = this.READYSTATE_WAITING;
	
	this.startTimer();
	this.currentTries = 0;
	
	this.currentRequest = new Ajax.Request( this.options.templateUrl, {
		parameters: {refresh:GSI.uId()},
		onSuccess: this.getTemplate_success.bind(this),
		onException: this.ajaxException.bind(this),
		onFailure: this.ajaxFailure.bind(this)
	});
	
	this.currentRequest.retryMethod = this.getTemplate.bind(this);
	return this;
};

/**
 * Start the timer for AJAX timeouts.
 * @private
 */
GSI.Minicart2.prototype.startTimer = function()
{
	this.timeout = setTimeout( this.dispatchTimeout.bind(this), this.options.timeout );
};
/**
 * Function called after timeout has been exceeded for AJAX Calls.
 * @private
 */
GSI.Minicart2.prototype.dispatchTimeout = function()
{
	this.onTimeout.dispatch();
};

/**
 * Stop the timer for AJAX timeouts (called after request completion)
 * @private
 */
GSI.Minicart2.prototype.stopTimer = function()
{
	clearTimeout( this.timeout );
};

/**
 * Success handler for getTemplate
 * @param {Object} transport
 * @private
 */
GSI.Minicart2.prototype.getTemplate_success = function( transport )
{
	this.stopTimer();

	if( this.zParse.parse( transport.responseText ) )
	{
		this.useAsync = true;
		this.readyState = this.READYSTATE_READY;
		this.onTemplateLoaded.dispatch();
	}
	else
	{
		this.ERRORS.push({message:"Could not parse template",text: transport.responseText});
		this.readyState = this.READYSTATE_ERROR;
		this.onError.dispatch();
	}
	if( this.cartQueue.length > 0 ) { this.ajaxAddToCart( this.cartQueue.pop() ); }
};


/**
 * Failure handler for AJAX calls
 * @param {Object} request
 * @private
 */
GSI.Minicart2.prototype.ajaxFailure = function( request )
{
	this.stopTimer();

	if( this.currentTries < this.options.retries && typeof request.retryMethod == 'function' )
	{
		request.retryMethod();
		this.onRetry.dispatch();
		return;
	}
	
	this.ERRORS.push(Object.extend({message:"AJAX call Failed"},request));
	this.readyState = this.READYSTATE_READY;
	this.onAjaxFailure.dispatch();
};


/**
 * Show a list of the current errors, followed by the JSON source (if applicable).
 * Useful for debugging, you can add to onError event handler method like so:
 *
 * @returns {GSI.Minicart2} the current instance
 * 
 * @example
 * 	myCart.onError.observe("cartError",myCart_error,this);
 * 	function myCart_error()
 * 	{
 * 		myCart.showErrors();	
 * 	}
 */
GSI.Minicart2.prototype.showErrors = function()
{
	var i,len,e,errors="";
	for(i=0,len=this.ERRORS.length;i<len;i++)
	{
		e = this.ERRORS[i];
		for( each in e )
		{
			try { errors += [i+1,". ",each,":",e[each],"\r\n"].join(""); } catch(err) {}
		}
	}
	alert(errors);
	if(this.jsonSource) { alert(this.jsonSource); }
	
	return this;
};

/**
 * Cancel the request. Not yet implemented.
 * @returns {GSI.Minicart2} the current instance
 */
GSI.Minicart2.prototype.cancel = function()
{	
//	TODO: Add AJAX and/or form cancellation
	return false;
};

/**
 * Exception handler for AJAX.
 * @param {object} request
 * @param {object} exception
 * @private
 */
GSI.Minicart2.prototype.ajaxException = function( request, exception )
{
	this.stopTimer();

	var e = exception.message || exception.toString();
	this.ERRORS.push({message:"AJAX call Exception", request: request, exception: exception, msg: e});
	
	this.readyState = this.READYSTATE_ERROR;
	this.onAjaxException.dispatch();	
};

/**
 * Add to cart via AJAX
 * @param {element}	formObj		The form Object
 * @returns {multiple} Ajax.Request if successful, string message otherwise
 */
GSI.Minicart2.prototype.ajaxAddToCart = function( formObject )
{
	clearTimeout(this.closeTimeout);
	this.itemAdded = false;
	this.formObject = $(formObject);
	
	if( !this.useAsync )
	{
		this.submitForm();
		return "Asynchronous cart addition has been disabled, most likely due to an exception.";
	}
	
	if( this.cartQueue.length > 0 ) { return "Please wait for the previous action to complete";}
	
	if( this.readyState !== this.READYSTATE_READY ) 
	{
		if( this.readyState !== this.READYSTATE_ERROR ){ this.cartQueue.push( formObject ); }		
		return "Request has been queued, will be called when ready.";
	}
	
	//	Handle unsupported browser versions
	var browser = GSI.Browser.browser;
	var version = GSI.Browser.version;
	var incompat = this.incompatibleBrowsers[browser];
	if( incompat && incompat[version] )
	{
		this.submitForm();
		return ["Incompatible browser version. (",browser,version,")"].join("");
	}
	
	this.readyState = this.READYSTATE_WAITING;
	this.onStart.dispatch();
	
	var pars = {
		values: {},
		add:function(name,value) {
			var val = this.values[name];
			
			if(val===undefined){ this.values[name] = value;}
			else if(val instanceof Array){ this.values[name].push(value); }
			else {this.values[name] = [val]; }
			
			return this;
		}
	};
	
	var elem = undefined;
	for(var i=0;i < formObject.elements.length;i++)
	{
		elem = formObject.elements[i];
		if( elem.type == 'checkbox' && !elem.checked ){break;}
		pars.add( elem.name, elem.value );
	}

// make sure the carthandler knows its getting an async call.
	pars.add("async","true").add("refresh",GSI.uId()); 
	
	this.startTimer();
	this.currentTries = 0;

	this.currentRequest = new Ajax.Request( this.options.actionUrl, {	
		method: 'post', 
		parameters: pars.values, 
		onSuccess: this.ajaxAddToCart_success.bind(this), 
		onFailure: this.ajaxFailure.bind(this),
		onException: this.ajaxException.bind(this)
	});
	this.currentRequest.retryMethod = this.ajaxAddToCart.bind(this,formObject);
	
	return this.currentRequest;
};

/**
 * Handle AJAX call success from addToCart
 * @param {Object} transport
 * @private
 */
GSI.Minicart2.prototype.ajaxAddToCart_success = function( transport )
{
	this.stopTimer();

	this.readyState = this.READYSTATE_READY;
	this.onDataReady.dispatch();
	
//	Scroll to the top to see the minicart
	scroll(0,0);
	
//	Get the response text
	var txt = transport.responseText;	

//	Check for success code sent from server
	var skusAdded,qtyAdded,itemCount,url;
	if (txt.indexOf("AJAX_SUCCESS") > -1)
	{
  //  Get Values from the XML
    skusAdded = GSI.Minicart2.getXmlValue(txt, "skusAdded");
    qtyAdded = GSI.Minicart2.getXmlValue(txt, "qtyAdded");
    itemCount = GSI.Minicart2.getXmlValue(txt,"itemCount");
    
  // Dispatch the onItemAdded event
		this.itemAdded = true;
    this.onItemAdded.dispatch({ skusAdded: skusAdded, qtyAdded: qtyAdded, itemsInCart: itemCount });
    
	//	Update the cart counter
		this.updateCartCount( itemCount );
	
	//	Get the order details
		this.getOrderDetails( skusAdded, qtyAdded );
	}
//	Otherwise redirect to the error page passed back
	else
	{
		url = GSI.Minicart2.getXmlValue( txt, 'rdir' );
		this.onRedirect.dispatch( url );
		window.location = url;
	}
};

/**
 * Update the element that displays the number of items in the cart.
 * @param {Number}	count		The number of items in the cart
 * @private
 */
GSI.Minicart2.prototype.updateCartCount = function( count )
{
	var el = this.options.cartCountEl;
	if( count === undefined ) {return;}
	if( el === undefined || $(el) === null ) {return;}

	$( el ).update( count+'<span class="itemCountLabel"> items</span>' );
};

/**
 * Get the order details via AJAX
 * @param {String}		skusAdded	The number of skus added
 * @param {Number}	qtyAdded	The quantity added
 * @private
 * @returns {Ajax.Request} The request used for the transaction
 */
GSI.Minicart2.prototype.getOrderDetails = function( skusAdded, qtyAdded )
{	

	this.readyState = this.READYSTATE_WAITING;

	this.startTimer();
	this.currentTries = 0;
	this.currentRequest = new Ajax.Request( this.options.jsonUrl, { 
		method: 'GET', 
		parameters:'skusAdded=' + skusAdded + '&qtyAdded=' + qtyAdded + '&refresh=' + GSI.uId(), 
		onSuccess: this.updateCart.bind(this),
		onFailure: this.ajaxFailure.bind(this),
		onException: this.ajaxException.bind(this)
	});
	this.currentRequest.retryMethod = this.getOrderDetails.bind(this,skusAdded,qtyAdded);
	return this.currentRequest;
};

/**
 * Update the Cart with the results of the call
 * @param {Object} transport
 * @private
 */
GSI.Minicart2.prototype.updateCart = function( transport )
{
	this.stopTimer();
	
//	Get the response text from the AJAX call
	var txt = transport.responseText;
	var i1 = txt.indexOf("{");
	var html = "";
	
	txt = txt.substring(i1);
	this.jsonSource = txt;	
	
	try
	{
		this.jsonData = txt.evalJSON();
		html = this.zParse.process( this.jsonData );
	}
	catch(e)
	{
		this.ERRORS.push(e);
		this.readyState = this.READYSTATE_ERROR;
		this.onError.dispatch();
		
		return;
	}

	this.getContainer().update( html );
	this.show();
	
	this.readyState = this.READYSTATE_READY;
	this.onContentReady.dispatch();
	this.onComplete.dispatch();
	this.closeTimeout = setTimeout(this.hide.bind(this), this.options.closeTimeout);
};

/**
 * Hide the minicart
 */
GSI.Minicart2.prototype.hide = function()
{
	GSI.Minicart2.toggleSelects(false);
	this.animation.reverse();
	clearTimeout( this.closeTimeout );
	setTimeout( function(){this.getContainer().hide()}.bind(this), 1000 );
	return;
};

/**
 * Show the minicart
 * @returns {GSI.Minicart2} The current instance
 */
GSI.Minicart2.prototype.show = function()
{
	GSI.Minicart2.toggleSelects(true);
	var e = this.getContainer();
	e.setStyle({opacity:0});
	e.show();
	
	this.animation.play();
	return this;
};


// Utility methods
//////////////////////////////////////////////////////////

/**
 * Toggle the select boxes for IE
 * @param {boolean} showSelects
 * @static
 */
GSI.Minicart2.toggleSelects = function(showSelects)
{
// Fix IE's SelectBox bleed-through issue
	if( !GSI.Browser.is("IE")) {return;}

	var s = document.getElementsByTagName("select");
	var i,len;
	if ( showSelects )
	{
		for (i=0,len=s.length; i<len; i++) 
		{
			s[i].hide();
		}
	}
	else
	{
		for (i=0,len=s.length;i<len; i++) 
		{
			s[i].show();
		}
	}
};


/**
 * Returns the value of a node "nodeName" within the XML text "xmlText"
 * @param {String} xmlText	The XML text to search within		
 * @param {String} nodeName	The node to search for
 * @type String
 * @static
 */
GSI.Minicart2.getXmlValue = function( xmlText, nodeName )
{
//	Match the beginning node by name
	var pattern = 	"<\\s*" + nodeName + "\\s*>";
//	Get the text within the node
			pattern += "([\\n\\r\\t\\v\\f\\d\\D\\w\\W\\s\\S]*)";
//	Match the close node
			pattern += "<\\s*/\\s*" + nodeName + "\\s*>";
	
	var result = new RegExp( pattern ).exec( xmlText );
	return ( result === null ) ? "" : result[1]; 
};
