import ID from '../ID/ID.js'
import { default as RootID, RootFakeUserID } from '../ID/RootID.js'

/**
 * A BinaryDecoder handles the decoding of an ArrayBuffer.
 */
export default class BinaryDecoder {
  /**
   * @param {Uint8Array|Buffer} buffer The binary data that this instance
   *                                   decodes.
   */
  constructor (buffer) {
    if (buffer instanceof ArrayBuffer) {
      this.uint8arr = new Uint8Array(buffer)
    } else if (
      buffer instanceof Uint8Array ||
      (
        typeof Buffer !== 'undefined' && buffer instanceof Buffer
      )
    ) {
      this.uint8arr = buffer
    } else {
      throw new Error('Expected an ArrayBuffer or Uint8Array!')
    }
    this.pos = 0
  }

  /**
   * Clone this decoder instance.
   * Optionally set a new position parameter.
   */
  clone (newPos = this.pos) {
    let decoder = new BinaryDecoder(this.uint8arr)
    decoder.pos = newPos
    return decoder
  }

  /**
   * Number of bytes.
   */
  get length () {
    return this.uint8arr.length
  }

  /**
   * Skip one byte, jump to the next position.
   */
  skip8 () {
    this.pos++
  }

  /**
   * Read one byte as unsigned integer.
   */
  readUint8 () {
    return this.uint8arr[this.pos++]
  }

  /**
   * Read 4 bytes as unsigned integer.
   *
   * @return {number} An unsigned integer.
   */
  readUint32 () {
    let uint =
      this.uint8arr[this.pos] +
      (this.uint8arr[this.pos + 1] << 8) +
      (this.uint8arr[this.pos + 2] << 16) +
      (this.uint8arr[this.pos + 3] << 24)
    this.pos += 4
    return uint
  }

  /**
   * Look ahead without incrementing position.
   * to the next byte and read it as unsigned integer.
   *
   * @return {number} An unsigned integer.
   */
  peekUint8 () {
    return this.uint8arr[this.pos]
  }

  /**
   * Read unsigned integer (32bit) with variable length.
   * 1/8th of the storage is used as encoding overhead.
   *  * numbers < 2^7 is stored in one byte.
   *  * numbers < 2^14 is stored in two bytes.
   *
   * @return {number} An unsigned integer.
   */
  readVarUint () {
    let num = 0
    let len = 0
    while (true) {
      let r = this.uint8arr[this.pos++]
      num = num | ((r & 0b1111111) << len)
      len += 7
      if (r < 1 << 7) {
        return num >>> 0 // return unsigned number!
      }
      if (len > 35) {
        throw new Error('Integer out of range!')
      }
    }
  }

  /**
   * Read string of variable length
   * * varUint is used to store the length of the string
   *
   * @return {String} The read String.
   */
  readVarString () {
    let len = this.readVarUint()
    let bytes = new Array(len)
    for (let i = 0; i < len; i++) {
      bytes[i] = this.uint8arr[this.pos++]
    }
    let encodedString = bytes.map(b => String.fromCodePoint(b)).join('')
    return decodeURIComponent(escape(encodedString))
  }

  /**
   * Look ahead and read varString without incrementing position
   */
  peekVarString () {
    let pos = this.pos
    let s = this.readVarString()
    this.pos = pos
    return s
  }

  /**
   * Read ID.
   * * If first varUint read is 0xFFFFFF a RootID is returned.
   * * Otherwise an ID is returned.
   *
   * @return ID
   */
  readID () {
    let user = this.readVarUint()
    if (user === RootFakeUserID) {
      // read property name and type id
      const rid = new RootID(this.readVarString(), null)
      rid.type = this.readVarUint()
      return rid
    }
    return new ID(user, this.readVarUint())
  }
}
