/**
 * @module encoding
 */
import * as globals from './globals.js'

const bits7 = 0b1111111
const bits8 = 0b11111111

/**
 * A BinaryEncoder handles the encoding to an ArrayBuffer.
 */
export class Encoder {
  constructor () {
    this.cpos = 0
    this.cbuf = globals.createUint8ArrayFromLen(1000)
    this.bufs = []
  }
}

/**
 * @function
 * @return {Encoder}
 */
export const createEncoder = () => new Encoder()

/**
 * The current length of the encoded data.
 *
 * @function
 * @param {Encoder} encoder
 * @return {number}
 */
export const length = encoder => {
  let len = encoder.cpos
  for (let i = 0; i < encoder.bufs.length; i++) {
    len += encoder.bufs[i].length
  }
  return len
}

/**
 * Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
 *
 * @function
 * @param {Encoder} encoder
 * @return {ArrayBuffer} The created ArrayBuffer.
 */
export const toBuffer = encoder => {
  const uint8arr = globals.createUint8ArrayFromLen(length(encoder))
  let curPos = 0
  for (let i = 0; i < encoder.bufs.length; i++) {
    let d = encoder.bufs[i]
    uint8arr.set(d, curPos)
    curPos += d.length
  }
  uint8arr.set(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos), curPos)
  return uint8arr.buffer
}

/**
 * Write one byte to the encoder.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} num The byte that is to be encoded.
 */
export const write = (encoder, num) => {
  if (encoder.cpos === encoder.cbuf.length) {
    encoder.bufs.push(encoder.cbuf)
    encoder.cbuf = globals.createUint8ArrayFromLen(encoder.cbuf.length * 2)
    encoder.cpos = 0
  }
  encoder.cbuf[encoder.cpos++] = num
}

/**
 * Write one byte at a specific position.
 * Position must already be written (i.e. encoder.length > pos)
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} pos Position to which to write data
 * @param {number} num Unsigned 8-bit integer
 */
export const set = (encoder, pos, num) => {
  let buffer = null
  // iterate all buffers and adjust position
  for (let i = 0; i < encoder.bufs.length && buffer === null; i++) {
    const b = encoder.bufs[i]
    if (pos < b.length) {
      buffer = b // found buffer
    } else {
      pos -= b.length
    }
  }
  if (buffer === null) {
    // use current buffer
    buffer = encoder.cbuf
  }
  buffer[pos] = num
}

/**
 * Write one byte as an unsigned integer.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} num The number that is to be encoded.
 */
export const writeUint8 = (encoder, num) => write(encoder, num & bits8)

/**
 * Write one byte as an unsigned Integer at a specific location.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} pos The location where the data will be written.
 * @param {number} num The number that is to be encoded.
 */
export const setUint8 = (encoder, pos, num) => set(encoder, pos, num & bits8)

/**
 * Write two bytes as an unsigned integer.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} num The number that is to be encoded.
 */
export const writeUint16 = (encoder, num) => {
  write(encoder, num & bits8)
  write(encoder, (num >>> 8) & bits8)
}
/**
 * Write two bytes as an unsigned integer at a specific location.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} pos The location where the data will be written.
 * @param {number} num The number that is to be encoded.
 */
export const setUint16 = (encoder, pos, num) => {
  set(encoder, pos, num & bits8)
  set(encoder, pos + 1, (num >>> 8) & bits8)
}

/**
 * Write two bytes as an unsigned integer
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} num The number that is to be encoded.
 */
export const writeUint32 = (encoder, num) => {
  for (let i = 0; i < 4; i++) {
    write(encoder, num & bits8)
    num >>>= 8
  }
}

/**
 * Write two bytes as an unsigned integer at a specific location.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} pos The location where the data will be written.
 * @param {number} num The number that is to be encoded.
 */
export const setUint32 = (encoder, pos, num) => {
  for (let i = 0; i < 4; i++) {
    set(encoder, pos + i, num & bits8)
    num >>>= 8
  }
}

/**
 * Write a variable length unsigned integer.
 *
 * Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} num The number that is to be encoded.
 */
export const writeVarUint = (encoder, num) => {
  while (num >= 0b10000000) {
    write(encoder, 0b10000000 | (bits7 & num))
    num >>>= 7
  }
  write(encoder, bits7 & num)
}

/**
 * Write a variable length string.
 *
 * @function
 * @param {Encoder} encoder
 * @param {String} str The string that is to be encoded.
 */
export const writeVarString = (encoder, str) => {
  const encodedString = unescape(encodeURIComponent(str))
  const len = encodedString.length
  writeVarUint(encoder, len)
  for (let i = 0; i < len; i++) {
    write(encoder, encodedString.codePointAt(i))
  }
}

/**
 * Write the content of another Encoder.
 *
 * TODO: can be improved!
 *
 * @function
 * @param {Encoder} encoder The enUint8Arr
 * @param {Encoder} append The BinaryEncoder to be written.
 */
export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder, toBuffer(append))

/**
 * Append an arrayBuffer to the encoder.
 *
 * @function
 * @param {Encoder} encoder
 * @param {ArrayBuffer} arrayBuffer
 */
export const writeArrayBuffer = (encoder, arrayBuffer) => {
  const prevBufferLen = encoder.cbuf.length
  // TODO: Append to cbuf if possible
  encoder.bufs.push(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos))
  encoder.bufs.push(globals.createUint8ArrayFromArrayBuffer(arrayBuffer))
  encoder.cbuf = globals.createUint8ArrayFromLen(prevBufferLen)
  encoder.cpos = 0
}

/**
 * @function
 * @param {Encoder} encoder
 * @param {ArrayBuffer} arrayBuffer
 */
export const writePayload = (encoder, arrayBuffer) => {
  writeVarUint(encoder, arrayBuffer.byteLength)
  writeArrayBuffer(encoder, arrayBuffer)
}