/* eslint-disable camelcase */
const b4a = require('b4a')
const argon2 = require('argon2hash-wasm')
const { randombytes_buf } = require('./randombytes')

const crypto_pwhash_argon2i_ALG_ARGON2I13 = 1
const crypto_pwhash_argon2i_BYTES_MIN = 16
const crypto_pwhash_argon2i_BYTES_MAX = 4294967295
const crypto_pwhash_argon2i_PASSWD_MIN = 0
const crypto_pwhash_argon2i_PASSWD_MAX = 4294967295
const crypto_pwhash_argon2i_SALTBYTES = 16
const crypto_pwhash_argon2i_STRBYTES = 128
const crypto_pwhash_argon2i_STRPREFIX = '$argon2i$'
const crypto_pwhash_argon2i_OPSLIMIT_MIN = 3
const crypto_pwhash_argon2i_OPSLIMIT_MAX = 4294967295
const crypto_pwhash_argon2i_MEMLIMIT_MIN = 8192
const crypto_pwhash_argon2i_MEMLIMIT_MAX = 2147483648
const crypto_pwhash_argon2i_OPSLIMIT_INTERACTIVE = 4
const crypto_pwhash_argon2i_MEMLIMIT_INTERACTIVE = 33554432
const crypto_pwhash_argon2i_OPSLIMIT_MODERATE = 6
const crypto_pwhash_argon2i_MEMLIMIT_MODERATE = 134217728
const crypto_pwhash_argon2i_OPSLIMIT_SENSITIVE = 8
const crypto_pwhash_argon2i_MEMLIMIT_SENSITIVE = 536870912

const crypto_pwhash_argon2id_ALG_ARGON2ID13 = 2
const crypto_pwhash_argon2id_BYTES_MIN = 16
const crypto_pwhash_argon2id_BYTES_MAX = 4294967295
const crypto_pwhash_argon2id_PASSWD_MIN = 0
const crypto_pwhash_argon2id_PASSWD_MAX = 4294967295
const crypto_pwhash_argon2id_SALTBYTES = 16
const crypto_pwhash_argon2id_STRBYTES = 128
const crypto_pwhash_argon2id_STRPREFIX = '$argon2id$'
const crypto_pwhash_argon2id_OPSLIMIT_MIN = 1
const crypto_pwhash_argon2id_OPSLIMIT_MAX = 4294967295
const crypto_pwhash_argon2id_MEMLIMIT_MIN = 8192
const crypto_pwhash_argon2id_MEMLIMIT_MAX = 2147483648
const crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE = 2
const crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE = 67108864
const crypto_pwhash_argon2id_OPSLIMIT_MODERATE = 3
const crypto_pwhash_argon2id_MEMLIMIT_MODERATE = 268435456
const crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE = 4
const crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE = 1073741824

const crypto_pwhash_ALG_ARGON2I13 = crypto_pwhash_argon2i_ALG_ARGON2I13
const crypto_pwhash_ALG_ARGON2ID13 = crypto_pwhash_argon2id_ALG_ARGON2ID13
const crypto_pwhash_ALG_DEFAULT = crypto_pwhash_ALG_ARGON2ID13
const crypto_pwhash_BYTES_MIN = crypto_pwhash_argon2id_BYTES_MIN
const crypto_pwhash_BYTES_MAX = crypto_pwhash_argon2id_BYTES_MAX
const crypto_pwhash_PASSWD_MIN = crypto_pwhash_argon2id_PASSWD_MIN
const crypto_pwhash_PASSWD_MAX = crypto_pwhash_argon2id_PASSWD_MAX
const crypto_pwhash_SALTBYTES = crypto_pwhash_argon2id_SALTBYTES
const crypto_pwhash_STRBYTES = crypto_pwhash_argon2id_STRBYTES
const crypto_pwhash_STRPREFIX = crypto_pwhash_argon2id_STRPREFIX
const crypto_pwhash_OPSLIMIT_MIN = crypto_pwhash_argon2id_OPSLIMIT_MIN
const crypto_pwhash_OPSLIMIT_MAX = crypto_pwhash_argon2id_OPSLIMIT_MAX
const crypto_pwhash_MEMLIMIT_MIN = crypto_pwhash_argon2id_MEMLIMIT_MIN
const crypto_pwhash_MEMLIMIT_MAX = crypto_pwhash_argon2id_MEMLIMIT_MAX
const crypto_pwhash_OPSLIMIT_INTERACTIVE = crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE
const crypto_pwhash_MEMLIMIT_INTERACTIVE = crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE
const crypto_pwhash_OPSLIMIT_MODERATE = crypto_pwhash_argon2id_OPSLIMIT_MODERATE
const crypto_pwhash_MEMLIMIT_MODERATE = crypto_pwhash_argon2id_MEMLIMIT_MODERATE
const crypto_pwhash_OPSLIMIT_SENSITIVE = crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE
const crypto_pwhash_MEMLIMIT_SENSITIVE = crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE

module.exports = {
  crypto_pwhash_ALG_ARGON2I13,
  crypto_pwhash_ALG_ARGON2ID13,
  crypto_pwhash_ALG_DEFAULT,
  crypto_pwhash_BYTES_MIN,
  crypto_pwhash_BYTES_MAX,
  crypto_pwhash_PASSWD_MIN,
  crypto_pwhash_PASSWD_MAX,
  crypto_pwhash_SALTBYTES,
  crypto_pwhash_STRBYTES,
  crypto_pwhash_STRPREFIX,
  crypto_pwhash_OPSLIMIT_MIN,
  crypto_pwhash_OPSLIMIT_MAX,
  crypto_pwhash_MEMLIMIT_MIN,
  crypto_pwhash_MEMLIMIT_MAX,
  crypto_pwhash_OPSLIMIT_INTERACTIVE,
  crypto_pwhash_MEMLIMIT_INTERACTIVE,
  crypto_pwhash_OPSLIMIT_MODERATE,
  crypto_pwhash_MEMLIMIT_MODERATE,
  crypto_pwhash_OPSLIMIT_SENSITIVE,
  crypto_pwhash_MEMLIMIT_SENSITIVE,
  crypto_pwhash_argon2i,
  crypto_pwhash_argon2id,
  crypto_pwhash_argon2i_str,
  crypto_pwhash_argon2id_str,
  crypto_pwhash,
  crypto_pwhash_str,
  crypto_pwhash_str_verify,
  crypto_pwhash_str_needs_rehash
}

function crypto_pwhash_argon2i (out, passwd, salt, passes, memory, alg) {
  const outlen = out.byteLength
  const passwdlen = passwd.byteLength
  out.fill(0)

  if (outlen > crypto_pwhash_argon2i_BYTES_MAX) {
    throw new Error('Too large')
  }

  if (outlen < crypto_pwhash_argon2i_BYTES_MIN) {
    throw new Error('Invalid opts')
  }

  if (passwdlen > crypto_pwhash_argon2i_PASSWD_MAX ||
    passes > crypto_pwhash_argon2i_OPSLIMIT_MAX ||
    memory > crypto_pwhash_argon2i_MEMLIMIT_MAX) {
    throw new Error('Too large')
  }

  if (passwdlen < crypto_pwhash_argon2i_PASSWD_MIN ||
    (passes && passes < crypto_pwhash_argon2i_OPSLIMIT_MIN) ||
    (memory && memory < crypto_pwhash_argon2i_MEMLIMIT_MIN)) {
    throw new Error('Invalid opts')
  }

  switch (alg) {
    case crypto_pwhash_argon2i_ALG_ARGON2I13: {
      const buf = argon2(passwd, salt, null, null, 'binary', { memory: memory >> 10, passes, outlen, type: argon2.ARGON2I })
      out.set(buf)
      return 0
    }

    default:
      throw new Error('Invalid input')
  }
}

function crypto_pwhash_argon2id (out, passwd, salt, passes, memory, alg) {
  const outlen = out.byteLength
  const passwdlen = passwd.byteLength
  out.fill(0)

  if (outlen > crypto_pwhash_argon2id_BYTES_MAX) {
    throw new Error('Too large')
  }

  if (outlen < crypto_pwhash_argon2id_BYTES_MIN) {
    throw new Error('Invalid opts')
  }

  if (passwdlen > crypto_pwhash_argon2id_PASSWD_MAX ||
    passes > crypto_pwhash_argon2id_OPSLIMIT_MAX ||
    memory > crypto_pwhash_argon2id_MEMLIMIT_MAX) {
    throw new Error('Too large')
  }

  if (passwdlen < crypto_pwhash_argon2id_PASSWD_MIN ||
    (passes && passes < crypto_pwhash_argon2id_OPSLIMIT_MIN) ||
    (memory && memory < crypto_pwhash_argon2id_MEMLIMIT_MIN)) {
    throw new Error('Invalid opts')
  }

  switch (alg) {
    case crypto_pwhash_argon2id_ALG_ARGON2ID13: {
      const buf = argon2(passwd, salt, null, null, 'binary', { memory: memory >> 10, passes, outlen, type: argon2.ARGON2ID })
      out.set(buf)
      return 0
    }

    default:
      throw new Error('Invalid input')
  }
}

function crypto_pwhash_argon2i_str (out, passwd, salt, passes, memory, alg) {
  const outlen = 32
  const passwdlen = passwd.byteLength

  if (passwdlen > crypto_pwhash_argon2i_PASSWD_MAX ||
    passes > crypto_pwhash_argon2i_OPSLIMIT_MAX ||
    memory > crypto_pwhash_argon2i_MEMLIMIT_MAX) {
    throw new Error('Too large')
  }

  if (passwdlen < crypto_pwhash_argon2i_PASSWD_MIN ||
    (passes && passes < crypto_pwhash_argon2i_OPSLIMIT_MIN) ||
    (memory && memory < crypto_pwhash_argon2i_MEMLIMIT_MIN)) {
    throw new Error('Invalid opts')
  }

  switch (alg) {
    case crypto_pwhash_argon2i_ALG_ARGON2I13:
      return argon2(passwd, salt, null, null, { memory: memory >> 10, passes, outlen, type: argon2.ARGON2I })

    default:
      throw new Error('Invalid input')
  }
}

function crypto_pwhash_argon2id_str (out, passwd, salt, passes, memory, alg) {
  const outlen = 32
  const passwdlen = passwd.byteLength

  if (passwdlen > crypto_pwhash_argon2id_PASSWD_MAX ||
    passes > crypto_pwhash_argon2id_OPSLIMIT_MAX ||
    memory > crypto_pwhash_argon2id_MEMLIMIT_MAX) {
    throw new Error('Too large')
  }

  if (passwdlen < crypto_pwhash_argon2id_PASSWD_MIN ||
    (passes && passes < crypto_pwhash_argon2id_OPSLIMIT_MIN) ||
    (memory && memory < crypto_pwhash_argon2id_MEMLIMIT_MIN)) {
    throw new Error('Invalid opts')
  }

  switch (alg) {
    case crypto_pwhash_argon2id_ALG_ARGON2ID13:
      out.write(argon2(passwd, salt, null, null, { memory: memory >> 10, passes, outlen, type: argon2.ARGON2ID }))
      return

    default:
      throw new Error('Invalid input')
  }
}

function crypto_pwhash (out, passwd, salt, opslimit, memlimit, alg) {
  switch (alg) {
    case crypto_pwhash_argon2i_ALG_ARGON2I13:
      return crypto_pwhash_argon2i(out, passwd, salt, opslimit, memlimit, alg)

    case crypto_pwhash_argon2id_ALG_ARGON2ID13:
      return crypto_pwhash_argon2id(out, passwd, salt, opslimit, memlimit, alg)
  }
}

function crypto_pwhash_str (out, passwd, opslimit, memlimit) {
  const salt = b4a.alloc(crypto_pwhash_SALTBYTES)
  randombytes_buf(salt)

  return crypto_pwhash_argon2id_str(out, passwd, salt, opslimit, memlimit, crypto_pwhash_ALG_DEFAULT)
}

function crypto_pwhash_str_verify (str, passwd) {
  if (b4a.isBuffer(str)) return crypto_pwhash_str_verify(str.toString(), passwd)

  if (str.slice(0, crypto_pwhash_argon2id_STRPREFIX.length) === crypto_pwhash_argon2id_STRPREFIX) {
    return argon2.verify(str, passwd, null)
  }

  if (str.slice(0, crypto_pwhash_argon2i_STRPREFIX.length) === crypto_pwhash_argon2i_STRPREFIX) {
    return argon2.verify(str, passwd, null)
  }

  return false
}

function crypto_pwhash_str_needs_rehash (str, opslimit, memlimit, type) {
  memlimit >>= 10

  if ((opslimit | memlimit) > 0xffffffff || (str.byteLength > crypto_pwhash_STRBYTES)) {
    throw new Error('Invalid opts.')
  }

  try {
    return argon2.needsRehash(str, opslimit, memlimit, type)
  } catch {
    return true
  }
}
