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

// https://www.geeksforgeeks.org/aho-corasick-algorithm-pattern-searching/

const a = 'a'.charCodeAt(0);
const A = 'A'.charCodeAt(0);
const Z = 'Z'.charCodeAt(0);
const l2u = a - A;

// initialize the lower -> upper and upper -> lower translation array.
const TRANSLATION = [];
for (let byte = A; byte <= Z; byte++) {
  // translate the uppercase character to lowercase
  TRANSLATION[byte] = byte + l2u;
}

class AhoCorasick {
  constructor(words) {
    // the maximum number of states is equal to the sum of the length of
    // the strings to be matched.
    this.maxStates = 0;
    for (const word of words) {
      this.maxStates += word.length;
    }

    // can optimize this by trading off computation for space.
    this.maxChars = 128;

    // if this state matches words each match will be in an array at
    // the state.
    this.out = Array(this.maxStates + 1);
    for (let i = 0; i <= this.maxStates; i++) {
      this.out[i] = [];
    }

    // state to transition to on failure
    this.fail = Array(this.maxStates + 1);

    // maxStates + 1 rows
    // maxChars columns
    this.goto = Array(this.maxStates + 1);
    for (let i = 0; i <= this.maxStates; i++) {
      this.goto[i] = Array();
    }

    this.words = words;

    this.stateCount = this.buildAutomata();

    // keep state so this can be called in a streaming fashion
    this.state = 0;
  }

  reset() {
    this.state = 0;
  }

  // eslint-disable-next-line complexity
  buildAutomata() {
    // only the root state (state 0) to start with
    let stateCount = 1;

    // fill in the goto matrix.
    for (let i = 0; i < this.words.length; i++) {
      const word = Buffer.from(this.words[i]);
      let state = 0;

      // create transitions for all character of the current word
      for (let byte of word) {
        if (byte & 0x80) {
          throw new Error('pattern character codes cannot exceed 127');
        } else if (TRANSLATION[byte]) {
          byte = TRANSLATION[byte];
        }
        if (this.goto[state][byte] === undefined) {
          this.goto[state][byte] = stateCount;
          stateCount += 1;
        }
        state = this.goto[state][byte];
      }

      // add current word to terminal list
      this.out[state].push(this.words[i]);
    }

    // for all byte values that don't have a transition from the root, add
    // a transition to the root.
    for (let byte = 0; byte < this.maxChars; byte++) {
      if (this.goto[0][byte] === undefined) {
        this.goto[0][byte] = 0;
      }
    }

    // failure function is computer using breadth first order.
    const queue = [];

    // iterate over all possible inputs
    for (let i = 0; i < this.maxChars; i++) {
      // all nodes of depth 1 have failure function value 0.
      if (this.goto[0][i] !== 0) {
        this.fail[this.goto[0][i]] = 0;
        queue.push(this.goto[0][i]);
      }
    }

    while (queue.length) {
      // get the first state in the queue
      const state = queue.shift();

      // for the removed state, find the failure function for all
      // characters for which a goto is not defined.
      for (let i = 0; i < this.maxChars; i++) {
        if (this.goto[state][i] === undefined) {
          continue;
        }

        // get failure transition
        let failure = this.fail[state];

        // find the deepest node ..

        while (this.goto[failure][i] === undefined) {
          failure = this.fail[failure];
        }

        failure = this.goto[failure][i];
        this.fail[this.goto[state][i]] = failure;

        // merge outputs
        this.out[this.goto[state][i]].push(...this.out[failure]);

        // insert the next level node (of trie) into queue
        queue.push(this.goto[state][i]);
      }
    }

    return stateCount;
  }

  check(byte) {
    // if it's > maxChars set it to a non-matching value to force failure.
    // this could allow some memory-reduction optimizations if worth it. i.e.,
    // maxChars only needs to be the highest charcode in the words the user
    // passes in. and they can be offset by the lowest charcode as well.
    if (byte >= this.maxChars) {
      byte = 0;
    } else if (TRANSLATION[byte]) {
      byte = TRANSLATION[byte];
    }
    let next = this.state;
    while (this.goto[next][byte] === undefined) {
      next = this.fail[next];
    }

    this.state = this.goto[next][byte];

    return this.out[this.state].length ? this.out[this.state] : null;
  }
}

module.exports = AhoCorasick;
