// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import {
  MAX_U128_BIG_INT,
  MAX_U16_NUMBER,
  MAX_U32_NUMBER,
  MAX_U64_BIG_INT,
  MAX_U8_NUMBER,
  MAX_U256_BIG_INT,
  MIN_I8_NUMBER,
  MAX_I8_NUMBER,
  MIN_I16_NUMBER,
  MAX_I16_NUMBER,
  MIN_I32_NUMBER,
  MAX_I32_NUMBER,
  MIN_I64_BIG_INT,
  MAX_I64_BIG_INT,
  MIN_I128_BIG_INT,
  MAX_I128_BIG_INT,
  MIN_I256_BIG_INT,
  MAX_I256_BIG_INT,
} from "./consts.js";
import { Hex } from "../core/hex.js";
import { AnyNumber, Uint16, Uint32, Uint8 } from "../types/index.js";
import { TEXT_ENCODER } from "../utils/const.js";

/**
 * This class serves as a base class for all serializable types. It facilitates
 * composable serialization of complex types and enables the serialization of
 * instances to their BCS (Binary Canonical Serialization) representation.
 * @group Implementation
 * @category BCS
 */
export abstract class Serializable {
  abstract serialize(serializer: Serializer): void;

  /**
   * Serializes a `Serializable` value to its BCS representation.
   * This function is the TypeScript SDK equivalent of `bcs::to_bytes` in Move.
   * @returns the BCS representation of the Serializable instance as a byte buffer.
   * @group Implementation
   * @category BCS
   */
  bcsToBytes(): Uint8Array {
    const serializer = new Serializer();
    this.serialize(serializer);
    return serializer.toUint8Array();
  }

  /**
   * Converts the BCS-serialized bytes of a value into a Hex instance.
   * This function provides a Hex representation of the BCS-serialized data for easier handling and manipulation.
   * @returns A Hex instance with the BCS-serialized bytes loaded into its underlying Uint8Array.
   * @group Implementation
   * @category BCS
   */
  bcsToHex(): Hex {
    const bcsBytes = this.bcsToBytes();
    return Hex.fromHexInput(bcsBytes);
  }

  /**
   * Returns the hex string representation of the `Serializable` value without the 0x prefix.
   * @returns the hex format as a string without `0x` prefix.
   */
  toStringWithoutPrefix(): string {
    return this.bcsToHex().toStringWithoutPrefix();
  }

  /**
   * Returns the hex string representation of the `Serializable` value with the 0x prefix.
   * @returns the hex format as a string prefixed by `0x`.
   */
  toString(): string {
    return `0x${this.toStringWithoutPrefix()}`;
  }
}

/**
 * Serialize a Serializable value as length-prefixed bytes into a Serializer,
 * with backwards compatibility for older Serializer implementations that lack
 * the `serializeAsBytes` method. This is critical for cross-version compatibility
 * when SDK objects built with a newer SDK are serialized by an older SDK's Serializer
 * (e.g., wallet extensions bundling an older SDK version).
 *
 * @param serializer - The serializer to write into (may be from any SDK version).
 * @param value - The Serializable value to serialize as bytes.
 */
export function serializeEntryFunctionBytesCompat(serializer: Serializer, value: Serializable): void {
  if ("serializeAsBytes" in serializer && typeof serializer.serializeAsBytes === "function") {
    serializer.serializeAsBytes(value);
  } else {
    const bcsBytes = value.bcsToBytes();
    serializer.serializeBytes(bcsBytes);
  }
}

/**
 * Minimum buffer growth increment to avoid too many small reallocations.
 */
const MIN_BUFFER_GROWTH = 256;

/**
 * Pool of reusable Serializer instances for temporary serialization operations.
 * This reduces allocations when using serializeAsBytes() repeatedly.
 */
const serializerPool: Serializer[] = [];
const MAX_POOL_SIZE = 8;

/**
 * Acquires a Serializer from the pool or creates a new one.
 * @internal
 */
function acquireSerializer(): Serializer {
  const serializer = serializerPool.pop();
  if (serializer) {
    serializer.reset();
    return serializer;
  }
  return new Serializer();
}

/**
 * Returns a Serializer to the pool for reuse.
 * @internal
 */
function releaseSerializer(serializer: Serializer): void {
  if (serializerPool.length < MAX_POOL_SIZE) {
    serializerPool.push(serializer);
  }
}

/**
 * A class for serializing various data types into a binary format.
 * It provides methods to serialize strings, bytes, numbers, and other serializable objects
 * using the Binary Coded Serialization (BCS) layout. The serialized data can be retrieved as a
 * Uint8Array.
 * @group Implementation
 * @category BCS
 */
export class Serializer {
  private buffer: ArrayBuffer;

  private offset: number;

  /**
   * Reusable DataView instance to reduce allocations during serialization.
   * Recreated when buffer is resized.
   */
  private dataView: DataView;

  /**
   * Constructs a serializer with a buffer of size `length` bytes, 64 bytes by default.
   * The `length` must be greater than 0.
   *
   * @param length - The size of the buffer in bytes.
   * @group Implementation
   * @category BCS
   */
  constructor(length: number = 64) {
    if (length <= 0) {
      throw new Error("Length needs to be greater than 0");
    }
    this.buffer = new ArrayBuffer(length);
    this.dataView = new DataView(this.buffer);
    this.offset = 0;
  }

  /**
   * Ensures that the internal buffer can accommodate the specified number of bytes.
   * This function dynamically resizes the buffer using a growth factor of 1.5x with
   * a minimum growth increment to balance memory usage and reallocation frequency.
   *
   * @param bytes - The number of bytes to ensure the buffer can handle.
   * @group Implementation
   * @category BCS
   */
  private ensureBufferWillHandleSize(bytes: number) {
    const requiredSize = this.offset + bytes;
    if (this.buffer.byteLength >= requiredSize) {
      return;
    }

    // Calculate new size: max of (1.5x current size) or (current + required + MIN_GROWTH)
    // Using 1.5x instead of 2x provides better memory efficiency
    const growthSize = Math.max(Math.floor(this.buffer.byteLength * 1.5), requiredSize + MIN_BUFFER_GROWTH);

    const newBuffer = new ArrayBuffer(growthSize);
    new Uint8Array(newBuffer).set(new Uint8Array(this.buffer, 0, this.offset));
    this.buffer = newBuffer;
    this.dataView = new DataView(this.buffer);
  }

  /**
   * Appends the specified values to the buffer, ensuring that the buffer can accommodate the new data.
   *
   * @param {Uint8Array} values - The values to be appended to the buffer.
   * @group Implementation
   * @category BCS
   */
  protected appendToBuffer(values: Uint8Array) {
    this.ensureBufferWillHandleSize(values.length);
    new Uint8Array(this.buffer, this.offset).set(values);
    this.offset += values.length;
  }

  /**
   * Serializes a value into the buffer using the provided function, ensuring the buffer can accommodate the size.
   * Uses the cached DataView instance for better performance.
   *
   * @param fn - The function to serialize the value, which takes a byte offset, the value to serialize, and an optional little-endian flag.
   * @param fn.byteOffset - The byte offset at which to write the value.
   * @param fn.value - The numeric value to serialize into the buffer.
   * @param fn.littleEndian - Optional flag indicating whether to use little-endian byte order (defaults to true).
   * @group Implementation
   * @category BCS
   */
  // TODO: JSDoc bytesLength and value
  private serializeWithFunction(
    fn: (byteOffset: number, value: number, littleEndian?: boolean) => void,
    bytesLength: number,
    value: number,
  ) {
    this.ensureBufferWillHandleSize(bytesLength);
    fn.apply(this.dataView, [this.offset, value, true]);
    this.offset += bytesLength;
  }

  /**
   * Serializes a string. UTF8 string is supported.
   * The number of bytes in the string content is serialized first, as a uleb128-encoded u32 integer.
   * Then the string content is serialized as UTF8 encoded bytes.
   *
   * BCS layout for "string": string_length | string_content
   * where string_length is a u32 integer encoded as a uleb128 integer, equal to the number of bytes in string_content.
   *
   * @param value - The string to serialize.
   *
   * @example
   * ```typescript
   * const serializer = new Serializer();
   * serializer.serializeStr("1234abcd");
   * assert(serializer.toUint8Array() === new Uint8Array([8, 49, 50, 51, 52, 97, 98, 99, 100]));
   * ```
   * @group Implementation
   * @category BCS
   */
  serializeStr(value: string) {
    this.serializeBytes(TEXT_ENCODER.encode(value));
  }

  /**
   * Serializes an array of bytes.
   *
   * This function encodes the length of the byte array as a u32 integer in uleb128 format, followed by the byte array itself.
   * BCS layout for "bytes": bytes_length | bytes
   * where bytes_length is a u32 integer encoded as a uleb128 integer, equal to the length of the bytes array.
   * @param value - The byte array to serialize.
   * @group Implementation
   * @category BCS
   */
  serializeBytes(value: Uint8Array) {
    this.serializeU32AsUleb128(value.length);
    this.appendToBuffer(value);
  }

  /**
   * Serializes an array of bytes with a known length, allowing for efficient deserialization without needing to serialize the
   * length itself.
   * When deserializing, the number of bytes to deserialize needs to be passed in.

   * @param value - The Uint8Array to be serialized.
   * @group Implementation
   * @category BCS
   */
  serializeFixedBytes(value: Uint8Array) {
    this.appendToBuffer(value);
  }

  /**
   * Serializes a boolean value into a byte representation.
   *
   * The BCS layout for a boolean uses one byte, where "0x01" represents true and "0x00" represents false.
   *
   * @param value - The boolean value to serialize.
   * @group Implementation
   * @category BCS
   */
  serializeBool(value: boolean) {
    /**
     * Ensures that the provided value is a boolean.
     * This function throws an error if the value is not a boolean, helping to enforce type safety in your code.
     *
     * @param value - The value to be checked for boolean type.
     * @throws {Error} Throws an error if the value is not a boolean.
     * @group Implementation
     * @category BCS
     */
    ensureBoolean(value);
    const byteValue = value ? 1 : 0;
    this.appendToBuffer(new Uint8Array([byteValue]));
  }

  /**
   * Serializes a Uint8 value and appends it to the buffer.
   * BCS layout for "uint8": One byte. Binary format in little-endian representation.
   *
   * @param value - The Uint8 value to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(0, MAX_U8_NUMBER)
  serializeU8(value: Uint8) {
    this.appendToBuffer(new Uint8Array([value]));
  }

  /**
   * Serializes a uint16 number.
   *
   * @group Implementation
   * @category BCS

   */

  /**
   * Serializes a 16-bit unsigned integer value into a binary format.
   * BCS layout for "uint16": Two bytes. Binary format in little-endian representation.
   *
   * @param value - The 16-bit unsigned integer value to serialize.
   * @example
   * ```typescript
   * const serializer = new Serializer();
   * serializer.serializeU16(4660);
   * assert(serializer.toUint8Array() === new Uint8Array([0x34, 0x12]));
   * ```
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(0, MAX_U16_NUMBER)
  serializeU16(value: Uint16) {
    this.serializeWithFunction(DataView.prototype.setUint16, 2, value);
  }

  /**
   * Serializes a 32-bit unsigned integer value into a binary format.
   * This function is useful for encoding data that needs to be stored or transmitted in a compact form.
   * @example
   * ```typescript
   * const serializer = new Serializer();
   * serializer.serializeU32(305419896);
   * assert(serializer.toUint8Array() === new Uint8Array([0x78, 0x56, 0x34, 0x12]));
   * ```
   * @param value - The 32-bit unsigned integer value to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(0, MAX_U32_NUMBER)
  serializeU32(value: Uint32) {
    this.serializeWithFunction(DataView.prototype.setUint32, 4, value);
  }

  /**
   * Serializes a 64-bit unsigned integer into a format suitable for storage or transmission.
   * This function breaks down the value into two 32-bit components and writes them in little-endian order.
   *
   * @param value - The 64-bit unsigned integer to serialize, represented as a number.
   * @example
   * ```ts
   * const serializer = new Serializer();
   * serializer.serializeU64(1311768467750121216);
   * assert(serializer.toUint8Array() === new Uint8Array([0x00, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12]));
   * ```
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(BigInt(0), MAX_U64_BIG_INT)
  serializeU64(value: AnyNumber) {
    const low = BigInt(value) & BigInt(MAX_U32_NUMBER);
    const high = BigInt(value) >> BigInt(32);

    // write little endian number
    this.serializeU32(Number(low));
    this.serializeU32(Number(high));
  }

  /**
   * Serializes a U128 value into a format suitable for storage or transmission.
   *
   * @param value - The U128 value to serialize, represented as a number.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(BigInt(0), MAX_U128_BIG_INT)
  serializeU128(value: AnyNumber) {
    const low = BigInt(value) & MAX_U64_BIG_INT;
    const high = BigInt(value) >> BigInt(64);

    // write little endian number
    this.serializeU64(low);
    this.serializeU64(high);
  }

  /**
   * Serializes a U256 value into a byte representation.
   * This function is essential for encoding large numbers in a compact format suitable for transmission or storage.
   *
   * @param value - The U256 value to serialize, represented as an AnyNumber.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(BigInt(0), MAX_U256_BIG_INT)
  serializeU256(value: AnyNumber) {
    const low = BigInt(value) & MAX_U128_BIG_INT;
    const high = BigInt(value) >> BigInt(128);

    // write little endian number
    this.serializeU128(low);
    this.serializeU128(high);
  }

  /**
   * Serializes an 8-bit signed integer value.
   * BCS layout for "int8": One byte. Binary format in little-endian representation.
   *
   * @param value - The 8-bit signed integer value to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(MIN_I8_NUMBER, MAX_I8_NUMBER)
  serializeI8(value: number) {
    this.serializeWithFunction(DataView.prototype.setInt8, 1, value);
  }

  /**
   * Serializes a 16-bit signed integer value into a binary format.
   * BCS layout for "int16": Two bytes. Binary format in little-endian representation.
   *
   * @param value - The 16-bit signed integer value to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(MIN_I16_NUMBER, MAX_I16_NUMBER)
  serializeI16(value: number) {
    this.serializeWithFunction(DataView.prototype.setInt16, 2, value);
  }

  /**
   * Serializes a 32-bit signed integer value into a binary format.
   *
   * @param value - The 32-bit signed integer value to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(MIN_I32_NUMBER, MAX_I32_NUMBER)
  serializeI32(value: number) {
    this.serializeWithFunction(DataView.prototype.setInt32, 4, value);
  }

  /**
   * Serializes a 64-bit signed integer into a format suitable for storage or transmission.
   * This function uses two's complement representation for negative values.
   *
   * @param value - The 64-bit signed integer to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(MIN_I64_BIG_INT, MAX_I64_BIG_INT)
  serializeI64(value: AnyNumber) {
    const val = BigInt(value);
    // Convert to unsigned representation using two's complement
    const unsigned = val < 0 ? (BigInt(1) << BigInt(64)) + val : val;
    const low = unsigned & BigInt(MAX_U32_NUMBER);
    const high = unsigned >> BigInt(32);

    // write little endian number
    this.serializeU32(Number(low));
    this.serializeU32(Number(high));
  }

  /**
   * Serializes a 128-bit signed integer value.
   *
   * @param value - The 128-bit signed integer value to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(MIN_I128_BIG_INT, MAX_I128_BIG_INT)
  serializeI128(value: AnyNumber) {
    const val = BigInt(value);
    // Convert to unsigned representation using two's complement
    const unsigned = val < 0 ? (BigInt(1) << BigInt(128)) + val : val;
    const low = unsigned & MAX_U64_BIG_INT;
    const high = unsigned >> BigInt(64);

    // write little endian number
    this.serializeU64(low);
    this.serializeU64(high);
  }

  /**
   * Serializes a 256-bit signed integer value.
   *
   * @param value - The 256-bit signed integer value to serialize.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(MIN_I256_BIG_INT, MAX_I256_BIG_INT)
  serializeI256(value: AnyNumber) {
    const val = BigInt(value);
    // Convert to unsigned representation using two's complement
    const unsigned = val < 0 ? (BigInt(1) << BigInt(256)) + val : val;
    const low = unsigned & MAX_U128_BIG_INT;
    const high = unsigned >> BigInt(128);

    // write little endian number
    this.serializeU128(low);
    this.serializeU128(high);
  }

  /**
   * Serializes a 32-bit unsigned integer as a variable-length ULEB128 encoded byte array.
   * BCS uses uleb128 encoding in two cases: (1) lengths of variable-length sequences and (2) tags of enum values
   *
   * @param val - The 32-bit unsigned integer value to be serialized.
   * @group Implementation
   * @category BCS
   */
  @checkNumberRange(0, MAX_U32_NUMBER)
  serializeU32AsUleb128(val: Uint32) {
    let value = val;
    const valueArray = [];
    while (value >>> 7 !== 0) {
      valueArray.push((value & 0x7f) | 0x80);
      value >>>= 7;
    }
    valueArray.push(value);
    this.appendToBuffer(new Uint8Array(valueArray));
  }

  /**
   * Returns the buffered bytes as a Uint8Array.
   *
   * This function allows you to retrieve the byte representation of the buffer up to the current offset.
   * For better performance, returns a view when the buffer is exactly the right size, or copies
   * only the used portion otherwise.
   *
   * @returns Uint8Array - The byte array representation of the buffer.
   * @group Implementation
   * @category BCS
   */
  toUint8Array(): Uint8Array {
    // Return a copy of only the used portion of the buffer
    // Using subarray + slice pattern for efficiency
    return new Uint8Array(this.buffer, 0, this.offset).slice();
  }

  /**
   * Resets the serializer to its initial state, allowing the buffer to be reused.
   * This clears the buffer contents to prevent data leakage between uses.
   *
   * @group Implementation
   * @category BCS
   */
  reset(): void {
    // Clear buffer contents to prevent data leakage when reusing pooled serializers
    // Only clear the portion that was used (up to offset) for efficiency
    if (this.offset > 0) {
      new Uint8Array(this.buffer, 0, this.offset).fill(0);
    }
    this.offset = 0;
  }

  /**
   * Returns the current number of bytes written to the serializer.
   *
   * @returns The number of bytes written.
   * @group Implementation
   * @category BCS
   */
  getOffset(): number {
    return this.offset;
  }

  /**
   * Returns a view of the serialized bytes without copying.
   * WARNING: The returned view is only valid until the next write operation.
   * Use toUint8Array() if you need a persistent copy.
   *
   * @returns A Uint8Array view of the buffer (not a copy).
   * @group Implementation
   * @category BCS
   */
  toUint8ArrayView(): Uint8Array {
    return new Uint8Array(this.buffer, 0, this.offset);
  }

  /**
   * Serializes a `Serializable` value, facilitating composable serialization.
   *
   * @param value The Serializable value to serialize.
   *
   * @returns the serializer instance
   * @group Implementation
   * @category BCS
   */
  serialize<T extends Serializable>(value: T): void {
    // NOTE: The `serialize` method called by `value` is defined in `value`'s
    // Serializable interface, not the one defined in this class.
    value.serialize(this);
  }

  /**
   * Serializes a Serializable value as a byte array with a length prefix.
   * This is the optimized pattern for entry function argument serialization.
   *
   * Instead of:
   * ```typescript
   * const bcsBytes = value.bcsToBytes();  // Creates new Serializer, copies bytes
   * serializer.serializeBytes(bcsBytes);
   * ```
   *
   * Use:
   * ```typescript
   * serializer.serializeAsBytes(value);  // Uses pooled Serializer, avoids extra copy
   * ```
   *
   * This method uses a pooled Serializer instance to reduce allocations and
   * directly appends the serialized bytes with a length prefix.
   *
   * @param value - The Serializable value to serialize as bytes.
   * @group Implementation
   * @category BCS
   */
  serializeAsBytes<T extends Serializable>(value: T): void {
    // Acquire a pooled serializer for temporary use
    const tempSerializer = acquireSerializer();

    try {
      // Serialize the value to the temporary serializer
      value.serialize(tempSerializer);

      // Get the serialized bytes (as a view to avoid copying)
      const bytes = tempSerializer.toUint8ArrayView();

      // Serialize with length prefix to this serializer
      this.serializeBytes(bytes);
    } finally {
      // Return the serializer to the pool for reuse
      releaseSerializer(tempSerializer);
    }
  }

  /**
   * Serializes an array of BCS Serializable values to a serializer instance.
   * The bytes are added to the serializer instance's byte buffer.
   *
   * @param values The array of BCS Serializable values
   * @example
   * const addresses = new Array<AccountAddress>(
   *   AccountAddress.from("0x1"),
   *   AccountAddress.from("0x2"),
   *   AccountAddress.from("0xa"),
   *   AccountAddress.from("0xb"),
   * );
   * const serializer = new Serializer();
   * serializer.serializeVector(addresses);
   * const serializedBytes = serializer.toUint8Array();
   * // serializedBytes is now the BCS-serialized bytes
   * // The equivalent value in Move would be:
   * // `bcs::to_bytes(&vector<address> [@0x1, @0x2, @0xa, @0xb])`;
   * @group Implementation
   * @category BCS
   */
  serializeVector<T extends Serializable>(values: Array<T>): void {
    this.serializeU32AsUleb128(values.length);
    values.forEach((item) => {
      item.serialize(this);
    });
  }

  /**
   * Serializes an optional value which can be a Serializable, string, or Uint8Array.
   * For strings and Uint8Arrays, it uses the appropriate serialization method.
   *
   * @param value The value to serialize (Serializable, string, Uint8Array, or undefined)
   * @param len Optional fixed length for Uint8Array serialization. If provided, uses serializeFixedBytes instead of serializeBytes
   *
   * @example
   * ```typescript
   * const serializer = new Serializer();
   * serializer.serializeOption("hello");  // Serializes optional string
   * serializer.serializeOption(new Uint8Array([1, 2, 3]));  // Serializes optional bytes
   * serializer.serializeOption(new Uint8Array([1, 2, 3]), 3);  // Serializes optional fixed-length bytes
   * serializer.serializeOption(new AccountAddress(...));  // Serializes optional Serializable
   * serializer.serializeOption(undefined);  // Serializes none case
   * ```
   * @group Implementation
   * @category BCS
   */
  serializeOption<T extends Serializable | string | Uint8Array>(value?: T, len?: number): void {
    const hasValue = value !== undefined;
    this.serializeBool(hasValue);
    if (hasValue) {
      if (typeof value === "string") {
        this.serializeStr(value);
      } else if (value instanceof Uint8Array) {
        if (len !== undefined) {
          this.serializeFixedBytes(value);
        } else {
          this.serializeBytes(value);
        }
      } else {
        value.serialize(this);
      }
    }
  }

  /**
   * @deprecated use `serializeOption` instead.
   * Serializes an optional string, supporting UTF8 encoding.
   * The function encodes the existence of the string first, followed by the length and content if it exists.
   *
   * BCS layout for optional "string": 1 | string_length | string_content
   * where string_length is a u32 integer encoded as a uleb128 integer, equal to the number of bytes in string_content.
   * BCS layout for undefined: 0
   *
   * @param value - The optional string to serialize. If undefined, it will serialize as 0.
   * @group Implementation
   * @category BCS
   */
  serializeOptionStr(value?: string): void {
    if (value === undefined) {
      this.serializeU32AsUleb128(0);
    } else {
      this.serializeU32AsUleb128(1);
      this.serializeStr(value);
    }
  }
}

/**
 * @group Implementation
 * @category BCS
 */
export function ensureBoolean(value: unknown): asserts value is boolean {
  if (typeof value !== "boolean") {
    throw new Error(`${value} is not a boolean value`);
  }
}
/**
 * @group Implementation
 * @category BCS
 */
export const outOfRangeErrorMessage = (value: AnyNumber, min: AnyNumber, max: AnyNumber) =>
  `${value} is out of range: [${min}, ${max}]`;

/**
 * Validates that a given number is within a specified range.
 * This function throws an error if the value is outside the defined minimum and maximum bounds.
 *
 * @param value - The number to validate.
 * @param minValue - The minimum allowable value (inclusive).
 * @param maxValue - The maximum allowable value (inclusive).
 * @group Implementation
 * @category BCS
 */
export function validateNumberInRange<T extends AnyNumber>(value: T, minValue: T, maxValue: T) {
  const valueBigInt = BigInt(value);
  if (valueBigInt > BigInt(maxValue) || valueBigInt < BigInt(minValue)) {
    throw new Error(outOfRangeErrorMessage(value, minValue, maxValue));
  }
}

/**
 * A decorator that validates that the input argument for a function is within a specified range.
 * This ensures that the function is only called with valid input values, preventing potential errors.
 *
 * @param minValue - The input argument must be greater than or equal to this value.
 * @param maxValue - The input argument must be less than or equal to this value.
 * @group Implementation
 * @category BCS
 */
function checkNumberRange<T extends AnyNumber>(minValue: T, maxValue: T) {
  return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => {
    const childFunction = descriptor.value;

    descriptor.value = function deco(value: AnyNumber) {
      validateNumberInRange(value, minValue, maxValue);
      return childFunction.apply(this, [value]);
    };

    return descriptor;
  };
}
