﻿var Page = {
	initialize: function() {
		this.ajax = this.doAsyncPostBack;

		if(typeof PageScriptInit == "function") {
			// Initialize WebMethods and Event Bindings if present
			PageScriptInit();
			// TODO: Find all buttons and add a function which sets them as the __EVENTTARGET when they are clicked
		}
	},
	
	requests: {},
	
	/**
	 * Proxies .Net __doPostBack
	 * @param {DOMEvent} e
	 */
	doPostBack: function(e) {
		// Find the element associated with the event
		var e = e || window.event;
		Event.stop(e);
		var element = Event.element(e);
		// Call __doPostBack, use element ID if element does not have a Name
		window.__doPostBack((element.name === "") ? element.id : element.name);
	},
	
	/**
	 * Initiates asynchronous request lifecycle
	 * @param {DOMEvent/Bool} e Use false if initiated programatically
	 * @param {Object} options An object with PostBack options:
	 *    target:Server-side event target (UniqueID of .Net control)
	 *    element: Associated element if 'e' === false
	 *    argument: Server-side event argument
	 *    onBeforeRequest: Custom event to fire before request is initiated
	 *    onAfterResponse: Custom event to fire once request is complete
	 */
	doAsyncPostBack: function(e, postBackOptions) {
		var options = postBackOptions || {};
		var targetForm, form, element;
	
		if (e === false) {
			// doAsyncPostBack invoked without DOM Event
			targetForm = $('__EVENTTARGET').up('form');
			form = $H(Form.serialize(targetForm,true));
			form._object.__EVENTTARGET = options.target || '';
			form._object.__EVENTARGUMENT = options.argument || '';
		} else {
			element = e.element();
			
			// Fix for Safari. If the Event Element is a child of the submitting control, move up the
			// DOM tree to find the BUTTON or A element
			if (element.nodeName != 'INPUT' && element.nodeName != 'SELECT' && element.nodeName != 'BUTTON' && element.nodeName != 'A') {
				element = element.up('button, a') || element;
			}
			// Do not stop the click event when the target is a radio or checkbox
			if (element.type != 'radio' && element.type != 'checkbox') {
				e.stop();
			}
			targetForm = element.up('form');
			// Create a Hash Object from the form values on the page
			form = $H(targetForm.serialize(true));
			
			if (element.nodeName == "A") {
				// Get Event Target and Event Argument from link buttons
				var query = element.href.toQueryParams();
				if (!options.target) {
					options.target = query["__EVENTTARGET"];
				}
				if (!options.argument) {
					options.argument = query["__EVENTARGUMENT"];
				}
			}
			if (element.nodeName == 'SELECT') {
				// Get Event Argument for Drop Downs
				options.argument = element.getValue();
			}
			if (options.target) {
				// Event Target set programatically, use that
				form.set('__EVENTTARGET', options.target);
			} else {
				// Event Target is a form element
				form.set('__EVENTTARGET', element.name);
			}
			if (options.argument) {
				form.set('__EVENTARGUMENT', options.argument);
			}
		}
		form.each(function(pair) {
			// Remove ALL Buttons from POST: they will override __EVENTTARGET if provided, confusing .Net
			// See: http://dev.rubyonrails.org/ticket/5031
			// TODO: This could be optimized via the document.forms object
			var el = document.getElementsByName(pair.key)[0];
			// Get elements by name instead
			// var el = $(pair.key.replace(/\$/g,'_'));
			if (el && ((el.nodeName == 'INPUT' && (el.type == "submit" || el.type == "image" || el.type == "button")) || el.nodeName == 'BUTTON')) {
				form.unset(pair.key);
			}
		});
		
		// Store options for the Ajax Request
		options = Object.extend(options, {
			form: form,
			method: 'post',
			action: targetForm.action,
			element: element || document,
			preRequestEventKey: 'pop:xhr' + Math.random().toString().split('.')[1]
		});
		
		this.requests[options.preRequestEventKey] = function(e2) {
			document.stopObserving(e2.eventName, this.requests[e2.eventName]);
			delete this.requests[e2.eventName]
			
			// Make the request with modified values if supplied (in the memo of the event)
			new Ajax.Request(e2.memo.action,{
				method: e2.memo.method,
				parameters: e2.memo.form.toObject(),
				onSuccess: function(transport) {
					// Process the response XML
					this.__processResponse(transport);
					if (e2.memo.onAfterResponse) {
						document.fire(
							e2.memo.onAfterResponse,
							Object.extend((e2.memo || {}), {
								transport: transport
							})
						);
					}
				}.bind(this),
				onException: function(transport, exception) {
					try { console.log(exception); } catch(e) {}
				}
			});
		}.bind(this);
		
		// Bind the actual Ajax.Request call to the custom event "preRequestEventKey"
		document.observe(options.preRequestEventKey, this.requests[options.preRequestEventKey]);
		
		if (!options.onBeforeRequest) {
			// If no custom pre-request event is defined, fire the request event right away.
			options.element.fire(options.preRequestEventKey, options);
		} else {
			// you MUST have this line in at least one of your pre request event handlers
			// or else the request life cycle will die:
			//    document.fire(e.memo.preRequestEventKey, e.memo);
			options.element.fire(options.onBeforeRequest, options);
		}
	},
	
	/**
	 * Process the Ajax Response
	 * 
	 * @param {DOMEvent} e
	 * @param {XmlHttpRequest} oXmlHttp XmlHttpRequest Object
	 */
	__processResponse: function(oXmlHttp) {
		this.response = new Page.ResponseHandler(oXmlHttp);
	},
	
	/**
	 * Collection of WebMethods and function for calling them on the server
	 * TODO: Strip this out if no web methods are present
	 */
	WebMethod: {
		/**
		 * Calls a method on the server
		 */
		_invoke: function(methodName) {
			var x = "";
			for (var i = 1; i < arguments.length; ++i) {
				if (i != 1) {
					x += "&";
				}
				// TODO: escape arguments
				x += i.toString() + "=" + arguments[i];
			}

			var options = {
				method: 'post',
				parameters: x,
				asynchronous: false,
				requestHeaders: { "X-WebMethod-Name": methodName }
			};

			var response = new Ajax.Request(location.href, options);
			var result = null;
			
			if (response.getHeader('Content-Type').match(/^text\/xml/gi)) {
				result = response.transport.responseXML;
			} else {
				result = response.transport.responseText;
			}
			return result;
		}
	},
	
	/**
	 * Class for processing ajax Response XML
	 * @property {String/Bool} redirect A URL to redirect to
	 * @property {String/Bool} callback The name of a JavaScript function
	 * @property {Array} errors Errors returned from postback processing
	 * @property {Array} contents Content items to dump back on the page
	 * @property {XMLDocument} xml XML Response document from ajax
	 */
	ResponseHandler: Class.create({
		/**
		 * Populates all of the Response properties, if found
		 * @param {XMLHttpRequest} xhr A XMLHttpRequest object after response
		 */
		initialize: function(xhr) {
			this.text = xhr.responseText;
			this.xml = xhr.responseXML;
			try {
				// Immediately redirect if instructed to
				this.redirect = this.xml.getElementsByTagName('redirect')[0].firstChild.nodeValue;
				if (this.redirect) {
					location.href = this.redirect;
				}
			} catch(e) { this.redirect = false; }
			try {
				// store callback script
				this.callback = this.xml.getElementsByTagName('callback')[0].firstChild.nodeValue;
			} catch(e) { this.callback = false; }
			try {
				// store response content items
				this.contents = this.xml.getElementsByTagName('contents')[0].childNodes
			} catch(e) { this.contents = []; }

			// Collect the viewstate text nodes and combine them into a single string
			var state = "";
			var nodes = this.xml.getElementsByTagName('viewstate')[0].childNodes;
			for(var i=0; i<nodes.length; i++) {
				state += nodes[i].nodeValue;
			}
			this.viewState = state;
			// Update the viewstate hidden input
			document.getElementsByName("__VIEWSTATE")[0].value = this.viewState;
			
			this.updateContent();
		},
		/**
		 * Places a single response content item on the page
		 *
		 * @param {DOMNode} nItem An XML content node from the response
		 */
		updateContentItem: function(nItem) {
			var tmp = document.createElement('div');
			tmp.innerHTML = nItem.firstChild.nodeValue;
			
			var newcontent = $(tmp.firstChild);
			var destination = $(nItem.getAttribute('rel'));

			if (newcontent.nodeType == 3) {
				// Text-Only - update destination element content with new string
				destination.update(newcontent.nodeValue)
			} else {
				if (destination.nodeName != 'INPUT') {
					// TODO: Should other elements be excluded from updating???
					// HTML - Update destination element with new content and attributes
					destination.update(newcontent.innerHTML);
				}
				// Updates the attributes of the old content item based on the new item
				// Can not iterate over Attributes collection because IE is broken -- make own collection from HTML
				var attributes = tmp.innerHTML.match(/.*>/i)[0].replace(/>.*/,'').replace(/<\w+\s+/,'').match(/\w+(?==)/gim);
				// Remove the "CLASS" attribute if none was sent
				if (attributes.indexOf("class") == -1 && destination.className.length > 0) {
					destination.className = "";
				}
				for(var i=0; i<attributes.length; i++) {
					if (attributes[i] == 'id') {
						continue;
					} else if (attributes[i] == 'style' && document.all) {
						// IE doesn't allow setAttribute on the 'style' attribute
						destination.style.cssText = newcontent.readAttribute(attributes[i]);
					} else if (attributes[i] == "class") {
						destination.className = newcontent.className;
					} else {
						destination.setAttribute(attributes[i], newcontent.readAttribute(attributes[i]));
					}
				}
			}
		},
		/**
		 * Loops throught the response content nodes and places each at the specified
		 * spot on the page
		 */
		updateContent: function() {
			for (var i=0; i<this.contents.length; i++) {
				this.updateContentItem(this.contents[i]);
			}
			// Evaluate callback script, if set
			if (this.callback) { eval(this.callback); }
		}
	})
}
// Initialize the page object
document.observe('dom:loaded',Page.initialize.bindAsEventListener(Page));