// Disable no-var because we need to support old IE for now.
/* eslint-disable no-var */
// Disable space-before-function-paren for compatibility with VS Code default JS formatter.
/* eslint-disable space-before-function-paren */

/*
 * This module is intended to be executed both on client side and server side.
 * No error should be thrown. (soft error handling)
 */

(function () {
  var root = {};
  // Dependencies --------------------------------------------------------------
  root.async = (typeof require === 'function') ? require('async') : window.async;
  if (typeof root.async !== 'object') {
    throw new Error('Module async is required (https://github.com/caolan/async)');
  }
  var async = root.async;

  function _extend(origin, add) {
    if (!add || typeof add !== 'object') {
      return origin;
    }
    var keys = Object.keys(add);
    var i = keys.length;
    while (i--) {
      origin[keys[i]] = add[keys[i]];
    }
    return origin;
  }

  function _merge() {
    var ret = {};
    var args = Array.prototype.slice.call(arguments);
    var keys = null;
    var i = null;

    args.forEach(function (arg) {
      if (arg && arg.constructor === Object) {
        keys = Object.keys(arg);
        i = keys.length;
        while (i--) {
          ret[keys[i]] = arg[keys[i]];
        }
      }
    });
    return ret;
  }

  // Customisable class (Base class) -------------------------------------------
  // Use with operation "new" to extend Validation and Sanitization themselves,
  // not their prototype. In other words, constructor shall be call to extend
  // those functions, instead of being in their constructor, like this:
  //   _extend(Validation, new Customisable);

  function Customisable() {
    this.custom = {};

    this.extend = function (custom) {
      return _extend(this.custom, custom);
    };

    this.reset = function () {
      this.custom = {};
    };

    this.remove = function (fields) {
      if (!_typeIs.array(fields)) {
        fields = [fields];
      }
      fields.forEach(function (field) {
        delete this.custom[field];
      }, this);
    };
  }

  // Inspection class (Base class) ---------------------------------------------
  // Use to extend Validation and Sanitization prototypes. Inspection
  // constructor shall be called in derived class constructor.

  function Inspection(schema, custom) {
    var _stack = ['@'];

    this._schema = schema;
    this._custom = {};
    if (custom != null) {
      for (var key in custom) {
        if (Object.prototype.hasOwnProperty.call(custom, key)) {
          this._custom['$' + key] = custom[key];
        }
      }
    }

    this._getDepth = function () {
      return _stack.length;
    };

    this._dumpStack = function () {
      /* eslint-disable no-control-regex */
      return _stack.map(function (i) { return i.replace(/^\[/g, '\u001b\u001c\u001d\u001e'); })
        .join('.').replace(/\.\u001b\u001c\u001d\u001e/g, '[');
      /* eslint-enable no-control-regex */
    };

    this._deeperObject = function (name) {
      _stack.push((/^[a-z$_][a-z0-9$_]*$/i).test(name) ? name : '["' + name + '"]');
      return this;
    };

    this._deeperArray = function (i) {
      _stack.push('[' + i + ']');
      return this;
    };

    this._back = function () {
      _stack.pop();
      return this;
    };
  }
  // Simple types --------------------------------------------------------------
  // If the property is not defined or is not in this list:
  var _typeIs = {
    function: function (element) {
      return typeof element === 'function';
    },
    string: function (element) {
      return typeof element === 'string';
    },
    number: function (element) {
      return typeof element === 'number' && !isNaN(element);
    },
    integer: function (element) {
      return typeof element === 'number' && element % 1 === 0;
    },
    NaN: function (element) {
      return typeof element === 'number' && isNaN(element);
    },
    boolean: function (element) {
      return typeof element === 'boolean';
    },
    null: function (element) {
      return element === null;
    },
    date: function (element) {
      return element != null && element instanceof Date;
    },
    object: function (element) {
      return typeof element === 'object' && element != null && element.constructor !== Array;
    },
    array: function (element) {
      return element != null && element.constructor === Array;
    },
    any: function () {
      return true;
    }
  };

  function _simpleType(type, candidate) {
    if (typeof type === 'function') {
      return candidate instanceof type;
    }
    type = type in _typeIs ? type : 'any';
    return _typeIs[type](candidate);
  }

  function _realType(candidate) {
    for (var i in _typeIs) {
      if (_simpleType(i, candidate)) {
        if (i !== 'any' && (i !== 'object' || candidate.constructor === Object)) { return i; }
        return 'an instance of ' + candidate.constructor.name;
      }
    }
  }

  function getIndexes(a, value) {
    var indexes = [];
    var i = a.indexOf(value);

    while (i !== -1) {
      indexes.push(i);
      i = a.indexOf(value, i + 1);
    }
    return indexes;
  }

  // Available formats ---------------------------------------------------------
  /* eslint-disable no-useless-escape */
  // TODO: Study these regex expressions and add more tests so we can consider removing
  // rule no-useless-escape.
  var _formats = {
    void: /^$/,
    url: /^(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,
    'date-time': /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z?|(-|\+)\d{2}:\d{2})$/,
    date: /^\d{4}-\d{2}-\d{2}$/,
    coolDateTime: /^\d{4}(-|\/)\d{2}(-|\/)\d{2}(T| )\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/,
    time: /^\d{2}\:\d{2}\:\d{2}$/,
    color: /^#([0-9a-f])+$/i,
    // Very flexible regular expression designed to catch only obvious mistakes
    // that a user might make that would have a 100% chance of causing email
    // delivery to the address to fail. The library takes a flexible over
    // strict approach. Users should use this only for basic front end email
    // address validation and perform more strict checking using server-side
    // code after the data has been sent to their server.
    // Sourced from https://www.regular-expressions.info/email.html, modified
    // to allow lowercase characters too.
    email: /^[^@]+@[^.]+\.[a-zA-Z]+$/,
    /* eslint-enable prefer-regex-literals */
    /* eslint-enable quotes */
    numeric: /^[0-9]+$/,
    integer: /^\-?[0-9]+$/,
    decimal: /^\-?[0-9]*\.?[0-9]+$/,
    alpha: /^[a-z]+$/i,
    alphaNumeric: /^[a-z0-9]+$/i,
    alphaDash: /^[a-z0-9_-]+$/i,
    javascript: /^[a-z_\$][a-z0-9_\$]*$/i,
    upperString: /^[A-Z ]*$/,
    lowerString: /^[a-z ]*$/,
    v4uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i
  };
  /* eslint-enable no-useless-escape */

  // Validation ------------------------------------------------------------------
  var _validationAttribut = {
    optional: function (schema, candidate) {
      var opt = typeof schema.optional === 'boolean' ? schema.optional : (schema.optional === 'true'); // Default is false

      if (opt === true) {
        return;
      }
      if (typeof candidate === 'undefined') {
        this.report('is missing and not optional', null, 'optional');
      }
    },
    type: function (schema, candidate) {
      // return because optional function already handle this case
      if (typeof candidate === 'undefined' || (typeof schema.type !== 'string' && !(schema.type instanceof Array) && typeof schema.type !== 'function')) {
        return;
      }
      var types = _typeIs.array(schema.type) ? schema.type : [schema.type];
      var typeIsValid = types.some(function (type) {
        return _simpleType(type, candidate);
      });
      if (!typeIsValid) {
        types = types.map(function (t) { return typeof t === 'function' ? 'an instance of ' + t.name : t; });
        this.report('must be ' + types.join(' or ') + ', but is ' + _realType(candidate), null, 'type');
      }
    },
    uniqueness: function (schema, candidate) {
      if (typeof schema.uniqueness === 'string') { schema.uniqueness = (schema.uniqueness === 'true'); }
      if (typeof schema.uniqueness !== 'boolean' || schema.uniqueness === false || (!_typeIs.array(candidate) && typeof candidate !== 'string')) {
        return;
      }
      var reported = [];
      for (var i = 0; i < candidate.length; i++) {
        if (reported.indexOf(candidate[i]) >= 0) {
          continue;
        }
        var indexes = getIndexes(candidate, candidate[i]);
        if (indexes.length > 1) {
          reported.push(candidate[i]);
          this.report('has value [' + candidate[i] + '] more than once at indexes [' + indexes.join(', ') + ']', null, 'uniqueness');
        }
      }
    },
    pattern: function (schema, candidate) {
      var self = this;
      var regexs = schema.pattern;
      if (typeof candidate !== 'string') {
        return;
      }
      var matches = false;
      if (!_typeIs.array(regexs)) {
        regexs = [regexs];
      }
      regexs.forEach(function (regex) {
        if (typeof regex === 'string' && regex in _formats) {
          regex = _formats[regex];
        }
        if (regex instanceof RegExp) {
          if (regex.test(candidate)) {
            matches = true;
          }
        }
      });
      if (!matches) {
        self.report('must match [' + regexs.join(' or ') + '], but is equal to "' + candidate + '"', null, 'pattern');
      }
    },
    validDate: function (schema, candidate) {
      if (String(schema.validDate) === 'true' && candidate instanceof Date && isNaN(candidate.getTime())) {
        this.report('must be a valid date', null, 'validDate');
      }
    },
    minLength: function (schema, candidate) {
      if (typeof candidate !== 'string' && !_typeIs.array(candidate)) {
        return;
      }
      var minLength = Number(schema.minLength);
      if (isNaN(minLength)) {
        return;
      }
      if (candidate.length < minLength) {
        this.report('must be longer than ' + minLength + ' elements, but it has ' + candidate.length, null, 'minLength');
      }
    },
    maxLength: function (schema, candidate) {
      if (typeof candidate !== 'string' && !_typeIs.array(candidate)) {
        return;
      }
      var maxLength = Number(schema.maxLength);
      if (isNaN(maxLength)) {
        return;
      }
      if (candidate.length > maxLength) {
        this.report('must be shorter than ' + maxLength + ' elements, but it has ' + candidate.length, null, 'maxLength');
      }
    },
    exactLength: function (schema, candidate) {
      if (typeof candidate !== 'string' && !_typeIs.array(candidate)) {
        return;
      }
      var exactLength = Number(schema.exactLength);
      if (isNaN(exactLength)) {
        return;
      }
      if (candidate.length !== exactLength) {
        this.report('must have exactly ' + exactLength + ' elements, but it have ' + candidate.length, null, 'exactLength');
      }
    },
    lt: function (schema, candidate) {
      var limit = Number(schema.lt);
      if (typeof candidate !== 'number' || isNaN(limit)) {
        return;
      }
      if (candidate >= limit) {
        this.report('must be less than ' + limit + ', but is equal to "' + candidate + '"', null, 'lt');
      }
    },
    lte: function (schema, candidate) {
      var limit = Number(schema.lte);
      if (typeof candidate !== 'number' || isNaN(limit)) {
        return;
      }
      if (candidate > limit) {
        this.report('must be less than or equal to ' + limit + ', but is equal to "' + candidate + '"', null, 'lte');
      }
    },
    gt: function (schema, candidate) {
      var limit = Number(schema.gt);
      if (typeof candidate !== 'number' || isNaN(limit)) {
        return;
      }
      if (candidate <= limit) {
        this.report('must be greater than ' + limit + ', but is equal to "' + candidate + '"', null, 'gt');
      }
    },
    gte: function (schema, candidate) {
      var limit = Number(schema.gte);
      if (typeof candidate !== 'number' || isNaN(limit)) {
        return;
      }
      if (candidate < limit) {
        this.report('must be greater than or equal to ' + limit + ', but is equal to "' + candidate + '"', null, 'gte');
      }
    },
    eq: function (schema, candidate) {
      if (typeof candidate !== 'number' && typeof candidate !== 'string' && typeof candidate !== 'boolean') {
        return;
      }
      var limit = schema.eq;
      if (typeof limit !== 'number' && typeof limit !== 'string' && typeof limit !== 'boolean' && !_typeIs.array(limit)) {
        return;
      }
      if (_typeIs.array(limit)) {
        for (var i = 0; i < limit.length; i++) {
          if (candidate === limit[i]) {
            return;
          }
        }
        this.report('must be equal to [' + limit.map(function (l) {
          return '"' + l + '"';
        }).join(' or ') + '], but is equal to "' + candidate + '"', null, 'eq');
      } else {
        if (candidate !== limit) {
          this.report('must be equal to "' + limit + '", but is equal to "' + candidate + '"', null, 'eq');
        }
      }
    },
    ne: function (schema, candidate) {
      if (typeof candidate !== 'number' && typeof candidate !== 'string') {
        return;
      }
      var limit = schema.ne;
      if (typeof limit !== 'number' && typeof limit !== 'string' && !_typeIs.array(limit)) {
        return;
      }
      if (_typeIs.array(limit)) {
        for (var i = 0; i < limit.length; i++) {
          if (candidate === limit[i]) {
            this.report('must not be equal to "' + limit[i] + '"', null, 'ne');
            return;
          }
        }
      } else {
        if (candidate === limit) {
          this.report('must not be equal to "' + limit + '"', null, 'ne');
        }
      }
    },
    multipleOf: function (schema, candidate) {
      const divisor = Number(schema.multipleOf);
      if (typeof candidate !== 'number' || isNaN(divisor)) {
        return;
      }
      if (candidate % divisor !== 0) {
        this.report(candidate + ' is not divisible by ' + divisor, null, 'multipleOf');
      }
    },
    someKeys: function (schema, candidat) {
      var _keys = schema.someKeys;
      if (!_typeIs.object(candidat)) {
        return;
      }
      var valid = _keys.some(function (action) {
        return (action in candidat);
      });
      if (!valid) {
        this.report('must have at least key ' + _keys.map(function (i) {
          return '"' + i + '"';
        }).join(' or '), null, 'someKeys');
      }
    },
    strict: function (schema, candidate) {
      if (typeof schema.strict === 'string') { schema.strict = (schema.strict === 'true'); }
      if (schema.strict !== true || !_typeIs.object(candidate) || !_typeIs.object(schema.properties)) {
        return;
      }
      var self = this;
      if (typeof schema.properties['*'] === 'undefined') {
        var intruder = Object.keys(candidate).filter(function (key) {
          return (typeof schema.properties[key] === 'undefined');
        });
        if (intruder.length > 0) {
          var msg = 'should not contains ' + (intruder.length > 1 ? 'properties' : 'property') +
            ' [' + intruder.map(function (i) { return '"' + i + '"'; }).join(', ') + ']';
          self.report(msg, null, 'strict');
        }
      }
    },
    exec: function (schema, candidate, callback) {
      var self = this;

      if (typeof callback === 'function') {
        return this.asyncExec(schema, candidate, callback);
      }
      (_typeIs.array(schema.exec) ? schema.exec : [schema.exec]).forEach(function (exec) {
        if (typeof exec === 'function') {
          exec.call(self, schema, candidate);
        }
      });
    },
    properties: function (schema, candidate, callback) {
      if (typeof callback === 'function') {
        return this.asyncProperties(schema, candidate, callback);
      }
      if (!(schema.properties instanceof Object) || !(candidate instanceof Object)) {
        return;
      }
      var properties = schema.properties;
      var i;
      if (properties['*'] != null) {
        for (i in candidate) {
          if (i in properties) {
            continue;
          }
          this._deeperObject(i);
          this._validate(properties['*'], candidate[i]);
          this._back();
        }
      }
      for (i in properties) {
        if (i === '*') {
          continue;
        }
        this._deeperObject(i);
        this._validate(properties[i], candidate[i]);
        this._back();
      }
    },
    items: function (schema, candidate, callback) {
      if (typeof callback === 'function') {
        return this.asyncItems(schema, candidate, callback);
      }
      if (!(schema.items instanceof Object) || !(candidate instanceof Object)) {
        return;
      }
      var items = schema.items;
      var i, l;
      // If provided schema is an array
      // then call validate for each case
      // else it is an Object
      // then call validate for each key
      if (_typeIs.array(items) && _typeIs.array(candidate)) {
        for (i = 0, l = items.length; i < l; i++) {
          this._deeperArray(i);
          this._validate(items[i], candidate[i]);
          this._back();
        }
      } else {
        for (var key in candidate) {
          if (Object.prototype.hasOwnProperty.call(candidate, key)) {
            this._deeperArray(key);
            this._validate(items, candidate[key]);
            this._back();
          }
        }
      }
    }
  };

  var _asyncValidationAttribut = {
    asyncExec: function (schema, candidate, callback) {
      var self = this;
      async.eachSeries(_typeIs.array(schema.exec) ? schema.exec : [schema.exec], function (exec, done) {
        if (typeof exec === 'function') {
          if (exec.length > 2) {
            return exec.call(self, schema, candidate, done);
          }
          exec.call(self, schema, candidate);
        }
        async.nextTick(done);
      }, callback);
    },
    asyncProperties: function (schema, candidate, callback) {
      if (!(schema.properties instanceof Object) || !_typeIs.object(candidate)) {
        return callback();
      }
      var self = this;
      var properties = schema.properties;
      async.series([
        function (next) {
          if (properties['*'] == null) {
            return next();
          }
          async.eachSeries(Object.keys(candidate), function (i, done) {
            if (i in properties) {
              return async.nextTick(done);
            }
            self._deeperObject(i);
            self._asyncValidate(properties['*'], candidate[i], function (err) {
              self._back();
              done(err);
            });
          }, next);
        },
        function (next) {
          async.eachSeries(Object.keys(properties), function (i, done) {
            if (i === '*') {
              return async.nextTick(done);
            }
            self._deeperObject(i);
            self._asyncValidate(properties[i], candidate[i], function (err) {
              self._back();
              done(err);
            });
          }, next);
        }
      ], callback);
    },
    asyncItems: function (schema, candidate, callback) {
      if (!(schema.items instanceof Object) || !(candidate instanceof Object)) {
        return callback();
      }
      var self = this;
      var items = schema.items;

      if (_typeIs.array(items) && _typeIs.array(candidate)) {
        async.timesSeries(items.length, function (i, done) {
          self._deeperArray(i);
          self._asyncValidate(items[i], candidate[i], function (err, res) {
            self._back();
            done(err, res);
          });
          self._back();
        }, callback);
      } else {
        async.eachSeries(Object.keys(candidate), function (key, done) {
          self._deeperArray(key);
          self._asyncValidate(items, candidate[key], function (err, res) {
            self._back();
            done(err, res);
          });
        }, callback);
      }
    }
  };

  // Validation Class ----------------------------------------------------------
  // inherits from Inspection class (actually we just call Inspection
  // constructor with the new context, because its prototype is empty
  function Validation(schema, custom) {
    Inspection.prototype.constructor.call(this, schema, _merge(Validation.custom, custom));
    var _error = [];

    this._basicFields = Object.keys(_validationAttribut);
    this._customFields = Object.keys(this._custom);
    this.origin = null;

    this.report = function (message, code, reason) {
      var newErr = {
        code: code || this.userCode || null,
        reason: reason || 'unknown',
        message: this.userError || message || 'is invalid',
        property: this.userAlias ? (this.userAlias + ' (' + this._dumpStack() + ')') : this._dumpStack()
      };
      _error.push(newErr);
      return this;
    };

    this.result = function () {
      return {
        error: _error,
        valid: _error.length === 0,
        format: function () {
          if (this.valid === true) {
            return 'Candidate is valid';
          }
          return this.error.map(function (i) {
            return 'Property ' + i.property + ': ' + i.message;
          }).join('\n');
        }
      };
    };
  }

  _extend(Validation.prototype, _validationAttribut);
  _extend(Validation.prototype, _asyncValidationAttribut);
  _extend(Validation, new Customisable());

  Validation.prototype.validate = function (candidate, callback) {
    this.origin = candidate;
    if (typeof callback === 'function') {
      var self = this;
      return async.nextTick(function () {
        self._asyncValidate(self._schema, candidate, function (err) {
          self.origin = null;
          callback(err, self.result());
        });
      });
    }
    return this._validate(this._schema, candidate).result();
  };

  Validation.prototype._validate = function (schema, candidate, callback) {
    this.userCode = schema.code || null;
    this.userError = schema.error || null;
    this.userAlias = schema.alias || null;
    this._basicFields.forEach(function (i) {
      if ((i in schema || i === 'optional') && typeof this[i] === 'function') {
        this[i](schema, candidate);
      }
    }, this);
    this._customFields.forEach(function (i) {
      if (i in schema && typeof this._custom[i] === 'function') {
        this._custom[i].call(this, schema, candidate);
      }
    }, this);
    return this;
  };

  Validation.prototype._asyncValidate = function (schema, candidate, callback) {
    var self = this;
    this.userCode = schema.code || null;
    this.userError = schema.error || null;
    this.userAlias = schema.alias || null;

    async.series([
      function (next) {
        async.eachSeries(Object.keys(_validationAttribut), function (i, done) {
          async.nextTick(function () {
            if ((i in schema || i === 'optional') && typeof self[i] === 'function') {
              if (self[i].length > 2) {
                return self[i](schema, candidate, done);
              }
              self[i](schema, candidate);
            }
            done();
          });
        }, next);
      },
      function (next) {
        async.eachSeries(Object.keys(self._custom), function (i, done) {
          async.nextTick(function () {
            if (i in schema && typeof self._custom[i] === 'function') {
              if (self._custom[i].length > 2) {
                return self._custom[i].call(self, schema, candidate, done);
              }
              self._custom[i].call(self, schema, candidate);
            }
            done();
          });
        }, next);
      }
    ], callback);
  };

  // Sanitization ----------------------------------------------------------------
  // functions called by _sanitization.type method.
  var _forceType = {
    number: function (post, schema) {
      var n;
      if (typeof post === 'number') {
        return post;
      } else if (post === '') {
        if (typeof schema.def !== 'undefined') {
          return schema.def;
        }
        return null;
      } else if (typeof post === 'string') {
        n = parseFloat(post.replace(/,/g, '.').replace(/ /g, ''));
        if (typeof n === 'number') {
          return n;
        }
      } else if (post instanceof Date) {
        return +post;
      }
      return null;
    },
    integer: function (post, schema) {
      var n;
      if (typeof post === 'number' && post % 1 === 0) {
        return post;
      } else if (post === '') {
        if (typeof schema.def !== 'undefined') {
          return schema.def;
        }
        return null;
      } else if (typeof post === 'string') {
        n = parseInt(post.replace(/ /g, ''), 10);
        if (typeof n === 'number') {
          return n;
        }
      } else if (typeof post === 'number') {
        return parseInt(post, 10);
      } else if (typeof post === 'boolean') {
        if (post) { return 1; }
        return 0;
      } else if (post instanceof Date) {
        return +post;
      }
      return null;
    },
    string: function (post, schema) {
      if (typeof post === 'boolean' || typeof post === 'number' || post instanceof Date) {
        return post.toString();
      } else if (_typeIs.array(post)) {
        // If user authorize array and strings...
        if (schema.items || schema.properties) {
          return post;
        }
        return post.join(String(schema.joinWith || ','));
      } else if (post instanceof Object) {
        // If user authorize objects ans strings...
        if (schema.items || schema.properties) {
          return post;
        }
        return JSON.stringify(post);
      } else if (typeof post === 'string' && post.length) {
        return post;
      }
      return null;
    },
    date: function (post, schema) {
      if (post instanceof Date) {
        return post;
      } else {
        var d = new Date(post);
        if (!isNaN(d.getTime())) { // if valid date
          return d;
        }
      }
      return null;
    },
    boolean: function (post, schema) {
      if (typeof post === 'undefined') return null;
      if (typeof post === 'string' && post.toLowerCase() === 'false') return false;
      return !!post;
    },
    object: function (post, schema) {
      if (typeof post !== 'string' || _typeIs.object(post)) {
        return post;
      }
      try {
        return JSON.parse(post);
      } catch (e) {
        return null;
      }
    },
    array: function (post, schema) {
      if (_typeIs.array(post)) {
        return post;
      }
      if (typeof post === 'undefined') {
        return null;
      }
      if (typeof post === 'string') {
        if (post.substring(0, 1) === '[' && post.slice(-1) === ']') {
          try {
            return JSON.parse(post);
          } catch (e) {
            return null;
          }
        }
        return post.split(String(schema.splitWith || ','));
      }
      if (!_typeIs.array(post)) {
        return [post];
      }
      return null;
    }
  };

  var _applyRules = {
    upper: function (post) {
      return post.toUpperCase();
    },
    lower: function (post) {
      return post.toLowerCase();
    },
    title: function (post) {
      // Fix by seb (replace \w\S* by \S* => exemple : coucou ça va)
      return post.replace(/\S*/g, function (txt) {
        return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
      });
    },
    capitalize: function (post) {
      return post.charAt(0).toUpperCase() + post.substr(1).toLowerCase();
    },
    ucfirst: function (post) {
      return post.charAt(0).toUpperCase() + post.substr(1);
    },
    trim: function (post) {
      return post.trim();
    }
  };

  // Every function return the future value of each property. Therefore you
  // have to return post even if you do not change its value
  var _sanitizationAttribut = {
    strict: function (schema, post) {
      if (typeof schema.strict === 'string') { schema.strict = (schema.strict === 'true'); }
      if (schema.strict !== true) {
        return post;
      }
      if (!_typeIs.object(schema.properties)) {
        return post;
      }
      if (!_typeIs.object(post)) {
        return post;
      }
      Object.keys(post).forEach(function (key) {
        if (!(key in schema.properties)) {
          delete post[key];
        }
      });
      return post;
    },
    optional: function (schema, post) {
      var opt = typeof schema.optional === 'boolean' ? schema.optional : (schema.optional !== 'false'); // Default: true
      if (opt === true) {
        return post;
      }
      if (typeof post !== 'undefined') {
        return post;
      }
      this.report();
      if (schema.def === Date) {
        return new Date();
      }
      return schema.def;
    },
    type: function (schema, post) {
      // if (_typeIs['object'](post) || _typeIs.array(post)) {
      //   return post;
      // }
      if (typeof schema.type !== 'string' || typeof _forceType[schema.type] !== 'function') {
        return post;
      }
      var n;
      var opt = typeof schema.optional === 'boolean' ? schema.optional : true;
      if (typeof _forceType[schema.type] === 'function') {
        n = _forceType[schema.type](post, schema);
        if ((n === null && !opt) || (!n && isNaN(n)) || (n === null && schema.type === 'string')) {
          n = schema.def;
        }
      } else if (!opt) {
        n = schema.def;
      }
      if ((n != null || (typeof schema.def !== 'undefined' && schema.def === n)) && n !== post) {
        this.report();
        return n;
      }
      return post;
    },
    rules: function (schema, post) {
      var rules = schema.rules;
      if (typeof post !== 'string' || (typeof rules !== 'string' && !_typeIs.array(rules))) {
        return post;
      }
      var modified = false;
      (_typeIs.array(rules) ? rules : [rules]).forEach(function (rule) {
        if (typeof _applyRules[rule] === 'function') {
          post = _applyRules[rule](post);
          modified = true;
        }
      });
      if (modified) {
        this.report();
      }
      return post;
    },
    min: function (schema, post) {
      var postTest = Number(post);
      if (isNaN(postTest)) {
        return post;
      }
      var min = Number(schema.min);
      if (isNaN(min)) {
        return post;
      }
      if (postTest < min) {
        this.report();
        return min;
      }
      return post;
    },
    max: function (schema, post) {
      var postTest = Number(post);
      if (isNaN(postTest)) {
        return post;
      }
      var max = Number(schema.max);
      if (isNaN(max)) {
        return post;
      }
      if (postTest > max) {
        this.report();
        return max;
      }
      return post;
    },
    minLength: function (schema, post) {
      var limit = Number(schema.minLength);
      if (typeof post !== 'string' || isNaN(limit) || limit < 0) {
        return post;
      }
      var str = '';
      var gap = limit - post.length;
      if (gap > 0) {
        for (var i = 0; i < gap; i++) {
          str += '-';
        }
        this.report();
        return post + str;
      }
      return post;
    },
    maxLength: function (schema, post) {
      var limit = Number(schema.maxLength);
      if (typeof post !== 'string' || isNaN(limit) || limit < 0) {
        return post;
      }
      if (post.length > limit) {
        this.report();
        return post.slice(0, limit);
      }
      return post;
    },
    properties: function (schema, post, callback) {
      if (typeof callback === 'function') {
        return this.asyncProperties(schema, post, callback);
      }
      if (!post || typeof post !== 'object') {
        return post;
      }
      var properties = schema.properties;
      var tmp;
      var i;
      if (typeof properties['*'] !== 'undefined') {
        for (i in post) {
          if (i in properties) {
            continue;
          }
          this._deeperObject(i);
          tmp = this._sanitize(properties['*'], post[i]);
          if (typeof tmp !== 'undefined' || 'exec' in properties['*']) {
            post[i] = tmp;
          }
          this._back();
        }
      }
      for (i in properties) {
        if (i !== '*') {
          this._deeperObject(i);
          tmp = this._sanitize(properties[i], post[i]);
          if (typeof tmp !== 'undefined' || 'exec' in properties[i]) {
            post[i] = tmp;
          }
          this._back();
        }
      }
      return post;
    },
    items: function (schema, post, callback) {
      if (typeof callback === 'function') {
        return this.asyncItems(schema, post, callback);
      }
      if (!(schema.items instanceof Object) || !(post instanceof Object)) {
        return post;
      }
      var i;
      if (_typeIs.array(schema.items) && _typeIs.array(post)) {
        var minLength = schema.items.length < post.length ? schema.items.length : post.length;
        for (i = 0; i < minLength; i++) {
          this._deeperArray(i);
          post[i] = this._sanitize(schema.items[i], post[i]);
          this._back();
        }
      } else {
        for (i in post) {
          if (Object.prototype.hasOwnProperty.call(post, i)) {
            this._deeperArray(i);
            post[i] = this._sanitize(schema.items, post[i]);
            this._back();
          }
        }
      }
      return post;
    },
    exec: function (schema, post, callback) {
      if (typeof callback === 'function') {
        return this.asyncExec(schema, post, callback);
      }
      var execs = _typeIs.array(schema.exec) ? schema.exec : [schema.exec];

      execs.forEach(function (exec) {
        if (typeof exec === 'function') {
          post = exec.call(this, schema, post);
        }
      }, this);
      return post;
    }
  };

  var _asyncSanitizationAttribut = {
    asyncExec: function (schema, post, callback) {
      var self = this;
      var execs = _typeIs.array(schema.exec) ? schema.exec : [schema.exec];

      async.eachSeries(execs, function (exec, done) {
        if (typeof exec === 'function') {
          if (exec.length > 2) {
            return exec.call(self, schema, post, function (err, res) {
              if (err) {
                return done(err);
              }
              post = res;
              done();
            });
          }
          post = exec.call(self, schema, post);
        }
        done();
      }, function (err) {
        callback(err, post);
      });
    },
    asyncProperties: function (schema, post, callback) {
      if (!post || typeof post !== 'object') {
        return callback(null, post);
      }
      var self = this;
      var properties = schema.properties;

      async.series([
        function (next) {
          if (properties['*'] == null) {
            return next();
          }
          var globing = properties['*'];
          async.eachSeries(Object.keys(post), function (i, next) {
            if (i in properties) {
              return next();
            }
            self._deeperObject(i);
            self._asyncSanitize(globing, post[i], function (err, res) {
              if (err) { /* Error can safely be ignored here */ }
              if (typeof res !== 'undefined') {
                post[i] = res;
              }
              self._back();
              next();
            });
          }, next);
        },
        function (next) {
          async.eachSeries(Object.keys(properties), function (i, next) {
            if (i === '*') {
              return next();
            }
            self._deeperObject(i);
            self._asyncSanitize(properties[i], post[i], function (err, res) {
              if (err) {
                return next(err);
              }
              if (typeof res !== 'undefined') {
                post[i] = res;
              }
              self._back();
              next();
            });
          }, next);
        }
      ], function (err) {
        return callback(err, post);
      });
    },
    asyncItems: function (schema, post, callback) {
      if (!(schema.items instanceof Object) || !(post instanceof Object)) {
        return callback(null, post);
      }
      var self = this;
      var items = schema.items;
      if (_typeIs.array(items) && _typeIs.array(post)) {
        var minLength = items.length < post.length ? items.length : post.length;
        async.timesSeries(minLength, function (i, next) {
          self._deeperArray(i);
          self._asyncSanitize(items[i], post[i], function (err, res) {
            if (err) {
              return next(err);
            }
            post[i] = res;
            self._back();
            next();
          });
        }, function (err) {
          callback(err, post);
        });
      } else {
        async.eachSeries(Object.keys(post), function (key, next) {
          self._deeperArray(key);
          self._asyncSanitize(items, post[key], function (err, res) {
            if (err) {
              return next();
            }
            post[key] = res;
            self._back();
            next();
          });
        }, function (err) {
          callback(err, post);
        });
      }
      return post;
    }
  };

  // Sanitization Class --------------------------------------------------------
  // inherits from Inspection class (actually we just call Inspection
  // constructor with the new context, because its prototype is empty
  function Sanitization(schema, custom) {
    Inspection.prototype.constructor.call(this, schema, _merge(Sanitization.custom, custom));
    var _reporting = [];

    this._basicFields = Object.keys(_sanitizationAttribut);
    this._customFields = Object.keys(this._custom);
    this.origin = null;

    this.report = function (message) {
      var newNot = {
        message: message || 'was sanitized',
        property: this.userAlias ? (this.userAlias + ' (' + this._dumpStack() + ')') : this._dumpStack()
      };
      if (!_reporting.some(function (e) { return e.property === newNot.property; })) {
        _reporting.push(newNot);
      }
    };

    this.result = function (data) {
      // For old IE.
      /* eslint-disable object-shorthand */
      return {
        data: data,
        reporting: _reporting,
        format: function () {
          return this.reporting.map(function (i) {
            return 'Property ' + i.property + ' ' + i.message;
          }).join('\n');
        }
      };
      /* eslint-enable object-shorthand */
    };
  }

  _extend(Sanitization.prototype, _sanitizationAttribut);
  _extend(Sanitization.prototype, _asyncSanitizationAttribut);
  _extend(Sanitization, new Customisable());

  Sanitization.prototype.sanitize = function (post, callback) {
    this.origin = post;
    if (typeof callback === 'function') {
      var self = this;
      return this._asyncSanitize(this._schema, post, function (err, data) {
        self.origin = null;
        callback(err, self.result(data));
      });
    }
    var data = this._sanitize(this._schema, post);
    this.origin = null;
    return this.result(data);
  };

  Sanitization.prototype._sanitize = function (schema, post) {
    this.userAlias = schema.alias || null;
    this._basicFields.forEach(function (i) {
      if ((i in schema || i === 'optional') && typeof this[i] === 'function') {
        post = this[i](schema, post);
      }
    }, this);
    this._customFields.forEach(function (i) {
      if (i in schema && typeof this._custom[i] === 'function') {
        post = this._custom[i].call(this, schema, post);
      }
    }, this);
    return post;
  };

  Sanitization.prototype._asyncSanitize = function (schema, post, callback) {
    var self = this;
    this.userAlias = schema.alias || null;

    async.waterfall([
      function (next) {
        async.reduce(self._basicFields, post, function (value, i, next) {
          async.nextTick(function () {
            if ((i in schema || i === 'optional') && typeof self[i] === 'function') {
              if (self[i].length > 2) {
                return self[i](schema, value, next);
              }
              value = self[i](schema, value);
            }
            next(null, value);
          });
        }, next);
      },
      function (inter, next) {
        async.reduce(self._customFields, inter, function (value, i, next) {
          async.nextTick(function () {
            if (i in schema && typeof self._custom[i] === 'function') {
              if (self._custom[i].length > 2) {
                return self._custom[i].call(self, schema, value, next);
              }
              value = self._custom[i].call(self, schema, value);
            }
            next(null, value);
          });
        }, next);
      }
    ], callback);
  };

  // ---------------------------------------------------------------------------

  var INT_MIN = -2147483648;
  var INT_MAX = 2147483647;

  var _rand = {
    int: function (min, max) {
      return min + (0 | Math.random() * (max - min + 1));
    },
    float: function (min, max) {
      return (Math.random() * (max - min) + min);
    },
    bool: function () {
      return (Math.random() > 0.5);
    },
    char: function (min, max) {
      return String.fromCharCode(this.int(min, max));
    },
    fromList: function (list) {
      return list[this.int(0, list.length - 1)];
    }
  };

  var _formatSample = {
    'date-time': function () {
      return new Date().toISOString();
    },
    date: function () {
      return new Date().toISOString().replace(/T.*$/, '');
    },
    time: function () {
      return new Date().toLocaleTimeString({}, { hour12: false });
    },
    color: function (min, max) {
      var s = '#';
      if (min < 1) {
        min = 1;
      }
      for (var i = 0, l = _rand.int(min, max); i < l; i++) {
        s += _rand.fromList('0123456789abcdefABCDEF');
      }
      return s;
    },
    numeric: function () {
      return '' + _rand.int(0, INT_MAX);
    },
    integer: function () {
      if (_rand.bool() === true) {
        return '-' + this.numeric();
      }
      return this.numeric();
    },
    decimal: function () {
      return this.integer() + '.' + this.numeric();
    },
    alpha: function (min, max) {
      var s = '';
      if (min < 1) {
        min = 1;
      }
      for (var i = 0, l = _rand.int(min, max); i < l; i++) {
        s += _rand.fromList('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
      }
      return s;
    },
    alphaNumeric: function (min, max) {
      var s = '';
      if (min < 1) {
        min = 1;
      }
      for (var i = 0, l = _rand.int(min, max); i < l; i++) {
        s += _rand.fromList('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
      }
      return s;
    },
    alphaDash: function (min, max) {
      var s = '';
      if (min < 1) {
        min = 1;
      }
      for (var i = 0, l = _rand.int(min, max); i < l; i++) {
        s += _rand.fromList('_-abcdefghijklmnopqrstuvwxyz_-ABCDEFGHIJKLMNOPQRSTUVWXYZ_-0123456789_-');
      }
      return s;
    },
    javascript: function (min, max) {
      var s = _rand.fromList('_$abcdefghijklmnopqrstuvwxyz_$ABCDEFGHIJKLMNOPQRSTUVWXYZ_$');
      for (var i = 0, l = _rand.int(min, max - 1); i < l; i++) {
        s += _rand.fromList('_$abcdefghijklmnopqrstuvwxyz_$ABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789_$');
      }
      return s;
    }
  };

  function _getLimits(schema) {
    var min = INT_MIN;
    var max = INT_MAX;

    if (schema.gte != null) {
      min = schema.gte;
    } else if (schema.gt != null) {
      min = schema.gt + 1;
    }
    if (schema.lte != null) {
      max = schema.lte;
    } else if (schema.lt != null) {
      max = schema.lt - 1;
    }
    // For old IE.
    /* eslint-disable object-shorthand */
    return { min: min, max: max };
    /* eslint-enable object-shorthand */
  }

  var _typeGenerator = {
    string: function (schema) {
      if (schema.eq != null) {
        return schema.eq;
      }
      var s = '';
      var minLength = schema.minLength != null ? schema.minLength : 0;
      var maxLength = schema.maxLength != null ? schema.maxLength : 32;
      if (typeof schema.pattern === 'string' && typeof _formatSample[schema.pattern] === 'function') {
        return _formatSample[schema.pattern](minLength, maxLength);
      }

      var l = schema.exactLength != null ? schema.exactLength : _rand.int(minLength, maxLength);
      for (var i = 0; i < l; i++) {
        s += _rand.char(32, 126);
      }
      return s;
    },
    number: function (schema) {
      if (schema.eq != null) {
        return schema.eq;
      }
      var limit = _getLimits(schema);
      var n = _rand.float(limit.min, limit.max);
      if (schema.ne != null) {
        var ne = _typeIs.array(schema.ne) ? schema.ne : [schema.ne];
        while (ne.indexOf(n) !== -1) {
          n = _rand.float(limit.min, limit.max);
        }
      }
      return n;
    },
    integer: function (schema) {
      if (schema.eq != null) {
        return schema.eq;
      }
      var limit = _getLimits(schema);
      var n = _rand.int(limit.min, limit.max);
      if (schema.ne != null) {
        var ne = _typeIs.array(schema.ne) ? schema.ne : [schema.ne];
        while (ne.indexOf(n) !== -1) {
          n = _rand.int(limit.min, limit.max);
        }
      }
      return n;
    },
    boolean: function (schema) {
      if (schema.eq != null) {
        return schema.eq;
      }
      return _rand.bool();
    },
    null: function (schema) {
      return null;
    },
    date: function (schema) {
      if (schema.eq != null) {
        return schema.eq;
      }
      return new Date();
    },
    object: function (schema) {
      var o = {};
      var prop = schema.properties || {};

      for (var key in prop) {
        if (Object.prototype.hasOwnProperty.call(prop, key)) {
          if (prop[key].optional === true && _rand.bool() === true) {
            continue;
          }
          if (key !== '*') {
            o[key] = this.generate(prop[key]);
          } else {
            var rk = '__random_key_';
            var randomKey = rk + 0;
            var n = _rand.int(1, 9);
            for (var i = 1; i <= n; i++) {
              if (!(randomKey in prop)) {
                o[randomKey] = this.generate(prop[key]);
              }
              randomKey = rk + i;
            }
          }
        }
      }
      return o;
    },
    array: function (schema) {
      var self = this;
      var items = schema.items || {};
      var minLength = schema.minLength != null ? schema.minLength : 0;
      var maxLength = schema.maxLength != null ? schema.maxLength : 16;
      var type;
      var candidate;
      var size;
      var i;

      if (_typeIs.array(items)) {
        size = items.length;
        if (schema.exactLength != null) {
          size = schema.exactLength;
        } else if (size < minLength) {
          size = minLength;
        } else if (size > maxLength) {
          size = maxLength;
        }
        candidate = new Array(size);
        type = null;
        for (i = 0; i < size; i++) {
          type = items[i].type || 'any';
          if (_typeIs.array(type)) {
            type = type[_rand.int(0, type.length - 1)];
          }
          candidate[i] = self[type](items[i]);
        }
      } else {
        size = schema.exactLength != null ? schema.exactLength : _rand.int(minLength, maxLength);
        candidate = new Array(size);
        type = items.type || 'any';
        if (_typeIs.array(type)) {
          type = type[_rand.int(0, type.length - 1)];
        }
        for (i = 0; i < size; i++) {
          candidate[i] = self[type](items);
        }
      }
      return candidate;
    },
    any: function (schema) {
      var fields = Object.keys(_typeGenerator);
      var i = fields[_rand.int(0, fields.length - 2)];
      return this[i](schema);
    }
  };

  // CandidateGenerator Class (Singleton) --------------------------------------
  function CandidateGenerator() {
    // Maybe extends Inspection class too ?
  }

  _extend(CandidateGenerator.prototype, _typeGenerator);

  var _instance = null;
  CandidateGenerator.instance = function () {
    if (!(_instance instanceof CandidateGenerator)) {
      _instance = new CandidateGenerator();
    }
    return _instance;
  };

  CandidateGenerator.prototype.generate = function (schema) {
    var type = schema.type || 'any';
    if (_typeIs.array(type)) {
      type = type[_rand.int(0, type.length - 1)];
    }
    return this[type](schema);
  };

  // Exports ---------------------------------------------------------------------
  var SchemaInspector = {};

  // if server-side (node.js) else client-side
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = SchemaInspector;
  } else {
    window.SchemaInspector = SchemaInspector;
  }

  SchemaInspector.newSanitization = function (schema, custom) {
    return new Sanitization(schema, custom);
  };

  SchemaInspector.newValidation = function (schema, custom) {
    return new Validation(schema, custom);
  };

  SchemaInspector.Validation = Validation;
  SchemaInspector.Sanitization = Sanitization;

  SchemaInspector.sanitize = function (schema, post, custom, callback) {
    if (arguments.length === 3 && typeof custom === 'function') {
      callback = custom;
      custom = null;
    }
    return new Sanitization(schema, custom).sanitize(post, callback);
  };

  SchemaInspector.validate = function (schema, candidate, custom, callback) {
    if (arguments.length === 3 && typeof custom === 'function') {
      callback = custom;
      custom = null;
    }
    return new Validation(schema, custom).validate(candidate, callback);
  };

  SchemaInspector.generate = function (schema, n) {
    if (typeof n === 'number') {
      var r = new Array(n);
      for (var i = 0; i < n; i++) {
        r[i] = CandidateGenerator.instance().generate(schema);
      }
      return r;
    }
    return CandidateGenerator.instance().generate(schema);
  };
})();