/* eslint-disable no-unused-vars */
"use strict";

const $Refs = require("./refs");
const _parse = require("./parse");
const normalizeArgs = require("./normalize-args");
const resolveExternal = require("./resolve-external");
const _bundle = require("./bundle");
const _dereference = require("./dereference");
const url = require("./util/url");
const { JSONParserError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError, JSONParserErrorGroup } = require("./util/errors");
const maybe = require("call-me-maybe");
const { ono } = require("@jsdevtools/ono");

module.exports = $RefParser;
module.exports.default = $RefParser;
module.exports.JSONParserError = JSONParserError;
module.exports.InvalidPointerError = InvalidPointerError;
module.exports.MissingPointerError = MissingPointerError;
module.exports.ResolverError = ResolverError;
module.exports.ParserError = ParserError;
module.exports.UnmatchedParserError = UnmatchedParserError;
module.exports.UnmatchedResolverError = UnmatchedResolverError;

/**
 * This class parses a JSON schema, builds a map of its JSON references and their resolved values,
 * and provides methods for traversing, manipulating, and dereferencing those references.
 *
 * @constructor
 */
function $RefParser () {
  /**
   * The parsed (and possibly dereferenced) JSON schema object
   *
   * @type {object}
   * @readonly
   */
  this.schema = null;

  /**
   * The resolved JSON references
   *
   * @type {$Refs}
   * @readonly
   */
  this.$refs = new $Refs();
}

/**
 * Parses the given JSON schema.
 * This method does not resolve any JSON references.
 * It just reads a single file in JSON or YAML format, and parse it as a JavaScript object.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed
 * @param {function} [callback] - An error-first callback. The second parameter is the parsed JSON schema object.
 * @returns {Promise} - The returned promise resolves with the parsed JSON schema object.
 */
$RefParser.parse = function parse (path, schema, options, callback) {
  let Class = this; // eslint-disable-line consistent-this
  let instance = new Class();
  return instance.parse.apply(instance, arguments);
};

/**
 * Parses the given JSON schema.
 * This method does not resolve any JSON references.
 * It just reads a single file in JSON or YAML format, and parse it as a JavaScript object.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed
 * @param {function} [callback] - An error-first callback. The second parameter is the parsed JSON schema object.
 * @returns {Promise} - The returned promise resolves with the parsed JSON schema object.
 */
$RefParser.prototype.parse = async function parse (path, schema, options, callback) {
  let args = normalizeArgs(arguments);
  let promise;

  if (!args.path && !args.schema) {
    let err = ono(`Expected a file path, URL, or object. Got ${args.path || args.schema}`);
    return maybe(args.callback, Promise.reject(err));
  }

  // Reset everything
  this.schema = null;
  this.$refs = new $Refs();

  // If the path is a filesystem path, then convert it to a URL.
  // NOTE: According to the JSON Reference spec, these should already be URLs,
  // but, in practice, many people use local filesystem paths instead.
  // So we're being generous here and doing the conversion automatically.
  // This is not intended to be a 100% bulletproof solution.
  // If it doesn't work for your use-case, then use a URL instead.
  let pathType = "http";
  if (url.isFileSystemPath(args.path)) {
    args.path = url.fromFileSystemPath(args.path);
    pathType = "file";
  }

  // Resolve the absolute path of the schema
  args.path = url.resolve(url.cwd(), args.path);

  if (args.schema && typeof args.schema === "object") {
    // A schema object was passed-in.
    // So immediately add a new $Ref with the schema object as its value
    let $ref = this.$refs._add(args.path);
    $ref.value = args.schema;
    $ref.pathType = pathType;
    promise = Promise.resolve(args.schema);
  }
  else {
    // Parse the schema file/url
    promise = _parse(args.path, this.$refs, args.options);
  }

  let me = this;
  try {
    let result = await promise;

    if (result !== null && typeof result === "object" && !Buffer.isBuffer(result)) {
      me.schema = result;
      return maybe(args.callback, Promise.resolve(me.schema));
    }
    else if (args.options.continueOnError) {
      me.schema = null; // it's already set to null at line 79, but let's set it again for the sake of readability
      return maybe(args.callback, Promise.resolve(me.schema));
    }
    else {
      throw ono.syntax(`"${me.$refs._root$Ref.path || result}" is not a valid JSON Schema`);
    }
  }
  catch (err) {
    if (!args.options.continueOnError || !isHandledError(err)) {
      return maybe(args.callback, Promise.reject(err));
    }

    if (this.$refs._$refs[url.stripHash(args.path)]) {
      this.$refs._$refs[url.stripHash(args.path)].addError(err);
    }

    return maybe(args.callback, Promise.resolve(null));
  }
};

/**
 * Parses the given JSON schema and resolves any JSON references, including references in
 * externally-referenced files.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed and resolved
 * @param {function} [callback]
 * - An error-first callback. The second parameter is a {@link $Refs} object containing the resolved JSON references
 *
 * @returns {Promise}
 * The returned promise resolves with a {@link $Refs} object containing the resolved JSON references
 */
$RefParser.resolve = function resolve (path, schema, options, callback) {
  let Class = this; // eslint-disable-line consistent-this
  let instance = new Class();
  return instance.resolve.apply(instance, arguments);
};

/**
 * Parses the given JSON schema and resolves any JSON references, including references in
 * externally-referenced files.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed and resolved
 * @param {function} [callback]
 * - An error-first callback. The second parameter is a {@link $Refs} object containing the resolved JSON references
 *
 * @returns {Promise}
 * The returned promise resolves with a {@link $Refs} object containing the resolved JSON references
 */
$RefParser.prototype.resolve = async function resolve (path, schema, options, callback) {
  let me = this;
  let args = normalizeArgs(arguments);

  try {
    await this.parse(args.path, args.schema, args.options);
    await resolveExternal(me, args.options);
    finalize(me);
    return maybe(args.callback, Promise.resolve(me.$refs));
  }
  catch (err) {
    return maybe(args.callback, Promise.reject(err));
  }
};

/**
 * Parses the given JSON schema, resolves any JSON references, and bundles all external references
 * into the main JSON schema. This produces a JSON schema that only has *internal* references,
 * not any *external* references.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed, resolved, and dereferenced
 * @param {function} [callback] - An error-first callback. The second parameter is the bundled JSON schema object
 * @returns {Promise} - The returned promise resolves with the bundled JSON schema object.
 */
$RefParser.bundle = function bundle (path, schema, options, callback) {
  let Class = this; // eslint-disable-line consistent-this
  let instance = new Class();
  return instance.bundle.apply(instance, arguments);
};

/**
 * Parses the given JSON schema, resolves any JSON references, and bundles all external references
 * into the main JSON schema. This produces a JSON schema that only has *internal* references,
 * not any *external* references.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed, resolved, and dereferenced
 * @param {function} [callback] - An error-first callback. The second parameter is the bundled JSON schema object
 * @returns {Promise} - The returned promise resolves with the bundled JSON schema object.
 */
$RefParser.prototype.bundle = async function bundle (path, schema, options, callback) {
  let me = this;
  let args = normalizeArgs(arguments);

  try {
    await this.resolve(args.path, args.schema, args.options);
    _bundle(me, args.options);
    finalize(me);
    return maybe(args.callback, Promise.resolve(me.schema));
  }
  catch (err) {
    return maybe(args.callback, Promise.reject(err));
  }
};

/**
 * Parses the given JSON schema, resolves any JSON references, and dereferences the JSON schema.
 * That is, all JSON references are replaced with their resolved values.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed, resolved, and dereferenced
 * @param {function} [callback] - An error-first callback. The second parameter is the dereferenced JSON schema object
 * @returns {Promise} - The returned promise resolves with the dereferenced JSON schema object.
 */
$RefParser.dereference = function dereference (path, schema, options, callback) {
  let Class = this; // eslint-disable-line consistent-this
  let instance = new Class();
  return instance.dereference.apply(instance, arguments);
};

/**
 * Parses the given JSON schema, resolves any JSON references, and dereferences the JSON schema.
 * That is, all JSON references are replaced with their resolved values.
 *
 * @param {string} [path] - The file path or URL of the JSON schema
 * @param {object} [schema] - A JSON schema object. This object will be used instead of reading from `path`.
 * @param {$RefParserOptions} [options] - Options that determine how the schema is parsed, resolved, and dereferenced
 * @param {function} [callback] - An error-first callback. The second parameter is the dereferenced JSON schema object
 * @returns {Promise} - The returned promise resolves with the dereferenced JSON schema object.
 */
$RefParser.prototype.dereference = async function dereference (path, schema, options, callback) {
  let me = this;
  let args = normalizeArgs(arguments);

  try {
    await this.resolve(args.path, args.schema, args.options);
    _dereference(me, args.options);
    finalize(me);
    return maybe(args.callback, Promise.resolve(me.schema));
  }
  catch (err) {
    return maybe(args.callback, Promise.reject(err));
  }
};

function finalize (parser) {
  const errors = JSONParserErrorGroup.getParserErrors(parser);
  if (errors.length > 0) {
    throw new JSONParserErrorGroup(parser);
  }
}
