/**
 * A helper class that generates ranges of dummy data for buffer or texture operations
 * efficiently. Tries to minimize allocations and data updates.
 */
export class DataArrayGenerator {
  private dataBuffer = new Uint8Array(256);

  private lastOffset = 0;
  private lastStart = 0;
  private lastByteSize = 0;

  /** Find the nearest power of two greater than or equal to the input value. */
  private nextPowerOfTwo(value: number) {
    return 1 << (32 - Math.clz32(value - 1));
  }

  private generateData(byteSize: number, start: number = 0, offset: number = 0) {
    const prevSize = this.dataBuffer.length;

    if (prevSize < byteSize) {
      // If the requested data is larger than the allocated buffer, reallocate it to a buffer large
      // enough to handle the new request.
      const newData = new Uint8Array(this.nextPowerOfTwo(byteSize));

      if (this.lastOffset === offset && this.lastStart === start && this.lastByteSize) {
        // Do a fast copy of any previous data that was generated.
        newData.set(this.dataBuffer);
      }

      this.dataBuffer = newData;
    } else if (this.lastOffset < offset) {
      // Ensure all values up to the offset are zeroed out.
      this.dataBuffer.fill(0, this.lastOffset, offset);
    }

    // If the offset or start values have changed, the whole data range needs to be regenerated.
    if (this.lastOffset !== offset || this.lastStart !== start) {
      this.lastByteSize = 0;
    }

    // Generate any new values that are required
    if (this.lastByteSize < byteSize) {
      for (let i = this.lastByteSize; i < byteSize - offset; ++i) {
        this.dataBuffer[i + offset] = ((i ** 3 + i + start) % 251) + 1; // Ensure data is always non-zero
      }

      this.lastOffset = offset;
      this.lastStart = start;
      this.lastByteSize = byteSize;
    }
  }

  /**
   * Returns a new view into the generated data that's the correct length. Because this is a view
   * previously returned views from the same generator will have their values overwritten as well.
   * @param {number} byteSize - Number of bytes the returned view should contain.
   * @param {number} [start] - The value of the first element generated in the view.
   * @param {number} [offset] - Offset of the generated data within the view. Preceeding values will be 0.
   * @returns {Uint8Array} A new Uint8Array view into the generated data.
   */
  generateView(byteSize: number, start: number = 0, offset: number = 0): Uint8Array {
    this.generateData(byteSize, start, offset);

    if (this.dataBuffer.length === byteSize) {
      return this.dataBuffer;
    }
    return new Uint8Array(this.dataBuffer.buffer, 0, byteSize);
  }

  /**
   * Returns a copy of the generated data. Note that this still changes the underlying buffer, so
   * any previously generated views will still be overwritten, but the returned copy won't reflect
   * future generate* calls.
   * @param {number} byteSize - Number of bytes the returned array should contain.
   * @param {number} [start] - The value of the first element generated in the view.
   * @param {number} [offset] - Offset of the generated data within the view. Preceeding values will be 0.
   * @returns {Uint8Array} A new Uint8Array copy of the generated data.
   */
  generateAndCopyView(byteSize: number, start: number = 0, offset: number = 0) {
    this.generateData(byteSize, start, offset);
    return this.dataBuffer.slice(0, byteSize);
  }
}
