import { secp256k1 } from '@noble/curves/secp256k1'

import Sha512 from '../../utils/Sha512'

const ZERO = BigInt(0)

function deriveScalar(bytes: Uint8Array, discrim?: number): bigint {
  const order = secp256k1.CURVE.n
  for (let i = 0; i <= 0xffff_ffff; i++) {
    // We hash the bytes to find a 256-bit number, looping until we are sure it
    // is less than the order of the curve.
    const hasher = new Sha512().add(bytes)
    // If the optional discriminator index was passed in, update the hash.
    if (discrim !== undefined) {
      hasher.addU32(discrim)
    }
    hasher.addU32(i)
    const key = hasher.first256BigInt()
    /* istanbul ignore else */
    if (key > ZERO && key < order) {
      return key
    }
  }
  // This error is practically impossible to reach.
  // The order of the curve describes the (finite) amount of points on the curve
  // https://github.com/indutny/elliptic/blob/master/lib/elliptic/curves.js#L182
  // How often will an (essentially) random number generated by Sha512 be larger than that?
  // There's 2^32 chances (the for loop) to get a number smaller than the order,
  // and it's rare that you'll even get past the first loop iteration.
  // Note that in TypeScript we actually need the throw, otherwise the function signature would be bigint | undefined
  //
  /* istanbul ignore next */
  throw new Error('impossible unicorn ;)')
}

/**
 * @param seed - Bytes.
 * @param [opts] - Object.
 * @param [opts.accountIndex=0] - The account number to generate.
 * @param [opts.validator=false] - Generate root key-pair,
 *                                              as used by validators.
 * @returns {bigint} 256 bit scalar value.
 *
 */
export function derivePrivateKey(
  seed: Uint8Array,
  opts: {
    validator?: boolean
    accountIndex?: number
  } = {},
): bigint {
  const root = opts.validator
  const order = secp256k1.CURVE.n

  // This private generator represents the `root` private key, and is what's
  // used by validators for signing when a keypair is generated from a seed.
  const privateGen = deriveScalar(seed)
  if (root) {
    // As returned by validation_create for a given seed
    return privateGen
  }
  const publicGen =
    secp256k1.ProjectivePoint.BASE.multiply(privateGen).toRawBytes(true)
  // A seed can generate many keypairs as a function of the seed and a uint32.
  // Almost everyone just uses the first account, `0`.
  const accountIndex = opts.accountIndex || 0
  return (deriveScalar(publicGen, accountIndex) + privateGen) % order
}

export function accountPublicFromPublicGenerator(publicGenBytes: Uint8Array) {
  const rootPubPoint = secp256k1.ProjectivePoint.fromHex(publicGenBytes)
  const scalar = deriveScalar(publicGenBytes, 0)
  const point = secp256k1.ProjectivePoint.BASE.multiply(scalar)
  const offset = rootPubPoint.add(point)
  return offset.toRawBytes(true)
}
