/**
 * Ext.FormXtender makes all the forms on a page to act via AJAX -
 * unless a special runat="client" attribute is provided.
 */

Ext.FormXtender = {
	each: function(form, callback) {
		for (var i = 0; i < form.elements.length; ++i)
			callback(form.elements[i], form)
	},
	/**
	 * Apply Ext.FormXtender to all forms below parent
	 * @param DOMElement parent
	 */
	run: function(parent) {
		var nodeList, f
		if (arguments.length < 1)
			nodeList = document.forms
		else
			nodeList = parent.getElementsByTagName('form')
		
		for (var i = 0; i < nodeList.length; ++i)
		{
			f = nodeList.item(i)
			Ext.FormXtender.xtend(f)
		}
	},
	/**
	 * Apply Ext.FormXtender to a single form.
	 * @param HTMLFormNode f
	 */
	xtend: function(f) {
		
		// sanitize form action
		if (! f.action) f.action = location.href.toString()
		
		// create error-list
		Ext.FormXtender.createErrorList(f)
		
		if ('client' != Ext.FormXtender.attr(f, 'runat'))
		{
			f.xServed = Ext.FormXtender.serveMultipart(f)

			// a little hack - just to let the server know we use AJAX
			f.innerHTML += '<input type="hidden" name="_x_" value="_x_" />'
		}	
		// listen to submissions
		Ext.EventManager.addListener(f, 'submit', function(e) {
			
			if (! Ext.FormXtender.validate(f))
			{
				e.stopEvent()
				return Ext.FormXtender.failure(f, f.validator.errors)
			}
			
			if (('client' != Ext.FormXtender.attr(f, 'runat')) && ! f.xServed)
			{
							
				e.stopEvent()
				new Ext.data.Connection().request({
					url: f.action,
					form: f,
					success: function(r) {
						Ext.FormXtender.respondToText(f, r.responseText)
					},
					failure: function(r) {
						Ext.FormXtender.failure(f, ["XMLHttpRequest failed"])
					}
				});
			}
		}, false);
		
		// apply validation
		f.validator = new Ext.FormXtender.validator(f)
		Ext.FormXtender.validate(f)
	},
	/**
	 * Handle form successful submission
	 * @param HTMLFormElement form
	 * @param mixed data
	 */
	success: function(form, data) {
		form.errorList.dom.innerHTML = ''
		form.errorList.setDisplayed(false)
		var handler = form.getAttribute("x: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 = form.getAttribute("x: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.dom.innerHTML = ''
			form.errorList.setDisplayed(true)
			for (var i = 0; i < list.length; ++i)
			{
				var li = form.errorList.dom.appendChild(document.createElement('li'))
				li.innerHTML += list[i]
			}
		}
	},
	/**
	 * Validates a form
	 * @param HTMLFormElement form
	 * @return bool
	 */
	validate: function(form) {
		form.validator.reset()// clear old errors
		Ext.FormXtender.each(form, function(el) {
			var err = Ext.FormXtender.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))
		{
			Ext.FormXtender.createValidators(el, el.form)
		}
		for (var i = 0; i < el.validators.length; ++i)
		{
			if (! el.validators[i].validate())
			{
				Ext.get(el).addClass('x-invalid')
				return el.validators[i].error
			}
		}
		
		Ext.get(el).removeClass('x-invalid')
		return false
	},
	/**
	 * Form-Validator Constructor.
	 * @param HTMLFormElement form
	 */
	validator: function(form) {
		this.errors = []
		Ext.FormXtender.each(form, function(el) {
			Ext.FormXtender.createValidators(el, form)
		});
	},
	/**
	 * Create validators for a form-element
	 * @param form-element el
	 * @param HTMLFormElement form
	 */
	createValidators: function(el, form) {
		var exel = Ext.get(el)
		el.validators = []
		
		var req = Ext.FormXtender.attr(el, 'required'), v
		if (req)
		{
			if (! Ext.FormXtender.attr(el, 'required-mark-set'))
			{
				el.setAttribute('x:required-mark-set', '1')
				var mr = Ext.FormXtender.attr(form, 'mark-required')
				if (mr)
				{// we should mark the field with an asterix
					var marked = Ext.DomQuery.selectNode('label[@for="' + 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)
					}
				}
			}

			exel.addClass('x-required')
			el.validators.push(new Ext.FormXtender.requiredValidator(el))
		}
		
		var ptrn = Ext.FormXtender.attr(el, 'pattern')
		if (ptrn)
		{
			el.validators.push(new Ext.FormXtender.patternValidator(el, ptrn))
		}
		
		var custom = Ext.FormXtender.attr(el, 'validator')
		if (custom)
		{
			var mx = custom.match(/^x\:(\S+)$/i)
			if (mx)
			{// FormXtender validator is to be applied
				custom = 'Ext.FormXtender.' + mx[1] + 'Validator'
			}
			
			el.validators.push(new Ext.FormXtender.customValidator(custom, el))
		}
		
		if (el.validators.length)
		{
			exel.on('change', function() {Ext.FormXtender.getError(this);}, exel.dom);
		}
	},
	/**
	 * Validator for required fields. Constructor
	 * @param form-element el
	 */
	requiredValidator: function(el) {
		this.element = el
		this.error = el.getAttribute("x: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) {
			throw "Invalid pattern: " + pattern
		}
		this.error = el.getAttribute("x: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) {
		try {
			eval('this.validator = new ' + name + "(el)")
		} catch (e) {
			throw "Invalid validator spec: " + name
		}
		this.error = el.getAttribute("x: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 = el.getAttribute("x: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 = el.getAttribute("x:validator-error")
		if (! this.error)
			this.error = "URL validation failes on " + el.getAttribute('name')
	},
	/**
	 * @access private
	 * @param HTMLFormElement f
	 * @param String t
	 */
	respondToText: function(f, t) {

		try {
			var j = Ext.util.JSON.decode(t)
		} 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(Ext.get(\'' + id + '\').dom.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)
		{
			Ext.FormXtender.failure(f, j.error_list)
		} else {
			if (! ('data' in j)) j.data = {}
			Ext.FormXtender.success(f, j.data)
		}
	},
	/**
	 * 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)
		errorList = Ext.get(errorList)
		f.errorList = errorList
	},
	/**
	 * 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 (Ext.FormXtender.isMultipart(f))
		{
			f.setAttribute('enctype', 'multipart/form-data')
			if (f.target)
			{// suppose the user has already prepared the iframe
				var frame = Ext.DomQuery.selectNode("iframe[@name='" + f.getAttribute('target') + "']")
				if (frame)
				{
					Ext.FormXtender.createIframeListener(f, frame)
				}
			} 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
				Ext.FormXtender.createIframeListener(f, Ext.get(name).dom)
			}
			
			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) {
		Ext.EventManager.addListener(frame, 'load', function() {
			var doc = ('undefined' == typeof(this.contentWindow)) ? 
				this.contentDocument :
				this.contentWindow.document;
			if (doc.body.innerHTML.match(/[^\s]/))
			{
				Ext.FormXtender.respondToText(f, doc.body.innerHTML)
			}
		}, frame);
	},
	/**
	 * Check whether the form is multipart
	 * @param HTMLFormElement f
	 */
	isMultipart: function(f) {
		return ('multipart/form-data' == f.getAttribute('enctype')) || Ext.FormXtender.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
	},
	attr: function(el, name) {
		var x = el.getAttribute('x:' + name)
		if (x) return x
		return el.getAttribute(name) 
	}
}

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

Ext.FormXtender.requiredValidator.prototype.validate = function() {
	return /[^\s]/.test(this.element.value)
}

Ext.FormXtender.customValidator.prototype.validate = function() {
	return this.validator.validate()
}

Ext.FormXtender.patternValidator.prototype.validate = function() {
	return this.re.test(this.element.value)
}

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

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

Ext.onReady(Ext.FormXtender.run, Ext.FormXtender);