'use strict'
var Buffer = require('safe-buffer').Buffer
var createHash = require('create-hash')
var HmacDRBG = require('drbg.js/hmac')
var messages = require('../messages.json')
var BN = require('./bn')
var ECPoint = require('./ecpoint')
var g = require('./ecpointg')

exports.privateKeyVerify = function (privateKey) {
  var bn = BN.fromBuffer(privateKey)
  return !(bn.isOverflow() || bn.isZero())
}

exports.privateKeyExport = function (privateKey, compressed) {
  var d = BN.fromBuffer(privateKey)
  if (d.isOverflow() || d.isZero()) throw new Error(messages.EC_PRIVATE_KEY_EXPORT_DER_FAIL)

  return g.mul(d).toPublicKey(compressed)
}

exports.privateKeyNegate = function (privateKey) {
  var bn = BN.fromBuffer(privateKey)
  if (bn.isZero()) return Buffer.alloc(32)

  if (bn.ucmp(BN.n) > 0) bn.isub(BN.n)
  return BN.n.sub(bn).toBuffer()
}

exports.privateKeyModInverse = function (privateKey) {
  var bn = BN.fromBuffer(privateKey)
  if (bn.isOverflow() || bn.isZero()) throw new Error(messages.EC_PRIVATE_KEY_RANGE_INVALID)

  return bn.uinvm().toBuffer()
}

exports.privateKeyTweakAdd = function (privateKey, tweak) {
  var bn = BN.fromBuffer(tweak)
  if (bn.isOverflow()) throw new Error(messages.EC_PRIVATE_KEY_TWEAK_ADD_FAIL)

  bn.iadd(BN.fromBuffer(privateKey))
  if (bn.isOverflow()) bn.isub(BN.n)
  if (bn.isZero()) throw new Error(messages.EC_PRIVATE_KEY_TWEAK_ADD_FAIL)

  return bn.toBuffer()
}

exports.privateKeyTweakMul = function (privateKey, tweak) {
  var bn = BN.fromBuffer(tweak)
  if (bn.isOverflow() || bn.isZero()) throw new Error(messages.EC_PRIVATE_KEY_TWEAK_MUL_FAIL)

  var d = BN.fromBuffer(privateKey)
  return bn.umul(d).ureduce().toBuffer()
}

exports.publicKeyCreate = function (privateKey, compressed) {
  var d = BN.fromBuffer(privateKey)
  if (d.isOverflow() || d.isZero()) throw new Error(messages.EC_PUBLIC_KEY_CREATE_FAIL)

  return g.mul(d).toPublicKey(compressed)
}

exports.publicKeyConvert = function (publicKey, compressed) {
  var point = ECPoint.fromPublicKey(publicKey)
  if (point === null) throw new Error(messages.EC_PUBLIC_KEY_PARSE_FAIL)

  return point.toPublicKey(compressed)
}

exports.publicKeyVerify = function (publicKey) {
  return ECPoint.fromPublicKey(publicKey) !== null
}

exports.publicKeyTweakAdd = function (publicKey, tweak, compressed) {
  var point = ECPoint.fromPublicKey(publicKey)
  if (point === null) throw new Error(messages.EC_PUBLIC_KEY_PARSE_FAIL)

  tweak = BN.fromBuffer(tweak)
  if (tweak.isOverflow()) throw new Error(messages.EC_PUBLIC_KEY_TWEAK_ADD_FAIL)

  var result = g.mul(tweak).add(point)
  if (result.inf) throw new Error(messages.EC_PUBLIC_KEY_TWEAK_ADD_FAIL)

  return result.toPublicKey(compressed)
}

exports.publicKeyTweakMul = function (publicKey, tweak, compressed) {
  var point = ECPoint.fromPublicKey(publicKey)
  if (point === null) throw new Error(messages.EC_PUBLIC_KEY_PARSE_FAIL)

  tweak = BN.fromBuffer(tweak)
  if (tweak.isOverflow() || tweak.isZero()) throw new Error(messages.EC_PUBLIC_KEY_TWEAK_MUL_FAIL)

  return point.mul(tweak).toPublicKey(compressed)
}

exports.publicKeyCombine = function (publicKeys, compressed) {
  var points = new Array(publicKeys.length)
  for (var i = 0; i < publicKeys.length; ++i) {
    points[i] = ECPoint.fromPublicKey(publicKeys[i])
    if (points[i] === null) throw new Error(messages.EC_PUBLIC_KEY_PARSE_FAIL)
  }

  var point = points[0]
  for (var j = 1; j < points.length; ++j) point = point.add(points[j])
  if (point.inf) throw new Error(messages.EC_PUBLIC_KEY_COMBINE_FAIL)

  return point.toPublicKey(compressed)
}

exports.signatureNormalize = function (signature) {
  var r = BN.fromBuffer(signature.slice(0, 32))
  var s = BN.fromBuffer(signature.slice(32, 64))
  if (r.isOverflow() || s.isOverflow()) throw new Error(messages.ECDSA_SIGNATURE_PARSE_FAIL)

  var result = Buffer.from(signature)
  if (s.isHigh()) BN.n.sub(s).toBuffer().copy(result, 32)

  return result
}

exports.signatureExport = function (signature) {
  var r = signature.slice(0, 32)
  var s = signature.slice(32, 64)
  if (BN.fromBuffer(r).isOverflow() || BN.fromBuffer(s).isOverflow()) throw new Error(messages.ECDSA_SIGNATURE_PARSE_FAIL)

  return { r: r, s: s }
}

exports.signatureImport = function (sigObj) {
  var r = BN.fromBuffer(sigObj.r)
  if (r.isOverflow()) r = BN.fromNumber(0)

  var s = BN.fromBuffer(sigObj.s)
  if (s.isOverflow()) s = BN.fromNumber(0)

  return Buffer.concat([r.toBuffer(), s.toBuffer()])
}

exports.sign = function (message, privateKey, noncefn, data) {
  var d = BN.fromBuffer(privateKey)
  if (d.isOverflow() || d.isZero()) throw new Error(messages.ECDSA_SIGN_FAIL)

  if (noncefn === null) {
    var drbg = new HmacDRBG('sha256', privateKey, message, data)
    noncefn = function () { return drbg.generate(32) }
  }

  var bnMessage = BN.fromBuffer(message)
  for (var count = 0; ; ++count) {
    var nonce = noncefn(message, privateKey, null, data, count)
    if (!Buffer.isBuffer(nonce) || nonce.length !== 32) throw new Error(messages.ECDSA_SIGN_FAIL)

    var k = BN.fromBuffer(nonce)
    if (k.isOverflow() || k.isZero()) continue

    var kp = g.mul(k)
    var r = kp.x.fireduce()
    if (r.isZero()) continue

    var s = k.uinvm().umul(r.umul(d).ureduce().iadd(bnMessage).fireduce()).ureduce()
    if (s.isZero()) continue

    var recovery = (kp.x.ucmp(r) !== 0 ? 2 : 0) | (kp.y.isOdd() ? 1 : 0)
    if (s.isHigh()) {
      s = BN.n.sub(s)
      recovery ^= 1
    }

    return {
      signature: Buffer.concat([r.toBuffer(), s.toBuffer()]),
      recovery: recovery
    }
  }
}

exports.verify = function (message, signature, publicKey) {
  var sigr = BN.fromBuffer(signature.slice(0, 32))
  var sigs = BN.fromBuffer(signature.slice(32, 64))
  if (sigr.isOverflow() || sigs.isOverflow()) throw new Error(messages.ECDSA_SIGNATURE_PARSE_FAIL)

  if (sigs.isHigh() || sigr.isZero() || sigs.isZero()) return false

  var pub = ECPoint.fromPublicKey(publicKey)
  if (pub === null) throw new Error(messages.EC_PUBLIC_KEY_PARSE_FAIL)

  var sinv = sigs.uinvm()
  var u1 = sinv.umul(BN.fromBuffer(message)).ureduce()
  var u2 = sinv.umul(sigr).ureduce()
  var point = g.mulAdd(u1, pub, u2)
  if (point.inf) return false

  // return ECPoint.fromECJPoint(point).x.fireduce().ucmp(sigr) === 0
  // Inversion-free
  var z2 = point.z.redSqr()
  if (sigr.redMul(z2).ucmp(point.x) === 0) return true
  if (sigr.ucmp(BN.psn) >= 0) return false

  return sigr.iadd(BN.psn).redMul(z2).ucmp(point.x) === 0
}

exports.recover = function (message, signature, recovery, compressed) {
  var sigr = BN.fromBuffer(signature.slice(0, 32))
  var sigs = BN.fromBuffer(signature.slice(32, 64))
  if (sigr.isOverflow() || sigs.isOverflow()) throw new Error(messages.ECDSA_SIGNATURE_PARSE_FAIL)

  do {
    if (sigr.isZero() || sigs.isZero()) break

    var kpx = sigr
    if (recovery >> 1) {
      if (kpx.ucmp(BN.psn) >= 0) break
      kpx = sigr.add(BN.n)
    }

    var kpPublicKey = Buffer.concat([Buffer.from([0x02 + (recovery & 0x01)]), kpx.toBuffer()])
    var kp = ECPoint.fromPublicKey(kpPublicKey)
    if (kp === null) break

    var rInv = sigr.uinvm()
    var s1 = BN.n.sub(BN.fromBuffer(message)).umul(rInv).ureduce()
    var s2 = sigs.umul(rInv).ureduce()
    var point = ECPoint.fromECJPoint(g.mulAdd(s1, kp, s2))
    return point.toPublicKey(compressed)
  } while (false)

  throw new Error(messages.ECDSA_RECOVER_FAIL)
}

exports.ecdh = function (publicKey, privateKey) {
  var shared = exports.ecdhUnsafe(publicKey, privateKey, true)
  return createHash('sha256').update(shared).digest()
}

exports.ecdhUnsafe = function (publicKey, privateKey, compressed) {
  var point = ECPoint.fromPublicKey(publicKey)
  if (point === null) throw new Error(messages.EC_PUBLIC_KEY_PARSE_FAIL)

  var scalar = BN.fromBuffer(privateKey)
  if (scalar.isOverflow() || scalar.isZero()) throw new Error(messages.ECDH_FAIL)

  return point.mul(scalar).toPublicKey(compressed)
}
