'use strict';

var MissingRefError = require('./error_classes').MissingRef;

module.exports = compileAsync;

/**

* Creates validating function for passed schema with asynchronous loading of missing schemas.
* `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema.
* @this  Ajv
* @param {Object}   schema schema object
* @param {Boolean}  meta optional true to compile meta-schema; this parameter can be skipped
* @param {Function} callback an optional node-style callback, it is called with 2 parameters: error (or null) and validating function.
* @return {Promise} promise that resolves with a validating function.
*/

function compileAsync(schema, meta, callback) {

/* eslint no-shadow: 0 */
/* global Promise */
/* jshint validthis: true */
var self = this;
if (typeof this._opts.loadSchema != 'function')
  throw new Error('options.loadSchema should be a function');

if (typeof meta == 'function') {
  callback = meta;
  meta = undefined;
}

var p = loadMetaSchemaOf(schema).then(function () {
  var schemaObj = self._addSchema(schema, undefined, meta);
  return schemaObj.validate || _compileAsync(schemaObj);
});

if (callback) {
  p.then(
    function(v) { callback(null, v); },
    callback
  );
}

return p;

function loadMetaSchemaOf(sch) {
  var $schema = sch.$schema;
  return $schema && !self.getSchema($schema)
          ? compileAsync.call(self, { $ref: $schema }, true)
          : Promise.resolve();
}

function _compileAsync(schemaObj) {
  try { return self._compile(schemaObj); }
  catch(e) {
    if (e instanceof MissingRefError) return loadMissingSchema(e);
    throw e;
  }

  function loadMissingSchema(e) {
    var ref = e.missingSchema;
    if (added(ref)) throw new Error('Schema ' + ref + ' is loaded but ' + e.missingRef + ' cannot be resolved');

    var schemaPromise = self._loadingSchemas[ref];
    if (!schemaPromise) {
      schemaPromise = self._loadingSchemas[ref] = self._opts.loadSchema(ref);
      schemaPromise.then(removePromise, removePromise);
    }

    return schemaPromise.then(function (sch) {
      if (!added(ref)) {
        return loadMetaSchemaOf(sch).then(function () {
          if (!added(ref)) self.addSchema(sch, ref, undefined, meta);
        });
      }
    }).then(function() {
      return _compileAsync(schemaObj);
    });

    function removePromise() {
      delete self._loadingSchemas[ref];
    }

    function added(ref) {
      return self._refs[ref] || self._schemas[ref];
    }
  }
}

}