/**
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 TagRangeRelationships = require('../models/tag-range/relationships');

const c = require('./common');
const { getTagRanges, getEvent, isTracked, escapeRegExp } = c;
const TagRange = require('../models/tag-range');
const tracker = require('../../tracker.js');

const _ = require('lodash');

const { PropagationEvent, Signature, CallContext } = require('../models');
const sig = new Signature('String.prototype.split');

/**
 * Check if we should skip propagation
 * @param {Object} data The string object being split
 * @return {boolean} true if we skip, false otherwise
 */
function shouldSkip(data) {
  if (!data.obj) {
    return true;
  }

  if (!_.isString(data.obj)) {
    return true;
  }

  if (!isTracked(data.obj)) {
    return true;
  }

  // split just returns the original string
  // if not passed an argument
  return data.args.length === 0;
}

/**
 * Get the indices where the input string will be split
 *
 * @param {String} origString the input string
 * @param {splitChar} {Object} the splitter (either a string or regex)
 */
function getSplitIndices(origString, splitChar) {
  const splitRegex = new RegExp(escapeRegExp(splitChar), 'g');
  const splitIndices = [];
  let matchResult;
  while ((matchResult = splitRegex.exec(origString)) !== null) {
    // advance on zero-length matches
    if (matchResult[0] === '') {
      splitIndices.push(matchResult.index);
      splitRegex.lastIndex += 1;
      continue;
    }
    splitIndices.push(matchResult.index);
  }
  return splitIndices;
}

module.exports.handle = {
  post(data) {
    if (shouldSkip(data)) {
      return;
    }

    const origString = data.obj;
    const { args, result } = data;
    const oldTagRanges = getTagRanges(origString);
    const oldEvent = getEvent(origString);

    const ctxt = CallContext.create(data);

    if (args[0].length === 0) {
      /***
       *  String.split has a special condition if given an empty string as a separator
       *  will return an array of that string's characters.
       *
       *  If we get this far, we know the orig String is tracked so we can
       *  re-track based on original tag ranges easily because we already know
       *  the indexes (start/stop) to reapply to the data.result array.
       *  (ex: 'taco'.split('') ===> ['t', 'a', 'c', 'o']
       */
      data.result = handleEmptySeperator(data, oldTagRanges, oldEvent);
      return;
    }

    const splitIndices = getSplitIndices(origString, args[0]);
    const indexOfFounds = new Set([]); // keeps track of the highest index we've seen in origString

    if (splitIndices.length === 0) {
      // no matches means no string splits occurred, and orig string was tracked, so just re-track
      data.result = transferTracking(origString, result);
    } else {
      for (let i = 0; i < result.length; i++) {
        const stringPart = result[i];
        if (!stringPart) {
          continue;
        }

        const stringPartLength = stringPart.length - 1;

        let stringStart = origString.indexOf(stringPart);
        let findingIdx = true;
        while (findingIdx) {
          if (
            Array.from(indexOfFounds).some(
              (currentValue) => currentValue >= stringStart
            )
          ) {
            stringStart = origString.indexOf(stringPart, stringStart + 1);
            findingIdx = true;
          } else {
            findingIdx = false;
          }
        }

        indexOfFounds.add(stringStart + stringPartLength);

        const newTagRanges = [];
        oldTagRanges.forEach((tag) => {
          const relation = new TagRange(
            stringStart,
            stringStart + stringPartLength,
            'split'
          ).compare(tag);
          let tagStart = null,
            tagStop = null;
          switch (relation) {
            case TagRangeRelationships.WITHOUT:
              tagStart = tag.start - stringStart;
              tagStop = tag.stop - stringStart;
              break;
            case TagRangeRelationships.WITHIN:
              tagStart = 0;
              tagStop = stringPartLength;
              break;
            case TagRangeRelationships.LOWSPAN:
              tagStart = tag.start - stringStart;
              tagStop = stringPartLength;
              break;
            case TagRangeRelationships.HIGHSPAN:
              tagStart = 0;
              tagStop = tag.stop - stringStart;
              break;
          }
          if (tagStart !== null) {
            newTagRanges.push(new TagRange(tagStart, tagStop, tag.tag));
            const tracked = tracker.track(stringPart);
            if (tracked) {
              tracked.props.tagRanges.push(new TagRange(tagStart, tagStop, tag.tag));
              result[i] = tracked.str;
            }
          }
        });
        if (newTagRanges.length > 0) {
          const tracked = tracker.track(stringPart);
          if (tracked) {
            tracked.props.tagRanges = newTagRanges;
            result[i] = tracked.str;
            const event = new PropagationEvent({
              context: ctxt,
              signature: sig,
              tagRanges: tracked.props.tagRanges,
              source: 'O',
              target: 'R'
            });
            event.parents.push(oldEvent);
            tracked.props.event = event;
          }
        }
      }
      data.result = result;
    }
  }
};

/**
 * When doing something like "hello".split(''), we should return ["h", "e", "l", "l", "o"] and re-track based on original
 * tag ranges.
 * @param data - data object from pre/post functions
 * @param oldTagRanges - set of old tags on the original string
 * @returns {String[]} - Original string (data.result) split into individual characters.
 */
function handleEmptySeperator(data, oldTagRanges, oldEvent) {
  if (!data || !oldTagRanges) {
    return data.result;
  }

  const { result } = data;

  const ctxt = CallContext.create(data);

  // map index --> { Event, tagRanges }, so that we can update existing properties in cases where
  // more than one tag was on the same index
  const sharedCharInfo = new Map();

  oldTagRanges.forEach((tag) => {
    for (let i = tag.start; i <= tag.stop; i++) {
      const char = result[i];
      if (char == null) {
        // char can be null if the tag range is outside the result (limited split)
        return;
      }

      let info;
      if (sharedCharInfo.has(i)) {
        info = sharedCharInfo.get(i);
      } else {
        const tracked = tracker.track(char);
        if (tracked) {
          const event = new PropagationEvent({
            context: ctxt,
            signature: sig,
            tagRanges: getTagRanges(tracked.str),
            source: 'O',
            target: 'R'
          });

          tracked.props.event = event;
          info = { event, tagRanges: tracked.props.tagRanges };
          sharedCharInfo.set(i, info);
          result[i] = tracked.str;
        }
      }

      info.tagRanges.push(new TagRange(0, 0, tag.tag));
      info.event.tagRanges = info.tagRanges;
    }
  });

  return result;
}

function transferTracking(origString, resultArray) {
  if (resultArray == null) {
    return resultArray;
  }

  for (let i = 0; i < resultArray.length; i++) {
    const tracked = tracker.track(resultArray[i]);
    if (tracked) {
      tracked.props.tagRanges = getTagRanges(origString);
      tracked.props.event = getEvent(origString);
      resultArray[i] = tracked.str;
    }
  }
  return resultArray;
}
