/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';

const SchemaContext = require('./schema-context');

// https://json-schema.org/specification-links.html
// https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#page-13
// when time to support 2019-09 https://json-schema.org/draft/2019-09/release-notes.html

/**
 * shadow an ajv instance. an ajv instance encapsulates a grouping of schemas,
 * the regexes they use, and evaluation functions.
 *
 * @param {ajv} ajv instance
 * @param {object} options
 * @param {function} options.logger set up logging using this (it should be
 * const logger = require('../../../core/logger'))
 */
class AjvShadow {
  constructor(ajv, options = {}) {
    this.ajv = ajv;

    // use this to hold references to schemas. this is used to resolve
    // $refs.
    this.schemaMap = new Map();

    // the regex cache avoids 1) recreating regexes and 2) relying on ajv's
    // internal cache of regexes.
    //
    // they're in the ajv.scope._values.pattern.get(regex-string).value.ref
    // but that seems like a very specific implementation detail to rely on.
    this.regexCache = new Map();

    // set up logging if present. if not present throw an error - used for
    // testing.
    if (options.logger) {
      this.logger = options.logger('contrast:assess:propagators:ajv');
    } else {
      const thrower = function(e) {
        throw e instanceof Error ? e : new Error(e);
      };
      this.logger = { trace: thrower };
    }
  }

  /**
   *
   * @param {object} object the object containing the data to be evaluated
   * @param {string} key object[key] is the source of data; needed to persist changes to data behind
   *                 a membrane. the data to be evaluated is object[key].
   * @param {object} options
   * @param {function} options.tagFunction the function to be called with tag updates
   * @param {boolean} options.allErrors whether to continue processing validations after the first error.
   *                  ajv's default is *not* to do so, but it is useful for testing. see schema-context
   *                  for details.
   * @return {[boolean, number]} validation status, number of strings for which tags were modified
   */
  evaluate(schema, object, key, options = {}) {
    const ctx = new SchemaContext(this, schema, object, key, options);

    try {
      // evaluate the data against the schema.
      const valid = ctx.evaluate(schema);

      // if any keys were modified then make sure the modified object is retained.
      if (ctx.tagModificationCount) {
        object[key] = ctx.data;
      }

      return [valid, ctx.tagModificationCount];
    } catch (e) {
      this.logger.trace(e);
      return [false, ctx.tagModificationCount];
    }
  }
}

module.exports = AjvShadow;
