const hexLookupTable = ['00','01','02','03','04','05','06','07','08','09','0a','0b','0c','0d','0e','0f','10','11','12','13','14','15','16','17','18','19','1a','1b','1c','1d','1e','1f','20','21','22','23','24','25','26','27','28','29','2a','2b','2c','2d','2e','2f','30','31','32','33','34','35','36','37','38','39','3a','3b','3c','3d','3e','3f','40','41','42','43','44','45','46','47','48','49','4a','4b','4c','4d','4e','4f','50','51','52','53','54','55','56','57','58','59','5a','5b','5c','5d','5e','5f','60','61','62','63','64','65','66','67','68','69','6a','6b','6c','6d','6e','6f','70','71','72','73','74','75','76','77','78','79','7a','7b','7c','7d','7e','7f','80','81','82','83','84','85','86','87','88','89','8a','8b','8c','8d','8e','8f','90','91','92','93','94','95','96','97','98','99','9a','9b','9c','9d','9e','9f','a0','a1','a2','a3','a4','a5','a6','a7','a8','a9','aa','ab','ac','ad','ae','af','b0','b1','b2','b3','b4','b5','b6','b7','b8','b9','ba','bb','bc','bd','be','bf','c0','c1','c2','c3','c4','c5','c6','c7','c8','c9','ca','cb','cc','cd','ce','cf','d0','d1','d2','d3','d4','d5','d6','d7','d8','d9','da','db','dc','dd','de','df','e0','e1','e2','e3','e4','e5','e6','e7','e8','e9','ea','eb','ec','ed','ee','ef','f0','f1','f2','f3','f4','f5','f6','f7','f8','f9','fa','fb','fc','fd','fe','ff']
  
export class Bitray extends Uint8Array {

  public binary: Uint8Array

  constructor(data: string, encoding: string) {

    let binary: Uint8Array

    if (typeof encoding !== 'string' || encoding === '') {

      encoding = 'utf8'

    }

    if (encoding === 'utf8' || encoding === 'utf-8') {

      binary = Uint8Array.wrap(String.UTF8.encode(data))

    } else if (['latin1', 'binary'].includes(encoding)) {
        
      binary = latinDecode(data)

    } else if (encoding === 'hex') {
        
      binary = hexDecode(data)

    } else if (['ucs2', 'ucs-2', 'utf16le', 'utf-16le'].includes(encoding)) {

     binary = Uint8Array.wrap(String.UTF16.encode(data))

    } else if (encoding === 'base64') {

      binary = toByteArray(data)

    } else {

      throw new Error('Unknown Encoding Provided. Recieved Encoding ' + encoding + '')

    }

    super(binary.length)

    for (let i = 0; i < binary.length; i++) {
        
      super.fill(binary[i], i, i + 1)

    }

    this.binary = binary

  }

  toFormat(encoding: string): string {

    if (['utf-8', 'utf8'].includes(encoding)) {

      return String.UTF8.decode(this.binary.buffer)

    } else if (['binary', 'latin1'].includes(encoding)) {
      let ret = ''
      for (let i = 0; i < this.binary.length; ++i) {
        ret += String.fromCharCode(this.binary[i])
      }
      return ret
    } else if (['hex'].includes(encoding)) {

      let out = ''

      for (let i = 0; i < this.binary.byteLength; ++i) {

        out += hexLookupTable[this.binary[i]]

      }

      return out

    } else if (['ucs2', 'ucs-2', 'utf16le', 'utf-16le'].includes(encoding)) {

      return String.UTF16.decode(this.buffer)

    } else if (encoding === 'base64') {

      return fromByteArray(this.binary)

    } else {

      return ''

    }

  }

  static from<T>(array: T): Bitray {

    if (array instanceof Uint8Array) {

      const bit = new Bitray('', '')
  
      bit.binary = array
      
      return bit
      
    } else if (array instanceof ArrayBuffer) {

      const bit = new Bitray('', '')
  
      bit.binary = Uint8Array.wrap(array)
      
      return bit

    } else {

      const bit = new Bitray('', '')
  
      bit.binary = changetype<Uint8Array>(array)
      
      return bit
      
    }
  
  }

}

let lookup: Array<string> = []
let revLookup: Array<u32> = []
let code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
for (let i = 0, len = code.length; i < len; ++i) {
  lookup[i] = code.charAt(i)
  revLookup[code.charCodeAt(i)] = i
}
revLookup['-'.charCodeAt(0)] = 62
revLookup['_'.charCodeAt(0)] = 63

function getLens (b64: string): Uint8Array {
  let len = b64.length

  if (len % 4 > 0) {
    throw new Error('Invalid string. Length must be a multiple of 4')
  }
  let validLen = b64.indexOf('=')
  if (validLen === -1) validLen = len
  let placeHoldersLen = validLen === len
    ? 0
    : 4 - (validLen % 4)

  const uin8 = new Uint8Array(2)
  uin8[0] = validLen
  uin8[1] = placeHoldersLen

  return uin8
}

function toByteArray (b64: string): Uint8Array {
  let tmp: u32
  let lens = getLens(b64)
  let validLen = lens[0]
  let placeHoldersLen = lens[1]
  let arr = new Uint8Array(((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen)
  let curByte = 0
  let len = placeHoldersLen > 0
    ? validLen - 4
    : validLen
  let i: u32
  for (i = 0; i < len; i += 4) {
    tmp =
      (revLookup[b64.charCodeAt(i)] << 18) |
      (revLookup[b64.charCodeAt(i + 1)] << 12) |
      (revLookup[b64.charCodeAt(i + 2)] << 6) |
      revLookup[b64.charCodeAt(i + 3)]
    arr[curByte++] = (tmp >> 16) & 0xFF
    arr[curByte++] = (tmp >> 8) & 0xFF
    arr[curByte++] = tmp & 0xFF
  }
  if (placeHoldersLen === 2) {
    tmp =
      (revLookup[b64.charCodeAt(i)] << 2) |
      (revLookup[b64.charCodeAt(i + 1)] >> 4)
    arr[curByte++] = tmp & 0xFF
  }
  if (placeHoldersLen === 1) {
    tmp =
      (revLookup[b64.charCodeAt(i)] << 10) |
      (revLookup[b64.charCodeAt(i + 1)] << 4) |
      (revLookup[b64.charCodeAt(i + 2)] >> 2)
    arr[curByte++] = (tmp >> 8) & 0xFF
    arr[curByte++] = tmp & 0xFF
  }
  return arr
}

function tripletToBase64 (num: u32): string {
  return lookup[num >> 18 & 0x3F] +
    lookup[num >> 12 & 0x3F] +
    lookup[num >> 6 & 0x3F] +
    lookup[num & 0x3F]
}

function encodeChunk (uint8: Uint8Array, start: u32, end: number): string {
  let tmp: u32
  let output: Array<string> = []
  for (let i: u32 = start; i < end; i += 3) {
    tmp =
      ((u32(uint8[i]) << 16) & 0xFF0000) +
      ((u32(uint8[i + 1]) << 8) & 0xFF00) +
      (u32(uint8[i + 2]) & 0xFF)
    output.push(tripletToBase64(tmp))
  }
  return output.join('')
}

function fromByteArray (uint8: Uint8Array): string {
  let tmp: u32
  let len = uint8.length
  let extraBytes = len % 3
  let parts: Array<string> = []
  let maxChunkLength = 16383
  for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
    parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength)))
  }
  if (extraBytes === 1) {
    tmp = uint8[len - 1]
    parts.push(
      lookup[tmp >> 2] +
      lookup[(tmp << 4) & 0x3F] +
      '=='
    )
  } else if (extraBytes === 2) {
    tmp = (uint8[len - 2] << 8) + uint8[len - 1]
    parts.push(
      lookup[tmp >> 10] +
      lookup[(tmp >> 4) & 0x3F] +
      lookup[(tmp << 2) & 0x3F] +
      '='
    )
  }
  return parts.join('')
}

function hexDecode (str: string): Uint8Array {
  const byteArray = new Uint8Array(str.length >>> 1)
  const strArray = str.split('')
  let pos = 0
  for (let i = 0; i < str.length / 2; ++i) {
      let hex = '' + strArray[pos] + '' + strArray[pos + 1] + ''
      byteArray[i] = u32(parseInt('0x' + hex + '', 16))
      pos = pos + 2
  }
  return byteArray
}
function latinDecode (str: string): Uint8Array {
  const byteArray = new Uint8Array(str.length)
  for (let i = 0; i < str.length; ++i) {
    byteArray[i] = str.charCodeAt(i)
  }
  return byteArray
}