{% if support_buffer %}
type SinkOrBuf = Sink | Buffer | ArrayBuffer | Uint8Array;
{% else %}
type SinkOrBuf = Sink | ArrayBuffer | Uint8Array;
{% endif %}

export class Sink {
  public view: DataView;
  public position: number;

  private constructor(input: ArrayBuffer) {
    this.view = new DataView(input);
    this.position = 0;
  }

  public reserve(extra: number): void {
    if (this.position + extra <= this.view.buffer.byteLength) {
      return;
    }

    const newBuffer = new ArrayBuffer(
      (this.view.buffer.byteLength + extra) * 2
    );
    new Uint8Array(newBuffer).set(new Uint8Array(this.view.buffer));
    this.view = new DataView(newBuffer);
  }

  public static create(input?: SinkOrBuf): Sink {
    if (input == undefined) {
      return new Sink(new ArrayBuffer(0));
    }

    if (input instanceof Sink) {
      return input;
    }
{% if support_buffer %}
    if (input instanceof Buffer) {
      return new Sink(
        input.buffer.slice(
          input.byteOffset,
          input.byteOffset + input.byteLength
        )
      );
    }
{%- endif %}

    if (input instanceof ArrayBuffer) {
      return new Sink(input);
    }

    if (input instanceof Uint8Array) {
      return new Sink(input.buffer);
    }

    throw new Error(
{%- if support_buffer %}
      `'input' was of incorrect type. Expected 'Sink | Buffer | ArrayBuffer | Uint8Array'`
{%- else %}
      `'input' was of incorrect type. Expected 'Sink | ArrayBuffer | Uint8Array'`
{%- endif %}
    );
  }

  public getUint8Array(): Uint8Array {
    return new Uint8Array(this.view.buffer.slice(0, this.position));
  }
{% if support_buffer %}
  public getBuffer(): Buffer {
    return Buffer.from(this.view.buffer, 0, this.position);
  }
{%- endif %}
}

const BIG_32 = BigInt(32);
const BIG_64 = BigInt(64);
const BIG_32Fs = BigInt('4294967295');
const BIG_64Fs = BigInt('18446744073709551615');

const textEncoder: TextEncoder = new TextEncoder();
const textDecoder: TextDecoder = new TextDecoder();

function readU8(sink: Sink): number {
  const value = sink.view.getUint8(sink.position);
  sink.position += 1;
  return value;
}

function writeU8(value: number, sink: Sink): Sink {
  sink.reserve(1);
  sink.view.setUint8(sink.position, value);
  sink.position += 1;
  return sink;
}

function readI8(sink: Sink): number {
  const value = sink.view.getInt8(sink.position);
  sink.position += 1;
  return value;
}

function writeI8(value: number, sink: Sink): Sink {
  sink.reserve(1);
  sink.view.setInt8(sink.position, value);
  sink.position += 1;
  return sink;
}

function readU16(sink: Sink): number {
  const value = sink.view.getUint16(sink.position, true);
  sink.position += 2;
  return value;
}

function writeU16(value: number, sink: Sink): Sink {
  sink.reserve(2);
  sink.view.setUint16(sink.position, value, true);
  sink.position += 2;
  return sink;
}

function readI16(sink: Sink): number {
  const value = sink.view.getInt16(sink.position, true);
  sink.position += 2;
  return value;
}

function writeI16(value: number, sink: Sink): Sink {
  sink.reserve(2);
  sink.view.setInt16(sink.position, value, true);
  sink.position += 2;
  return sink;
}

function readU32(sink: Sink): number {
  const value = sink.view.getUint32(sink.position, true);
  sink.position += 4;
  return value;
}

function writeU32(value: number, sink: Sink): Sink {
  sink.reserve(4);
  sink.view.setUint32(sink.position, value, true);
  sink.position += 4;
  return sink;
}

function readI32(sink: Sink): number {
  const value = sink.view.getInt32(sink.position, true);
  sink.position += 4;
  return value;
}

function writeI32(value: number, sink: Sink): Sink {
  sink.reserve(4);
  sink.view.setInt32(sink.position, value, true);
  sink.position += 4;
  return sink;
}

function readF32(sink: Sink): number {
  const value = sink.view.getFloat32(sink.position, true);
  sink.position += 4;
  return value;
}

function writeF32(value: number, sink: Sink): Sink {
  sink.reserve(4);
  sink.view.setFloat32(sink.position, value, true);
  sink.position += 4;
  return sink;
}

function readF64(sink: Sink): number {
  const value = sink.view.getFloat64(sink.position, true);
  sink.position += 8;
  return value;
}

function writeF64(value: number, sink: Sink): Sink {
  sink.reserve(8);
  sink.view.setFloat64(sink.position, value, true);
  sink.position += 8;
  return sink;
}

function readU64(sink: Sink): bigint {
  const high = readU32(sink);
  const low = readU32(sink);

  return (BigInt(low) << BIG_32) | BigInt(high);
}

function writeU64(value: bigint, sink: Sink): Sink {
  const low = value & BIG_32Fs;
  const high = value >> BIG_32;

  writeU32(Number(low), sink);
  writeU32(Number(high), sink);
  return sink;
}

function readI64(sink: Sink): bigint {
  const high = readI32(sink);
  const low = readI32(sink);

  return (BigInt(low) << BIG_32) | BigInt(high);
}

function writeI64(value: bigint, sink: Sink): Sink {
  const low = value & BIG_32Fs;
  const high = value >> BIG_32;

  writeI32(Number(low), sink);
  writeI32(Number(high), sink);
  return sink;
}

function readUSize(sink: Sink): bigint {
  return readU64(sink);
}

function writeUSize(value: bigint, sink: Sink): Sink {
  writeU64(value, sink);
  return sink;
}

function readISize(sink: Sink): bigint {
  return readI64(sink);
}

function writeISize(value: bigint, sink: Sink): Sink {
  writeI64(value, sink);
  return sink;
}

function readU128(sink: Sink): bigint {
  const high = readU64(sink);
  const low = readU64(sink);

  return (low << BIG_64) | high;
}

function writeU128(value: bigint, sink: Sink): Sink {
  const low = value & BIG_64Fs;
  const high = value >> BIG_64;

  writeU64(low, sink);
  writeU64(high, sink);
  return sink;
}

function readI128(sink: Sink): bigint {
  const high = readI64(sink);
  const low = readI64(sink);

  return (low << BIG_64) | high;
}

function writeI128(value: bigint, sink: Sink): Sink {
  const low = value & BIG_64Fs;
  const high = value >> BIG_64;

  writeI64(low, sink);
  writeI64(high, sink);
  return sink;
}

function readBool(sink: Sink): boolean {
  return readU8(sink) == 1;
}

function writeBool(value: boolean, sink: Sink): Sink {
  writeU8(value ? 1 : 0, sink);
  return sink;
}

function writeBytes(value: Uint8Array, sink: Sink): Sink {
  sink.reserve(value.length + 1);
  new Uint8Array(sink.view.buffer, sink.position).set(value);
  sink.position += value.length;
  return sink;
}

function readBytes(sink: Sink, length: number): Uint8Array {
  const bytes = sink.view.buffer.slice(sink.position, sink.position + length);
  sink.position += length;
  return new Uint8Array(bytes);
}

function writeString(value: string, sink: Sink): Sink {
  const bytes = textEncoder.encode(value);
  writeU64(BigInt(bytes.length), sink);
  writeBytes(bytes, sink);
  return sink;
}

function readString(sink: Sink): string {
  const length = readU64(sink);
  const bytes = readBytes(sink, Number(length));
  return textDecoder.decode(bytes);
}

function writeOption<T>(
  writeFunc: (value: T, sink: Sink) => Sink
): (value: T | undefined, sink: Sink) => Sink {
  return (value: T | undefined, sink: Sink) => {
    writeBool(value != undefined, sink);

    if (value != undefined) {
      writeFunc(value, sink);
    }

    return sink;
  };
}

function readOption<T>(
  readFunc: (sink: Sink) => T
): (sink: Sink) => T | undefined {
  return (sink: Sink) => {
    const some = readBool(sink);
    if (!some) {
      return undefined;
    }

    return readFunc(sink);
  };
}

function writeSeq<T>(
  writeFunc: (value: T, sink: Sink) => Sink
): (value: Array<T>, sink: Sink) => Sink {
  return (value: Array<T>, sink: Sink) => {
    writeU64(BigInt(value.length), sink);

    for (const each of value) {
      writeFunc(each, sink);
    }

    return sink;
  };
}

function readSeq<T>(readFunc: (sink: Sink) => T): (sink: Sink) => Array<T> {
  return (sink: Sink) => {
    const length = readU64(sink);
    const output = new Array();

    for (var i = 0; i < length; i++) {
      output.push(readFunc(sink));
    }

    return output;
  };
}

type TypedArray = ArrayLike<any> & {
  BYTES_PER_ELEMENT: number;
  readonly length: number;
  readonly buffer: ArrayBuffer;
  readonly byteLength: number;
  readonly byteOffset: number;
};

type TypedArrayConstructor<T> = {
  new (buffer: ArrayBuffer, offset?: number, length?: number): T;
  BYTES_PER_ELEMENT: number;
};

function writeTypedArray<T extends TypedArray>(value: T, sink: Sink): Sink {
  writeU64(BigInt(value.length), sink);

  writeBytes(
    new Uint8Array(value.buffer.slice(value.byteOffset, value.byteLength)),
    sink
  );

  return sink;
}

function readTypedArray<T extends TypedArray>(
  array_type: TypedArrayConstructor<T>
): (sink: Sink) => T {
  return (sink: Sink) => {
    const length = readU64(sink);
    const bytes = readBytes(sink, Number(length) * array_type.BYTES_PER_ELEMENT);
    return new array_type(bytes.buffer, 0, Number(length));
  };
}

function writeMap<TK, TV>(
  writeKeyFunc: (value: TK, sink: Sink) => Sink,
  writeValueFunc: (value: TV, sink: Sink) => Sink
): (value: Map<TK, TV>, sink: Sink) => Sink {
  return (value: Map<TK, TV>, sink: Sink) => {
    writeU64(BigInt(value.size), sink);

    value.forEach((v, k) => {
      writeKeyFunc(k, sink);
      writeValueFunc(v, sink);
    });

    return sink;
  };
}

function readMap<TK, TV>(
  readKeyFunc: (sink: Sink) => TK,
  readValueFunc: (sink: Sink) => TV
): (sink: Sink) => Map<TK, TV> {
  return (sink: Sink) => {
    const length = readU64(sink);
    const output = new Map();

    for (var i = 0; i < length; i++) {
      output.set(readKeyFunc(sink), readValueFunc(sink));
    }

    return output;
  };
}

function writeTuple<T extends any[]>(
  ...writeFns: Array<(value: any, sink: Sink) => Sink>
): (value: T, sink: Sink) => Sink {
  return (value: T, sink: Sink) => {
    for (let i = 0; i < writeFns.length; i++) {
      writeFns[i](value[i], sink);
    }

    return sink;
  };
}

function readTuple<T extends any[]>(
  ...readFns: Array<(sink: Sink) => any>
): (sink: Sink) => T {
  return (sink: Sink) => {
    const out = new Array();

    for (const readFn of readFns) {
      out.push(readFn(sink));
    }

    return out as T;
  };
}
