// <m name="Input Checks"
//    author="Edwin Rijvordt (erijvordt@consultdata.nl)"
//    version="1.0"
//    type="library"
//    language="JavaScript"
//    dependencies="Message Handler"> Client-side Javascript library defining several
//    functions useful when validating user input.  The following functions are defined:
//
//    <ul>
//    <li>setConstraintsError()</li>
//    <li>getConstraintsError()</li>
//    <li>clearConstraintsError()</li>
//    <li>checkText()</li>
//    <li>checkRadioGroup()</li>
//    <li>checkOptionList()</li>
//    <li>checkDate()</li>
//    <li>checkDateBefore()</li>
//    <li>checkDateOptions()</li>
//    <li>parseInteger()</li>
//    <li>parseyear()</li>
//    <li>isLeapYear()</li>
//    </ul>
//
// </m>

// Constraint/parameter types.
var CST_ANY         =  1; // any characters are allowed in the value
var CST_NUMERIC     =  2; // only numbers are allowed in the value
var CST_NUMERIC_POS =  3; // only positive numbers are allowed in the value
var CST_ALPHA       =  4; // only letters are allowed in the value
var CST_ALNUM       =  5; // only letters, numbers and underscores are allowed in the value
var CST_ALNUMEX     =  6; // same as CST_ALNUM but characters ' ', '@', '-' and '.' are also allowed
var CST_ALPHA_PUNCT =  7; // only letters and punctuation characters (space and '`"-,.) are allowed in the value
var CST_FLOAT       =  8; // a floating point value
var CST_FLOAT_POS   =  9; // a positive floating point value
var CST_FIXED144    = 10; // a floating point value
var CST_FIXED144_POS= 11; // a positive floating point value
var CST_DATE        = 12; // actually checks three form elements, named by the given element name,
                          // suffixed with "Day", "Month" and "Year"; this checks if the three
                          // elements form a valid date.
var CST_DATE_CMP    = 13; // compares two date fields (like in DATE), the second set of form elements is
                          // specified by i+8'th element of the constraints array.  This checks that the
                          // second date lies after the first date.
var CST_MONEY       = 14; // the value must a money amount; i.e. either an integer number, or
                          // a floating value with 2 numbers after the comma (or dot); e.g. 34,50 or 24.50
var CST_MONEY_POS   = 15; // a positive money amount
var CST_OPTION      = 16; // the form element is an option list: min-max give the required indices
var CST_RADIO       = 17; // the form element is a radio group: checks if any radion button is checked
var CST_EMAIL       = 18; // the value must be an email address
var CST_PHONE       = 19; // the value must be a phone number



var checkConstraints_error = "";

function setConstraintsError(msg) {
    checkConstraints_error = msg;
    return false;
}

function clearConstraintsError() {
    checkConstraints_error = null;
    return true;
}

function getConstraintsError() {
    return checkConstraints_error;
}

// Check a given string for minimum and maximum length, and if it contains
// only alfanumeric characters, including underscore (optionally).
// Set the constraint error if a check fails.  The text of the error is constructed
// by calling the MsgHandler.
function checkText(type, required, str, fieldDesc, minLength, maxLength, minValue, maxValue) {
    if (str == null) {
        str = "";
    } else {
		str = wtrim(str);
    }

    if (required && str.length == 0) {
        return setConstraintsError(MsgHandler.emptyItem(fieldDesc));
    }

    if (str.length > 0) {
		if (str.length < minLength) {
            return setConstraintsError(MsgHandler.notEnoughCharacters(fieldDesc, minLength));
		} else if (maxLength > 0 && str.length > maxLength) {
            return setConstraintsError(MsgHandler.tooManyCharacters(fieldDesc, maxLength));
		}
    }

    if (type == CST_ANY) {
        return true;
    }

	var i, c, cc;
    if (type == CST_NUMERIC || type == CST_NUMERIC_POS) {
		for( i = 0; i < str.length; ++i ) {
			c = str.charAt(i);
            if (type == CST_NUMERIC) {
				if (!((c >= '0' && c <= '9') || c == '-' || c == '+')) {;
					return setConstraintsError(MsgHandler.invalidNumber(fieldDesc));
                }
            } else {
				if (!(c >= '0' && c <= '9')) {
					return setConstraintsError(MsgHandler.invalidPositiveNumber(fieldDesc));
                }
            }
		}
		i = parseInt(str);
		if (minValue != -1 && i < minValue) {
			return setConstraintsError(MsgHandler.numberTooSmall(fieldDesc, minValue));
		} else if (maxValue != -1 & i > maxValue) {
			return setConstraintsError(MsgHandler.numberTooLarge(fieldDesc, maxValue));
		}
    } else if (type == CST_MONEY) {
        if (str.length > 0) {
			var matches = str.match(/^[+-]{0,1}(([0-9]+([.,][0-9]{2})?)|([.,][0-9]{2}))$/i);
			if (matches == null) {
				return setConstraintsError(MsgHandler.invalidMoneyAmount(fieldDesc));
			}
			var m = parseFloat(str);
			if (minValue != -1 && m < minValue) {
				return setConstraintsError(MsgHandler.numberTooSmall(fieldDesc, minValue));
			} else if (maxValue != -1 & m > maxValue) {
				return setConstraintsError(MsgHandler.numberTooLarge(fieldDesc, maxValue));
			}
        }
    } else if (type == CST_MONEY_POS) {
        if (str.length > 0) {
			var matches = str.match(/^(([0-9]+([.,][0-9]{2})?)|([.,][0-9]{2}))$/i);
			if (matches == null) {
				return setConstraintsError(MsgHandler.invalidPositiveMoneyAmount(fieldDesc));
			}
			var m = parseFloat(str.replace(/,/g, "."));
			if (minValue != -1 && m < minValue) {
				return setConstraintsError(MsgHandler.numberTooSmall(fieldDesc, minValue));
			} else if (maxValue != -1 & m > maxValue) {
				return setConstraintsError(MsgHandler.numberTooLarge(fieldDesc, maxValue));
			}
        }
    } else if (type == CST_FLOAT) {
        if (str.length > 0) {
			var f = parseFloat(str);
			if (isNaN(f)) {
				return setConstraintsError(MsgHandler.invalidFloat(fieldDesc));
			} else {
				if (minValue != -1 && f < minValue) {
					return setConstraintsError(MsgHandler.numberTooSmall(fieldDesc, minValue));
				} else if (maxValue != -1 & f > maxValue) {
					return setConstraintsError(MsgHandler.numberTooLarge(fieldDesc, maxValue));
				}
            }
        }
    } else if (type == CST_FLOAT_POS) {
        if (str.length > 0) {
			var f = parseFloat(str);
			if (isNaN(f) || f < 0.0) {
				return setConstraintsError(MsgHandler.invalidPositiveFloat(fieldDesc));
			} else {
				if (minValue != -1 && f < minValue) {
					return setConstraintsError(MsgHandler.numberTooSmall(fieldDesc, minValue));
				} else if (maxValue != -1 & f > maxValue) {
					return setConstraintsError(MsgHandler.numberTooLarge(fieldDesc, maxValue));
				}
            }
        }
    } else if (type == CST_FIXED144) {
        if (str.length > 0) {
			var matches = str.match(/^[+-]{0,1}(([0-9]{1,14}([.,][0-9]{1,4})?)|([.,][0-9]{1,4}))$/i);
			if (matches == null) {
				return setConstraintsError(MsgHandler.invalidFixed144(fieldDesc));
			}
			var f = parseFloat(str);
			if (isNaN(f)) {
				return setConstraintsError(MsgHandler.invalidFixed144(fieldDesc));
			} else {
				if (minValue != -1 && f < minValue) {
					return setConstraintsError(MsgHandler.numberTooSmall(fieldDesc, minValue));
				} else if (maxValue != -1 & f > maxValue) {
					return setConstraintsError(MsgHandler.numberTooLarge(fieldDesc, maxValue));
				}
            }
        }
    } else if (type == CST_FIXED144_POS) {
        if (str.length > 0) {
			var matches = str.match(/^(([0-9]{1,14}([.,][0-9]{1,4})?)|([.,][0-9]{1,4}))$/i);
			if (matches == null) {
				return setConstraintsError(MsgHandler.invalidPositiveFixed144(fieldDesc));
			}
			var f = parseFloat(str);
			if (isNaN(f)) {
				return setConstraintsError(MsgHandler.invalidPositiveFixed144(fieldDesc));
			} else {
				if (minValue != -1 && f < minValue) {
					return setConstraintsError(MsgHandler.numberTooSmall(fieldDesc, minValue));
				} else if (maxValue != -1 & f > maxValue) {
					return setConstraintsError(MsgHandler.numberTooLarge(fieldDesc, maxValue));
				}
            }
        }
    } else if (type == CST_ALPHA) {
		for( i = 0; i < str.length; ++i ) {
			c = str.charAt(i);
			cc = str.charCodeAt(i);
			if (!( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (cc >= 192 && cc <= 254) )) {
				return setConstraintsError(MsgHandler.invalidAlphabetic(fieldDesc));
            }
		}
    } else if (type == CST_ALPHA_PUNCT) {
		for( i = 0; i < str.length; ++i ) {
			c = str.charAt(i);
			cc = str.charCodeAt(i);
			if (!( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (cc >= 192 && cc <= 254) ||
				   c == ' ' || c == '\'' || c == '`' || c == '"' || c == '-' ||
                   c == '.' || c == ',' )) {
				return setConstraintsError(MsgHandler.invalidAlphaPunct(fieldDesc));
            }
		}
    } else if (type == CST_ALNUM) {
		for( i = 0; i < str.length; ++i ) {
			c = str.charAt(i);
			cc = str.charCodeAt(i);
			if (!( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (cc >= 192 && cc <= 254) ||
				   (c >= '0' && c <= '9') || c == '_' )) {
				return setConstraintsError(MsgHandler.invalidAlphaNumeric(fieldDesc));
            }
		}
    } else if (type == CST_ALNUMEX) {
		for( i = 0; i < str.length; ++i ) {
			c = str.charAt(i);
			if (!( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (cc >= 192 && cc <= 254) ||
				   (c >= '0' && c <= '9') || c == ' ' || c == '_' || c == '@' || c == '-' || c == '.')) {
				return setConstraintsError(MsgHandler.invalidAlphaNumericExtended(fieldDesc));
            }
		}
    } else if (type == CST_EMAIL) {
        if (str.length > 0) {
			var matches = str.match(/^[.a-z0-9]+[.a-z0-9_-]*@[a-z0-9_-]+\..+$/i);
			if (matches == null) {
				return setConstraintsError(MsgHandler.invalidEmailAddress(fieldDesc));
			}
        }
    } else if (type == CST_PHONE) {
        if (str.length > 0) {
			var matches = str.match(/^[+0-9-()]*$/i);
			if (matches == null) {
				return setConstraintsError(MsgHandler.invalidPhoneNumber(fieldDesc));
			}
        }
    } else {
        return setConstraintsError("Invalid constraint type '" + type + "' specified.");
    }

    return true;
}

// Check if a button in this radio group is checked.  Returns
// true if a button is checked, else false.
function checkRadioGroup(group, fieldDesc) {
    var i;
    for( i = 0; i < group.length; ++i ) {
		if (group[i].checked) {
            return true;
        }
    }
    return setConstraintsError(MsgHandler.noRadioButtonSelected(fieldDesc));
}

// Check if the index of the selected option is between minIndex and
// maxIndex (use -1 to ignore either).  Returns true if ok, else
// sets the constraints error and returns false.
function checkOptionList(select, fieldDesc, minIndex, maxIndex) {
    var i = select.selectedIndex;

    if (minIndex != -1 && i < minIndex) {
        return setConstraintsError(MsgHandler.noOptionSelected(fieldDesc));
    }

    if (maxIndex != -1 && i > maxIndex) {
        return setConstraintsError(MsgHandler.noOptionSelected(fieldDesc));
    }
    return true;
}

function parseInteger(str, desc, minLength, maxLength, minValue, maxValue) {
	if (str.length < minLength) {
		setConstraintsError(MsgHandler.notEnoughCharacters(desc, minLength));
		return NaN;
	}
	if (str.length > maxLength) {
		setConstraintsError(MsgHandler.tooManyCharacters(desc, maxLength));
		return NaN;
	}
	var n = parseInt(str, 10);
	if (isNaN(n)) {
		setConstraintsError(MsgHandler.invalidPositiveNumber(desc));
		return NaN;
	}
	if (n < minValue) {
		setConstraintsError(MsgHandler.numberTooSmall(desc, minValue));
		return NaN;
	}
	if (n > maxValue) {
		setConstraintsError(MsgHandler.numberTooLarge(desc, maxValue));
		return NaN;
	}
    return n;
}

// Parse a year.
function parseYear(year, desc, minValue, maxValue) {
    if (year.length == 0) {
        return -1;
    }

	year = parseInteger(year, desc, 2, 4, -1, -1);
	if (isNaN(year)) {
		return -1;
	}

    // Fix year if specified as a 2-digit number.
    if (year < 100) {
        if (year < 30) {
            year += 2000;
        } else {
            year += 1900;
        }
    }

    if (minValue != -1 && year < minValue) {
		setConstraintsError(MsgHandler.numberTooSmall(desc, minValue));
        return -1;
    }

    if (maxValue != -1 && year > maxValue) {
		setConstraintsError(MsgHandler.numberTooLarge(desc, maxValue));
        return -1;
    }

    return year;
}

// Check a date specified by day, month, year text values.
// Returns (this can be used by the caller to see which item to focus):
//    - 0 if all OK,
//    - 1 empty: if no fields were filled in
//    - 2 if the year is empty or invalid,
//    - 3 if the month is empty or invalid,
//    - 4 if the day is empty or invalid
function checkDate(required, allOrNone, day, month, year, fieldDesc) {
    if (required && (day.length == 0 || month.length == 0 || year.length == 0)) {
        if (day.length == 0 && month.length == 0 && year.length == 0) {
			setConstraintsError(MsgHandler.emptyItem(fieldDesc));
			return 1;
        } else {
			setConstraintsError(MsgHandler.incompleteItem(fieldDesc));
            if (year.length == 0) {
				return 2;
            } else if (month.length == 0) {
				return 3;
            }
			return 4;
        }
    }

    // Check day number and convert to int.
    if (day.length > 0) {
        day = parseInteger(day, "Day " + fieldDesc, 1, 2, 1, 31);
        if (isNaN(day)) {
            return 4;
        }
    } else {
        day = NaN;
    }

    // Check month number and convert to int.
    if (month.length > 0) {
        month = parseInteger(month, "Month " + fieldDesc, 1, 2, 1, 12);
        if (isNaN(month)) {
            return 3;
        }
    } else {
        month = NaN;
    }

    // Check year number and convert to int.
    if (year.length > 0) {
        year = parseInteger(year, "Year " + fieldDesc, 4, 4, 1900, 2075);
        if (isNaN(year)) {
            return 2;
        }
    } else {
        year = NaN;
    }

    // Check if all fields have been filled in if any have been filled in.
    if (allOrNone) {
		var givenDay = !isNaN(day);
		var givenMonth = !isNaN(month);
		var givenYear = !isNaN(year);

		var partial = false;
		if (givenDay || givenMonth || givenYear) {
			partial = true;
		}
		if (partial && (!givenDay || !givenMonth || !givenYear)) {
			setConstraintsError(MsgHandler.incompleteItem(fieldDesc));
            if (!givenYear) {
                return 2;
            } else if (!givenMonth) {
                return 3;
            }
            return 4;
		}
    }

    // If a day and a month have been given, we can check maxday of the month.
    if (!isNaN(day) && !isNaN(month)) {
        var dayCounts = ['31', '29', '31', '30', '31', '30', '31', '31', '30', '31', '30', '31' ];
        if (!isNaN(year) && !isLeapYear(year)) {
            dayCounts[1] = '28';
		}
        if (day > dayCounts[month-1]) {
            setConstraintsError(MsgHandler.invalidMonthDay(fieldDesc, day, dayCounts[month-1], month));
            return 4;
        }
    }

    // If the year has been given as well, we can also check for the validity of
    // 29 february.
    if (!isNaN(year)) {
        if (month == 2 && day == 29) {
            // Check leap year.
            if (!isLeapYear(year)) {
                setConstraintsError(MsgHandler.invalidLeapYear(fieldDesc, year));
				return 2;
            }
        }
    }
    return 0;
}

// Check a date specified by day, month, year.  The values are the numbers from day,
// month and year options lists, meaning that they are already integers, possibly -1
// for non filled-in items.
function checkDateOptions(required, allOrNone, day, month, year, fieldDesc) {
    day = parseInt(day, 10);
    month = parseInt(month, 10);
    year = parseInt(year, 10);

    if (required && (day == -1 || month == -1 || year == -1)) {
        if (day == -1 && month == -1 && year == -1) {
			return setConstraintsError(MsgHandler.emptyItem(fieldDesc));
        } else {
			return setConstraintsError(MsgHandler.incompleteItem(fieldDesc));
        }
    }

    // Check if all fields have been filled in if any have been filled in.
    if (allOrNone) {
		var givenDay = day != -1;
		var givenMonth = month != -1;
		var givenYear = year != -1;

		var partial = false;
		if (givenDay || givenMonth || givenYear) {
			partial = true;
		}
		if (partial && (!givenDay || !givenMonth || !givenYear)) {
			return setConstraintsError(MsgHandler.incompleteItem(fieldDesc));
		}
    }

    // If a day and a month have been given, we can check maxday of the month.
    if (day != -1 && month != -1) {
        var dayCounts = ['31', '29', '31', '30', '31', '30', '31', '31', '30', '31', '30', '31' ];
        if (year != -1 && !isLeapYear(year)) {
            dayCounts[1] = '28';
		}
        if (day > dayCounts[month-1]) {
            return setConstraintsError(MsgHandler.invalidMonthDay(fieldDesc, day, dayCounts[month-1], month));
        }
    }

    // If the year has been given as well, we can also check for the validity of
    // 29 february.
    if (year != -1) {
        if (month == 2 && day == 29) {
            // Check leap year.
            if (!isLeapYear(year)) {
                return setConstraintsError(MsgHandler.invalidLeapYear(fieldDesc, year));
            }
        }
    }
    return true;
}

// Check if the given year is a leap year.  Returns true if so, else false.
function isLeapYear(year) {
	if (year % 4 != 0) {
		return false;
	} else if  (year % 400 == 0) {
		return true;
	} else if (year % 100 == 0) {
		return false;
	} else {
		return true;
	}
}

// Check if the first date is before the second date.  Equal dates are disallowed
// as well.  Both dates are specified by day, month, year.  The second date is
// allowed to be empty, in which this function returns true.
function checkDateBefore(day1, month1, year1, day2, month2, year2, desc) {
// puts("checkDateBefore " + day2 + "-" + month2 + "-" + year2);
    if (day2.length == 0 || month2.length == 0 || year2.length == 0) {
        return true;
    }

    var date1 = new Date(month1 + "/" + day1 + "/" + year1);
    var date2 = new Date(month2 + "/" + day2 + "/" + year2);

    if (date2 < date1) {
        if (desc.charAt(0) == "=") {
            return setConstraintsError(desc.substr(1));
        } else {
			return setConstraintsError(MsgHandler.dateNotBefore(desc, date1, date2));
        }
    }

    return true;
}

