'use strict';

module.exports = $Ref;

var Pointer = require('./pointer');

/**
 * This class represents a single JSON reference and its resolved value.
 *
 * @constructor
 */
function $Ref() {
  /**
   * The file path or URL of the referenced file.
   * This path is relative to the path of the main JSON schema file.
   *
   * This path does NOT contain document fragments (JSON pointers). It always references an ENTIRE file.
   * Use methods such as {@link $Ref#get}, {@link $Ref#resolve}, and {@link $Ref#exists} to get
   * specific JSON pointers within the file.
   *
   * @type {string}
   */
  this.path = undefined;

  /**
   * The resolved value of the JSON reference.
   * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
   * @type {?*}
   */
  this.value = undefined;

  /**
   * The {@link $Refs} object that contains this {@link $Ref} object.
   * @type {$Refs}
   */
  this.$refs = undefined;

  /**
   * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
   * @type {?string}
   */
  this.pathType = undefined;
}

/**
 * Determines whether the given JSON reference exists within this {@link $Ref#value}.
 *
 * @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
 * @param {$RefParserOptions} options
 * @returns {boolean}
 */
$Ref.prototype.exists = function(path, options) {
  try {
    this.resolve(path, options);
    return true;
  }
  catch (e) {
    return false;
  }
};

/**
 * Resolves the given JSON reference within this {@link $Ref#value} and returns the resolved value.
 *
 * @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
 * @param {$RefParserOptions} options
 * @returns {*} - Returns the resolved value
 */
$Ref.prototype.get = function(path, options) {
  return this.resolve(path, options).value;
};

/**
 * Resolves the given JSON reference within this {@link $Ref#value}.
 *
 * @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
 * @param {$RefParserOptions} options
 * @returns {Pointer}
 */
$Ref.prototype.resolve = function(path, options) {
  var pointer = new Pointer(this, path);
  return pointer.resolve(this.value, options);
};

/**
 * Sets the value of a nested property within this {@link $Ref#value}.
 * If the property, or any of its parents don't exist, they will be created.
 *
 * @param {string} path - The full path of the property to set, optionally with a JSON pointer in the hash
 * @param {*} value - The value to assign
 */
$Ref.prototype.set = function(path, value) {
  var pointer = new Pointer(this, path);
  this.value = pointer.set(this.value, value);
};

/**
 * Determines whether the given value is a JSON reference.
 *
 * @param {*} value - The value to inspect
 * @returns {boolean}
 */
$Ref.is$Ref = function(value) {
  return value && typeof value === 'object' && typeof value.$ref === 'string' && value.$ref.length > 0;
};

/**
 * Determines whether the given value is an external JSON reference.
 *
 * @param {*} value - The value to inspect
 * @returns {boolean}
 */
$Ref.isExternal$Ref = function(value) {
  return $Ref.is$Ref(value) && value.$ref[0] !== '#';
};

/**
 * Determines whether the given value is a JSON reference, and whether it is allowed by the options.
 * For example, if it references an external file, then options.resolve.external must be true.
 *
 * @param {*} value - The value to inspect
 * @param {$RefParserOptions} options
 * @returns {boolean}
 */
$Ref.isAllowed$Ref = function(value, options) {
  if ($Ref.is$Ref(value)) {
    if (value.$ref.substr(0, 2) === '#/' || value.$ref === '#') {
      // It's a JSON Pointer reference, which is always allowed
      return true;
    }
    else if (value.$ref[0] !== '#' && (!options || options.resolve.external)) {
      // It's an external reference, which is allowed by the options
      return true;
    }
  }
};

/**
 * Determines whether the given value is a JSON reference that "extends" its resolved value.
 * That is, it has extra properties (in addition to "$ref"), so rather than simply pointing to
 * an existing value, this $ref actually creates a NEW value that is a shallow copy of the resolved
 * value, plus the extra properties.
 *
 * @example:
 *  {
 *    person: {
 *      properties: {
 *        firstName: { type: string }
 *        lastName: { type: string }
 *      }
 *    }
 *    employee: {
 *      properties: {
 *        $ref: #/person/properties
 *        salary: { type: number }
 *      }
 *    }
 *  }
 *
 *  In this example, "employee" is an extended $ref, since it extends "person" with an additional
 *  property (salary).  The result is a NEW value that looks like this:
 *
 *  {
 *    properties: {
 *      firstName: { type: string }
 *      lastName: { type: string }
 *      salary: { type: number }
 *    }
 *  }
 *
 * @param {*} value - The value to inspect
 * @returns {boolean}
 */
$Ref.isExtended$Ref = function(value) {
  return $Ref.is$Ref(value) && Object.keys(value).length > 1;
};

/**
 * Returns the resolved value of a JSON Reference.
 * If necessary, the resolved value is merged with the JSON Reference to create a new object
 *
 * @example:
 *  {
 *    person: {
 *      properties: {
 *        firstName: { type: string }
 *        lastName: { type: string }
 *      }
 *    }
 *    employee: {
 *      properties: {
 *        $ref: #/person/properties
 *        salary: { type: number }
 *      }
 *    }
 *  }
 *
 *  When "person" and "employee" are merged, you end up with the following object:
 *
 *  {
 *    properties: {
 *      firstName: { type: string }
 *      lastName: { type: string }
 *      salary: { type: number }
 *    }
 *  }
 *
 * @param {object} $ref - The JSON reference object (the one with the "$ref" property)
 * @param {*} resolvedValue - The resolved value, which can be any type
 * @returns {*} - Returns the dereferenced value
 */
$Ref.dereference = function($ref, resolvedValue) {
  if (resolvedValue && typeof resolvedValue === 'object' && $Ref.isExtended$Ref($ref)) {
    var merged = {};
    Object.keys($ref).forEach(function(key) {
      if (key !== '$ref') {
        merged[key] = $ref[key];
      }
    });
    Object.keys(resolvedValue).forEach(function(key) {
      if (!(key in merged)) {
        merged[key] = resolvedValue[key];
      }
    });
    return merged;
  }
  else {
    // Completely replace the original reference with the resolved value
    return resolvedValue;
  }
};
