/**
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');

module.exports.handle = function handle(data) {
  // untrackable result
  if (!(data.result.length > 0)) {
    return;
  }

  // this handles join() and join(undefined)
  const del = data.args[0] === undefined ? ',' : data.args[0];

  const parentEvents = [];

  const delimiterProperties = tracker.getData(del);
  const delimiterTagRanges = [];
  const delimiterLength = `${del}`.length;

  let delimiterTracked = false;

  if (delimiterProperties) {
    delimiterTracked = true;
    parentEvents.push(delimiterProperties.event);
    delimiterTagRanges.push(...delimiterProperties.tagRanges);
  }

  const targetData = {
    parentEvents,
    resultTagRanges: [],
    elementTracked: false
  };

  propagateArrayData(targetData, data.obj, delimiterTagRanges, delimiterLength);

  const { elementTracked, resultTagRanges } = targetData;

  if (delimiterTracked || elementTracked) {
    const tracked = tracker.track(data.result);
    if (!tracked) {
      return;
    }

    tracked.props.event = createEvent(
      data,
      resultTagRanges,
      parentEvents,
      delimiterTracked,
      elementTracked
    );
    tracked.props.tagRanges = resultTagRanges;

    data.result = tracked.str;
  }
};

/**
 * Loop over the array and update the running collections tags and events
 * based on delimiter tags and individual element data.
 *
 * @param {Object} targetData mutable object - lets us modify/"return" multiple things using a single loop
 * @param {BaseEvent[]} targetData.parentEvents running list of events we pull from array elements and the delimiter
 * @param {TagRange[]} targetData.resultTagRanges tag ranges to put on the result string
 * @param {boolean} targetData.elementTracked whether or not the loop found any tracked elements in the array
 * @param {Array} array the array being .join()'d
 * @param {TagRange[]} delimiterTagRanges TagRanges we found on the delimiter argument to .join()
 * @param {number} delimiterLength the length of the delimiter string, used to update the offset
 */
function propagateArrayData(
  targetData,
  array,
  delimiterTagRanges,
  delimiterLength
) {
  let offset = 0;
  const { parentEvents } = targetData;
  for (let i = 0; i < array.length; i++) {
    const elem = array[i];
    const elemProperties = tracker.getData(elem);

    if (elem && elemProperties) {
      parentEvents.push(elemProperties.event);

      targetData.elementTracked = true;

      tagRangeUtil.addAllWithOffsetInPlace(
        targetData.resultTagRanges,
        elemProperties.tagRanges,
        offset
      );
    }

    // shift the offset before calculating tags from delimiter
    offset += `${elem}`.length;

    // don't add delimiter data after the last element
    if (i !== array.length - 1) {
      tagRangeUtil.addAllWithOffsetInPlace(
        targetData.resultTagRanges,
        delimiterTagRanges,
        offset
      );

      offset += delimiterLength;
    }
  }
}

/**
 * Create the propagation event for the call to Array.prototype.join
 *
 * @param {Object} data the patcher data (result, obj, args)
 * @param {TagRange[]} resultTagRanges the full list of TagRanges we pinned to the result's metadata
 * @param {BaseEvent[]} parentEvents the events from all the sources to set as parents of the new event
 * @param {boolean} delimiterTracked whether or not we propagated data from the delimiter string
 * @param {boolean} elementTracked whether or not we propagated data from any of the array elements
 * @return {PropagationEvent} the event we are pinning to the result string
 */
function createEvent(
  data,
  resultTagRanges,
  parentEvents,
  delimiterTracked,
  elementTracked
) {
  let srcType = 'A';
  if (!delimiterTracked) {
    srcType = 'O';
  } else if (!elementTracked) {
    srcType = 'P';
  }
  const signature = new Signature('Array.prototype.join');
  const context = CallContext.create(data);
  const event = new PropagationEvent({
    context,
    signature,
    tagRanges: resultTagRanges,
    source: srcType,
    target: 'R',
    parents: parentEvents
  });

  return event;
}
