import type { BufferWriterOptions } from "../common/interfaces";
import type BN from "../crypto/bn.extension";
import BufferUtils from "../utils/buffer.utils";
import ValidationUtils from "../utils/validation.utils";

export default class BufferWriter {

  private bufs: Uint8Array[];
  private bufLen: number;
  
  constructor(obj?: BufferWriterOptions) {
    this.bufs = [];
    this.bufLen = 0;
    
    if (obj) {
      this.set(obj); 
    }
  }

  public set(obj: BufferWriterOptions): this {
    this.bufs = obj.bufs || this.bufs;
    this.bufLen = this.bufs.reduce((prev, buf) => prev + buf.length, 0);
    return this;
  }

  public toBuffer(): Uint8Array {
    return this.concat();
  }
  
  public concat(): Uint8Array {
    return BufferUtils.concat(this.bufs, this.bufLen);
  }

  public write(buf: Uint8Array): this {
    ValidationUtils.validateArgument(BufferUtils.isBuffer(buf), "buf");
    this.bufs.push(buf);
    this.bufLen += buf.length;
    return this;
  }

  public writeReverse(buf: Uint8Array): this {
    ValidationUtils.validateArgument(BufferUtils.isBuffer(buf), "buf");
    this.bufs.push(BufferUtils.reverse(buf));
    this.bufLen += buf.length;
    return this;
  }

  public writeUInt8(n: number): this {
    const buf = new Uint8Array(1);
    new DataView(buf.buffer).setUint8(0, n);
    this.write(buf);
    return this;
  }

  public writeUInt16BE(n: number): this {
    const buf = new Uint8Array(2);
    new DataView(buf.buffer).setUint16(0, n);
    this.write(buf);
    return this;
  }

  public writeUInt16LE(n: number): this {
    const buf = new Uint8Array(2);
    new DataView(buf.buffer).setUint16(0, n, true);
    this.write(buf);
    return this;
  }

  public writeUInt32BE(n: number): this {
    const buf = new Uint8Array(4);
    new DataView(buf.buffer).setUint32(0, n);
    this.write(buf);
    return this;
  }

  public writeInt32LE(n: number): this {
    const buf = new Uint8Array(4);
    new DataView(buf.buffer).setInt32(0, n, true);
    this.write(buf);
    return this;
  }

  public writeUInt32LE(n: number): this {
    const buf = new Uint8Array(4);
    new DataView(buf.buffer).setUint32(0, n, true);
    this.write(buf);
    return this;
  }

  public writeUInt64BEBN(bn: BN): this {
    let buf = bn.toByteArray({size: 8});
    this.write(buf);
    return this;
  }

  public writeUInt64LEBN(bn: BN): this {
    let buf = bn.toByteArray({size: 8});
    this.writeReverse(buf);
    return this;
  }

  public writeVarintNum(n: number): this {
    let buf = BufferWriter.varintBufNum(n);
    this.write(buf);
    return this;
  }

  public writeVarintBN(bn: BN): this {
    let buf = BufferWriter.varintBufBN(bn);
    this.write(buf);
    return this;
  }

  public writeVarLengthBuf(buf: Uint8Array): this {
    ValidationUtils.validateArgument(BufferUtils.isBuffer(buf), "buf");
    this.writeVarintNum(buf.length);
    this.write(buf);
    return this;
  }

  public writeCoreVarintNum(n: number): this {
    let tmp = [];
    let len = 0;

    while (true)
    {
      tmp.push((n & 0x7F) | (len ? 0x80 : 0x00));
      if (n <= 0x7F)
          break;
      n = (n >> 7) - 1;
      len++;
    }

    this.write(Uint8Array.from(tmp).reverse());
    return this;
  }

  public static varintBufNum(n: number): Uint8Array {
    let buf = undefined;
    if (n < 253) {
      buf = new Uint8Array(1);
      buf[0] = n & 0xff;
    } else if (n < 0x10000) {
      buf = new Uint8Array(1 + 2);
      const view = new DataView(buf.buffer);
      buf[0] = 253;
      view.setUint16(1, n, true);
    } else if (n < 0x100000000) {
      buf = new Uint8Array(1 + 4);
      const view = new DataView(buf.buffer);
      buf[0] = 254;
      view.setUint32(1, n, true);
    } else {
      buf = new Uint8Array(1 + 8);
      const view = new DataView(buf.buffer);
      buf[0] = 255;
      view.setBigUint64(1, BigInt(n), true);
    }
    return buf;
  }

  public static varintBufBN(bn: BN): Uint8Array {
    let buf = undefined;
    let n = bn.toNumber();
    if (n < 253) {
      buf = new Uint8Array(1);
      buf[0] = n & 0xff;
    } else if (n < 0x10000) {
      buf = new Uint8Array(1 + 2);
      const view = new DataView(buf.buffer);
      buf[0] = 253;
      view.setUint16(1, n, true);
    } else if (n < 0x100000000) {
      buf = new Uint8Array(1 + 4);
      const view = new DataView(buf.buffer);
      buf[0] = 254;
      view.setUint32(1, n, true);
    } else {
      buf = new Uint8Array(1 + 8);
      const view = new DataView(buf.buffer);
      buf[0] = 255;
      view.setBigUint64(1, bn.toBigInt(), true);
    }
    return buf;
  }
}