'use strict';

var resolve = require('./resolve')

, util = require('./util')
, errorClasses = require('./error_classes')
, stableStringify = require('fast-json-stable-stringify');

var validateGenerator = require('../dotjs/validate');

/**

* Functions below are used inside compiled validations function
*/

var ucs2length = util.ucs2length; var equal = require('fast-deep-equal');

// this error is thrown by async schemas to return validation errors via exception var ValidationError = errorClasses.Validation;

module.exports = compile;

/**

* Compiles schema to validation function
* @this   Ajv
* @param  {Object} schema schema object
* @param  {Object} root object with information about the root schema for this schema
* @param  {Object} localRefs the hash of local references inside the schema (created by resolve.id), used for inline resolution
* @param  {String} baseId base ID for IDs in the schema
* @return {Function} validation function
*/

function compile(schema, root, localRefs, baseId) {

/* jshint validthis: true, evil: true */
/* eslint no-shadow: 0 */
var self = this
  , opts = this._opts
  , refVal = [ undefined ]
  , refs = {}
  , patterns = []
  , patternsHash = {}
  , defaults = []
  , defaultsHash = {}
  , customRules = [];

root = root || { schema: schema, refVal: refVal, refs: refs };

var c = checkCompiling.call(this, schema, root, baseId);
var compilation = this._compilations[c.index];
if (c.compiling) return (compilation.callValidate = callValidate);

var formats = this._formats;
var RULES = this.RULES;

try {
  var v = localCompile(schema, root, localRefs, baseId);
  compilation.validate = v;
  var cv = compilation.callValidate;
  if (cv) {
    cv.schema = v.schema;
    cv.errors = null;
    cv.refs = v.refs;
    cv.refVal = v.refVal;
    cv.root = v.root;
    cv.$async = v.$async;
    if (opts.sourceCode) cv.source = v.source;
  }
  return v;
} finally {
  endCompiling.call(this, schema, root, baseId);
}

/* @this   {*} - custom context, see passContext option */
function callValidate() {
  /* jshint validthis: true */
  var validate = compilation.validate;
  var result = validate.apply(this, arguments);
  callValidate.errors = validate.errors;
  return result;
}

function localCompile(_schema, _root, localRefs, baseId) {
  var isRoot = !_root || (_root && _root.schema == _schema);
  if (_root.schema != root.schema)
    return compile.call(self, _schema, _root, localRefs, baseId);

  var $async = _schema.$async === true;

  var sourceCode = validateGenerator({
    isTop: true,
    schema: _schema,
    isRoot: isRoot,
    baseId: baseId,
    root: _root,
    schemaPath: '',
    errSchemaPath: '#',
    errorPath: '""',
    MissingRefError: errorClasses.MissingRef,
    RULES: RULES,
    validate: validateGenerator,
    util: util,
    resolve: resolve,
    resolveRef: resolveRef,
    usePattern: usePattern,
    useDefault: useDefault,
    useCustomRule: useCustomRule,
    opts: opts,
    formats: formats,
    logger: self.logger,
    self: self
  });

  sourceCode = vars(refVal, refValCode) + vars(patterns, patternCode)
                 + vars(defaults, defaultCode) + vars(customRules, customRuleCode)
                 + sourceCode;

  if (opts.processCode) sourceCode = opts.processCode(sourceCode);
  // console.log('\n\n\n *** \n', JSON.stringify(sourceCode));
  var validate;
  try {
    var makeValidate = new Function(
      'self',
      'RULES',
      'formats',
      'root',
      'refVal',
      'defaults',
      'customRules',
      'equal',
      'ucs2length',
      'ValidationError',
      sourceCode
    );

    validate = makeValidate(
      self,
      RULES,
      formats,
      root,
      refVal,
      defaults,
      customRules,
      equal,
      ucs2length,
      ValidationError
    );

    refVal[0] = validate;
  } catch(e) {
    self.logger.error('Error compiling schema, function code:', sourceCode);
    throw e;
  }

  validate.schema = _schema;
  validate.errors = null;
  validate.refs = refs;
  validate.refVal = refVal;
  validate.root = isRoot ? validate : _root;
  if ($async) validate.$async = true;
  if (opts.sourceCode === true) {
    validate.source = {
      code: sourceCode,
      patterns: patterns,
      defaults: defaults
    };
  }

  return validate;
}

function resolveRef(baseId, ref, isRoot) {
  ref = resolve.url(baseId, ref);
  var refIndex = refs[ref];
  var _refVal, refCode;
  if (refIndex !== undefined) {
    _refVal = refVal[refIndex];
    refCode = 'refVal[' + refIndex + ']';
    return resolvedRef(_refVal, refCode);
  }
  if (!isRoot && root.refs) {
    var rootRefId = root.refs[ref];
    if (rootRefId !== undefined) {
      _refVal = root.refVal[rootRefId];
      refCode = addLocalRef(ref, _refVal);
      return resolvedRef(_refVal, refCode);
    }
  }

  refCode = addLocalRef(ref);
  var v = resolve.call(self, localCompile, root, ref);
  if (v === undefined) {
    var localSchema = localRefs && localRefs[ref];
    if (localSchema) {
      v = resolve.inlineRef(localSchema, opts.inlineRefs)
          ? localSchema
          : compile.call(self, localSchema, root, localRefs, baseId);
    }
  }

  if (v === undefined) {
    removeLocalRef(ref);
  } else {
    replaceLocalRef(ref, v);
    return resolvedRef(v, refCode);
  }
}

function addLocalRef(ref, v) {
  var refId = refVal.length;
  refVal[refId] = v;
  refs[ref] = refId;
  return 'refVal' + refId;
}

function removeLocalRef(ref) {
  delete refs[ref];
}

function replaceLocalRef(ref, v) {
  var refId = refs[ref];
  refVal[refId] = v;
}

function resolvedRef(refVal, code) {
  return typeof refVal == 'object' || typeof refVal == 'boolean'
          ? { code: code, schema: refVal, inline: true }
          : { code: code, $async: refVal && !!refVal.$async };
}

function usePattern(regexStr) {
  var index = patternsHash[regexStr];
  if (index === undefined) {
    index = patternsHash[regexStr] = patterns.length;
    patterns[index] = regexStr;
  }
  return 'pattern' + index;
}

function useDefault(value) {
  switch (typeof value) {
    case 'boolean':
    case 'number':
      return '' + value;
    case 'string':
      return util.toQuotedString(value);
    case 'object':
      if (value === null) return 'null';
      var valueStr = stableStringify(value);
      var index = defaultsHash[valueStr];
      if (index === undefined) {
        index = defaultsHash[valueStr] = defaults.length;
        defaults[index] = value;
      }
      return 'default' + index;
  }
}

function useCustomRule(rule, schema, parentSchema, it) {
  if (self._opts.validateSchema !== false) {
    var deps = rule.definition.dependencies;
    if (deps && !deps.every(function(keyword) {
      return Object.prototype.hasOwnProperty.call(parentSchema, keyword);
    }))
      throw new Error('parent schema must have all required keywords: ' + deps.join(','));

    var validateSchema = rule.definition.validateSchema;
    if (validateSchema) {
      var valid = validateSchema(schema);
      if (!valid) {
        var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors);
        if (self._opts.validateSchema == 'log') self.logger.error(message);
        else throw new Error(message);
      }
    }
  }

  var compile = rule.definition.compile
    , inline = rule.definition.inline
    , macro = rule.definition.macro;

  var validate;
  if (compile) {
    validate = compile.call(self, schema, parentSchema, it);
  } else if (macro) {
    validate = macro.call(self, schema, parentSchema, it);
    if (opts.validateSchema !== false) self.validateSchema(validate, true);
  } else if (inline) {
    validate = inline.call(self, it, rule.keyword, schema, parentSchema);
  } else {
    validate = rule.definition.validate;
    if (!validate) return;
  }

  if (validate === undefined)
    throw new Error('custom keyword "' + rule.keyword + '"failed to compile');

  var index = customRules.length;
  customRules[index] = validate;

  return {
    code: 'customRule' + index,
    validate: validate
  };
}

}

/**

* Checks if the schema is currently compiled
* @this   Ajv
* @param  {Object} schema schema to compile
* @param  {Object} root root object
* @param  {String} baseId base schema ID
* @return {Object} object with properties "index" (compilation index) and "compiling" (boolean)
*/

function checkCompiling(schema, root, baseId) {

/* jshint validthis: true */
var index = compIndex.call(this, schema, root, baseId);
if (index >= 0) return { index: index, compiling: true };
index = this._compilations.length;
this._compilations[index] = {
  schema: schema,
  root: root,
  baseId: baseId
};
return { index: index, compiling: false };

}

/**

* Removes the schema from the currently compiled list
* @this   Ajv
* @param  {Object} schema schema to compile
* @param  {Object} root root object
* @param  {String} baseId base schema ID
*/

function endCompiling(schema, root, baseId) {

/* jshint validthis: true */
var i = compIndex.call(this, schema, root, baseId);
if (i >= 0) this._compilations.splice(i, 1);

}

/**

* Index of schema compilation in the currently compiled list
* @this   Ajv
* @param  {Object} schema schema to compile
* @param  {Object} root root object
* @param  {String} baseId base schema ID
* @return {Integer} compilation index
*/

function compIndex(schema, root, baseId) {

/* jshint validthis: true */
for (var i=0; i<this._compilations.length; i++) {
  var c = this._compilations[i];
  if (c.schema == schema && c.root == root && c.baseId == baseId) return i;
}
return -1;

}

function patternCode(i, patterns) {

return 'var pattern' + i + ' = new RegExp(' + util.toQuotedString(patterns[i]) + ');';

}

function defaultCode(i) {

return 'var default' + i + ' = defaults[' + i + '];';

}

function refValCode(i, refVal) {

return refVal[i] === undefined ? '' : 'var refVal' + i + ' = refVal[' + i + '];';

}

function customRuleCode(i) {

return 'var customRule' + i + ' = customRules[' + i + '];';

}

function vars(arr, statement) {

if (!arr.length) return '';
var code = '';
for (var i=0; i<arr.length; i++)
  code += statement(i, arr);
return code;

}