/**
 * Immutable Array cannot be changed once created
 */
export class ImmutableArray {
  /**
   * The constructor of ImmutableArray
   *
   * @typedef T generic type parameter
   * @param {T[]} initialArray - Optional initial array
   * @param {() => void} onMutate - Optional callback invoked with the new ImmutableArray whenever a mutation is performed
   */
  constructor(initialArray, onMutate) {
    this.length = 0;
    this.array = [];
    if (initialArray) {
      this.array = initialArray;
      this.length = initialArray.length;
    }
    if (onMutate) {
      this.onMutate = onMutate;
    }
  }
  /**
   * The function is called when the array is mutated
   *
   * @typedef T generic type parameter
   * @param {ImmutableArray<T>} _newArr - The new array that was created.
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  onMutate(_newArr) {
    return;
  }
  /**
   * Append items to the array and return a new ImmutableArray
   *
   * @param {T[]} items - The items to be appended to the array
   * @returns {ImmutableArray<T>} - a new ImmutableArray with new items pushed.
   */
  push(...items) {
    return this.withMutation((arr) => {
      arr.push(...items);
    });
  }
  /**
   * Remove the last item from the array and return a new ImmutableArray.
   * If the array is empty, a new ImmutableArray with the same (empty) contents is returned.
   *
   * @returns {ImmutableArray<T>} - a new ImmutableArray with the last one popped
   */
  pop() {
    return this.withMutation((arr) => {
      if (arr.length > 0)
        arr.pop();
    });
  }
  /**
   * Update the item at the specified index and return a new ImmutableArray.
   * If the index is out of range, no change is made and a new ImmutableArray (same contents) is returned.
   *
   * @param {number} index - Index of the item to update.
   * @param {T} value - New value to set at the given index.
   * @returns {ImmutableArray<T>} - a new ImmutableArray with updated item.
   */
  set(index, value) {
    return this.withMutation((arr) => {
      if (index >= 0)
        arr[index] = value;
    });
  }
  /**
   * Set values from the provided array into this ImmutableArray one by one
   *
   * @param {T[]} values - Array of values to write into this array.
   * @returns {ImmutableArray<T>} - a new ImmutableArray with updated items.
   */
  setAll(values) {
    return this.withMutation((arr) => {
      arr.length = 0;
      for (let i = 0; i < values.length; i++) {
        arr[i] = values[i];
      }
    });
  }
  /**
   * Clear the array and return a new ImmutableArray
   *
   * @returns {ImmutableArray<T>} return a new ImmutableArray.
   */
  clear() {
    return this.withMutation((arr) => {
      arr.length = 0;
    });
  }
  /**
   * Performs a mutation on a copy of the internal array and returns a new ImmutableArray
   *
   * @param {(arr: T[]) => void} fn - (arr: T[]) => void
   * @returns {ImmutableArray<T>} A new ImmutableArray
   */
  withMutation(fn) {
    const newArr = [...this.array];
    fn(newArr);
    const newImmutable = new ImmutableArray(newArr, this.onMutate);
    this.onMutate(newImmutable);
    return newImmutable;
  }
  /**
   * Get item by index
   *
   * @param {number} index - index of the array items
   * @returns {T | undefined} The array item with the index
   */
  get(index) {
    return this.array[index];
  }
  /**
   * Join array elements into a string separated by the given separator.
   *
   * @param {string} [separator=','] - Separator to use between elements.
   * @returns {string} The joined string.
   */
  join(separator) {
    return this.array.join(separator);
  }
  /**
   * It returns an array of the values in the array
   *
   * @returns {T[]} An array of the values in the array.
   */
  toArray() {
    return this.array;
  }
}
