/**
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:tracker');
const distringuish = require('@contrast/distringuish');

class Tracker {
  constructor() {
    /** @type {WeakMap<String, ContrastProperties>} */
    this.metadata = new WeakMap();
  }

  /**
   * Externalizes a given primitive string, attaching a ContrastProperties
   * object. Returns null if the string is unable to be externalized.
   * @param {string} str
   * @returns {{ str: str | string, props: ContrastProperties} | null}
   */
  trackString(str) {
    let props = distringuish.getProperties(str);
    if (props) {
      return { str, props };
    }

    str = distringuish.externalize(str);

    if (typeof str === 'number') {
      switch (str) {
        case distringuish.ExternalizeErrorCode.ZERO_LENGTH_STRING:
          logger.warn('Cannot externalize and track empty strings.'); // we should never see this error
          return null;
        case distringuish.ExternalizeErrorCode.INVALID_STRING_ENCODING:
          logger.error('Unable to externalize non-two-byte strings.');
          return null;
        case distringuish.ExternalizeErrorCode.TO_LOCAL_FAILURE:
          logger.error(
            'An error occurred when creating a new external string.'
          );
          return null;
        case distringuish.ExternalizeErrorCode.EXTERNALIZATION_FAILURE:
          logger.error('The provided string was unable to be externalized.');
          return null;
        default:
          logger.error('An unknown error occurred');
          return null;
      }
    }

    props = distringuish.getProperties(str);
    Object.assign(props, {
      event: null,
      tagRanges: [],
      tracked: true,
    });

    return { str, props };
  }

  /**
   * Tracks a given String object using the `metadata` WeakMap.
   * @param {String} str
   * @returns {str}
   */
  trackStringObject(str) {
    if (!this.metadata.has(str)) {
      this.metadata.set(str, {
        event: null,
        tagRanges: [],
        tracked: true,
      });
    }

    return { str, props: this.metadata.get(str) };
  }

  /**
   * Return the properties associated with a given string. Returns null if the
   * string has not previously been tracked.
   * @param {string | String} str
   * @return {ContrastProperties | null}
   */
  getData(str) {
    let prop;
    if (typeof str === 'string') {
      prop = distringuish.getProperties(str);
    } else if (str instanceof String) {
      prop = this.metadata.get(str);
    }

    return prop || null;
  }

  /**
   * Associate properties with a string. Returns null if str is not a string,
   * is a zero-length string, or any internal error takes place.
   * @param {string | String} str
   * @returns {{ str: str | string, props: ContrastProperties} | null}
   */
  track(str) {
    if (typeof str === 'string' && str.length > 0) {
      return this.trackString(str);
    }

    if (str instanceof String && str.length > 0) {
      return this.trackStringObject(str);
    }

    return null;
  }

  /**
   * Resets a string's tracked data metadata to the default contrast properties.
   * If the string is a primitive, we return a new, non-externalized copy of the
   * string.
   * If the string was an Object, we remove the properties from the `metadata`
   * WeakMap.
   * @param {string | String} str
   * @returns {str | null}
   */
  untrack(str) {
    if (typeof str === 'string') {
      const props = distringuish.getProperties(str);
      if (props) {
        Object.assign(props, {
          event: null,
          tagRanges: [],
          tracked: false,
        });
      }
      return distringuish.internalize(str);
    }

    if (str instanceof String) {
      this.metadata.delete(str);
      return str;
    }

    return null;
  }
}

module.exports = new Tracker();
module.exports.Tracker = Tracker;
