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

export default class BufferReader {

  public buf: Uint8Array;
  public pos: number;

  constructor(buf?: Uint8Array | string | BufferParams) {
    this.buf = new Uint8Array();
    this.pos = 0;

    if (buf == null) {
      return;
    }
    
    if (BufferUtils.isBuffer(buf)) {
      this.set({ buf: buf });
    } else if (typeof buf === 'string') {
      let b = BufferUtils.hexToBuffer(buf);
      if (b.length * 2 != buf.length)  {
        throw new TypeError('Invalid hex string');
      }
      this.set({ buf: b });
    } else if (typeof buf === 'object') {
      let obj = buf;
      this.set(obj);
    } else {
      throw new TypeError('Unrecognized argument for BufferReader');
    }
  }

  public set(obj: BufferParams): this {
    this.buf = obj.buf || this.buf;
    this.pos = obj.pos || this.pos || 0;
    return this;
  }

  public eof(): boolean {
    return this.pos >= this.buf.length;
  }
  
  public finished = this.eof;

  public read(len: number): Uint8Array {
    ValidationUtils.validateArgument(len != null, 'Must specify a length');
    let buf = this.buf.subarray(this.pos, this.pos + len);
    this.pos = this.pos + len;
    return buf;
  }
  
  public readAll(): Uint8Array {
    let buf = this.buf.subarray(this.pos, this.buf.length);
    this.pos = this.buf.length;
    return buf;
  }

  public readUInt8(): number {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getUint8(this.pos);
    this.pos += 1;
    return val;
  }

  public readUInt16BE(): number {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getUint16(this.pos);
    this.pos += 2;
    return val;
  }

  public readUInt16LE(): number {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getUint16(this.pos, true);
    this.pos += 2;
    return val;
  }
  
  public readUInt32BE(): number {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getUint32(this.pos);
    this.pos += 4;
    return val;
  }

  public readUInt32LE(): number {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getUint32(this.pos, true);
    this.pos += 4;
    return val;
  }
  
  public readInt32LE(): number {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getInt32(this.pos, true);
    this.pos += 4;
    return val;
  }

  public readUInt64BEBN(): BN {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getBigUint64(this.pos);
    this.pos += 8;
    return BN.fromBigInt(val);
  }

  public readUInt64LEBN(): BN {
    const view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
    const val = view.getBigUint64(this.pos, true); 
    this.pos += 8;
    return BN.fromBigInt(val);
  }
  
  public readVarintNum(): number {
    let first = this.readUInt8();
    switch (first) {
      case 0xFD:
        return this.readUInt16LE();
      case 0xFE:
        return this.readUInt32LE();
      case 0xFF:
        let bn = this.readUInt64LEBN();
        let n = bn.toNumber();
        if (n <= Math.pow(2, 53)) {
          return n;
        } else {
          throw new Error('number too large to retain precision - use readVarintBN');
        }
    }
    return first;
  }
  
  /**
   * reads a length prepended buffer
   */
  public readVarLengthBuffer(): Uint8Array {
    let len = this.readVarintNum();
    let buf = this.read(len);
    ValidationUtils.validateState(buf.length === len, 'Invalid length while reading varlength buffer. ' +
      'Expected to read: ' + len + ' and read ' + buf.length);
    return buf;
  }
  
  public readVarintBuf(): Uint8Array {
    let first = this.buf[this.pos];
    switch (first) {
      case 0xFD:
        return this.read(1 + 2);
      case 0xFE:
        return this.read(1 + 4);
      case 0xFF:
        return this.read(1 + 8);
      default:
        return this.read(1);
    }
  }
  
  public readVarintBN(): BN {
    let first = this.readUInt8();
    switch (first) {
      case 0xFD:
        return new BN(this.readUInt16LE());
      case 0xFE:
        return new BN(this.readUInt32LE());
      case 0xFF:
        return this.readUInt64LEBN();
      default:
        return new BN(first);
    }
  }
  
  public reverse(): this {
    let buf = BufferUtils.reverse(this.buf);
    this.buf = buf;
    return this;
  }

  public readReverse(len?: number): Uint8Array {
    if (len == null) {
      len = this.buf.length;
    }
    let buf = this.buf.subarray(this.pos, this.pos + len);
    this.pos = this.pos + len;
    return BufferUtils.reverse(buf);
  };
  
  public readCoreVarintNum(): number {
    let n = 0;
    while (true) {
      let chData = this.readUInt8();
      n = (n << 7) | (chData & 0x7F);
      if (chData & 0x80) {
        n++;
      } else {
        return n;
      }
    }
  }
}