"use strict";

const { ono } = require("@jsdevtools/ono");
const $Ref = require("./ref");
const url = require("./util/url");

module.exports = $Refs;

/**
 * This class is a map of JSON references and their resolved values.
 */
function $Refs () {
  /**
   * Indicates whether the schema contains any circular references.
   *
   * @type {boolean}
   */
  this.circular = false;

  /**
   * A map of paths/urls to {@link $Ref} objects
   *
   * @type {object}
   * @protected
   */
  this._$refs = {};

  /**
   * The {@link $Ref} object that is the root of the JSON schema.
   *
   * @type {$Ref}
   * @protected
   */
  this._root$Ref = null;
}

/**
 * Returns the paths of all the files/URLs that are referenced by the JSON schema,
 * including the schema itself.
 *
 * @param {...string|string[]} [types] - Only return paths of the given types ("file", "http", etc.)
 * @returns {string[]}
 */
$Refs.prototype.paths = function (types) {    // eslint-disable-line no-unused-vars
  let paths = getPaths(this._$refs, arguments);
  return paths.map((path) => {
    return path.decoded;
  });
};

/**
 * Returns the map of JSON references and their resolved values.
 *
 * @param {...string|string[]} [types] - Only return references of the given types ("file", "http", etc.)
 * @returns {object}
 */
$Refs.prototype.values = function (types) {   // eslint-disable-line no-unused-vars
  let $refs = this._$refs;
  let paths = getPaths($refs, arguments);
  return paths.reduce((obj, path) => {
    obj[path.decoded] = $refs[path.encoded].value;
    return obj;
  }, {});
};

/**
 * Returns a POJO (plain old JavaScript object) for serialization as JSON.
 *
 * @returns {object}
 */
$Refs.prototype.toJSON = $Refs.prototype.values;

/**
 * Determines whether the given JSON reference exists.
 *
 * @param {string} path - The path being resolved, optionally with a JSON pointer in the hash
 * @param {$RefParserOptions} [options]
 * @returns {boolean}
 */
$Refs.prototype.exists = function (path, options) {
  try {
    this._resolve(path, "", options);
    return true;
  }
  catch (e) {
    return false;
  }
};

/**
 * Resolves the given JSON reference and returns the resolved value.
 *
 * @param {string} path - The path being resolved, with a JSON pointer in the hash
 * @param {$RefParserOptions} [options]
 * @returns {*} - Returns the resolved value
 */
$Refs.prototype.get = function (path, options) {
  return this._resolve(path, "", options).value;
};

/**
 * 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 path of the property to set, optionally with a JSON pointer in the hash
 * @param {*} value - The value to assign
 */
$Refs.prototype.set = function (path, value) {
  let absPath = url.resolve(this._root$Ref.path, path);
  let withoutHash = url.stripHash(absPath);
  let $ref = this._$refs[withoutHash];

  if (!$ref) {
    throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
  }

  $ref.set(absPath, value);
};

/**
 * Creates a new {@link $Ref} object and adds it to this {@link $Refs} object.
 *
 * @param {string} path  - The file path or URL of the referenced file
 */
$Refs.prototype._add = function (path) {
  let withoutHash = url.stripHash(path);

  let $ref = new $Ref();
  $ref.path = withoutHash;
  $ref.$refs = this;

  this._$refs[withoutHash] = $ref;
  this._root$Ref = this._root$Ref || $ref;

  return $ref;
};

/**
 * Resolves the given JSON reference.
 *
 * @param {string} path - The path being resolved, optionally with a JSON pointer in the hash
 * @param {string} pathFromRoot - The path of `obj` from the schema root
 * @param {$RefParserOptions} [options]
 * @returns {Pointer}
 * @protected
 */
$Refs.prototype._resolve = function (path, pathFromRoot, options) {
  let absPath = url.resolve(this._root$Ref.path, path);
  let withoutHash = url.stripHash(absPath);
  let $ref = this._$refs[withoutHash];

  if (!$ref) {
    throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
  }

  return $ref.resolve(absPath, options, path, pathFromRoot);
};

/**
 * Returns the specified {@link $Ref} object, or undefined.
 *
 * @param {string} path - The path being resolved, optionally with a JSON pointer in the hash
 * @returns {$Ref|undefined}
 * @protected
 */
$Refs.prototype._get$Ref = function (path) {
  path = url.resolve(this._root$Ref.path, path);
  let withoutHash = url.stripHash(path);
  return this._$refs[withoutHash];
};

/**
 * Returns the encoded and decoded paths keys of the given object.
 *
 * @param {object} $refs - The object whose keys are URL-encoded paths
 * @param {...string|string[]} [types] - Only return paths of the given types ("file", "http", etc.)
 * @returns {object[]}
 */
function getPaths ($refs, types) {
  let paths = Object.keys($refs);

  // Filter the paths by type
  types = Array.isArray(types[0]) ? types[0] : Array.prototype.slice.call(types);
  if (types.length > 0 && types[0]) {
    paths = paths.filter((key) => {
      return types.indexOf($refs[key].pathType) !== -1;
    });
  }

  // Decode local filesystem paths
  return paths.map((path) => {
    return {
      encoded: path,
      decoded: $refs[path].pathType === "file" ? url.toFileSystemPath(path, true) : path
    };
  });
}
