//
// Register schemas and retrieve previously-registered schemas.
//

var uri = require('./uri.js')
  , url = require('url')
  ;


// Schemas for which we have our own bundled copy. These will be
// registered automatically.
var EMBEDDED_SCHEMAS = {
  'http://json-schema.org/draft-04/schema#':
    require('./suites/draft-04/json-schema-draft-v4.json')
};


// ******************************************************************
// Constructor
// ******************************************************************
var SchemaRegistry = function() {
  this._schemas = {};
  this._missingSchemas = {};

  // register embedded schemas
  var keys = Object.keys(EMBEDDED_SCHEMAS);
  keys.forEach(function(key) { this.register(EMBEDDED_SCHEMAS[key]); }, this);
};


// ******************************************************************
// [static] Decode an escaped JSON pointer
// ******************************************************************
SchemaRegistry._decodeJsonPointer = function(pointer) {
  var result = pointer.replace(/\~1/g, '/');
  return result.replace(/\~0/g, '~');
};


// ******************************************************************
// [static] Given a schema and a JSON pointer, return the
// corresponding sub-schema referenced by the JSON pointer.
// ******************************************************************
SchemaRegistry._resolveJsonPointer = function(schema, jp) {
  if (jp === '#') {
    return schema;
  }

  if (jp.slice(0, 2) !== '#/') {
    // not a JSON pointer fragment
    // (may be a valid id ref, but that’s not our problem here)
    return null;
  }

  var path = jp.slice(2).split('/');
  var currentSchema = schema;
  while (path.length) {
    var element = SchemaRegistry._decodeJsonPointer(path.shift());
    if (!Object.prototype.hasOwnProperty.call(currentSchema, element)) {
      return null;
    }
    currentSchema = currentSchema[element];
  }

  return currentSchema;
};


// ******************************************************************
// [static] Normalize schema ids so they can be looked up.
// ******************************************************************
SchemaRegistry._normalizeId = function(id) {
  // for internal use we add the '#' (empty fragment) if missing
  if (id.indexOf('#') === -1) { return id + '#'; }
  var uriObj = uri.parse(id);
  if (uriObj.kind === 'url') { id = url.resolve(id, id); }
  return id;
};


// ******************************************************************
// Return boolean indicating whether the specified schema id has
// previously been registered.
// ******************************************************************
SchemaRegistry.prototype.isRegistered = function(id) {
  if (!id) { return false; }
  id = SchemaRegistry._normalizeId(id);
  if (this._schemas.hasOwnProperty(id)) { return true; }
  var uriObj = uri.parse(id);
  return this._schemas.hasOwnProperty(uriObj.baseUri);
};


// ******************************************************************
// [static] Helper to descend into an object, recursively gathering
// all $refs values from the given object and its sub-objects.
// ******************************************************************
SchemaRegistry._gatherRefs = function(obj) {
  var result = [];

  var currentObj = obj;
  var subObjects = [];

  do {

    if (Object.prototype.hasOwnProperty.call(currentObj, '$ref')) {
      result.push(currentObj.$ref);
    }

    var keys = Object.keys(currentObj);
    for (var index = 0, len = keys.length; index !== len; ++index) {
      var prop = currentObj[keys[index]];
      if (typeof prop === 'object') { subObjects.push(prop); }
    }

    currentObj = subObjects.pop();

  } while(currentObj);

  return result;
};


// ******************************************************************
// [static] Helper to descend into an object, recursively gathering
// all sub-objects that contain an id property.
//
// Returns an array where each element is an array of the form:
//
// [ schema, resolutionScope ]
// ******************************************************************
SchemaRegistry._getSubObjectsHavingIds = function(obj, resolutionScope) {
  var result = [];

  resolutionScope = resolutionScope || '#';
  var currentObj = obj;
  var subObjects = [];
  var nextItem;

  do {

    if (currentObj !== null && currentObj !== undefined) {
      if (Object.prototype.hasOwnProperty.call(currentObj, 'id') &&
          typeof currentObj.id === 'string')
      {
        result.push([currentObj, resolutionScope]);
        resolutionScope = uri.resolve(resolutionScope, currentObj.id);
      }

      var keys = Object.keys(currentObj);
      for (var index = 0, len = keys.length; index !== len; ++index) {
        var prop = currentObj[keys[index]];
        if (typeof prop === 'object') {
          subObjects.push([prop, resolutionScope + '/' + keys[index]]);
        }
      }
    }

    nextItem = subObjects.pop();
    if (nextItem) {
      currentObj = nextItem[0];
      resolutionScope = nextItem[1];
    }
  } while(nextItem);

  return result;
};


// ******************************************************************
// Return currently-unregistered schemas $referenced by the given
// schema.
// ******************************************************************
SchemaRegistry.prototype._missingRefsForSchema = function(schema) {
  var allRefs = SchemaRegistry._gatherRefs(schema);
  var missingRefs = [];

  allRefs.forEach(function(ref) {
    if (ref[0] !== '#') {
      var uriObj = uri.parse(ref);
      if (!this.isRegistered(uriObj.baseUri)) {
        missingRefs.push(uriObj.baseUri);
      }
    }
  }, this);

  return missingRefs;
};


// ******************************************************************
// Register a schema (internal implementation)
// ******************************************************************
SchemaRegistry.prototype._registerImpl = function(schema, id, _resolutionScope)
{
  // sanity check
  if (typeof schema !== 'object' || Array.isArray(schema)) { return []; }

  if (id) {
    var resolvedId = uri.resolve(_resolutionScope || id, id);
    if (this.isRegistered(resolvedId)) { return; }

    var uriObj = uri.parse(resolvedId);
    if (!uriObj.baseUri) { return; }

    this._schemas[resolvedId] = schema;
   }
};

// ******************************************************************
// Register a schema (public interface)
// ******************************************************************
SchemaRegistry.prototype.register = function(schema, id) {
  id = id || schema.id;
  this._registerImpl(schema, id);

  // register any id'd sub-objects
  var toRegister = SchemaRegistry._getSubObjectsHavingIds(schema, id);
  toRegister.forEach(function(item) {
    this._registerImpl(item[0], item[0].id, item[1]);
  }, this);

  // save any missing refs to support the getMissingSchemas method
  var missing = this._missingRefsForSchema(schema);
  missing.forEach(function(item) {
    this._missingSchemas[item] = true;
  }, this);

  return missing;
};

// ******************************************************************
// Get an array of $refs for which we don’t have a schema yet.
// ******************************************************************
SchemaRegistry.prototype.getMissingSchemas = function() {
  var result = Object.keys(this._missingSchemas);
  result = result.filter(function(item) {
    return !this.isRegistered(item);
  }, this);
  return result;
};

// ******************************************************************
// Retrieve a previously-registered schema.
// ******************************************************************
SchemaRegistry.prototype.get = function(id) {

  id = SchemaRegistry._normalizeId(id);

  if (this.isRegistered(id)) {
    return this._schemas[id];
  }

  var uriObj = uri.parse(id);

  if (uriObj.fragment.slice(0, 2) === '#/') {
      return SchemaRegistry._resolveJsonPointer(
      this._schemas[uriObj.baseUri + '#'],
      uriObj.fragment
    );
  }
};

module.exports = SchemaRegistry;
