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

import { MAX_U32_NUMBER } from "./consts.js";
import { Uint8, Uint16, Uint32, Uint64, Uint128, Uint256, HexInput } from "../types/index.js";
import { Hex } from "../core/hex.js";

/**
 * Shared TextDecoder instance for string deserialization to avoid repeated instantiation.
 */
const TEXT_DECODER = new TextDecoder();

/**
 * Maximum allowed length for deserialized byte arrays and strings.
 * This prevents memory exhaustion attacks from malformed BCS data.
 * Set to 10MB which should be sufficient for any legitimate use case.
 */
const MAX_DESERIALIZE_BYTES_LENGTH = 10 * 1024 * 1024; // 10MB

/**
 * Maximum allowed vector element count. Without this cap a malformed BCS blob
 * with a ULEB128 length close to `MAX_U32_NUMBER` (~4.29 billion) would cause
 * `deserializeVector` to spin for billions of iterations before the inner
 * `read()` bounds check trips, effectively a CPU-exhaustion DoS. Matches the
 * canonical `MAX_SEQUENCE_LENGTH = 2^31 - 1` bound used elsewhere in BCS.
 *
 * Note: written as `2 ** 31 - 1` rather than `(1 << 31) - 1` because the
 * bitwise shift operates on 32-bit *signed* integers in JavaScript, which
 * would produce a negative value here.
 */
const MAX_DESERIALIZE_VECTOR_LENGTH = 2 ** 31 - 1;

/**
 * This interface exists to define Deserializable<T> inputs for functions that
 * deserialize a byte buffer into a type T.
 * It is not intended to be implemented or extended, because Typescript has no support
 * for static methods in interfaces.
 *
 * @template T - The type that this will deserialize into.
 * @group Implementation
 * @category BCS
 */
export interface Deserializable<T> {
  /**
   * Deserializes the buffered bytes into an instance of the specified class type.
   * This function provides an alternative syntax for deserialization, allowing users to call
   * `deserializer.deserialize(MyClass)` instead of `MyClass.deserialize(deserializer)`.
   *
   * @param deserializer - The deserializer instance with the buffered bytes.
   * @returns The deserialized value of class type T.
   * @example
   * ```typescript
   * const deserializer = new Deserializer(new Uint8Array([1, 2, 3]));
   * const value = deserializer.deserialize(MyClass); // where MyClass has a `deserialize` function
   * // value is now an instance of MyClass
   * // equivalent to `const value = MyClass.deserialize(deserializer)`
   * ```
   * @group Implementation
   * @category BCS
   */
  deserialize(deserializer: Deserializer): T;
}

/**
 * A class that provides methods for deserializing various data types from a byte buffer.
 * It supports deserialization of primitive types, strings, and complex objects using a BCS (Binary Common Serialization) layout.
 * @group Implementation
 * @category BCS
 */
export class Deserializer {
  private buffer: ArrayBuffer;

  private offset: number;

  /**
   * Creates a new instance of the class with a copy of the provided data buffer.
   * This prevents outside mutation of the buffer.
   *
   * @param data - The data to be copied into the internal buffer as a Uint8Array.
   * @group Implementation
   * @category BCS
   */
  constructor(data: Uint8Array) {
    // copies data to prevent outside mutation of buffer.
    this.buffer = new ArrayBuffer(data.length);
    new Uint8Array(this.buffer).set(data, 0);
    this.offset = 0;
  }

  static fromHex(hex: HexInput): Deserializer {
    const data = Hex.hexInputToUint8Array(hex);
    return new Deserializer(data);
  }

  /**
   * Reads a specified number of bytes from the buffer and advances the offset.
   * Returns a view into the buffer rather than copying for better performance.
   *
   * SECURITY NOTE: This returns a view, not a copy. Callers that expose the result
   * externally MUST call .slice() to create a copy. Internal numeric deserializers
   * (deserializeU8, deserializeU16, etc.) only read values and don't expose the view.
   * deserializeBytes() and deserializeFixedBytes() call .slice() before returning.
   *
   * @param length - The number of bytes to read from the buffer.
   * @throws Throws an error if the read operation exceeds the buffer's length.
   * @group Implementation
   * @category BCS
   */
  private read(length: number): Uint8Array {
    if (this.offset + length > this.buffer.byteLength) {
      throw new Error("Reached to the end of buffer");
    }

    // Use subarray to return a view instead of slice which copies
    // SECURITY: View is safe because numeric deserializers only read values,
    // and byte deserializers call .slice() before returning to caller
    const bytes = new Uint8Array(this.buffer, this.offset, length);
    this.offset += length;
    return bytes;
  }

  /**
   * Returns the number of bytes remaining in the buffer.
   *
   * This information is useful to determine if there's more data to be read.
   *
   * @returns The number of bytes remaining in the buffer.
   * @group Implementation
   * @category BCS
   */
  remaining(): number {
    return this.buffer.byteLength - this.offset;
  }

  /**
   * Asserts that the buffer has no remaining bytes.
   *
   * @throws {Error} Throws an error if there are remaining bytes in the buffer.
   * @group Implementation
   * @category BCS
   */
  assertFinished(): void {
    if (this.remaining() !== 0) {
      throw new Error("Buffer has remaining bytes");
    }
  }

  /**
   * Deserializes a UTF-8 encoded string from a byte array. It first reads the length of the string in bytes,
   * followed by the actual byte content, and decodes it into a string.
   *
   * 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.
   *
   * @example
   * ```typescript
   * const deserializer = new Deserializer(new Uint8Array([8, 49, 50, 51, 52, 97, 98, 99, 100]));
   * assert(deserializer.deserializeStr() === "1234abcd");
   * ```
   * @group Implementation
   * @category BCS
   */
  deserializeStr(): string {
    const value = this.deserializeBytes();
    return TEXT_DECODER.decode(value);
  }

  /**
   * @deprecated use `deserializeOption("string")` instead.
   *
   * The BCS layout for Optional<String> is 0 if none, else 1 followed by the string length and string content.
   * @returns The deserialized string if it exists, otherwise undefined.
   * @example
   * ```typescript
   * const deserializer = new Deserializer(new Uint8Array([0x00]));
   * assert(deserializer.deserializeOptionStr() === undefined);
   * const deserializer = new Deserializer(new Uint8Array([1, 8, 49, 50, 51, 52, 97, 98, 99, 100]));
   * assert(deserializer.deserializeOptionStr() === "1234abcd");
   * ```
   */
  deserializeOptionStr(): string | undefined {
    return this.deserializeOption("string");
  }

  /**
   * Deserializes an optional value from the buffer.
   *
   * The BCS layout for Optional<T> starts with a boolean byte (0 if none, 1 if some),
   * followed by the value if present.
   *
   * @template T - The type of the value to deserialize
   * @param type - Either a Deserializable class or one of the string literals: "string", "bytes", or "fixedBytes"
   * @param len - Required length when type is "fixedBytes", ignored otherwise
   * @returns The deserialized value if present, undefined otherwise
   *
   * @throws {Error} When "fixedBytes" is specified without a length
   *
   * @example
   * ```typescript
   * // Deserialize an optional string
   * const deserializer = new Deserializer(new Uint8Array([1, 3, 97, 98, 99]));
   * const optStr = deserializer.deserializeOption("string");
   * // optStr === "abc"
   *
   * // Deserialize an optional custom type
   * const deserializer = new Deserializer(new Uint8Array([0]));
   * const optValue = deserializer.deserializeOption(MyClass);
   * // optValue === undefined
   *
   * // Deserialize optional bytes
   * const deserializer = new Deserializer(new Uint8Array([1, 3, 1, 2, 3]));
   * const optBytes = deserializer.deserializeOption("bytes");
   * // optBytes === Uint8Array[1, 2, 3]
   *
   * // Deserialize optional fixed bytes
   * const deserializer = new Deserializer(new Uint8Array([1, 1, 2, 3, 4]));
   * const optBytes = deserializer.deserializeOption("fixedBytes", 4);
   * // optBytes === Uint8Array[1, 2, 3, 4]
   * ```
   * @group Implementation
   * @category BCS
   */
  deserializeOption(type: "string"): string | undefined;
  deserializeOption(type: "bytes"): Uint8Array | undefined;
  deserializeOption(type: "fixedBytes", len: number): Uint8Array | undefined;
  deserializeOption<T>(type: Deserializable<T>): T | undefined;
  deserializeOption<T>(
    type: Deserializable<T> | "string" | "bytes" | "fixedBytes",
    len?: number,
  ): T | string | Uint8Array | undefined {
    const exists = this.deserializeBool();
    if (!exists) return undefined;

    if (type === "string") {
      return this.deserializeStr();
    }
    if (type === "bytes") {
      return this.deserializeBytes();
    }
    if (type === "fixedBytes") {
      if (len === undefined) {
        throw new Error("Fixed bytes length not provided");
      }
      return this.deserializeFixedBytes(len);
    }

    return this.deserialize(type);
  }

  /**
   * Deserializes an array of bytes.
   *
   * The BCS layout for "bytes" consists of a bytes_length followed by the bytes themselves, where bytes_length is a u32 integer
   * encoded as a uleb128 integer, indicating the length of the bytes array.
   *
   * @returns {Uint8Array} The deserialized array of bytes (a copy, safe to modify).
   * @throws {Error} If the length exceeds the maximum allowed (10MB) to prevent memory exhaustion.
   * @group Implementation
   * @category BCS
   */
  deserializeBytes(): Uint8Array {
    const len = this.deserializeUleb128AsU32();
    // Security: Prevent memory exhaustion from malformed data
    if (len > MAX_DESERIALIZE_BYTES_LENGTH) {
      throw new Error(
        `Deserialization error: byte array length ${len} exceeds maximum allowed ${MAX_DESERIALIZE_BYTES_LENGTH}`,
      );
    }
    // Return a copy so caller can safely modify without affecting buffer
    return this.read(len).slice();
  }

  /**
   * Deserializes an array of bytes of a specified length.
   *
   * @param len - The number of bytes to read from the source.
   * @returns {Uint8Array} The deserialized array of bytes (a copy, safe to modify).
   * @group Implementation
   * @category BCS
   */
  deserializeFixedBytes(len: number): Uint8Array {
    // Return a copy so caller can safely modify without affecting buffer
    return this.read(len).slice();
  }

  /**
   * Deserializes a boolean value from a byte stream.
   *
   * The BCS layout for a boolean uses one byte, where "0x01" represents true and "0x00" represents false.
   * An error is thrown if the byte value is not valid.
   *
   * @returns The deserialized boolean value.
   * @throws Throws an error if the boolean value is invalid.
   * @group Implementation
   * @category BCS
   */
  deserializeBool(): boolean {
    const bool = this.read(1)[0];
    if (bool !== 1 && bool !== 0) {
      throw new Error("Invalid boolean value");
    }
    return bool === 1;
  }

  /**
   * Deserializes a uint8 number from the binary data.
   *
   * BCS layout for "uint8": One byte. Binary format in little-endian representation.
   *
   * @returns {number} The deserialized uint8 number.
   * @group Implementation
   * @category BCS
   */
  deserializeU8(): Uint8 {
    return this.read(1)[0];
  }

  /**
   * Deserializes a uint16 number from a binary format in little-endian representation.
   *
   * BCS layout for "uint16": Two bytes.
   * @example
   * ```typescript
   * const deserializer = new Deserializer(new Uint8Array([0x34, 0x12]));
   * assert(deserializer.deserializeU16() === 4660);
   * ```
   * @group Implementation
   * @category BCS
   */
  deserializeU16(): Uint16 {
    const bytes = this.read(2);
    return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint16(0, true);
  }

  /**
   * Deserializes a uint32 number from a binary format in little-endian representation.
   *
   * BCS layout for "uint32": Four bytes.
   * @example
   * ```typescript
   * const deserializer = new Deserializer(new Uint8Array([0x78, 0x56, 0x34, 0x12]));
   * assert(deserializer.deserializeU32() === 305419896);
   * ```
   * @group Implementation
   * @category BCS
   */
  deserializeU32(): Uint32 {
    const bytes = this.read(4);
    return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true);
  }

  /**
   * Deserializes a uint64 number.
   *
   * This function combines two 32-bit values to return a 64-bit unsigned integer in little-endian representation.
   * @example
   * ```typescript
   * const deserializer = new Deserializer(new Uint8Array([0x00, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12]));
   * assert(deserializer.deserializeU64() === 1311768467750121216);
   * ```
   * @group Implementation
   * @category BCS
   */
  deserializeU64(): Uint64 {
    const low = this.deserializeU32();
    const high = this.deserializeU32();

    // combine the two 32-bit values and return (little endian)
    return BigInt((BigInt(high) << BigInt(32)) | BigInt(low));
  }

  /**
   * Deserializes a uint128 number from its binary representation.
   * This function combines two 64-bit values to return a single uint128 value in little-endian format.
   *
   * @returns {BigInt} The deserialized uint128 number.
   * @group Implementation
   * @category BCS
   */
  deserializeU128(): Uint128 {
    const low = this.deserializeU64();
    const high = this.deserializeU64();

    // combine the two 64-bit values and return (little endian)
    return BigInt((high << BigInt(64)) | low);
  }

  /**
   * Deserializes a uint256 number from its binary representation.
   *
   * The BCS layout for "uint256" consists of thirty-two bytes in little-endian format.
   *
   * @returns {BigInt} The deserialized uint256 number.
   * @group Implementation
   * @category BCS
   */
  deserializeU256(): Uint256 {
    const low = this.deserializeU128();
    const high = this.deserializeU128();

    // combine the two 128-bit values and return (little endian)
    return BigInt((high << BigInt(128)) | low);
  }

  /**
   * Deserializes an 8-bit signed integer from the binary data.
   * BCS layout for "int8": One byte. Binary format in little-endian representation.
   *
   * @returns {number} The deserialized int8 number.
   * @group Implementation
   * @category BCS
   */
  deserializeI8(): number {
    const bytes = this.read(1);
    return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt8(0);
  }

  /**
   * Deserializes a 16-bit signed integer from a binary format in little-endian representation.
   * BCS layout for "int16": Two bytes.
   *
   * @returns {number} The deserialized int16 number.
   * @group Implementation
   * @category BCS
   */
  deserializeI16(): number {
    const bytes = this.read(2);
    return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt16(0, true);
  }

  /**
   * Deserializes a 32-bit signed integer from a binary format in little-endian representation.
   * BCS layout for "int32": Four bytes.
   *
   * @returns {number} The deserialized int32 number.
   * @group Implementation
   * @category BCS
   */
  deserializeI32(): number {
    const bytes = this.read(4);
    return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt32(0, true);
  }

  /**
   * Deserializes a 64-bit signed integer.
   * This function combines two 32-bit values to return a 64-bit signed integer in little-endian representation.
   *
   * @returns {bigint} The deserialized int64 number.
   * @group Implementation
   * @category BCS
   */
  deserializeI64(): bigint {
    const low = this.deserializeU32();
    const high = this.deserializeU32();

    // combine the two 32-bit values (little endian)
    const unsigned = BigInt((BigInt(high) << BigInt(32)) | BigInt(low));

    // Convert from unsigned to signed using two's complement
    const signBit = BigInt(1) << BigInt(63);
    if (unsigned >= signBit) {
      return unsigned - (BigInt(1) << BigInt(64));
    }
    return unsigned;
  }

  /**
   * Deserializes a 128-bit signed integer from its binary representation.
   * This function combines two 64-bit values to return a single int128 value in little-endian format.
   *
   * @returns {bigint} The deserialized int128 number.
   * @group Implementation
   * @category BCS
   */
  deserializeI128(): bigint {
    const low = this.deserializeU64();
    const high = this.deserializeU64();

    // combine the two 64-bit values (little endian)
    const unsigned = BigInt((high << BigInt(64)) | low);

    // Convert from unsigned to signed using two's complement
    const signBit = BigInt(1) << BigInt(127);
    if (unsigned >= signBit) {
      return unsigned - (BigInt(1) << BigInt(128));
    }
    return unsigned;
  }

  /**
   * Deserializes a 256-bit signed integer from its binary representation.
   * BCS layout for "int256": Thirty-two bytes in little-endian format.
   *
   * @returns {bigint} The deserialized int256 number.
   * @group Implementation
   * @category BCS
   */
  deserializeI256(): bigint {
    const low = this.deserializeU128();
    const high = this.deserializeU128();

    // combine the two 128-bit values (little endian)
    const unsigned = BigInt((high << BigInt(128)) | low);

    // Convert from unsigned to signed using two's complement
    const signBit = BigInt(1) << BigInt(255);
    if (unsigned >= signBit) {
      return unsigned - (BigInt(1) << BigInt(256));
    }
    return unsigned;
  }

  /**
   * Deserializes a uleb128 encoded uint32 number.
   *
   * This function is used for interpreting lengths of variable-length sequences and tags of enum values in BCS encoding.
   *
   * @throws {Error} Throws an error if the parsed value exceeds the maximum uint32 number.
   * @throws {Error} Throws an error if the uleb128 encoding is malformed (too many bytes).
   * @returns {number} The deserialized uint32 value.
   * @group Implementation
   * @category BCS
   */
  deserializeUleb128AsU32(): Uint32 {
    let value: bigint = BigInt(0);
    let shift = 0;
    // Maximum 5 bytes for uleb128-encoded u32 (7 bits per byte, 5*7=35 bits > 32 bits needed)
    const MAX_ULEB128_BYTES = 5;
    let bytesRead = 0;

    while (bytesRead < MAX_ULEB128_BYTES) {
      const byte = this.deserializeU8();
      bytesRead += 1;

      value |= BigInt(byte & 0x7f) << BigInt(shift);

      // Early overflow check before continuing
      if (value > MAX_U32_NUMBER) {
        throw new Error("Overflow while parsing uleb128-encoded uint32 value");
      }

      if ((byte & 0x80) === 0) {
        break;
      }

      shift += 7;
    }

    // If we read MAX_ULEB128_BYTES and the last byte had continuation bit set, it's malformed
    if (bytesRead === MAX_ULEB128_BYTES && value > MAX_U32_NUMBER) {
      throw new Error("Overflow while parsing uleb128-encoded uint32 value");
    }

    return Number(value);
  }

  /**
   * Helper function that primarily exists to support alternative syntax for deserialization.
   * That is, if we have a `const deserializer: new Deserializer(...)`, instead of having to use
   * `MyClass.deserialize(deserializer)`, we can call `deserializer.deserialize(MyClass)`.
   *
   * @example const deserializer = new Deserializer(new Uint8Array([1, 2, 3]));
   * const value = deserializer.deserialize(MyClass); // where MyClass has a `deserialize` function
   * // value is now an instance of MyClass
   * // equivalent to `const value = MyClass.deserialize(deserializer)`
   * @param cls The BCS-deserializable class to deserialize the buffered bytes into.
   *
   * @returns the deserialized value of class type T
   * @group Implementation
   * @category BCS
   */
  deserialize<T>(cls: Deserializable<T>): T {
    // NOTE: `deserialize` in `cls.deserialize(this)` here is a static method defined in `cls`,
    // It is separate from the `deserialize` instance method defined here in Deserializer.
    return cls.deserialize(this);
  }

  /**
   * Deserializes an array of BCS Deserializable values given an existing Deserializer instance with a loaded byte buffer.
   *
   * @param cls The BCS-deserializable class to deserialize the buffered bytes into.
   * @returns An array of deserialized values of type T.
   * @example
   * // serialize a vector of addresses
   * 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();
   *
   * // deserialize the bytes into an array of addresses
   * const deserializer = new Deserializer(serializedBytes);
   * const deserializedAddresses = deserializer.deserializeVector(AccountAddress);
   * // deserializedAddresses is now an array of AccountAddress instances
   * @group Implementation
   * @category BCS
   */
  deserializeVector<T>(cls: Deserializable<T>): Array<T> {
    const length = this.deserializeUleb128AsU32();
    if (length > MAX_DESERIALIZE_VECTOR_LENGTH) {
      throw new Error(`Vector length ${length} exceeds maximum allowed length of ${MAX_DESERIALIZE_VECTOR_LENGTH}`);
    }
    const vector = new Array<T>();
    for (let i = 0; i < length; i += 1) {
      vector.push(this.deserialize(cls));
    }
    return vector;
  }
}
