function toByteArray(bytes) {
  if (!bytes) return []
  if (bytes instanceof ArrayBuffer) return Array.prototype.slice.call(new Uint8Array(bytes))
  if (ArrayBuffer.isView(bytes)) return Array.prototype.slice.call(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength))

  return Array.prototype.slice.call(bytes)
}

function bytesToBase64(bytes) {
  const source = toByteArray(bytes)
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
  let output = ''

  for (let index = 0; index < source.length; index += 3) {
    const first = source[index] & 0xFF
    const second = index + 1 < source.length ? source[index + 1] & 0xFF : 0
    const third = index + 2 < source.length ? source[index + 2] & 0xFF : 0
    const triple = (first << 16) | (second << 8) | third

    output += alphabet[(triple >> 18) & 0x3F]
    output += alphabet[(triple >> 12) & 0x3F]
    output += index + 1 < source.length ? alphabet[(triple >> 6) & 0x3F] : '='
    output += index + 2 < source.length ? alphabet[triple & 0x3F] : '='
  }

  return output
}

function bytesToBin(bytes) {
  return toByteArray(bytes).map((byte) => (byte & 0xFF).toString(2).padStart(8, '0')).join('')
}

function bytesToHex(bytes, separator = '') {
  return toByteArray(bytes).map((byte) => (byte & 0xFF).toString(16).toUpperCase().padStart(2, '0')).join(separator)
}

function readByte(bytes, offset) {
  if (!bytes) return 0
  if (bytes instanceof ArrayBuffer) return (new Uint8Array(bytes)[offset] || 0) & 0xFF

  return (bytes[offset] || 0) & 0xFF
}

function formatHexNumber(value, length = 2) {
  const numberValue = Math.max(0, Math.floor(Number(value) || 0))

  return numberValue.toString(16).toUpperCase().padStart(length, '0')
}

function readUint16BE(bytes = [], offset = 0) {
  return ((readByte(bytes, offset) << 8) | readByte(bytes, offset + 1)) & 0xFFFF
}

function readUint16LE(bytes = [], offset = 0) {
  return (readByte(bytes, offset) | (readByte(bytes, offset + 1) << 8)) & 0xFFFF
}

function readUint32BE(bytes = [], offset = 0) {
  return (
    (readByte(bytes, offset) * 0x1000000)
    + ((readByte(bytes, offset + 1) << 16) >>> 0)
    + ((readByte(bytes, offset + 2) << 8) >>> 0)
    + readByte(bytes, offset + 3)
  ) >>> 0
}

function readUint32LE(bytes = [], offset = 0) {
  return (
    readByte(bytes, offset)
    + ((readByte(bytes, offset + 1) << 8) >>> 0)
    + ((readByte(bytes, offset + 2) << 16) >>> 0)
    + (readByte(bytes, offset + 3) * 0x1000000)
  ) >>> 0
}

function splitUint16BE(value) {
  const numberValue = Number(value) & 0xFFFF

  return [(numberValue >> 8) & 0xFF, numberValue & 0xFF]
}

function splitUint16LE(value) {
  const numberValue = Number(value) & 0xFFFF

  return [numberValue & 0xFF, (numberValue >> 8) & 0xFF]
}

function splitUint32BE(value) {
  const numberValue = Number(value) >>> 0

  return [
    (numberValue >>> 24) & 0xFF,
    (numberValue >>> 16) & 0xFF,
    (numberValue >>> 8) & 0xFF,
    numberValue & 0xFF
  ]
}

function splitUint32LE(value) {
  const numberValue = Number(value) >>> 0

  return [
    numberValue & 0xFF,
    (numberValue >>> 8) & 0xFF,
    (numberValue >>> 16) & 0xFF,
    (numberValue >>> 24) & 0xFF
  ]
}

function bytesToAsciiText(bytes = []) {
  return String.fromCharCode.apply(null, trimTrailingNullBytes(bytes).map((byte) => byte & 0xFF))
}

function bytesToUtf8Text(bytes = []) {
  const trimmed = trimTrailingNullBytes(bytes)
  if (!trimmed.length) return ''

  let encoded = ''

  trimmed.forEach((byte) => {
    encoded += `%${(byte & 0xFF).toString(16).padStart(2, '0').toUpperCase()}`
  })

  try {
    return decodeURIComponent(encoded)
  } catch (error) {
    return bytesToAsciiText(trimmed)
  }
}

function decodeBytesWithTextDecoder(bytes = [], encoding) {
  if (typeof TextDecoder === 'undefined') return null

  try {
    const decoder = new TextDecoder(encoding, { fatal: true })

    return decoder.decode(new Uint8Array(bytes))
  } catch (error) {
    return null
  }
}

function decodeUtf8Strict(bytes = []) {
  if (!bytes.length) return ''

  let encoded = ''

  bytes.forEach((byte) => {
    encoded += `%${(byte & 0xFF).toString(16).padStart(2, '0').toUpperCase()}`
  })

  try {
    return decodeURIComponent(encoded)
  } catch (error) {
    return null
  }
}

function bytesToAutoText(bytes = []) {
  const trimmed = trimTrailingNullBytes(bytes)
  if (!trimmed.length) return ''

  const utf8Text = decodeBytesWithTextDecoder(trimmed, 'utf-8')
    || decodeUtf8Strict(trimmed)
  if (utf8Text !== null) return utf8Text

  const gbkText = decodeBytesWithTextDecoder(trimmed, 'gbk')
    || decodeBytesWithTextDecoder(trimmed, 'gb18030')
  if (gbkText !== null) return gbkText

  return bytesToAsciiText(trimmed)
}

function formatBytes(byteLength) {
  const length = Number(byteLength) || 0
  if (length >= 1024 && length % 1024 === 0) return `${length / 1024} KB`
  if (length >= 1024) return `${(length / 1024).toFixed(2)} KB`

  return `${length} bytes`
}

function bytesToWords(bytes = []) {
  const words = []

  for (let index = 0; index + 1 < bytes.length; index += 2) {
    const highByte = bytes[index] || 0
    const lowByte = bytes[index + 1] || 0
    words.push(((highByte << 8) | lowByte) & 0xFFFF)
  }

  return words
}

function bytesToWordsLE(bytes = []) {
  const words = []

  for (let index = 0; index + 1 < bytes.length; index += 2) {
    const lowByte = bytes[index] || 0
    const highByte = bytes[index + 1] || 0
    words.push(((highByte << 8) | lowByte) & 0xFFFF)
  }

  return words
}

function getByteFromWord(word, byteOffset = 0) {
  const value = Number(word) & 0xFFFF

  return byteOffset === 0 ? ((value >> 8) & 0xFF) : (value & 0xFF)
}

function stringToUtf8Bytes(text) {
  const bytes = []
  const encoded = encodeURIComponent(String(text || ''))

  for (let index = 0; index < encoded.length; index += 1) {
    const char = encoded[index]
    if (char === '%') {
      bytes.push(parseInt(encoded.slice(index + 1, index + 3), 16) & 0xFF)
      index += 2
    } else {
      bytes.push(char.charCodeAt(0) & 0xFF)
    }
  }

  return bytes
}

function trimTrailingNullBytes(bytes = []) {
  let end = bytes.length

  while (end > 0 && bytes[end - 1] === 0x00) {
    end -= 1
  }

  return bytes.slice(0, end)
}

function wordsToBytes(words = [], byteLength = words.length * 2) {
  const bytes = []

  for (let index = 0; index < words.length; index += 1) {
    const word = Number(words[index]) & 0xFFFF
    bytes.push((word >> 8) & 0xFF, word & 0xFF)
  }

  return bytes.slice(0, Math.max(0, byteLength))
}

function wordsToBytesLE(words = [], byteLength = words.length * 2) {
  const bytes = []

  for (let index = 0; index < words.length; index += 1) {
    const word = Number(words[index]) & 0xFFFF
    bytes.push(word & 0xFF, (word >> 8) & 0xFF)
  }

  return bytes.slice(0, Math.max(0, byteLength))
}

module.exports = {
  bytesToAutoText,
  bytesToBase64,
  bytesToBin,
  bytesToHex,
  bytesToAsciiText,
  bytesToUtf8Text,
  bytesToWords,
  bytesToWordsLE,
  formatBytes,
  formatHexNumber,
  getByteFromWord,
  readByte,
  readUint16BE,
  readUint16LE,
  readUint32BE,
  readUint32LE,
  splitUint16BE,
  splitUint16LE,
  splitUint32BE,
  splitUint32LE,
  stringToUtf8Bytes,
  toByteArray,
  trimTrailingNullBytes,
  wordsToBytes,
  wordsToBytesLE
}
