/**
 * Filename    : schema.js
 * Author      : Robert Cerny
 * Created     : 2006-07-25
 * Last Change : 2006-11-26
 *
 * Description:

 *   The schema package provides functions for validating a JavaScript
 *   object against a schema. A schema is a JavaScript object
 *   describing a shape (form, schema, pattern). It is composed of
 *   aspects, which have a name and a value. These aspects are the
 *   members of the schema. The name of the aspect is mapped on an
 *   aspect value, which can be a primitive value, a function, a
 *   regular expression or a schema.
 *
 *   The function validate is used to perform validation of an
 *   object against a schema. It returns an object (called validation
 *   report), which contains an error message for every aspect of the
 *   schema, that the object does not conform to. Thus the report
 *   builds up the *same structure* as the the schema. If the object
 *   conforms to the schema, validate returns an empty object. For
 *   convenience the function isValid takes a validation report and
 *   returns true for an empty result and false, if the report
 *   contains errors.
 *
 *   If the aspect value is a function this can be used within the
 *   function to navigate to properties within the object. With
 *   this._parent you can navigate up the object tree. Thus it is not
 *   recommended to perform validation on objects that do contain a
 *   _parent property. When specifying a schema, you should not use
 *   the _parent property either.
 *
 *   If the aspect value is a function the following rules apply. The
 *   function must return true, if the value of the object fulfills
 *   the aspect, otherwise the function should return a String
 *   containing an error message or false, which will produce a
 *   generic error message. Exceptions that occur within the function
 *   are considered an error message also.
 *
 * Usage:
 *   var firefox = {vendor: "Firefox"};
 *   var report = CERNY.schema.validate(window.navigator, firefox);
 *   var valid = CERNY.schema.isValid(report);
 *
 * History:
 *   2006-07-25 Created.
 */

CERNY.namespace("schema");

CERNY.require("CERNY.schema",
              "CERNY.js.Date",
              "CERNY.js.String",
              "CERNY.js.Array");

CERNY.schema.Log = CERNY.Logger("CERNY.schema");

/**
 * Validate an object against a schema.
 *
 * The schema is a JavaScript object, which is seen as map of
 * properties to either values, functions, regular expressions or
 * schemas. The values of the properties in a schema are called
 * aspects. The object is said to confirm to schema, if all properties
 * of schema exist in object and all values of object match the
 * corresponding aspect.
 *
 * object - the object to validate
 * schema - the schema to validate the object against
 * return - a report of the validation results, if the report is empty
 *          the object confirms to the schema, otherwise it contains the
 *          validation errors
 */
CERNY.schema.validate = function(object, schema, parent) {
    var name, aspect, value, result = {}, str,
        subResult, message, log = CERNY.schema.Log;

    if (!isObject(object) || isNull(object)) {
        throw "object must be an Object.";
    }

    if (!isObject(schema) || isNull(schema)) {
        throw "schema must be an Object.";
    }

    log.info("start validate");
    log.debug("schema: " + schema);
    log.debug("parent: " + parent);

    object._parent = parent;

    for (name in schema) {
        if (schema.hasOwnProperty(name)) {
            aspect = schema[name];
            value = object[name];

            log.debug("name: " + name);
            log.debug("value: " + value);

            // First check if it is a RegExp. Mozilla treats RegExps as
            // functions, thus one can write /\.txt/("info.txt"). In
            // this sense we could skip the distinction between
            // RegExps and Functions, but Opera does not treat them as
            // functions, so we have to treat RegExps separately
            if (aspect && isRegexp(aspect)) {
                str = value + "";
                if (!str.match(aspect)) {
                    message = "'" + str + "' must match " + aspect + ".";
                    log.debug("message: " + message);
                    result[name] = message;
                }

            // If the value of the property of the schema is a
            // function, apply the function to the value of the
            // object. Bind object to this in the function.
            } else if (isFunction(aspect)) {

                // Since validation is gathering all the errors, it
                // might be that TypeErrors occur within a function
                // call. These must be caught.
                try {
                    subResult = aspect.call(object, value);
                } catch (e) {
                    subResult = "" + e;
                }
                message = null;
                if (isString(subResult)) {
                    message = subResult;
                } else if (subResult === false) {
                    message = CERNY.schema.printValue(value) + " does not fulfill predicate.";
                } else if (isObject(subResult)) {
                    if (!CERNY.schema.isValid(subResult)) {
                        result[name] = subResult;
                    }
                }

                if (message !== null) {
                    log.debug("message: " + message);
                    result[name] = message;
                }

            // If the aspect is an object (meaning a schema), see
            // if the value matches that pattern
            } else if (aspect && isObject(aspect)) {
                if (!isUndefined(value)) {
                    subResult = CERNY.schema.validate(value, aspect, object);
                    if (!CERNY.schema.isValid(subResult)) {
                        log.debug("subResult: " + subResult);
                        result[name] =  subResult;
                    }
                }

            // Finally if the aspect is just a primitive value, see if
            // they are equal
            } else {
                if (aspect !== value) {
                    message = "Must be " + CERNY.schema.printValue(aspect) + " " +
                        "but is " + CERNY.schema.printValue(value) + ".";
                    log.debug("message: " + message);
                    result[name] = message;
                }
            }
        }
    }

    delete(object._parent);

    log.info("result: " + result);
    log.info("end validate");
    return result;
};

/**
 * Return true, if a validation result is empty (containing no
 * validation errors). False, otherwise.
 *
 * validationResult - a return value of CERNY.schema.validate
 * return - True, if no validation errors are in result and false,
 *          otherwise.
 */
CERNY.schema.isValid = function(validationResult) {
    for (var propertyName in validationResult) {
        if (validationResult.hasOwnProperty(propertyName)) {
            return false;
        }
    }
    return true;
};

/**
 * Defines the term optional for use in schemas. Works only for
 * functions at the moment.
 *
 * f - the aspect that is optional
 * return - a function that evaluates f on it's argument only if the
 *          argument is not undefined or null
 */
CERNY.schema.optional = function(f) {
    return function(y) {
        if (!y) {
            return true;
        } else {
            return f.call(this, y);
        }
    }
};

/**
 * Defines the term arrayOf for use in schemas. Works on functions
 * and schemas.
 *
 * type - the schema or function the array should consist of.
 * return a function that tests it's arguments against type
 */
CERNY.schema.arrayOf = function(type) {
    return function(x) {
        var result = {}, subResult, log = CERNY.Logger("CERNY.schema");
        log.debug("start arrayOf");
        log.debug("x: " + x);
        if (isArray(x)) {
            for (var i = 0; i < x.length; i++) {
                log.debug("i: " + i);
                if (isFunction(type)) {
                    subResult = type(x[i]);
                    if (subResult !== true) {
                        log.debug("subResult: " + subResult);
                        result[i] = subResult;
                    }
                } else if (isObject(type)) {
                    subResult = CERNY.schema.validate(x[i], type, this);
                    if (!CERNY.schema.isValid(subResult)) {
                        log.debug("subResult: " + subResult);
                        result[i] = subResult;
                    }
                }
            }
        } else {
            return "Must be an array.";
        }
        log.debug("end arrayOf");
        return result;
    }
};

CERNY.schema.oneOf = function(array) {
    return function(x) {
        if (isArray(array)) {
            if (array.contains(x)) {
                return true;
            } else {
                return false;
            }
        }
    };
};

CERNY.schema.not = function(x) {
    return function(y) {
        x.call(this,y);
    }
};

CERNY.schema.or = function(x, y) {
    return function(z) {
        x.call(this, z) || y.call(this, z);
    }
};

CERNY.schema.and = function(x, y) {
    return function(z) {
        x.call(this, z) && y.call(this, z);
    }
};

CERNY.schema.number = function(x) {
    if (isNumber(x)) {
        return true;
    }
    return CERNY.schema.printValue(x) + " is not a number.";
};

CERNY.schema.nonEmptyString = function(value) {
    if (value && isNonEmptyString(value)) {
        return true;
    }
    return CERNY.schema.printValue(value) + " must be a non empty string.";
};

/**
 * Defines the term ISO date for use in Schemas.
 *
 * str - the string value to check if it is an ISO date
 * return - true, if it str is an ISO date string, an
 *          error message otherwise
 */
CERNY.schema.isoDate = function(str) {
    if (str && Date._parse(str, CERNY.text.DateFormat.ISO) !== null) {
        return true;
    }
    return CERNY.schema.printValue(str) + " must be an ISO date string (yyyy-mm-dd).";
};

CERNY.schema.printValue = function(value) {
    return "Value '" + value + "' (" + typeof value + ")";
};
