import { ProtobufWire } from "@typia/interface";
import { Singleton } from "@typia/utils";

/// @reference https://github.com/piotr-oles/as-proto/blob/main/packages/as-proto/assembly/internal/FixedReader.ts
export class _ProtobufReader {
  /** Read buffer */
  private buf: Uint8Array;

  /** Read buffer pointer. */
  private ptr: number;

  /** DataView for buffer. */
  private view: DataView;

  /** Current length-delimited boundary. */
  private end: number;

  public constructor(buf: Uint8Array) {
    this.buf = buf;
    this.ptr = 0;
    this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
    this.end = buf.length;
  }

  public index(): number {
    return this.ptr;
  }

  public size(): number {
    return this.end;
  }

  public uint32(): number {
    return this.varint32() >>> 0;
  }

  public int32(): number {
    return this.varint32();
  }

  public sint32(): number {
    const value: number = this.varint32();
    return (value >>> 1) ^ -(value & 1);
  }

  public uint64(): bigint {
    return this.varint64();
  }

  public int64(): bigint {
    return BigInt.asIntN(64, this.varint64());
  }

  public sint64(): bigint {
    const value = this.varint64();
    return (value >> BigInt(0x01)) ^ -(value & BigInt(0x01));
  }

  public bool(): boolean {
    return this.varint32() !== 0;
  }

  public float(): number {
    return this.view.getFloat32(this.take(4), true);
  }

  public double(): number {
    return this.view.getFloat64(this.take(8), true);
  }

  public bytes(): Uint8Array {
    return this.atomic(() => {
      const length: number = this.uint32();
      const from: number = this.take(length);
      return this.buf.subarray(from, from + length);
    });
  }

  public string(): string {
    const bytes: Uint8Array = this.bytes();
    const decoder: TextDecoder = utf8.get();
    try {
      return decoder.decode(bytes);
    } catch {
      throw error("invalid UTF-8 string.");
    }
  }

  public fork(): number {
    return this.atomic(() => {
      const previous: number = this.end;
      const length: number = this.uint32();
      this.validate(length);
      this.end = this.ptr + length;
      return previous;
    });
  }

  public close(previous: number): void {
    if (this.ptr !== this.end) throw error("buffer overflow.");
    this.end = previous;
  }

  /** Advance by exactly `length` bytes, for every length including zero. */
  public skip(length: number): void {
    this.take(length);
  }

  /**
   * Advance by exactly one varint, which carries no length prefix.
   *
   * A varint terminates at its first byte without the continuation bit, and may
   * occupy no more than {@link VARINT_MAX_BYTES}. Its tenth byte is bounded
   * twice over, by {@link lastVarintByte}: it may neither continue into an
   * eleventh byte nor carry payload above bit 63. Skipping is where that is
   * easiest to get wrong, because this path discards the bytes it reads and so
   * never notices a value it could not have represented. Relaxing either bound
   * here would accept a varint that `varint32` and `varint64` reject, making
   * the limit depend on which method happens to consume the value.
   */
  public skipVarint(): void {
    this.atomic(() => {
      for (let i: number = 1; i < VARINT_MAX_BYTES; ++i)
        if ((this.u8() & 0x80) === 0) return;
      this.lastVarintByte();
    });
  }

  public skipType(wireType: ProtobufWire): void {
    this.atomic(() => {
      switch (wireType) {
        case ProtobufWire.VARIANT:
          this.skipVarint();
          break;
        case ProtobufWire.I64:
          this.skip(8);
          break;
        case ProtobufWire.LEN:
          this.skip(this.uint32());
          break;
        case ProtobufWire.START_GROUP:
          while ((wireType = this.uint32() & 0x07) !== ProtobufWire.END_GROUP)
            this.skipType(wireType);
          break;
        case ProtobufWire.I32:
          this.skip(4);
          break;
        default:
          throw error(`invalid wire type ${wireType} at offset ${this.ptr}.`);
      }
    });
  }

  private varint32(): number {
    let loaded: number;
    let value: number;

    value = (loaded = this.u8()) & 0x7f;
    if (loaded < 0x80) return value;

    value |= ((loaded = this.u8()) & 0x7f) << 7;
    if (loaded < 0x80) return value;

    value |= ((loaded = this.u8()) & 0x7f) << 14;
    if (loaded < 0x80) return value;

    value |= ((loaded = this.u8()) & 0x7f) << 21;
    if (loaded < 0x80) return value;

    value |= ((loaded = this.u8()) & 0xf) << 28;
    if (loaded < 0x80) return value;

    // increment position until there is no continuation bit, or until this read
    // reaches VARINT_MAX_BYTES and the value can hold no further bits
    if (this.u8() < 0x80) return value;
    if (this.u8() < 0x80) return value;
    if (this.u8() < 0x80) return value;
    if (this.u8() < 0x80) return value;
    this.lastVarintByte();
    return value;
  }

  private varint64(): bigint {
    let loaded: bigint;
    let value: bigint;

    value = (loaded = this.u8n()) & BigInt(0x7f);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(7);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(14);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(21);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(28);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(35);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(42);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(49);
    if (loaded < BigInt(0x80)) return value;

    value |= ((loaded = this.u8n()) & BigInt(0x7f)) << BigInt(56);
    if (loaded < BigInt(0x80)) return value;

    value |= BigInt(this.lastVarintByte()) << BigInt(63);
    return BigInt.asUintN(64, value);
  }

  /**
   * Read the only byte whose varint payload is limited to bit 63.
   *
   * Two distinct faults meet on this byte, and each is named for what it is. A
   * continuation bit means the varint would occupy an eleventh byte, which no
   * 64-bit value can reach. Any other payload bit means the varint terminates
   * within its ten bytes but carries a value bit above 63. Reporting the second
   * as a length fault would tell the caller something untrue about a payload
   * that is exactly ten bytes long.
   */
  private lastVarintByte(): number {
    const loaded: number = this.u8();
    if ((loaded & 0x80) !== 0)
      throw error(`varint exceeds ${VARINT_MAX_BYTES} bytes.`);
    if (loaded > 0x01) throw error(`varint exceeds ${VARINT_MAX_BITS} bits.`);
    return loaded;
  }

  private u8(): number {
    return this.view.getUint8(this.take(1));
  }

  private u8n(): bigint {
    return BigInt(this.u8());
  }

  private take(length: number): number {
    this.validate(length);
    const from: number = this.ptr;
    this.ptr += length;
    return from;
  }

  private atomic<T>(closure: () => T): T {
    const index: number = this.ptr;
    const end: number = this.end;
    try {
      return closure();
    } catch (thrown) {
      this.ptr = index;
      this.end = end;
      throw thrown;
    }
  }

  private validate(length: number): void {
    if (
      Number.isSafeInteger(length) === false ||
      length < 0 ||
      length > this.size() - this.ptr
    )
      throw error("buffer overflow.");
  }
}

/**
 * Builds every fault this reader raises, so each one names typia as its source.
 *
 * Every decode error a caller can see must be attributable, and the prefix is
 * what attributes it. Composing it here rather than at each throw keeps a new
 * fault from silently shipping without it.
 */
const error = (message: string): Error =>
  new Error(`Error on typia.protobuf.decode(): ${message}`);

/**
 * The most bytes a varint may occupy: a 64-bit value in seven-bit groups.
 *
 * Every path reads no further than its tenth byte and accepts only bit 63 from
 * that byte. This constant keeps the loop-driven skip path on the same limit.
 */
const VARINT_MAX_BYTES = 10;

/**
 * The widest value a varint may carry: the 64-bit Protocol Buffer wire domain.
 *
 * The tenth byte reaches only bit 63, so a payload bit above it belongs to no
 * representable value. This names the overflow fault apart from the length
 * one.
 */
const VARINT_MAX_BITS = 64;

const utf8 = new Singleton(
  () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }),
);
