# src/Util/Binary/Decoder.js

    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())
      }
    }