/*
This file is part of MonoLyth.

MonoLyth is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

MonoLyth is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with MonoLyth. If not, see <http://www.gnu.org/licenses/>.
By default, the license is located in ./info/license/gpl.txt.
*/

/**
 * The Javascript Form class is used to validate forms client-side.
 *
 * The syntax of its methods follows roughly the same path as methods
 * in MonoLyth PHP-models: there are variable arguments with a number
 * of fixed required arguments.
 *
 * Example usage:
 * <code>
 * <form name="foo"> </form>
 * <script type="text/javascript">//<![CDATA[
 * var f = new Form(document.forms['foo']);
 * f.required(
 *     'You must fill in this field',
 *     'bar'
 * ); // field bar MUST have a value
 * document.forms['foo'].onsubmit = function() { return f.validate(); };
 * //]]></script>
 * </code>
 *
 * @package MonoLyth
 * @subpackage Script
 * @author Marijn Ophorst <marijn@monomelodies.nl>
 * @copyright MonoMelodies 2009
 */
function Form(f)
{
	this.form = f;
	this.rules = new Object();
	this.messages = new Object();
	this.handler = null;
	this.skipcheck = false;

	/** Some default regexes. */
	this.regex = {
		'email' : /^[-_a-zA-Z0-9.]+@[a-zA-Z0-9][-a-zA-Z0-9.]+\.[a-zA-Z]{2,4}$/
	};

	/**
	 * You can add rules directly yourself to specify custom validation.
	 * fn should be a valid function accepting exactly one argument:
	 * The fieldname, or an object of the form {fieldname : description}.
	 */
	this.rule = function(type, msg, field, fn)
	{
		var idx = type + '/' + msg;
		if (typeof(this.rules[idx]) == 'undefined') {
			this.rules[idx] = {
				'type' : type,
				'msg' : msg,
				'fn' : fn,
				'fields' : new Array()
			};
		}
		this.rules[idx].fields.push(field);
	}

	/**
	 * Define for which clicks the check on validation should be overridden.
	 * This comes in handy if a form contains multiple submit buttons, e.g.
	 * 'next' and 'previous'.
	 */
	this.overridecheck = function(field, action)
	{
		if (typeof(action) == 'undefined') {
			action = 'onclick';
		}
		var self = this;
		this.form.elements[field][action] = function() {
			self.skipcheck = true;
			return true;
		};
	}
	/**
	 * Does the actual validation.
	 * Response is based on the function defined using add_handler,
	 * or a "simple" window.alert if undefined.
	 */
	this.validate = function()
	{
		if (this.skipcheck) {
			return true;
		}
		var valid = true;
		for (idx in this.rules) {
			var msg = this.rules[idx].msg;
			var fn = this.rules[idx].fn;
			for (var j = 0; j < this.rules[idx].fields.length; j++) {
				var field = this.rules[idx].fields[j];
				if (!fn(field)) {
					if (typeof(this.messages[msg]) == 'undefined') {
						this.messages[msg] = new Array();
					}
					var submsg = field;
					if (typeof(field) == 'object') {
						for (k in field) {
							submsg = field[k];
							field = k;
							break;
						}
					}
					this.messages[msg].push(submsg.replace(/%s/, field));
					valid = false;
				}
			}
		}

		if (!valid) {
			this.notify();
		}
		return valid;
	}

	/**
	 * Notify the user of the error of their ways.
	 * Use the custom handler-function defined by add_handler if available.
	 * Otherwise, use simple window.alert.
	 */
	this.notify = function()
	{
		if (this.handler != null && typeof(this.handler) != 'undefined') {
			return this.handler(this.messages);
		}
		var str = '';
		for (i in this.messages) {
			if (i != 'null') {
				/**
				 * Messages starting with _ are used verbatim,
				 * without adding fieldnames etc.
				 */
				if (i.test('^_')) {
					str += i.substring(1) + '\n\n';
					continue;
				}
				str += i + '\n';
			}
			str += this.messages[i].join('\n') + '\n\n';
		}
		window.alert(str);
		this.messages = new Object(); // reset for next try...
	}

	/**
	 * Set a custom handler.
	 * Handlers should be prepared to accept an object in the following form:
	 * {'group' : [msg1[, msg2[, ...]]][, 'group2' : ...}
	 */
	this.add_handler = function(fn)
	{
		this.handler = fn;
	}

	/**
	 * Get the value contained in the field element specified.
	 * Field can be either a simple string, or an object of the form
	 * {fieldname : description}.
	 * Fields with values mapping to an array (selects with multiple,
	 * groups of checkboxes or radiobuttons) are return as an array of values.
	 */
	this.value = function(field)
	{
		field = this.form.elements[this.name(field)];
		if (typeof(field.value) != 'undefined') {
			return field.value.toString();
		}
		var values = new Array();
		for (var i = 0; i < field.length; i++) {
			var child = field[i];
			if (typeof(child.value != 'undefined') && (child.selected || child.checked)) {
				values.push(child.value.toString());
			}
		}
		return values;
	}

	/**
	 * Retrieve the correct fieldname bast on variable 'field' argument.
	 */
	this.name = function(field)
	{
		if (typeof(field) == 'object') {
			for (i in field) {
				return this.form.id + ':' + i;
			}
		}
		return this.form.id + ':' + field;
	}

	/**
	 * Specify required fields.
	 * this.required(msg, field1[, field2[, field3[, ...]]])
	 */
	this.required = function(msg, field)
	{
		var self = this;
		for (var i = 1; i < arguments.length; i++) {
			this.rule('required', msg, arguments[i], function(field) {
				var value = self.value(field);
				if (typeof(value) == 'object') {
					var newvalue = new Array();
					for (var j = 0; j < value.length; j++) {
						if (typeof(value[j]) == 'object') {
							value[j] = value[j].value;
						}
						if (typeof(value[j]) != 'undefined' && value[j].replace(/^\s+(.*?)\s+$/, '\\1').length) {
							newvalue.push(value[j]);
						}
					}
					return newvalue.length;
				}
				return typeof(value) != 'undefined' && value.replace(/^\s+(.*?)\s+$/, '\\1').length;
			});
		}
	}

	/**
	 * Require checkboxes/radiobuttons to be checked.
	 * this.checkrequired(msg, value, field1[, field2[, field3[, ...]]])
	 */
	this.checkrequired = function(msg, value, field)
	{
		var self = this;
		for (var i = 2; i < arguments.length; i++) {
			self.rule('checkrequired', msg, arguments[i], function(field) {
				if (!self.form.elements[self.name(field)]) {
					return true;
				}
				var els = self.form.elements[self.name(field)];
				if (els.value) {
					return self.form.elements[self.name(field)] && self.form.elements[self.name(field)].checked;
				}
				for (var j = 0; j < els.length; j++) {
					if (els[j].checked) {
						return els[j].value == value;
					}
				}
				return false;
			});
		}
	}

	/**
	 * Specify regex for fields to match to.
	 * this.match(msg, regex, field1[, field2[, ...]])
	 */
	this.match = function(msg, regex, field)
	{
		var self = this;
		for (var i = 2; i < arguments.length; i++) {
			this.rule('match', msg, arguments[i], function(field) {
				var value = self.value(field);
				if (typeof(value) == 'array') {
					var test = value;
				} else {
					var test = [value];
				}
				for (i = 0; i < test.length; i++) {
					if (typeof(test[i]) == 'undefined' || !('' + test[i]).replace(/^\s+(.*?)\s+$/, '\\1').length) {
						return true; // don't error for unfilled fields; they might not be required
					}
					if (('' + test[i]).match(regex)) {
						return true;
					}
				}
				return false;
			});
		}
	}

	/**
	 * Specify fields with a minimum length.
	 * For text fields, this is the length of the value.
	 * For selects, radiobuttons and groups of checkboxes
	 * this is the minimum checked/selected number.
	 */
	this.minlength = function(msg, length, field)
	{
		var self = this;
		for (var i = 2; i < arguments.length; i++) {
			this.rule('minlength', msg, arguments[i], function(field) {
				var value = self.value(field);
				return value.length >= length;
			});
		}
	}

	/**
	 * Specify fields with a maximum length.
	 * For text fields, this is the length of the value.
	 * For selects, radiobuttons and groups of checkboxes
	 * this is the maximum checked/selected number.
	 */
	this.maxlength = function(msg, length, field)
	{
		var self = this;
		for (var i = 2; i < arguments.length; i++) {
			this.rule('maxlength', msg, arguments[i], function(field) {
				var value = self.value(field);
				return value.length <= length;
			});
		}
	}

	/**
	 * A combination of minlength and maxlength.
	 * Works the same as its simpler counterparts.
	 */
	this.betweenlength = function(msg, minlength, maxlength, field)
	{
		var self = this;
		for (var i = 3; i < arguments.length; i++) {
			this.rule('betweenlength', msg, arguments[i], function(field) {
				var value = self.value(field);
				return value.length >= minlength && value.length <= maxlength;
			});
		}
	}

	/**
	 * Check if the date entered is valid (according to our calendar).
	 * Date-fields are widgets consisting of NAME:y, NAME:m and NAME:d.
	 */
	this.isdate = function(msg, field)
	{
		var self = this;
		for (i = 1; i < arguments.length; i++) {
			this.rule('datevalid', msg, arguments[i], function(field) {
				for (var j in {'y' : null, 'd' : null, 'm' : null}) {
					if (typeof self.form.elements[field + ':' + j] == 'undefined') {
						return false;
					}
					eval('var ' + j + ' = self.forms.elements[field:' + j + '].value;');
				}
				var check = new Date(y, m - 1, d);
				return check.getYear() == y && check.getMonth() == m - 1 && check.getDate() == d;
			});
		}
	}
}

