/** Global function to format text messages
 * @param string sKey - key of message
 * @param string aParam - params to substitute
 * @return  string formatted message.
 */
function formatMes(sKey, aParam)
{
    // aValMes - global array
    var sStr = aValMes[sKey];
	for(var m = 0; m < aParam.length; m++)
        sStr = sStr.replace('%s'+m, aParam[m]);
    return sStr;
};

/** Factory of FormElements
 *  Instantiate proper FormElement object depending on Html object
 *  @param object oForm  html form
 */
function FormElementFactory(oForm)
{
    this.aHtmlEl = oForm.elements;
};

/**
 * @access public
 * @param string sName name of html element in form
 * @return object HtmlElement
 */
FormElementFactory.prototype.create = function(sName)
{
    var oHtmlEl = this.aHtmlEl[sName];

    if (!oHtmlEl)
    {
        alert('Error: Invalid html element! Name:'+sName);
        return null;
    }
    return this._make(oHtmlEl);
};

FormElementFactory.prototype._make = function(oHtmlEl)
{
    //alert('oHtmlEl = '+oHtmlEl);

    if (oHtmlEl[0]) //array
    {
        var oFormEl = new FormElement_Group();
        for(var i=0; i<oHtmlEl.length; ++i)
            oFormEl.add(this._make(oHtmlEl[i]));
        return oFormEl;
    }
    else //single element
    {
        if ('input' == oHtmlEl.tagName.toLowerCase())
        {
            switch(oHtmlEl.type.toLowerCase())
            {
                case 'radio':
                case 'checkbox':
                    return new FormElement_Cbox(oHtmlEl);
                case 'text':
                case 'hidden':
                case 'password':
                    return new FormElement(oHtmlEl);
                default: //debug
                    alert('Error: Unknown type ('+oHtmlEl.type.toLowerCase()+') of input element!');
            }
        }
        else if ('textarea' == oHtmlEl.tagName.toLowerCase())
        {
            return new FormElement(oHtmlEl);
        }
        else if('option' == oHtmlEl.tagName.toLowerCase())
        {
            return new FormElement_Cbox(oHtmlEl);
        }
        else
        {
            alert('Error: Unknown tag name of html element! Tag='+oHtmlEl.tagName.toLowerCase());
            return null;
        }
    }
    return null;
};


/** Base class of form elements
 *
 */
function FormElement(oHtml)
{
    this.oHtml   = oHtml;
    this.aChilds = [];
};

FormElement.prototype.add = function(oEl)
{
    return false;
};

FormElement.prototype.getValue = function()
{
    return this.trim(this.oHtml.value);
};

FormElement.prototype.trim = function(sStr)
{
        return sStr.toString().replace(/^\s+/g, '').replace(/\s+$/g, '');
};

FormElement.prototype.isMore = function (oEl)
{
    if (!oEl.aChilds.length && !this.aChilds.length)
        return (parseFloat(this.getValue()) > parseFloat(oEl.getValue()));

    if (oEl.aChilds.length && this.aChilds.length)
    {
        var n = Math.min(this.aChilds.length, oEl.aChilds.length);
        for (var i=0; i<n; ++i)
            if(!this.aChilds[i].isMore(oEl))
                return false;
        return (this.aChilds.length > oEl.aChilds.length);
    }

    if (!oEl.aChilds.length && this.aChilds.length)
        return this._isArrMoreStr(this.getValue(), oEl.getValue());

    if (oEl.aChilds.length && !this.aChilds.length)
        return this._isArrMoreStr(oEl.getValue(), this.getValue());

    alert('Unespected Error');
};

/** compare array and string
 * @access private
 * @return boolean is array more string
 */
FormElement.prototype._isArrMoreStr = function (aArr, sVal)
{
    if (aArr.length > 1)
        return true;
    if (aArr.length == 1)
        return (parseFloat(aArr[0]) > parseFloat(sVal));

    return false;
};


FormElement.prototype.isEqual = function (oEl)
{
    return (this.getValue().toString() == oEl.getValue().toString());
};

FormElement.prototype.getCount = function()
{
    return 1;
};

// return true or false
FormElement.prototype.setFocus = function()
{
    try
    {
        if (this.oHtml.parentNode.tagName.toLowerCase() == 'select')
            this.oHtml.parentNode.focus();
        else
            this.oHtml.focus();
    }
    catch (e)
    {
        //alert(e);
        return false;
    }
    return true;
};

// @coauthor Nikolay Severikov
FormElement.prototype.markAsValid = function()
{
    if ('option' == this.oHtml.tagName.toLowerCase())
        this.oHtml = this.oHtml.parentNode;
    var sClass = this.oHtml.className;
    if ('val_error' == sClass.substr(sClass.length-9, 9))
        this.oHtml.className = sClass.substr(0, sClass.length-10);
    return true;
};

// @coauthor Nikolay Severikov
FormElement.prototype.markAsInvalid = function()
{
    if ('option' == this.oHtml.tagName.toLowerCase())
        this.oHtml = this.oHtml.parentNode;
    var sClass = this.oHtml.className;
    if ('val_error' != sClass.substr(sClass.length-9, 9))
        this.oHtml.className = sClass + ' val_error';
    if (this.oHtml.className == ' val_error') this.oHtml.className = 'val_error';
    return true;
};

//============================================================================//

/** Derived class of group of elements
 *
 */
function FormElement_Group()
{
    this.aChilds = [];
    this.oHtml = null;
};
FormElement_Group.prototype = new FormElement();

FormElement_Group.prototype.add = function(oEl)
{
    this.aChilds[this.aChilds.length] = oEl;
    return true;
};

FormElement_Group.prototype.getValue = function()
{
    var a=[];
	for (var i = 0; i < this.aChilds.length; i++)
        if (this.aChilds[i].getCount())
            a[a.length] = this.aChilds[i].getValue();
    return a;
};

FormElement_Group.prototype.getCount = function()
{
    var n = 0;
    for (var i = 0; i < this.aChilds.length; i++)
        n += this.aChilds[i].getCount();
    return n;
};

// return true or false
FormElement_Group.prototype.setFocus = function()
{
    for (var i = 0; i < this.aChilds.length; i++)
        if(this.aChilds[i].setFocus())
            return true;
    return false;
};

FormElement_Group.prototype.markAsValid = function()
{
    for (var i = 0; i < this.aChilds.length; i++)
        this.aChilds[i].markAsValid();
    return true;
};

FormElement_Group.prototype.markAsInvalid = function()
{
    for (var i = 0; i < this.aChilds.length; i++)
        this.aChilds[i].markAsInvalid();
    return true;
};



//============================================================================//

/** Derived class of single cbox/radio element
 *
 */
function FormElement_Cbox(oHtml)
{
    this.oHtml = oHtml;
    this.aChilds = [];
};
FormElement_Cbox.prototype = new FormElement();

FormElement_Cbox.prototype.getValue = function()
{
    if (this.getCount())
        return this.trim(this.oHtml.value);
    return '';
};

FormElement_Cbox.prototype.getCount = function()
{
    return (this.oHtml.checked || this.oHtml.selected) ? 1 : 0;
};



//=============================================================




/**
 * Class used to validate html form against validation rules
 * @param object oForm  - html form to validate
 */
function Validator(oForm)
{
    this.aFields = {};   // hash 'field_name' => 'has error'
    this.aErrors = [];   // array of errors (strings)
    this.oFactory = new FormElementFactory(oForm);
};


/**
 * Checks is form valid
 * @access public
 * @param array aSchemes   - array of validation schemas of FormElements
 * @param array aRules     - array of validation rules bentween 2 FormElements
 * @param array aCallbacks - array of callback functions for advansed validation
 * @return  boolean
 */
Validator.prototype.isValid = function(aSchemes, aRules, aCallbacks)
{
    this.aErrors = []; //clear errors
    //check each field
    for (var i = 0; i < aSchemes.length; i++)
    {
		var oEl = this.oFactory.create(aSchemes[i].field);
		
        if (oEl.oHtml != null)
        {
			if (oEl.oHtml.disabled == true)
                continue;
        }
        var bInvalid = this._checkField(oEl, aSchemes[i]);

        //remember validation result
        this.aFields[aSchemes[i].field] = bInvalid;
    }
	
    // check each rule
    for (var i = 0; i < aRules.length; i++)
    {
        var oEl1 = this.oFactory.create(aRules[i][0]);
        var oEl2 = this.oFactory.create(aRules[i][1]);
        var bInvalid = this._checkRule(oEl1, oEl2, aRules[i][2]);
        if (bInvalid)
        {
            // add error messages if any
            this.aErrors[this.aErrors.length] = aRules[i][3];
            //remember validation result for each member
            this.aFields[aRules[i][0]] = true;
            this.aFields[aRules[i][1]] = true;
        }
    }

    // process callback functions
    for (var i = 0; i < aCallbacks.length; i++)
            aCallbacks[i](this);

    return (this.aErrors.length == 0);
};

/**
 * Outputs validation errors in <div> or make alert,
 *  sets focus to first field with error,
 *  marks fields with errors using CSS,
 * @access public
 * @param string sDiv id of <div> element for output
 * @return false
 */
Validator.prototype.outputErrors = function(sDiv)
{
    //output errors
    var sOut = '';
    var oDiv = document.getElementById(sDiv);

    if (oDiv)
    {
		for (var i = 0; i < this.aErrors.length; i++)
            sOut += '<span class="error">'+this.aErrors[i]+'</span><br>';
        oDiv.innerHTML = sOut;
    } else
        alert(this.aErrors.join("\n")); // no div in document

    var bFocusIsSet = false;

	for (var sName = 0; sName < this.aFields.length; sName++)
    {
        var oEl = this.oFactory.create(sName);
        var bHasErr = this.aFields[sName];

        if (bHasErr)
        {
            oEl.markAsInvalid();
            if (!bFocusIsSet)
                bFocusIsSet = oEl.setFocus();
        }
        else
            oEl.markAsValid();
    }
    return false;
};


/**
 * Checks is single HtmlElement valid, store error messages
 * @access private
 * @param object  oEl
 * @param object  oScheme
 * @return  boolean has any erros occurs
 */
Validator.prototype._checkField = function(oEl, oScheme)
{   
    var mVal = oEl.getValue();

    //optional param - skip any validation if field is empty
    if (oScheme.optional && !mVal.length)
        return false;

    var aErr = [];

   // check size
   if ('undefined' != typeof(oScheme.minsize) &&  oScheme.minsize  > oEl.getCount())
        aErr[aErr.length] = formatMes('minsize', [oScheme.title, oScheme.minsize, oEl.getCount()]);
   if ('undefined' != typeof(oScheme.maxsize) &&  oScheme.maxsize < oEl.getCount())
        aErr[aErr.length] = formatMes('maxsize', [oScheme.title, oScheme.maxsize, oEl.getCount()]);

    // convert string to array
    if (oEl.getCount() < 2)
    {
        mVal = [];
        mVal[0] = oEl.getValue();
    }
    // validation
    for (var i=0; i<mVal.length; ++i)
    {
        var sVal = mVal[i];
        if ('undefined' != typeof(oScheme.min) &&  oScheme.min > sVal)
            aErr[aErr.length] = formatMes('min', [oScheme.title, oScheme.min, sVal]);
        if ('undefined' != typeof(oScheme.max) &&  oScheme.max < sVal)
            aErr[aErr.length] = formatMes('max', [oScheme.title, oScheme.max, sVal]);
        if ('undefined' != typeof(oScheme.mineq) &&  oScheme.mineq >= sVal)
            aErr[aErr.length] = formatMes('mineq', [oScheme.title, oScheme.mineq, sVal]);
        if ('undefined' != typeof(oScheme.maxeq) &&  oScheme.maxeq <= sVal)
            aErr[aErr.length] = formatMes('maxeq', [oScheme.title, oScheme.maxeq, sVal]);
        if (oScheme.callback)
        {
            var bValid = oScheme.callback(sVal);
            if (!bValid)
			{
                aErr[aErr.length] = formatMes('pattern', [oScheme.title]);
			}
        }
        sVal = sVal.toString();
        if (oScheme.minlen &&  oScheme.minlen > sVal.length)
            aErr[aErr.length] = formatMes('minlen', [oScheme.title, oScheme.minlen, sVal.length]);
        if (oScheme.maxlen &&  oScheme.maxlen < sVal.length)
            aErr[aErr.length] = formatMes('maxlen', [oScheme.title, oScheme.maxlen, sVal.length]);
        if (oScheme.pattern && sVal.search(oScheme.pattern) == -1)
            aErr[aErr.length] = formatMes('pattern', [oScheme.title]);
        if (aErr.length) // break if first field with error found
            break;
    } 
    // store error messages for field
    var sMes = oScheme.message ? oScheme.message : '';
    if (sMes && aErr.length) // if given custom error message store only it
        this.aErrors[this.aErrors.length] = sMes;
    else
        this.aErrors = this.aErrors.concat(aErr);
    return aErr.length;
};


/** Checks are 2 HtmlElement elements valid in the same time
 * @access private
 * @param object  oLeft element
 * @param object  oRight element
 * @param string  sOperation // ==, <, < ...
 * @return  boolean has any errors occurs
 */
Validator.prototype._checkRule = function(oLeft, oRight, sOperation)
{
    var aErr = [];
    var bHasErr = false;
    switch (sOperation)
    {
        case '==' :
            bHasErr =  !oLeft.isEqual(oRight);
            break;
        case '<=' :
            bHasErr = ( oLeft.getCount()
                        && oRight.getCount()
                        && oLeft.isMore(oRight)
                       );
            break;
        case '<' :
            bHasErr = ( oLeft.getCount()
                        && oRight.getCount()
                        && (oLeft.isMore(oRight) || oLeft.isEqual(oRight))
                       );
            break;
        case '>=' :
            bHasErr = ( oLeft.getCount()
                        && oRight.getCount()
                        && !oLeft.isMore(oRight)
                        && !oLeft.isEqual(oRight)
                       );
            break;
        case '>' :
            bHasErr = ( oLeft.getCount()
                        && oRight.getCount()
                        && !oLeft.isMore(oRight)
                       );
            break;
        case '!=' :
            bHasErr = oLeft.isEqual(oRight);
            break;
        case 'req' :
            bHasErr = (oLeft.getValue() && !oRight.getValue());
            break;
        case 'more' :
            bHasErr = (oLeft.getCount() < oRight.getCount());
            break;
        case 'less' :
            bHasErr = (oLeft.getCount() > oRight.getCount());
            break;
        default:
            alert('Validator: unknown rule. Operation='+sOperation);
    }//switch
    return bHasErr;
};

/**
 * Checks HTML form
 * @param  object oForm html form
 * @param  array aSchemes
 * @param  array aRules
 * @param  array aCallbacks
 * @return string sDiv for error output
 */
function validator_isValid(oForm, aSchemes, aRules, aCallbacks, sDiv)
{
	var oVal = new Validator(oForm);
    var bValid = oVal.isValid(aSchemes, aRules, aCallbacks);
    if (!bValid)
        oVal.outputErrors(sDiv);
    return bValid;
	
};

/**
 * Checks if ip is in valid range
 * @param string sIp
 * @return bool result
 */
function validator_isIp(sIp)
{
    return  (sIp != '0.0.0.0' && sIp != '255.255.255.255')
};


// end of validator
