const {
  bytesToBase64,
  bytesToBin,
  bytesToHex,
  stringToUtf8Bytes,
  toByteArray
} = require('../../utils/binary-utils.js')

const MASK_64 = 0xFFFFFFFFFFFFFFFFn

const HASH_ALGORITHM_PRESETS = [
  { key: 'md5', label: 'MD5', kind: 'hash', hash: 'md5' },
  { key: 'sha1', label: 'SHA1', kind: 'hash', hash: 'sha1' },
  { key: 'sha224', label: 'SHA224', kind: 'hash', hash: 'sha224' },
  { key: 'sha256', label: 'SHA256', kind: 'hash', hash: 'sha256' },
  { key: 'sha384', label: 'SHA384', kind: 'hash', hash: 'sha384' },
  { key: 'sha512', label: 'SHA512', kind: 'hash', hash: 'sha512' },
  { key: 'hmac-md5', label: 'HMAC-MD5', kind: 'hmac', hash: 'md5' },
  { key: 'hmac-sha1', label: 'HMAC-SHA1', kind: 'hmac', hash: 'sha1' },
  { key: 'hmac-sha224', label: 'HMAC-SHA224', kind: 'hmac', hash: 'sha224' },
  { key: 'hmac-sha256', label: 'HMAC-SHA256', kind: 'hmac', hash: 'sha256' },
  { key: 'hmac-sha384', label: 'HMAC-SHA384', kind: 'hmac', hash: 'sha384' },
  { key: 'hmac-sha512', label: 'HMAC-SHA512', kind: 'hmac', hash: 'sha512' },
  { key: 'pbkdf2', label: 'PBKDF2', kind: 'pbkdf2', hash: 'sha256' }
]

const SHA256_K = [
  0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5,
  0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
  0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,
  0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
  0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC,
  0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
  0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7,
  0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
  0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13,
  0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
  0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3,
  0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
  0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5,
  0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
  0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208,
  0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
]

const SHA512_K = [
  0x428A2F98D728AE22n, 0x7137449123EF65CDn, 0xB5C0FBCFEC4D3B2Fn, 0xE9B5DBA58189DBBCn,
  0x3956C25BF348B538n, 0x59F111F1B605D019n, 0x923F82A4AF194F9Bn, 0xAB1C5ED5DA6D8118n,
  0xD807AA98A3030242n, 0x12835B0145706FBEn, 0x243185BE4EE4B28Cn, 0x550C7DC3D5FFB4E2n,
  0x72BE5D74F27B896Fn, 0x80DEB1FE3B1696B1n, 0x9BDC06A725C71235n, 0xC19BF174CF692694n,
  0xE49B69C19EF14AD2n, 0xEFBE4786384F25E3n, 0x0FC19DC68B8CD5B5n, 0x240CA1CC77AC9C65n,
  0x2DE92C6F592B0275n, 0x4A7484AA6EA6E483n, 0x5CB0A9DCBD41FBD4n, 0x76F988DA831153B5n,
  0x983E5152EE66DFABn, 0xA831C66D2DB43210n, 0xB00327C898FB213Fn, 0xBF597FC7BEEF0EE4n,
  0xC6E00BF33DA88FC2n, 0xD5A79147930AA725n, 0x06CA6351E003826Fn, 0x142929670A0E6E70n,
  0x27B70A8546D22FFCn, 0x2E1B21385C26C926n, 0x4D2C6DFC5AC42AEDn, 0x53380D139D95B3DFn,
  0x650A73548BAF63DEn, 0x766A0ABB3C77B2A8n, 0x81C2C92E47EDAEE6n, 0x92722C851482353Bn,
  0xA2BFE8A14CF10364n, 0xA81A664BBC423001n, 0xC24B8B70D0F89791n, 0xC76C51A30654BE30n,
  0xD192E819D6EF5218n, 0xD69906245565A910n, 0xF40E35855771202An, 0x106AA07032BBD1B8n,
  0x19A4C116B8D2D0C8n, 0x1E376C085141AB53n, 0x2748774CDF8EEB99n, 0x34B0BCB5E19B48A8n,
  0x391C0CB3C5C95A63n, 0x4ED8AA4AE3418ACBn, 0x5B9CCA4F7763E373n, 0x682E6FF3D6B2B8A3n,
  0x748F82EE5DEFB2FCn, 0x78A5636F43172F60n, 0x84C87814A1F0AB72n, 0x8CC702081A6439ECn,
  0x90BEFFFA23631E28n, 0xA4506CEBDE82BDE9n, 0xBEF9A3F7B2C67915n, 0xC67178F2E372532Bn,
  0xCA273ECEEA26619Cn, 0xD186B8C721C0C207n, 0xEADA7DD6CDE0EB1En, 0xF57D4F7FEE6ED178n,
  0x06F067AA72176FBAn, 0x0A637DC5A2C898A6n, 0x113F9804BEF90DAEn, 0x1B710B35131C471Bn,
  0x28DB77F523047D84n, 0x32CAAB7B40C72493n, 0x3C9EBE0A15C9BEBCn, 0x431D67C49C100D4Cn,
  0x4CC5D4BECB3E42B6n, 0x597F299CFC657E2An, 0x5FCB6FAB3AD6FAECn, 0x6C44198C4A475817n
]

function add32(...values) {
  return values.reduce((sum, value) => (sum + (value >>> 0)) >>> 0, 0)
}

function rotl32(value, bits) {
  return ((value << bits) | (value >>> (32 - bits))) >>> 0
}

function rotr32(value, bits) {
  return ((value >>> bits) | (value << (32 - bits))) >>> 0
}

function writeWord32BE(value, output) {
  output.push((value >>> 24) & 0xFF, (value >>> 16) & 0xFF, (value >>> 8) & 0xFF, value & 0xFF)
}

function writeWord32LE(value, output) {
  output.push(value & 0xFF, (value >>> 8) & 0xFF, (value >>> 16) & 0xFF, (value >>> 24) & 0xFF)
}

function padBlock64BE(bytes) {
  const message = toByteArray(bytes)
  const bitLength = BigInt(message.length) * 8n
  const padded = message.slice()

  padded.push(0x80)
  while (padded.length % 64 !== 56) padded.push(0)
  for (let shift = 56; shift >= 0; shift -= 8) {
    padded.push(Number((bitLength >> BigInt(shift)) & 0xFFn))
  }

  return padded
}

function md5(bytes) {
  const shifts = [
    7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
    5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
    4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
    6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
  ]
  const constants = []
  const message = toByteArray(bytes)
  const bitLength = BigInt(message.length) * 8n
  const padded = message.slice()

  for (let index = 0; index < 64; index += 1) {
    constants[index] = Math.floor(Math.abs(Math.sin(index + 1)) * 0x100000000) >>> 0
  }

  padded.push(0x80)
  while (padded.length % 64 !== 56) padded.push(0)
  for (let shift = 0; shift < 64; shift += 8) {
    padded.push(Number((bitLength >> BigInt(shift)) & 0xFFn))
  }

  let a0 = 0x67452301
  let b0 = 0xEFCDAB89
  let c0 = 0x98BADCFE
  let d0 = 0x10325476

  for (let offset = 0; offset < padded.length; offset += 64) {
    const words = []
    for (let index = 0; index < 16; index += 1) {
      const base = offset + index * 4
      words[index] = (
        (padded[base] & 0xFF) |
        ((padded[base + 1] & 0xFF) << 8) |
        ((padded[base + 2] & 0xFF) << 16) |
        ((padded[base + 3] & 0xFF) << 24)
      ) >>> 0
    }

    let a = a0
    let b = b0
    let c = c0
    let d = d0

    for (let index = 0; index < 64; index += 1) {
      let f
      let g

      if (index < 16) {
        f = (b & c) | ((~b) & d)
        g = index
      } else if (index < 32) {
        f = (d & b) | ((~d) & c)
        g = (5 * index + 1) % 16
      } else if (index < 48) {
        f = b ^ c ^ d
        g = (3 * index + 5) % 16
      } else {
        f = c ^ (b | (~d))
        g = (7 * index) % 16
      }

      const next = d
      d = c
      c = b
      b = add32(b, rotl32(add32(a, f, constants[index], words[g]), shifts[index]))
      a = next
    }

    a0 = add32(a0, a)
    b0 = add32(b0, b)
    c0 = add32(c0, c)
    d0 = add32(d0, d)
  }

  const output = []
  ;[a0, b0, c0, d0].forEach((word) => writeWord32LE(word, output))
  return output
}

function sha1(bytes) {
  const padded = padBlock64BE(bytes)
  const words = []
  let h0 = 0x67452301
  let h1 = 0xEFCDAB89
  let h2 = 0x98BADCFE
  let h3 = 0x10325476
  let h4 = 0xC3D2E1F0

  for (let offset = 0; offset < padded.length; offset += 64) {
    for (let index = 0; index < 16; index += 1) {
      const base = offset + index * 4
      words[index] = (
        ((padded[base] & 0xFF) << 24) |
        ((padded[base + 1] & 0xFF) << 16) |
        ((padded[base + 2] & 0xFF) << 8) |
        (padded[base + 3] & 0xFF)
      ) >>> 0
    }
    for (let index = 16; index < 80; index += 1) {
      words[index] = rotl32(words[index - 3] ^ words[index - 8] ^ words[index - 14] ^ words[index - 16], 1)
    }

    let a = h0
    let b = h1
    let c = h2
    let d = h3
    let e = h4

    for (let index = 0; index < 80; index += 1) {
      let f
      let k

      if (index < 20) {
        f = (b & c) | ((~b) & d)
        k = 0x5A827999
      } else if (index < 40) {
        f = b ^ c ^ d
        k = 0x6ED9EBA1
      } else if (index < 60) {
        f = (b & c) | (b & d) | (c & d)
        k = 0x8F1BBCDC
      } else {
        f = b ^ c ^ d
        k = 0xCA62C1D6
      }

      const temp = add32(rotl32(a, 5), f, e, k, words[index])
      e = d
      d = c
      c = rotl32(b, 30)
      b = a
      a = temp
    }

    h0 = add32(h0, a)
    h1 = add32(h1, b)
    h2 = add32(h2, c)
    h3 = add32(h3, d)
    h4 = add32(h4, e)
  }

  const output = []
  ;[h0, h1, h2, h3, h4].forEach((word) => writeWord32BE(word, output))
  return output
}

function sha256Family(bytes, mode) {
  const padded = padBlock64BE(bytes)
  const words = []
  const hash = mode === 'sha224'
    ? [0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4]
    : [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]

  for (let offset = 0; offset < padded.length; offset += 64) {
    for (let index = 0; index < 16; index += 1) {
      const base = offset + index * 4
      words[index] = (
        ((padded[base] & 0xFF) << 24) |
        ((padded[base + 1] & 0xFF) << 16) |
        ((padded[base + 2] & 0xFF) << 8) |
        (padded[base + 3] & 0xFF)
      ) >>> 0
    }
    for (let index = 16; index < 64; index += 1) {
      const s0 = rotr32(words[index - 15], 7) ^ rotr32(words[index - 15], 18) ^ (words[index - 15] >>> 3)
      const s1 = rotr32(words[index - 2], 17) ^ rotr32(words[index - 2], 19) ^ (words[index - 2] >>> 10)
      words[index] = add32(words[index - 16], s0, words[index - 7], s1)
    }

    let a = hash[0]
    let b = hash[1]
    let c = hash[2]
    let d = hash[3]
    let e = hash[4]
    let f = hash[5]
    let g = hash[6]
    let h = hash[7]

    for (let index = 0; index < 64; index += 1) {
      const s1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25)
      const ch = (e & f) ^ ((~e) & g)
      const temp1 = add32(h, s1, ch, SHA256_K[index], words[index])
      const s0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22)
      const maj = (a & b) ^ (a & c) ^ (b & c)
      const temp2 = add32(s0, maj)

      h = g
      g = f
      f = e
      e = add32(d, temp1)
      d = c
      c = b
      b = a
      a = add32(temp1, temp2)
    }

    hash[0] = add32(hash[0], a)
    hash[1] = add32(hash[1], b)
    hash[2] = add32(hash[2], c)
    hash[3] = add32(hash[3], d)
    hash[4] = add32(hash[4], e)
    hash[5] = add32(hash[5], f)
    hash[6] = add32(hash[6], g)
    hash[7] = add32(hash[7], h)
  }

  const output = []
  hash.slice(0, mode === 'sha224' ? 7 : 8).forEach((word) => writeWord32BE(word, output))
  return output
}

function rotr64(value, bits) {
  const shift = BigInt(bits)
  return ((value >> shift) | (value << (64n - shift))) & MASK_64
}

function writeWord64BE(value, output) {
  for (let shift = 56; shift >= 0; shift -= 8) {
    output.push(Number((value >> BigInt(shift)) & 0xFFn))
  }
}

function readWord64BE(bytes, offset) {
  let value = 0n
  for (let index = 0; index < 8; index += 1) {
    value = (value << 8n) | BigInt(bytes[offset + index] & 0xFF)
  }

  return value
}

function padBlock128BE(bytes) {
  const message = toByteArray(bytes)
  const bitLength = BigInt(message.length) * 8n
  const padded = message.slice()

  padded.push(0x80)
  while (padded.length % 128 !== 112) padded.push(0)
  for (let shift = 120; shift >= 0; shift -= 8) {
    padded.push(Number((bitLength >> BigInt(shift)) & 0xFFn))
  }

  return padded
}

function sha512Family(bytes, mode) {
  const padded = padBlock128BE(bytes)
  const words = []
  const hash = mode === 'sha384'
    ? [
        0xCBBB9D5DC1059ED8n, 0x629A292A367CD507n, 0x9159015A3070DD17n, 0x152FECD8F70E5939n,
        0x67332667FFC00B31n, 0x8EB44A8768581511n, 0xDB0C2E0D64F98FA7n, 0x47B5481DBEFA4FA4n
      ]
    : [
        0x6A09E667F3BCC908n, 0xBB67AE8584CAA73Bn, 0x3C6EF372FE94F82Bn, 0xA54FF53A5F1D36F1n,
        0x510E527FADE682D1n, 0x9B05688C2B3E6C1Fn, 0x1F83D9ABFB41BD6Bn, 0x5BE0CD19137E2179n
      ]

  for (let offset = 0; offset < padded.length; offset += 128) {
    for (let index = 0; index < 16; index += 1) {
      words[index] = readWord64BE(padded, offset + index * 8)
    }
    for (let index = 16; index < 80; index += 1) {
      const s0 = rotr64(words[index - 15], 1) ^ rotr64(words[index - 15], 8) ^ (words[index - 15] >> 7n)
      const s1 = rotr64(words[index - 2], 19) ^ rotr64(words[index - 2], 61) ^ (words[index - 2] >> 6n)
      words[index] = (words[index - 16] + s0 + words[index - 7] + s1) & MASK_64
    }

    let a = hash[0]
    let b = hash[1]
    let c = hash[2]
    let d = hash[3]
    let e = hash[4]
    let f = hash[5]
    let g = hash[6]
    let h = hash[7]

    for (let index = 0; index < 80; index += 1) {
      const s1 = rotr64(e, 14) ^ rotr64(e, 18) ^ rotr64(e, 41)
      const ch = (e & f) ^ ((MASK_64 ^ e) & g)
      const temp1 = (h + s1 + ch + SHA512_K[index] + words[index]) & MASK_64
      const s0 = rotr64(a, 28) ^ rotr64(a, 34) ^ rotr64(a, 39)
      const maj = (a & b) ^ (a & c) ^ (b & c)
      const temp2 = (s0 + maj) & MASK_64

      h = g
      g = f
      f = e
      e = (d + temp1) & MASK_64
      d = c
      c = b
      b = a
      a = (temp1 + temp2) & MASK_64
    }

    hash[0] = (hash[0] + a) & MASK_64
    hash[1] = (hash[1] + b) & MASK_64
    hash[2] = (hash[2] + c) & MASK_64
    hash[3] = (hash[3] + d) & MASK_64
    hash[4] = (hash[4] + e) & MASK_64
    hash[5] = (hash[5] + f) & MASK_64
    hash[6] = (hash[6] + g) & MASK_64
    hash[7] = (hash[7] + h) & MASK_64
  }

  const output = []
  hash.slice(0, mode === 'sha384' ? 6 : 8).forEach((word) => writeWord64BE(word, output))
  return output
}

function digestBytes(hash, bytes) {
  if (hash === 'md5') return md5(bytes)
  if (hash === 'sha1') return sha1(bytes)
  if (hash === 'sha224') return sha256Family(bytes, 'sha224')
  if (hash === 'sha256') return sha256Family(bytes, 'sha256')
  if (hash === 'sha384') return sha512Family(bytes, 'sha384')
  if (hash === 'sha512') return sha512Family(bytes, 'sha512')

  throw new Error('不支持的哈希算法')
}

function getBlockSize(hash) {
  return hash === 'sha384' || hash === 'sha512' ? 128 : 64
}

function hmacBytes(hash, keyBytes, dataBytes) {
  const blockSize = getBlockSize(hash)
  let key = toByteArray(keyBytes)

  if (key.length > blockSize) {
    key = digestBytes(hash, key)
  }
  while (key.length < blockSize) key.push(0)

  const innerPad = []
  const outerPad = []
  for (let index = 0; index < blockSize; index += 1) {
    innerPad[index] = key[index] ^ 0x36
    outerPad[index] = key[index] ^ 0x5C
  }

  return digestBytes(hash, outerPad.concat(digestBytes(hash, innerPad.concat(toByteArray(dataBytes)))))
}

function parsePositiveInteger(value, label, fallback, maxValue) {
  const numberValue = Number(value || fallback)
  if (!Number.isInteger(numberValue) || numberValue <= 0) {
    throw new Error(`${label}需为正整数`)
  }
  if (numberValue > maxValue) {
    throw new Error(`${label}不能超过 ${maxValue}`)
  }

  return numberValue
}

function pbkdf2Bytes(hash, passwordBytes, saltBytes, iterations, outputLength) {
  const rounds = parsePositiveInteger(iterations, '迭代次数', 1000, 100000)
  const length = parsePositiveInteger(outputLength, '输出长度', 32, 4096)
  const hashLength = digestBytes(hash, []).length
  const blockCount = Math.ceil(length / hashLength)
  const output = []

  for (let blockIndex = 1; blockIndex <= blockCount; blockIndex += 1) {
    const blockIndexBytes = [
      (blockIndex >>> 24) & 0xFF,
      (blockIndex >>> 16) & 0xFF,
      (blockIndex >>> 8) & 0xFF,
      blockIndex & 0xFF
    ]
    let previous = hmacBytes(hash, passwordBytes, toByteArray(saltBytes).concat(blockIndexBytes))
    const block = previous.slice()

    for (let round = 1; round < rounds; round += 1) {
      previous = hmacBytes(hash, passwordBytes, previous)
      for (let index = 0; index < block.length; index += 1) {
        block[index] ^= previous[index]
      }
    }

    output.push(...block)
  }

  return output.slice(0, length)
}

function getPreset(key) {
  return HASH_ALGORITHM_PRESETS.find((preset) => preset.key === key) || HASH_ALGORITHM_PRESETS[0]
}

function calculateHash(bytes, config = {}) {
  const preset = getPreset(config.key)
  const dataBytes = toByteArray(bytes)
  let resultBytes

  if (preset.kind === 'hmac') {
    resultBytes = hmacBytes(preset.hash, stringToUtf8Bytes(config.hmacKey || ''), dataBytes)
  } else if (preset.kind === 'pbkdf2') {
    resultBytes = pbkdf2Bytes(
      preset.hash,
      dataBytes,
      stringToUtf8Bytes(config.pbkdf2Salt || ''),
      config.pbkdf2Iterations,
      config.pbkdf2Length
    )
  } else {
    resultBytes = digestBytes(preset.hash, dataBytes)
  }

  return {
    base64: bytesToBase64(resultBytes),
    bin: bytesToBin(resultBytes),
    bytes: resultBytes,
    hex: bytesToHex(resultBytes),
    width: resultBytes.length * 8
  }
}

module.exports = {
  HASH_ALGORITHM_PRESETS,
  bytesToBase64,
  bytesToBin,
  bytesToHex,
  calculateHash,
  digestBytes,
  hmacBytes,
  pbkdf2Bytes,
  stringToUtf8Bytes,
  toByteArray
}
