/**
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 logger = require('../../../core/logger')('contrast:tagRange');

const Relationships = require('./relationships');

const DEFAULT_TAG = 'untrusted';

/**
 * TagRanges specify a tag type and the portion of the tagged data for which
 * the tag applies.
 */
class TagRange {
  /**
   * @param {number} start The starting index of string tracking on the data having the tag.
   * @param {number} stop  The stopping index of string tracking on the data having the tag.
   * @param {string?} tag  The name of the tag (default is "untrusted").
   */
  constructor(start, stop, tag = DEFAULT_TAG) {
    // Validates the arguments to the contructor call.
    if (!(start <= stop && start >= 0)) {
      logger.debug(
        'could not create tag %s with invalid range start: %s, stop %s.',
        tag,
        start,
        stop
      );
    }

    /** @type {string} */
    this.tag = tag;
    /** @type {number} */
    this.start = start;
    /** @type {number} */
    this.stop = stop;
  }

  /**
   * Is the parameter range adjacent to this range
   * @param {TagRange} range The TagRange to test
   * @return {boolean}
   */
  adjacent({ start, stop }) {
    return this.start === stop + 1 || this.stop === start - 1;
  }

  /**
   * Does the range end within the parameter range
   * @param {TagRange} range The TagRange to test
   * @return {boolean}
   */
  endsWithin({ start, stop }) {
    return this.stop >= start && this.stop <= stop;
  }

  /**
   * Does the range start within the parameter range
   * @param {TagRange} range The TagRange to test
   * @return {boolean}
   */
  startsWithin({ start, stop }) {
    return this.start >= start && this.start <= stop;
  }

  /**
   * Does a second tag range overlap or is it adjacent to this range (for merging)
   * @param  {TagRange} range The TagRange to test
   * @return {boolean}        True, if the range overlaps with this range
   */
  overlaps(range) {
    if (range.tag !== this.tag && 'tag' in range) {
      return false;
    }

    return (
      (this.start <= range.start && this.stop >= range.stop) ||
      this.adjacent(range) ||
      this.endsWithin(range) ||
      this.startsWithin(range)
    );
  }

  /**
   * Merge another tag range into this range.
   * @param {TagRange} range the TagRange to merge into this one
   */
  merge(range) {
    if (range.start < this.start) {
      this.start = range.start;
    }
    if (range.stop > this.stop) {
      this.stop = range.stop;
    }
  }

  /**
   * @param {TagRange} range
   * @return {boolean}
   */
  isBelow({ start }) {
    return this.start < start && this.stop < start;
  }

  /**
   * @param {TagRange} range
   * @return {boolean}
   */
  isLowSpan({ start, stop }) {
    return this.start < start && this.stop >= start && this.stop <= stop;
  }

  /**
   * @param {TagRange} range
   * @return {boolean}
   */
  isWithin({ start, stop }) {
    return this.start >= start && this.start < stop && this.stop <= stop;
  }

  /**
   * @param {TagRange} range
   * @return {boolean}
   */
  isWithout({ start, stop }) {
    return this.start < start && this.stop > stop;
  }

  /**
   * @param {TagRange} range
   * @return {boolean}
   */
  isHighSpan({ start, stop }) {
    return this.start >= start && this.start <= stop && this.stop > stop;
  }

  /**
   * @param {TagRange} range
   * @return {boolean}
   */
  isAbove({ stop }) {
    return this.start > stop && this.stop > stop;
  }

  /**
   * Returns the relationship of this tag range to another
   *
   * @param {TagRange} range A second TagRange to compare
   * @param {number}   range.start
   * @param {number}   range.stop
   * @return {string}  Where, in relation to the first tag range, the indices of the second are.
   */
  compare(range) {
    if (this.isBelow(range)) return Relationships.BELOW;
    if (this.isLowSpan(range)) return Relationships.LOWSPAN;
    if (this.isWithin(range)) return Relationships.WITHIN;
    if (this.isWithout(range)) return Relationships.WITHOUT;
    if (this.isHighSpan(range)) return Relationships.HIGHSPAN;
    if (this.isAbove(range)) return Relationships.ABOVE;
  }

  clone() {
    return new TagRange(this.start, this.stop, this.tag);
  }
}

module.exports = TagRange;
