//===============================================================================
// * Namespace:   FormValidator
// * Description: Form framework to handle validation and wizard style page
// *              navigation with client and server validation routines.
// * Author:      Denis Zenkovich
// *              Modified by Paul Carroll
//===============================================================================
var FormValidator = new function () {

	var _Form = null;
	var _NextBtn = null;
	var _PreviousBtn = null;
	var _SubmitBtn = null;
    var _Pages = [];
	var _CurrentPage = null;
	var _CurrentPageIdx = 0;
    var _ClassNames = {
        Validating: 'validationInProcess',
        Active: 'currentField',
        Passed: 'searchSuccess',
        Failed: 'validationError',
        Counter: 'countTextSpan',
        Messages: 'messagesContainer',
        MError: 'errorMessage',
        MSuccess: 'successMessage',
        MCurrent: 'currentMessage',
        FormMessages: 'formMessagesContainer'
    };
    var _Cache = []; //cache of ajax validators
    

	/* PRIVATE: Calls the current page's pass routine
	==================================================*/
    var _pass = function(pass, Data){ //evaluate the return for onpass
        if(typeof pass == 'function') 
			return pass(Data);

        return true;
    };

	/* PRIVATE: Called the current page's fail routine
	==================================================*/
    var _fail = function(fail, Data){ //evaluate the return for onfail
        if(typeof fail == 'function') 
			return fail(Data);

        return false;
    };

	/* PRIVATE: 
	==================================================*/
    var _searchCache = function(Call){
        for(var I=0; I<_Cache.length; I++){
            if($.param(_Cache[I].Call) == $.param(Call)) 
				return _Cache[I];
        }
        return false;
    };

	/* PRIVATE: Function to move to the next page 
	==================================================*/
	var _nextPage = function() {

		_CurrentPage.validate();

		if (_CurrentPage.Valid === true) {
			// Shutdown the current page
			_CurrentPage.Deactivate();

			// Move to the next page
			_CurrentPage = _Pages[++_CurrentPageIdx];
			_CurrentPage.Activate();

			if (_CurrentPage.Options.onPageActivated !== undefined) {
				_CurrentPage.Options.onPageActivated();
			}
		}

		_updateButtonVisibility();

		return false;
	};

	/* PRIVATE: Function to move to the previous page
	==================================================*/
	var _previousPage = function() {
		_CurrentPage.Deactivate();

		// Move the current page back one
		_CurrentPage = _Pages[--_CurrentPageIdx];
		_CurrentPage.Activate();

		_updateButtonVisibility();
	};

	/* PRIVATE: Function to submit the current form
	==================================================*/
	var _submitForm = function() {
		
		_CurrentPage.validate();
		
		if (_CurrentPage.Valid === true) {
			_Form.submit();
		}

		return false;
	};

	/* PRIVATE: Updates the pager button visibility 
	 *          depending where the user currently is.
	==================================================*/
	var _updateButtonVisibility = function() {
		// If the next or previous buttons were not provided
		// then just show the submit and bail
		if (_NextBtn === null || _PreviousBtn === null) {
			_SubmitBtn.show();
			return;
		}

		// Update the previous button visibility
		_CurrentPageIdx > 0 ? _PreviousBtn.show() : _PreviousBtn.hide();

		// Update the next button visibility
		_CurrentPageIdx < _Pages.length - 1 ? _NextBtn.show() : _NextBtn.hide();

		// Update the submit button visibility
		_CurrentPageIdx == _Pages.length - 1 ? _SubmitBtn.show() : _SubmitBtn.hide();
	};

	/* PUBLIC: Initialises the namespace functionality
	 *         for the wizard.
	==================================================*/
    this.Initialise = function(formId, previousBtnId, nextBtnId, submitBtnId) {

		_Form = $("#" + formId);

		if (nextBtnId !== null && nextBtnId !== undefined) {
			_NextBtn = $("#" + nextBtnId);
			_NextBtn.click(jQuery.proxy(_nextPage, this));
		}

		if (_previousPage !== null && _previousPage !== undefined) {
			_PreviousBtn = $("#" + previousBtnId);
			_PreviousBtn.click(jQuery.proxy(_previousPage, this));
		}

		_SubmitBtn = $("#" + submitBtnId);
		_SubmitBtn.click(jQuery.proxy(_submitForm, this));
	};

	/* PUBLIC: Defines the available validators (used
	 *         by the assisstant class declared further on).
	==================================================*/
	this.Validators = {
        required: function(Val, Data, pass, fail){
            return Val?_pass(pass, Data):_fail(fail, Data);
        },
        minlength: function(Val, Data, pass, fail){
            return Val.toString().length<Data.length?_fail(fail, Data):_pass(pass, Data);
        },
        maxlength: function(Val, Data, pass, fail){
            return Val.toString().length>Data.length?_fail(fail, Data):_pass(pass, Data);
        },
		alphanum: function(Val, Data, pass, fail) {
			return /^\w+$/.test(Val)?_pass(pass, Data):_fail(fail, Data);
		},
		regexp: function(Val, Data, pass, fail) {
			return Data.regexp.test(Val)?_pass(pass, Data):_fail(fail, Data);
		},
		digits: function(Val, Data, pass, fail) {
			return /^\d+$/.test(Val)?_pass(pass, Data):_fail(fail, Data);
		},
		number: function(Val, Data, pass, fail) {
			return /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(Val)?_pass(pass, Data):_fail(fail, Data);
		},
		email: function(Val, Data, pass, fail) {
			// contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/
			return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(Val)
            ?_pass(pass, Data):_fail(fail, Data);
		},
		url: function(Val, Data, pass, fail) {
			// contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/
			return /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(Val)
            ?_pass(pass, Data):_fail(fail, Data);
		},
		equalTo: function(Val, Data, pass, fail) {
            var ValEq = Data.value.jquery ? Data.value.val() : Data.value;
			return Val == ValEq ? _pass(pass, Data) : _fail(fail, Data);
		},
        ajax: function(Val, Data, pass, fail){
            var onAjax = function(Json){
				//If cached Call var is defined above
				if (Data.cache) { 
                    _Cache.push({Call: Call, Resp: Json, Created: new Date()});
                }
                
				var Resp = null;
                
				try{
                    Resp = eval(Json);
                }
                catch(E){
                    Data.error = '<pre>'+Json+'</pre>';
                    _fail(fail, Data);
                }
                
				Data.Resp = Resp;
                
				if(Resp.valid) {
					_pass(pass, Data);
				}
                else {
					_fail(fail, Data);
				}
            }
            if(Data.cache){
                var Call = {Data: Data, Value: Val};
                var Cached = _searchCache(Call);
                if(Cached){
                    onAjax(Cached.Resp);
                    return;
                }
            }
            $.post(Data.url, {value: Val}, onAjax);
        }
    };

	/* PUBLIC: Call this from your form init code to 
	 *         add another page manager instance to 
	 *         the manager. Automatically initialises
	 *         the current page if not already set.
	==================================================*/
    this.AddPage = function (page) {
        _Pages.push(page);

		if (_CurrentPage == null) {
			_CurrentPageIdx = 0;
			_CurrentPage = page;
			_CurrentPage.Activate();
		}

		_updateButtonVisibility();
    };

	/* PUBLIC: Get accessor to lookup a defined class
	 *         name.
	==================================================*/
    this.className = function(Type){
        return _ClassNames[Type];
    }

	/* PUBLIC: 
	==================================================*/
    this.setClassNames = function(Obj){
        for(var I in _ClassNames){
            if (Obj[I]) 
				_ClassNames[I] = Obj[I];
        }
    }
	    
	/* PUBLIC: Validate - works as a queue of validator 
	 *         checks, main reason for the queue - ajax 
	 *         validators
	==================================================*/
    this.validate = function(Value, Rules, onReady){
        var Num = 0;
        var Validators = []; //array of validator names
        
		for(var I in Rules) 
			Validators.push(I);
        
		if(!Validators.length) 
			onReady(true);
        
		var pass = function(Data){
            Num++;

			//stop validate process
            if(Num == Validators.length){ 
                if(typeof onReady == 'function') onReady(true, Data);
            }
            else{
                check(Validators[Num]);
            }
        };

        var fail = function(Data){
            if(typeof onReady == 'function') 
				onReady(false, Data);
        };

        var check = function(Name){
            if(!FormValidator.Validators[Name]) 
				throw "Unknown FormValidator validator named: "+Name;

            Rules[Name].Validator = Name;
            FormValidator.Validators[Name](Value, Rules[Name], pass, fail);
        };

        check(Validators[0]);
    };
} ();


//===============================================================================
// * Class:       FormValidator.Page
// * Description: Provides functionality to define a page instance for the 
// *              validation manager.
// * Author:      Denis Zenkovich
// *              Modified by Paul Carroll
//===============================================================================
FormValidator.Page = function (options) {

    var self = this;
    var _SkipValidation = false;

    this.scrollToFirstInvalid = true;
    this.Options = options;
    this.Control = $('#' + options.pageId);
    this.Message = this.Control.find('.' + FormValidator.className('FormMessages'));
    this.Valid = true;

	/* PRIVATE: Get accessor for the first invalid 
	 *          on the page.
	==================================================*/
    var _findFirstInvalid = function(){
        
		$.each(self.Options.elements, function(i, elem) {
			if(!elem.Valid) 
				return elem;
		});

        return null;
    };
	
	/* PUBLIC: Call this to validate the elements on the
	 *         page. Check the 'Valid' public property
	 *         after for the result.
	==================================================*/
    this.validate = function(){

        if(_SkipValidation){
			this.Valid = true;
            _SkipValidation = false;
            return true;
        }

        var Num = -1;
        var next = function(){
            Num++;
            
			//element invalid
			if(Num > 0 && !self.Options.elements[Num-1].Valid) { 
                self.Valid = false;
            }

			//passed through and all are valid
            if(Num == self.Options.elements.length){ 
                self.doneValidation();
                return;
            }

			//start next element validator
            if(self.Options.elements[Num].validate) {
				self.Options.elements[Num].validate(next); 
			}
        };
        
		this.Valid = true;
        this.Message.html('Checking...');
        
		next();

        return false; //don't let form to submit
    };

	/* PUBLIC: 
	==================================================*/
    this.doneValidation = function(){
        this.Message.html('');

        if (this.Valid) {
            _SkipValidation = true;
        }
        else{
            if(this.scrollToFirstInvalid) {
                var El = _findFirstInvalid();

                if (El) {
					El.Container.get(0).scrollIntoView(true);
				}
            }
                
        }
    };

	/* PUBLIC: Call this to activate the page.
	 *         Shows the page container and sets up 
	 *         event listeners on elements
	==================================================*/
	this.Activate = function() {
		this.Control.show();

		$.each(self.Options.elements, function(i, elem) {
			if (elem.Activate !== undefined) {
				elem.Activate();
			}
		});
	};

	/* PUBLIC: Call this to deativate the page.
	 *         Hides the page container and removes the 
	 *         event listeners on elements.
	==================================================*/
	this.Deactivate = function() {
		this.Control.hide();

		$.each(self.Options.elements, function(i, elem) {
			if (elem.Activate !== undefined) {
				elem.Deactivate();
			}
		});
	};
}


//===============================================================================
// * Class:       FormValidator.Element
// * Description: Simple input element - field that you can assign validation rules on
// * Author:      Denis Zenkovich
// *              Modified by Paul Carroll
//===============================================================================
FormValidator.Element = function (Id, Data) {
    
	var self = this;
    
	this.Data = Data;
    this.Container = $('#'+Id);
    this.Input = this.Container.find('input').length == 0 ? this.Container.find('textarea') : this.Container.find('input');
    this.Counter = this.Container.find('.'+FormValidator.className('Counter'));
    this.Message = this.Container.find('.'+FormValidator.className('Messages'));
    this.Message.Orig = this.Message.html();
    this.Valid = false;

    
	/* PUBLIC: Called to apply validators to the input
	==================================================*/
	this.validate = function(onReady){
        var ready = function(Valid, Data){
            self.Container.removeClass(FormValidator.className('Validating'));
            
			if(!Valid){
                self.error(Data);
                self.Valid = false;
            }
            else{
                self.Valid = true;
                self.error();
                self.Container.addClass(FormValidator.className('Passed'));
                self.Message.addClass(FormValidator.className('MSuccess'));

                if(self.Data.valid){
                    self.Message.html(self.Data.valid);
                }
            }
            
			if(typeof onReady == 'function') {
				onReady();
			}
        };
        this.Container.addClass(FormValidator.className('Validating'));
        this.Message.html('Checking...');

        FormValidator.validate(this.Input.val(), this.Data.rules, ready);
    };

	/* PUBLIC: Removes the error indicators and shows
	 *         help while the user corrects any errors.
	==================================================*/
    this.focus = function(){
        this.error();
        this.Container.removeClass(FormValidator.className('Passed'));
        this.Container.addClass(FormValidator.className('Active'));
        this.Message.addClass(FormValidator.className('MCurrent'));
    };

	/* PUBLIC: Applies the validators and sets the appropriate
	 *         error classes on the container.
	==================================================*/
    this.blur = function(){
        this.validate();
        this.Container.removeClass(FormValidator.className('Active'));
        this.Message.removeClass(FormValidator.className('MCurrent'));
    };

	/* PUBLIC: 
	==================================================*/
    this.error = function(Data){
        this.Message.removeClass(FormValidator.className('MError') + " " + FormValidator.className('MSuccess') + " " + FormValidator.className('MCurrent'));
        
		//no data = clear error
		if (!Data) { 
            this.Message.addClass(FormValidator.className('MCurrent')).html(this.Message.Orig);
            this.Container.removeClass(FormValidator.className('Failed'));
            return;
        }
        
		this.Message.addClass(FormValidator.className('MError'));
        
		if (Data.Validator == 'ajax' && Data.Resp.error) {
			this.Message.html(Data.Resp.error);
		}
        else {
			this.Message.html(Data.error);
		}

        this.Container.addClass(FormValidator.className('Failed'));
    };

	/* PUBLIC: 
	==================================================*/
	this.Activate = function() {
    	this.Input.focus( function(){self.focus()} ).blur( function(){self.blur()} );
	};

	/* PUBLIC: 
	==================================================*/
	this.Deactivate = function() {
    	this.Input.focus( function(){} );
	};
};


//===============================================================================
// * Class:       FormValidator.SelectElement
// * Description: Select drop down element - field that you can assign validation 
// *              rules on
// * Author:      Denis Zenkovich
// *              Modified by Paul Carroll
//===============================================================================
FormValidator.SelectElement = function(Id, Data){

    var self = this;

    this.Data = Data;
    this.Container = $('#'+Id);
    this.Input = this.Container.find('select');
    this.Counter = this.Container.find('.'+FormValidator.className('Counter'));
    this.Message = this.Container.find('.'+FormValidator.className('Messages'));
    this.Message.Orig = this.Message.html();
    this.Valid = false;

	/* PUBLIC: 
	==================================================*/
    this.validate = function(onReady){
        var ready = function(Valid, Data){
            self.Container.removeClass(FormValidator.className('Validating'));

            if(!Valid){
                self.error(Data);
                self.Valid = false;
            }
            else{
                self.Valid = true;
                self.error();
                self.Container.addClass(FormValidator.className('Passed'));
                self.Message.addClass(FormValidator.className('MSuccess'));

                if(self.Data.valid){
                    self.Message.html(self.Data.valid);
                }
            }
            if(typeof onReady == 'function') onReady();
        };
        this.Container.addClass(FormValidator.className('Validating'));
        this.Message.html('Checking...');
        FormValidator.validate(this.Input.val(), this.Data.rules, ready);
    };

	/* PUBLIC: 
	==================================================*/
    this.focus = function(){
        this.error();
        this.Container.removeClass(FormValidator.className('Passed'));
        this.Container.addClass(FormValidator.className('Active'));
        this.Message.addClass(FormValidator.className('MCurrent'));
    };

	/* PUBLIC: 
	==================================================*/
    this.blur = function(){
        this.validate();
        this.Container.removeClass(FormValidator.className('Active'));
        this.Message.removeClass(FormValidator.className('MCurrent'));
    };

	/* PUBLIC: 
	==================================================*/
    this.error = function(Data){
        this.Message.removeClass(FormValidator.className('MError')+" "+FormValidator.className('MSuccess')+" "+FormValidator.className('MCurrent'));
        if(!Data){ //no data = clear error
            this.Message.addClass(FormValidator.className('MCurrent')).html(this.Message.Orig);
            this.Container.removeClass(FormValidator.className('Failed'));
            return;
        }
        this.Message.addClass(FormValidator.className('MError'));
        if(Data.Validator == 'ajax' && Data.Resp.error) this.Message.html(Data.Resp.error);
        else this.Message.html(Data.error);
        this.Container.addClass(FormValidator.className('Failed'));
    };

	/* PUBLIC: 
	==================================================*/
	this.Activate = function() {
    	this.Input.focus( function(){self.focus()} ).blur( function(){self.blur()} );
	};

	/* PUBLIC: 
	==================================================*/
	this.Deactivate = function() {
    	this.Input.focus( function(){} );
	};
};


//===============================================================================
// * Class:       FormValidator.CheckElement
// * Description: Checkbox/Radio input element - field that you can assign 
// *              validation rules on
// * Author:      Denis Zenkovich
//===============================================================================
FormValidator.CheckElement = function(Id, Data){

    var self = this;

    this.Data = Data;
    this.Container = $('#'+Id);
    this.Input = this.Container.find('input');
    this.Counter = this.Container.find('.'+FormValidator.className('Counter'));
    this.Message = this.Container.find('.'+FormValidator.className('Messages'));
    this.Message.Orig = this.Message.html();
    this.Valid = false;

	/* PUBLIC: 
	==================================================*/
    this.validate = function(onReady){
        var ready = function(Valid, Data){
            self.Container.removeClass(FormValidator.className('Validating'));
            
			if (!Valid) {
                self.error(Data);
                self.Valid = false;
            }
            else {
                self.Valid = true;
                self.error();
                self.Container.addClass(FormValidator.className('Passed'));
                self.Message.addClass(FormValidator.className('MSuccess'));

                if(self.Data.valid){
                    self.Message.html(self.Data.valid);
                }
            }
            
			if(typeof onReady == 'function') {
				onReady();
			}
        };
        
		this.Container.addClass(FormValidator.className('Validating'));
        this.Message.html('Checking...');

        FormValidator.validate(this.Input.is(':checked'), this.Data.rules, ready);
    };

	/* PUBLIC: 
	==================================================*/
    this.error = function(Data){
        this.Message.removeClass(FormValidator.className('MError')+" "+FormValidator.className('MSuccess')+" "+FormValidator.className('MCurrent'));
        
		//no data = clear error
		if (!Data) { 
            this.Message.addClass(FormValidator.className('MCurrent')).html(this.Message.Orig);
            this.Container.removeClass(FormValidator.className('Failed'));
            return;
        }
        
		this.Message.addClass(FormValidator.className('MError'));
        
		if(Data.Validator == 'ajax' && Data.Resp.error) {
			this.Message.html(Data.Resp.error);
		}
        else {
			this.Message.html(Data.error);
		}

        this.Container.addClass(FormValidator.className('Failed'));
    };
};


//===============================================================================
// * Class:       FormValidator.AutoComplete
// * Description: Extended simple input element - has autocomplete drop down
// * Author:      Denis Zenkovich
//===============================================================================
FormValidator.AutoComplete = function(){
    this.superclass = FormValidator.Element(Id);
    $.extend(this, this.superclass);

//TODO

}