/**
 * @author Victor Bolshov <crocodile2u@yandex.ru>
 * @version 1.0 (2007-12-04)
 *
 * Morf makes all the forms on a page to act via AJAX -
 * unless a special runat="client" attribute is provided.
 */

Morf = {
	NS: 'x',// Morf namespace for attributes
	
	// -=-=-=-=- Start: public interface -=-=-=-=-
	/**
	 * Apply Morf to all forms below parent
	 * @param DOMElement parent
	 */
	run: function(parent) {
		var nodeList, f;
		if ((arguments.length < 1) || !parent.getElementsByTagName)
			nodeList = document.forms;
		else
			nodeList = parent.getElementsByTagName('form');
		
		for (var i = 0; i < nodeList.length; ++i)
		{
			Morf.apply(nodeList.item(i));
		}
	},
	// -=-=-=-=- End: public interface -=-=-=-=-
	
	// -=-=-=-=- Start: form handling functions -=-=-=-=-
	/**
	 * Iterate through form elements' collection
	 * @param HTMLFormElement form
	 * @param callback callback
	 */
	each: function(form, callback) {
		for (var i = 0; i < form.elements.length; ++i)
		{
			callback(form.elements[i], form);
		}
	},
	/**
	 * Apply Morf to a single form.
	 * @param HTMLFormNode f
	 */
	apply: function(f) {
		// sanitize form action
		if (! f.action)
		{
			f.action = location.href.toString();
		}
		// create error-list
		Morf.createErrorList(f);
		if ('client' != Morf.attr(f, 'runat'))
		{
			f.xServed = Morf.serveMultipart(f);
		}	
		// listen to submissions
		Morf.createListener(f, 'submit', function(e) {
			e = Morf.event(e);
			// validate form
			if (! Morf.validate(f))
			{
				Morf.failure(f, f.validator.errors);
				return false;
			}
			// submission handler
			if (('client' != Morf.attr(f, 'runat')) && ! f.xServed)
			{// we really should listen
				Morf.ajax(
					f, // the form
					function(text) {// success callback
						Morf.respondToText(f, text);
					},
					function(text) {// failure callback
						Morf.failure(f, ["XMLHttpRequest failed"]);
					});
				return false;
			}
		});
		// apply validation
		f.validator = new Morf.validator(f);
		Morf.validate(f);
	},
	/**
	 * Handle form successful submission
	 * @param HTMLFormElement form
	 * @param mixed data
	 */
	success: function(form, data) {
		form.errorList.innerHTML = '';
		Morf.hide(form.errorList);
		var handler = Morf.attr(form, "done");
		if (handler)
		{
			try {
				eval("var cb = " + handler);
				cb(data);
			} catch(e) {
				alert("Invalid success callback");
			}
		} else {
			location.reload();
		}
	},
	/**
	 * Handle form submission errors
	 * @param HTMLFormElement form
	 * @param String[] list error list
	 */
	failure: function(form, list) {
		var handler = Morf.attr(form, "failed");
		if ('alert' == handler)
		{
			alert(list.join("\n"));
		} else if (handler)	{
			try {
				eval("var cb = " + handler);
				cb(list);
			} catch(e) {
				alert("Invalid failure callback");
			}
		} else {
			form.errorList.innerHTML = '';
			Morf.show(form.errorList);
			for (var i = 0; i < list.length; ++i)
			{
				var li = form.errorList.appendChild(document.createElement('li'));
				li.innerHTML += list[i];
			}
		}
	},
	/**
	 * Multipart forms are served differently.
	 * They are not submitted via AJAX, instead the data is sent to <iframe>s,
	 * either created by user or by the script itself (the script examines the `target' attribute of the form, 
	 * and if it is set, the script attempts to use the frame specified there; if the `target' attribute is
	 * not specified, then the script creates the <iframe> itself, and make the form submitted into this <iframe>).
	 * @param HTMLFormElement f
	 */
	serveMultipart: function(f) {
		if (f.xServed)
		{
			return true;
		}
		
		if (Morf.isMultipart(f))
		{
			f.setAttribute('enctype', 'multipart/form-data');
			if (f.target)
			{// suppose the user has already prepared the iframe
				var frames = document.getElementsByTagName('iframe'), frame;
				for (var i = 0; i < frames.length; ++i)
				{
					frame = frames.item(i);
					if (frame.getAttribute('name') == f.target)
					{
						Morf.createIframeListener(f, frame);
						break;
					}
				}
			} else {
				// create the iframe, for the form to be submitted into it,
				// and listen to that frame's load event
				var name = 'i' + Math.random();
				f.innerHTML += '<iframe id="' + name + '" name="' + name + '" class="x-iframe"></iframe>';
				f.target = name;
				Morf.createIframeListener(f, Morf.get(name));
			}
			
			return f.xServed = true;
		} else {
			return f.xServed = false;
		}
	},
	/**
	 * Creates <iframe> listener for multipart forms submissions
	 * @param HTMLFormElement f
	 * @param HTMLIFrameElement frame - the #ID is also accepted
	 */
	createIframeListener: function(f, frame) {
		Morf.createListener(frame, 'load', function() {
			var doc = ('undefined' == typeof(frame.contentWindow)) ? 
				frame.contentDocument :
				frame.contentWindow.document;
			if (doc.body.innerHTML.match(/[^\s]/))
			{// the iframe is not empty
				Morf.respondToText(f, doc.body.innerHTML);
			}
		});
	},
	/**
	 * Check whether the form is multipart
	 * @param HTMLFormElement f
	 */
	isMultipart: function(f) {
		return ('multipart/form-data' == f.getAttribute('enctype')) || Morf.containsFileUploads(f);
	},
	/**
	 * Check whether the form contains <input type="file"> elements
	 * @param HTMLFormElement f
	 */
	containsFileUploads: function(f) {
		for (var i = 0; i < f.elements.length; ++i)
		{
			if ('file' == f.elements[i].getAttribute('type'))
			{
				return true;
			}
		}
			
		return false;
	},
	/**
	 * @access private
	 * @param HTMLFormElement f
	 * @param String t
	 */
	respondToText: function(f, t) {
		try {
			eval('var j = (' + t + ')');
			'x' in j;// should not issue exception-throw
		} catch (e) {
			var truncated = t.substring(0, 100).replace(/\</g, "&lt;").replace(/\>/g, "&gt;");
			var append;
			if (t == truncated)
			{
				append = "";
			}
			else
			{
				var id = "d" + Math.random();
				append = '... <button class="x-invalid-response" onclick="alert(document.getElementById(\'' + id + '\').innerHTML)">Full text</button><div id="' + id + '" style="display:none;">' + t + '</div>';
			}
			j = {error_list: ["Invalid JSON response: " + truncated + append]};
		}

		if (('error_list' in j) &&  (j.error_list != null) && j.error_list.length)
		{
			Morf.failure(f, j.error_list);
		} else {
			if (! ('data' in j))
			{
				j.data = {};
			}
			Morf.success(f, j.data);
		}
	},
	/**
	 * A function used to submit a form via script.
	 * Using myform.submit() is no good - because it does not issue a 'submit' event
	 * and thus, Morf is not called :(
	 * Calling Morf.submit(myform) ensures that submit event is fired.
	 */
	submit: function(form) {
		form = Morf.get(form);
		if (form.fireEvent)
		{
			form.fireEvent('onsubmit')
		} else {
			var event = document.createEvent('HTMLEvents');
			event.initEvent('submit', true, true);
			form.dispatchEvent(event);
		}
	},
	// -=-=-=-=- End: form handling functions -=-=-=-=-
	
	// -=-=-=-=- Start: validation -=-=-=-=-
	/**
	 * Validates a form
	 * @param HTMLFormElement form
	 * @return bool
	 */
	validate: function(form) {
		form.validator.reset();// clear old errors
		Morf.each(form, function(el) {
			var err = Morf.getError(el);
			if (err)
			{
				form.validator.addError(err);
			}
		});
		return form.validator.ok();
	},
	/**
	 * Get form-element error
	 * @param form-element el
	 * @return String error message or bool false if the element passes validation
	 */
	getError: function(el) {
		if ('object' != typeof(el.validators))
		{
			Morf.createValidators(el, el.form);
		}
		for (var i = 0; i < el.validators.length; ++i)
		{
			if (! el.validators[i].validate())
			{
				Morf.addClass(el, 'x-invalid');
				return el.validators[i].error;
			}
		}
		
		Morf.removeClass(el, 'x-invalid');
		return false;
	},
	/**
	 * Form-Validator Constructor.
	 * @param HTMLFormElement form
	 */
	validator: function(form) {
		this.errors = [];
		Morf.each(form, function(el) {
			Morf.createValidators(el, form);
		});
	},
	/**
	 * Check whether value is empty.
	 * @param string value
	 * @return bool
	 */
	empty: function(value) {
		return ! value.match(/\S/);
	},
	/**
	 * Create validators for a form-element
	 * @param form-element el
	 * @param HTMLFormElement form
	 */
	createValidators: function(el, form) {
		el.validators = [];
		
		var req = Morf.attr(el, 'required'), v;
		if (req)
		{
			if (! Morf.attr(el, 'required-mark-set'))
			{
				el.setAttribute(Morf.NS + ':required-mark-set', '1');// @todo use setAttributeNS
				var mr = Morf.attr(form, 'mark-required');
				if (mr)
				{// we should mark the field with an asterix
					var marked = Morf.label(form, el.getAttribute('id'));
					if (marked)
					{// mark label
						marked.innerHTML = '<span class="x-required">' + mr + '</span> ' + marked.innerHTML;
					} else {// mark the element itself
						var s = document.createElement('span');
						s.className = 'x-required';
						s.innerHTML = mr;
						el.parentNode.insertBefore(s, el.nextSibling);
					}
				}
			}

			Morf.addClass(el, 'x-required');
			el.validators.push(new Morf.requiredValidator(el));
		}
		
		var ptrn = Morf.attr(el, 'pattern');
		if (ptrn)
		{
			el.validators.push(new Morf.patternValidator(el, ptrn));
		}
		
		var custom = Morf.attr(el, 'validator');
		if (custom)
		{
			var mx = custom.match(/^x\:(\S+)$/i);
			if (mx)
			{// Morf built-in validator is to be applied
				custom = 'Morf.' + mx[1] + 'Validator';
			}
			
			el.validators.push(new Morf.customValidator(custom, el));
		}
		
		if (el.validators.length)
		{
			Morf.createListener(el, 'change', function() {Morf.getError(el);});
		}
	},
	/**
	 * Validator for required fields. Constructor
	 * @param form-element el
	 */
	requiredValidator: function(el) {
		this.element = el;
		this.error = Morf.attr(el, "required-error");
		if (! this.error)
		{
			this.error = "Required rule failed on " + el.getAttribute('name');
		}
	},
	/**
	 * Validator for pattern-matching fields. Constructor
	 * @param form-element el
	 * @param string pattern
	 */
	patternValidator: function(el, pattern) {
		this.element = el;
		try {
			eval('this.re = ' + pattern);
		} catch (e) {
			alert("Invalid pattern: " + pattern);
		}
		this.error = Morf.attr(el, "pattern-error");
		if (! this.error)
		{
			this.error = "Pattern match failes on " + el.getAttribute('name');
		}
	},
	/**
	 * Custom-validator. Constructor
	 * @param String name
	 * @param form-element el
	 */
	customValidator: function(name, el) {
		this.element = el;
		try {
			eval('this.validator = new ' + name + "(el)");
		} catch (e) {
			alert("Invalid validator spec: " + name);
		}
		this.error = Morf.attr(el, "validator-error");
		if (! this.error)
		{
			this.error = "Validation failes on " + el.getAttribute('name');
		}
	},
	/**
	 * Validator for email fields. Constructor
	 * @param form-element el
	 */
	emailValidator: function(el) {
		this.element = el;
		this.error = Morf.attr(el, "validator-error");
		if (! this.error)
		{
			this.error = "Email-address validation failes on " + el.getAttribute('name');
		}
	},
	/**
	 * Validator for URL fields. Constructor
	 * @param form-element el
	 */
	urlValidator: function(el) {
		this.element = el;
		this.error = Morf.attr(el, "validator-error");
		if (! this.error)
		{
			this.error = "URL validation failes on " + el.getAttribute('name');
		}
	},
	/**
	 * Create a "standard" error-list
	 */
	createErrorList: function(f) {
		var errorList = document.createElement('ul');
		errorList.style.display = 'none';
		errorList.className = 'x-error-list';
		f.parentNode.insertBefore(errorList, f);
		f.errorList = errorList;
	},
	// -=-=-=-=- End: validation -=-=-=-=-
	
	// -=-=-=-=- Start: Events -=-=-=-=-
	/**
	 * get event object
	 */
	event: function(e) {
		if (e) return e;
		else return window.event;
	},
	/**
	 * event listener.
	 */
	createListener: function(element, eventType, callback) {
		var on = 'on' + eventType;
		var old = (element[on]) ? element[on] : function () {};
		element[on] = function (e) {
			e = Morf.event(e);
			var stop = (false === old.call(element, e)) || (false === callback(e));
			return stop ? false : true;
		};
	},
	// -=-=-=-=- End: Events -=-=-=-=-
	
	// -=-=-=-=- Start: Utils -=-=-=-=-
	/**
	 * get "x:*" attribute value regardles of DOCTYPE
	 */
	attr: function(el, name) {
		var x = el.getAttribute(Morf.NS + ':' + name);
		if (x)
		{
			return x;
		}
		return el.getAttribute(name) ;
	},
	/**
	 * Hide element
	 */
	hide: function(el) {
		el.style.display = 'none';
	},
	/**
	 * Show element
	 */
	show: function(el) {
		el.style.display = 'block';
	},
	/**
	 * get <label> element for @element with id = @id inside form @form
	 */
	label: function(form, id) {
		var labels = form.getElementsByTagName('label');
		for (var i = 0; i < labels.length; ++i)
		{
			var l = labels.item(i);
			if (id == l.getAttribute('for'))
			{
				return l;
			}
		}
	},
	/**
	 * get DOMNode from the argument
	 * @param string | object o
	 * @return DOMNode
	 */
	get: function(o) {
		if ('object' == typeof(o))
		{
			return o;
		}
		else
		{
			return document.getElementById(o);
		}
	}
	// -=-=-=-=- End: Utils -=-=-=-=-
};

/**
 * Validators
 */

Morf.validator.prototype = {
	reset: function() {
		this.errors = [];
	},
	addError: function(err) {
		this.errors.push(err);
	},
	ok: function() {
		return 0 == this.errors.length;
	}
};

Morf.requiredValidator.prototype.validate = function() {
	return ! Morf.empty(this.element.value);
};

Morf.customValidator.prototype.validate = function() {
	if (Morf.empty(this.element.value)) return true;
	return this.validator.validate();
};

Morf.patternValidator.prototype.validate = function() {
	if (Morf.empty(this.element.value)) return true;
	return this.re.test(this.element.value);
};

Morf.emailValidator.prototype.validate = function() {
	if (Morf.empty(this.element.value)) return true;
	return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(this.element.value);
};

Morf.urlValidator.prototype.validate = function() {
	return /[-\w\.]+:\/\/([-\w\.]+)+(:\d+)?(:\w+)?(@\d+)?(@\w+)?([-\w\.]+)(\/([\w/_\.]*(\?\S+)?)?)?(#\S*)?/i.test(this.element.value);
};

//Morf.createListener(window, 'load', Morf.run);
/**
 * To function properly, Morf needs some methods from this file.
 * However, these methods here do things that could be also done with
 * various JS-frameworks like jQuery, Prototype etc.
 *
 * Methods in this file contain Morf native implementation of AJAX and some other utils.
 */

/**
 * Serialize form
 * @param HTMLFormElement form
 * @return array for example: ["x=y", "asdf[qwe][rty]=1234"]
 */
Morf.serialize = function(form) {
	var data = [], token;
	for (var i = 0; i < form.elements.length; ++i)
	{
		token = Morf.serializeElement(form.elements[i]);
		if (token)
		{
			data.push(token)
		}
	}
	return data;
};
/**
 * Serialize single element
 * @param HTMLElement element
 * @return string
 */
Morf.serializeElement = function(element)
{
	if (element.disabled) {
		return false;
	}
	switch (element.tagName.toLowerCase()) {
		case 'input':
			try {
				// capitalize type
				var type = element.getAttribute('type');
				type = type.charAt(0).toUpperCase() + type.substring(1).toLowerCase();
				// find appropriate serializer
				eval('var serialized = Morf.serialize' + type + '(element);');
				return serialized;
			} catch (e) {
				
			}
			break;
		case 'select':
			if (element.getAttribute('multiple'))
			{
				return Morf.serializeMultipleSelect(element);
			}
			break;
	}
	/* none of the above returned a value: let's do a default */
	var name = element.getAttribute('name');
	if (name)
	{
		return element.getAttribute('name') + '=' + encodeURIComponent(element.value);
	}
};
/**
 * serializer for <select multiple>
 * @param HTMLSelectElement element
 * @return string
 */
Morf.serializeMultipleSelect = function(element) {
	var selected = [], opt, name = element.getAttribute('name');
	for (var i = 0; i < element.options.length; ++i)
	{
		opt = element.options[i];
		if (opt.selected)
		{
			selected.push(name + '=' + encodeURIComponent(opt.value));
		}
	}
	return selected.join('&');
};
/**
 * serializer for <input type=checkbox>
 * @param HTMLInputElement element
 * @return string
 */
Morf.serializeCheckbox = function(element) {
	if (element.checked)
	{
		return element.getAttribute('name') + '=' + encodeURIComponent(element.value);
	}
};
/**
 * serializer for <input type=radio>
 * @param HTMLInputElement element
 * @return string
 */
Morf.serializeRadio = function(element) {
	return Morf.serializeCheckbox(element);
};

/**
 * Submit a form via AJAX
 */
Morf.ajax = function(form, success, failure) {
	var url = form.action;
	method = form.method.toUpperCase();
	// serialize form.elements' values
	var data = Morf.serialize(form);
	// @todo check serializer on complex forms, improve if necessary
	if (data.length)
	{
		var dataString = data.join('&');
		if (method == 'GET')
		{
			if (url.indexOf('?') == -1)
			{
				url += '?' + dataString;
			}
			else
			{
				url += '&' + dataString;
			}
			
			dataString = null;
		}
	} else {
		dataString = null;
	}
	
	// the Request
	var req = Morf.createRequestObject();
	req.onreadystatechange = function() {
		if (req.readyState == 4) {
			if (req.status == 200) {// OK
				success(req.responseText);
			} else {// Failed for some reason
				failure("{code: 'error', error_list: 'HTTP request failed: "+req.statusText+"', data: null}");
			}
		}
	};
	req.open(method, url, true);
	req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
	req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
//	alert(encodeURI(dataString));return;
//	alert(encodeURI(dataString));return;
	req.send(dataString);
};
/**
 * @return XMLHttpRequest
 */
Morf.createRequestObject = function()
{
    if (window.XMLHttpRequest) {
        try {
            return new XMLHttpRequest();
        } catch (e){}
    } else if (window.ActiveXObject) {
        try {
            return new ActiveXObject('Msxml2.XMLHTTP');
        } catch (e){}
        try {
            return new ActiveXObject('Microsoft.XMLHTTP');
        } catch (e){}
    }
    return null;
};

/**
 * Add CSS class to element
 */
Morf.addClass = function(el, name) {
	var c = el.className.split(/\s+/);
	c.push(name);
	el.className = c.join(" ");
};
/**
 * Remove CSS class from element
 */
Morf.removeClass = function(el, name) {
	var c = el.className.split(/\s+/);
	var r = [];
	for (var i = 0; i < c.length; ++i)
	{
		if (name != c[i])
		{
			r.push(c[i]);
		}
	}
	el.className = r.join(" ");
};