/** CompareValidator */
function CompareValidator(operator)
{
    this.operator = operator;
}

CompareValidator.prototype.validate = function(context, field, value,
                                               dependencies)
{
    // Compare Each Dependent
    var dependents = dependencies.values();
    for (var i = 0; i < dependents.length; i++)
    {
        // Get Dependent, but ignore if invalid
        // The dependent will be invalid if does not exist or has no field
        var dependent = dependents[i];
        if (!dependent.isValid())
        {
            continue;
        }

        // Compare the Value
        var depValue = dependent.getValue();
        if (!this.compareTo(value, depValue))
        {
            throw new ValidatorException(
                new FacesMessage("Values do not compare",
                     "Value for field '" + id + "' does not compare"));
        }
    }

    // Success
    return true;
};

CompareValidator.prototype.compareTo = function(value1, value2)
{
    var compare = 0;

    // Handle Same Data Types
    if (typeof value1 === typeof value2)
    {
        if (typeof value1 === "number")
        {
            compare = (value1 < value2 ? -1 :
                      (value1 === value2 ? 0 :
                      (value1 > value2 ? 1 : 0)));
        }
        else if (typeof value1 === "boolean")
        {
            compare = (value1 === value2 ? 0 : -1);
        }
        else if (typeof value1 === "string")
        {
            compare = (value1.compareTo(value2));
        }
        else if (value1 instanceof Date)
        {
            var date1 = value1.getTime();
            var date2 = value2.getTime();
            compare = (date1 < date2 ? -1 :
                      (date1 === date2 ? 0 :
                      (date1 > date2 ? 1 : 0)));
        }
        else if (value1.compareTo &&
                 typeof value1.compareTo === "function")
        {
            compare = value1.compareTo(value2);
        }
    }

    // Otherwise, Convert to Strings
    else
    {
        var str1 = String.getAsString(value1);
        var str2 = String.getAsString(value2);

        if (str1 === null || str2 === null)
        {
            compare = (str1 === null && str2 === null ? 0 :
                          (str1 === null ? -1 : 1));
        }
        else
        {
            compare = str1.compareTo(str2);
        }
    }

    // Return Comparison w/ Operator
    switch (this.operator)
    {
        case "eq":  return (compare === 0);
        case "neq": return (compare !== 0);
        case "lt":  return (compare < 0);
        case "lte": return (compare <= 0);
        case "gt":  return (compare > 0);
        case "gte": return (compare >= 0);
        default:    return false;
    }
};

/** CustomValidator */

function CustomValidator(clientMethod)
{
    this.clientMethod = clientMethod;
}

CustomValidator.prototype.validate = function(context, field, value,
                                              dependencies)
{
    // Invoke the Client Method
    if (this.clientMethod !== null)
    {
        // Handle Actual Functions
        if (typeof this.clientMethod === "function")
        {
            this.clientMethod(field, value, dependencies);
        }

        // Handle Named Scripts
        else if (typeof this.clientMethod === "string")
        {
            // Create Script
            var clientScript = this.clientMethod + "(" +
                field + "," + value + "," + dependencies + ");";

            // Evaluate Script
            eval(clientScript);
        }

        // Otherwise, Unsupported Validation
        else
        {
            throw new ValidatorException(
                new FacesMessage("Unsupported data type"));
        }
    }
};

/** DoubleRangeValidator */

function DoubleRangeValidator(minimum, maximum)
{
    this.minimum = minimum;
    this.maximum = maximum;
}

DoubleRangeValidator.prototype.validate = function(context, field, value)
{
    // Convert to Number
    var fValue = 0;
    if (typeof value === "number")
    {
        fValue = value;
    }
    else
    {
        fValue = parseFloat(String.getAsString(value));
    }

    // Verify Numeric Format
    if (isNaN(fValue))
    {
        throw new ValidatorException(
            new FacesMessage("Invalid numeric format"));
    }

    // Always Valid if no min/max
    if (this.minimum === null && this.maximum === null)
    {
        return true;
    }

    // Handle Min/Max
    else if (this.minimum !== null && this.maximum !== null)
    {
        if (fValue < this.minimum || fValue > this.maximum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid value not within min/max values"));
        }
    }

    // Handle Min Only
    else if (this.minimum !== null)
    {
        if (fValue < this.minimum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid value not within min value"));
        }
    }

    // Handle Max Only
    else if (this.maximum !== null)
    {
        if (fValue > this.maximum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid value not within max value"));
        }
    }
};

/** EmailValidator */

function EmailValidator()
{
}

EmailValidator.prototype.validate = function(context, field, value)
{
    // Convert Value
    var cvalue = String.getAsString(value);

    // Ignore Empty Values
    if (cvalue === null || cvalue.length === 0)
    {
        return true;
    }

    // TODO: Tokenize Based on Delimiter for Multiple Email Addresses

    // Check Email
    if (!this.checkEmail(cvalue))
    {
        throw new ValidatorException(
            new FacesMessage("Invalid email address"));
    }
};

EmailValidator.prototype.checkEmail = function(emailStr)
{
    // TLD checking turned off by default
    var checkTLD = 0;
    var knownDomsPat = /^(com|net|org|edu|int|mil|gov|arpa|biz|aero|name|coop|info|pro|museum|us)$/;
    var emailPat = /^(.+)@(.+)$/;
    var specialChars = "\\(\\)><@,;:\\\\\\\"\\.\\[\\]";
    var validChars = "\[^\\s" + specialChars + "\]";
    var quotedUser = "(\"[^\"]*\")";
    var ipDomainPat = /^\[(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\]$/;
    var atom = validChars + '+';
    var word = "(" + atom + "|" + quotedUser + ")";
    var userPat = new RegExp("^" + word + "(\\." + word + ")*$");
    var domainPat = new RegExp("^" + atom + "(\\." + atom +")*$");
    var matchArray = emailStr.match(emailPat);

    if (matchArray === null)
    {
        return false;
    }

    var user = matchArray[1];
    var domain = matchArray[2];
    for (var i = 0; i < user.length; i++)
    {
        if (user.charCodeAt(i) > 127)
        {
            return false;
        }
    }

    for (i = 0; i < domain.length; i++)
    {
        if (domain.charCodeAt(i) > 127)
        {
            return false;
        }
    }

    if (user.match(userPat) === null)
    {
        return false;
    }

    var IPArray = domain.match(ipDomainPat);
    if (IPArray !== null)
    {
        for (i = 1; i <= 4; i++)
        {
            if (IPArray[i] > 255)
            {
                return false;
            }
        }

        return true;
    }

    var atomPat = new RegExp("^" + atom + "$");
    var domArr = domain.split(".");
    var len = domArr.length;

    for (i = 0; i < len; i++)
    {
        if (domArr[i].search(atomPat) === -1)
        {
            return false;
        }
    }

    if (checkTLD && domArr[domArr.length-1].length !== 2 &&
        domArr[domArr.length-1].search(knownDomsPat) === -1)
    {
        return false;
    }

    if (len < 2)
    {
        return false;
    }

    return true;
};

/** IpValidator */

function IpValidator(allowZero, allowBroadcast, allowMulticast, allowLoopback)
{
    this.allowZero = allowZero;
    this.allowBroadcast = allowBroadcast;
    this.allowMulticast = allowMulticast;
    this.allowLoopback = allowLoopback;
}

IpValidator.prototype.validate = function(context, field, value)
{
    // Convert Value to Integer
    var address = 0;
    if (value instanceof InetAddress)
    {
        address = value.getAddress();
    }
    else if (typeof value === "number")
    {
        address = value;
    }
    else
    {
        try
        {
            var strValue = String.getAsString(value);
            var inetAddress = InetAddress.getByAddress(strValue);
            address = inetAddress.getAddress();
        }
        catch (exception)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid IP address"));
        }
    }

    // Verify Zero-Based Address
    if (!this.allowZero && address === 0)
    {
        throw new ValidatorException(
            new FacesMessage("Invalid zero-based address"));
    }

    // Verify Broadcast Address
    if (!this.allowBroadcast &&
        (((address & 0xff000000) === 0xff000000) ||
         ((address & 0x00ff0000) === 0x00ff0000) ||
         ((address & 0x0000ff00) === 0x0000ff00) ||
         ((address & 0x000000ff) === 0x000000ff)))
    {
        throw new ValidatorException(
            new FacesMessage("Invalid broadcast address"));
    }

    // Verify Multicast Address
    if (!this.allowMulticast && ((address & 0xf0000000) === 0xe0000000))
    {
        throw new ValidatorException(
            new FacesMessage("Invalid multicast address"));
    }

    // Verify Loopback Address
    if (!this.allowLoopback && ((address & 0xff000000) === 0x7f000000))
    {
        throw new ValidatorException(
            new FacesMessage("Invalid loopback address"));
    }
};

/** LengthValidator */

function LengthValidator(minimum, maximum)
{
    this.minimum = minimum;
    this.maximum = maximum;
}

LengthValidator.prototype.validate = function(context, field, value)
{
    // Convert to String
    value = String.getAsString(value);

    // Verify Valid Value
    if (value === null)
    {
        throw new ValidatorException(
            new FacesMessage("Missing field value"));
    }

    // Get Length
    var length = value.length;

    // Always Valid if no this.minimum/this.maximum
    if (this.minimum === null && this.maximum === null)
    {
        return true;
    }

    // Handle Min/Max
    else if (this.minimum !== null && this.maximum !== null)
    {
        if (length < this.minimum || length > this.maximum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid length not within min/max value"));
        }
    }

    // Handle Min Only
    else if (this.minimum !== null)
    {
        if (length < this.minimum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid length not within min value"));
        }
    }

    // Handle Max Only
    else if (this.maximum !== null)
    {
        if (length > this.maximum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid length not within max value"));
        }
    }
};

/** LongRangeValidator */

function LongRangeValidator(minimum, maximum)
{
    this.minimum = minimum;
    this.maximum = maximum;
}

LongRangeValidator.prototype.validate = function(context, field, value)
{
    // Convert to Number
    var iValue = 0;
    if (typeof value === "number")
    {
        iValue = value;
    }
    else
    {
        // Parse Number -JAS JSLint wants a radix... since parseInt("010") would
        //                   actually return a value of 8 (octal mode), not 10 as
        //                   may be expected.
        iValue = parseInt(String.getAsString(value), 10);
    }

    // Verify Numeric Format
    if (isNaN(iValue))
    {
        throw new ValidatorException(
            new FacesMessage("Invalid numeric format"));
    }

    // Always Valid if no min/max
    if (this.minimum === null && this.maximum === null)
    {
        return true;
    }

    // Handle Min/Max
    else if (this.minimum !== null && this.maximum !== null)
    {
        if (iValue < this.minimum || iValue > this.maximum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid value not within min/max values"));
        }
    }

    // Handle Min Only
    else if (this.minimum !== null)
    {
        if (iValue < this.minimum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid value not within min value"));
        }
    }

    // Handle Max Only
    else if (this.maximum !== null)
    {
        if (iValue > this.maximum)
        {
            throw new ValidatorException(
                new FacesMessage("Invalid value not within max value"));
        }
    }
};

/** NetworkPortValidator */

var MIN_RESERVED_PORT = 1;
var MAX_RESERVED_PORT = 1024;

function NetworkPortValidator(minimum, maximum, allowZero, allowReserved)
{
    this.minimum = minimum;
    this.maximum = maximum;
    this.allowZero = allowZero;
    this.allowReserved = allowReserved;
}

NetworkPortValidator.prototype.validate = function(context, field, value)
{
    // Convert to Number
    var iValue = 0;
    if (typeof value === "number")
    {
        iValue = value;
    }
    else
    {
        // Parse Number -JAS JSLint wants a radix... since parseInt("010") would
        //                   actually return a value of 8 (octal mode), not 10 as
        //                   may be expected.
        iValue = parseInt(String.getAsString(value), 10);
    }

    // Verify Numeric Format
    if (isNaN(iValue))
    {
        throw new ValidatorException(
            new FacesMessage("Invalid numeric format"));
    }

    // Verify Valid Range
    if (iValue < this.minimum || iValue > this.maximum)
    {
        throw new ValidatorException(new FacesMessage(
            "Network port is out of range",
            "The specified network port is out of range"));
    }

    // Verify Non-Zero if Set
    if (!this.allowZero && iValue === 0)
    {
        throw new ValidatorException(new FacesMessage(
            "Unallowed zero-based network port",
            "The network port can not be a zero-based value"));
    }

    // Verify Non-Reserved if Set
    if (!this.allowReserved &&
        iValue >= MIN_RESERVED_PORT &&
        iValue <= MAX_RESERVED_PORT)
    {
        throw new ValidatorException(new FacesMessage(
            "Unallowed reserved network port (1 - 1024)",
            "The network port can not be a reserved value (1 - 1024)"));
    }
};

/** RegExpValidator */

function RegExpValidator(pattern)
{
    this.pattern = pattern;
}

RegExpValidator.prototype.validate = function(context, field, value)
{
    // Get String Value
    value = String.getAsString(value);

    // Ignore if Invalid Value
    if (value === null)
    {
        return true;
    }

    // Match Regular Expression
    var regexp = new RegExp(this.pattern);
    var regvalue = regexp.exec(value);
    if (regvalue === null || regvalue === -1)
    {
        throw new ValidatorException(
            new FacesMessage("Invalid field value does not match pattern"));
    }
};

/** RequiredValidator */

function RequiredValidator()
{
}

RequiredValidator.prototype.validate = function(context, field, value)
{
    // Check Validity
    var valid = false;

    // Handle String Objects
    if (typeof value === "string")
    {
        valid = (value.length === 0 ? false : true);
    }

    // Otherwise, Handle Any Value
    else
    {
        valid = (value === null ? false : true);
    }

    // Verify Valid String
    if (!valid)
    {
        throw new ValidatorException(
            new FacesMessage("Missing field value"));
    }
};

/** UrlValidator */

function UrlValidator()
{
}

UrlValidator.prototype.validate = function(context, field, value)
{
    // Get String Value
    value = String.getAsString(value);

    // Ignore Empty Values
    if (value === null || value.length === 0)
    {
        return true;
    }

    // TODO: Add protocol support

    // Verify Valid URL
    var prefixTest = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?([a-zA-Z0-9_.-]+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/])+)?$/;
    var urlTest = /^(\w+:{0,1}\w*@)?([a-zA-Z0-9_.-]+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/])+)?$/;

    var prefixIndex = value.search(prefixTest);
    var urlIndex = value.search(urlTest);
    if (prefixIndex === -1 && urlIndex === -1)
    {
        throw new ValidatorException(
            new FacesMessage("Invalid URL address"));
    }
};
