/**
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.
*/
// https://github.com/google/closure-compiler/blob/master/externs/es3.js#L1826
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr
'use strict';

const tagRangeUtil = require('../models/tag-range/util');
const tracker = require('../../tracker.js');

const { PropagationEvent, Signature, CallContext } = require('../models');
const { isString } = require('../../util/is-string');
const injections = require('../../core/rewrite/injections');

const ContrastMethods = injections.get('ContrastMethods');

/**
 * In order to propagate through template literals, we leverage rewriting to
 * insert a custom `tag' expression that puts the template first through this
 * function.
 * The result looks like:
 *    let a = __contrastTag`original template expression ${withInserted()}`;
 * @param {String[]} strings     the literal portion of the template `This ${not_this} and this`
 * @param {...*}     expressions the inserted expressions `Not this ${this} not this {and_this}`
 * @returns {*     }             Whatever the original return of the template was.
 */
function __contrastTag(...args) {
  const [strings] = args;
  // rest params/variadic signatures not yet supported in v4,
  // so we have to loop arguments
  const expressions = Array.from(args);
  expressions.shift();

  let tagRanges = [];
  const sourceEvents = [];
  let result = '';
  strings.forEach((quasi, i) => {
    const exp = expressions[i];

    result += quasi;

    // usually append 'undefined', but the exp is always undefined if the
    // template ends in an quasi expression-less quasi
    if (strings.length - 1 === i && exp === undefined) {
      return;
    }

    const offset = result.length;

    result += exp;
    // if the expression toString'd into empty string, don't bother
    // pulling metadata
    if (result.length === offset) {
      return;
    }

    // FIXME(NODE-156): this prevents propagation through custom
    // object toStrings with tracked data
    if (!isString(exp)) {
      return;
    }

    tagRanges = moveTags(exp, sourceEvents, tagRanges, offset);
  });

  if (tagRanges.length > 0) {
    const tracked = tracker.track(result);
    if (tracked) {
      buildProperties(
        tracked.props,
        tagRanges,
        expressions,
        tracked.str,
        strings,
        sourceEvents
      );
      result = tracked.str;
    }
  }

  return result;
}

/**
 * Build resulting contrast properties for new tracked string with propagation events.
 * @param {ContrastProperties} props     reference to properties object
 * @param {TagRange[]}         tagRanges new tags
 * @param {String[]}           exps      array of template expressions
 * @param {String}             result    result string
 * @param {Signature}          sig       reporting signature
 * @param {Event[]}            events    source events
 */
function buildProperties(props, tagRanges, exps, result, strings, events) {
  const sig = new Signature({
    moduleName: 'global',
    methodName: 'ContrastMethods.__contrastTag',
    isModule: false
  });

  props.tagRanges = tagRanges;

  const raw = String.raw.apply(
    this,
    [strings].concat(exps.map((exp, i) => `$\{exp${i}}`))
  );
  const ctxt = CallContext.create({
    obj: raw,
    args: exps,
    result,
    stackOpts: {
      constructorOpt: __contrastTag
    }
  });
  const event = new PropagationEvent({
    context: ctxt,
    signature: sig,
    tagRanges,
    source: 'P0',
    target: 'R',
    parents: events
  });
  props.event = event;
}

/**
 * move tags from tracked string into original if the tags are valid
 * @param   {String}             exp                tracked string
 * @param   {SourceEvent[]}      sourceEvents       reference to propagator list of events
 * @param   {TagRange[]}         tagRanges          original tagRange before this exp
 * @param   {Number}             offset             expression offset for tag
 * @returns {TagRange[]}                            adjusted tags after propagation of current exp
 */
function moveTags(exp, sourceEvents, tagRanges, offset) {
  const contrastProperties = tracker.getData(exp);
  const tracked = contrastProperties || tagRanges.length > 0;
  if (!tracked) {
    return tagRanges;
  }

  const event = contrastProperties && contrastProperties.event;
  if (event) {
    sourceEvents.push(event);
  }

  let newTagRanges = [];
  if (contrastProperties) newTagRanges = contrastProperties.tagRanges;

  return tagRangeUtil.addAllWithOffset(tagRanges, newTagRanges, offset);
}

function handle() {
  ContrastMethods.enable();
  ContrastMethods.value.__contrastTag = __contrastTag;
}

module.exports.__contrastTag = __contrastTag;
module.exports.handle = handle;
