text: 'use strict';
  
  var resolve = require('./resolve')
    , util = require('./util')
    , errorClasses = require('./error_classes')
    , stableStringify = require('json-stable-stringify');
  
  var validateGenerator = require('../dotjs/validate');
  
  /**
   * Functions below are used inside compiled validations function
   */
  
  var co = require('co');
  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);
    }
  
    function callValidate() {
      var validate = compilation.validate;
      var result = validate.apply(null, 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,
        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',
          'co',
          'equal',
          'ucs2length',
          'ValidationError',
          sourceCode
        );
  
        validate = makeValidate(
          self,
          RULES,
          formats,
          root,
          refVal,
          defaults,
          customRules,
          co,
          equal,
          ucs2length,
          ValidationError
        );
  
        refVal[0] = validate;
      } catch(e) {
        console.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) {
      var validateSchema = rule.definition.validateSchema;
      if (validateSchema && self._opts.validateSchema !== false) {
        var valid = validateSchema(schema);
        if (!valid) {
          var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors);
          if (self._opts.validateSchema == 'log') console.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;
  }
  