/*!
 * secp256k1-elliptic.js - wrapper for elliptic
 * Copyright (c) 2014-2015, Fedor Indutny (MIT License)
 * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('assert');
const elliptic = require('elliptic');
const secp256k1 = elliptic.ec('secp256k1');
const Signature = require('elliptic/lib/elliptic/ec/signature');
const BN = require('./bn');
const curve = secp256k1.curve;

/**
 * @exports crypto/secp256k1-elliptic
 * @ignore
 */

const ec = exports;

/**
 * Whether we're using native bindings.
 * @const {Boolean}
 */

ec.binding = false;

/**
 * Generate a private key.
 * @returns {Buffer} Private key.
 */

ec.generatePrivateKey = function generatePrivateKey() {
  const key = secp256k1.genKeyPair();
  return key.getPrivate().toArrayLike(Buffer, 'be', 32);
};

/**
 * Create a public key from a private key.
 * @param {Buffer} priv
 * @param {Boolean?} compress
 * @returns {Buffer}
 */

ec.publicKeyCreate = function publicKeyCreate(priv, compress) {
  assert(Buffer.isBuffer(priv));

  if (compress == null)
    compress = true;

  const key = secp256k1.keyPair({ priv: priv });

  return Buffer.from(key.getPublic(compress, 'array'));
};

/**
 * Compress or decompress public key.
 * @param {Buffer} pub
 * @returns {Buffer}
 */

ec.publicKeyConvert = function publicKeyConvert(key, compress) {
  const point = curve.decodePoint(key);

  if (compress == null)
    compress = true;

  return Buffer.from(point.encode('array', compress));
};

/**
 * ((tweak + key) % n)
 * @param {Buffer} privateKey
 * @param {Buffer} tweak
 * @returns {Buffer} privateKey
 */

ec.privateKeyTweakAdd = function privateKeyTweakAdd(privateKey, tweak) {
  const key = new BN(tweak)
    .add(new BN(privateKey))
    .mod(curve.n)
    .toArrayLike(Buffer, 'be', 32);

  // Only a 1 in 2^127 chance of happening.
  if (!ec.privateKeyVerify(key))
    throw new Error('Private key is invalid.');

  return key;
};

/**
 * ((g * tweak) + key)
 * @param {Buffer} publicKey
 * @param {Buffer} tweak
 * @returns {Buffer} publicKey
 */

ec.publicKeyTweakAdd = function publicKeyTweakAdd(publicKey, tweak, compress) {
  const key = curve.decodePoint(publicKey);
  const point = curve.g.mul(new BN(tweak)).add(key);

  if (compress == null)
    compress = true;

  const pub = Buffer.from(point.encode('array', compress));

  if (!ec.publicKeyVerify(pub))
    throw new Error('Public key is invalid.');

  return pub;
};

/**
 * Create an ecdh.
 * @param {Buffer} pub
 * @param {Buffer} priv
 * @returns {Buffer}
 */

ec.ecdh = function ecdh(pub, priv) {
  priv = secp256k1.keyPair({ priv: priv });
  pub = secp256k1.keyPair({ pub: pub });
  return priv.derive(pub.getPublic()).toArrayLike(Buffer, 'be', 32);
};

/**
 * Recover a public key.
 * @param {Buffer} msg
 * @param {Buffer} sig
 * @param {Number?} j
 * @param {Boolean?} compress
 * @returns {Buffer[]|Buffer|null}
 */

ec.recover = function recover(msg, sig, j, compress) {
  if (!j)
    j = 0;

  if (compress == null)
    compress = true;

  let point;
  try {
    point = secp256k1.recoverPubKey(msg, sig, j);
  } catch (e) {
    return null;
  }

  return Buffer.from(point.encode('array', compress));
};

/**
 * Verify a signature.
 * @param {Buffer} msg
 * @param {Buffer} sig - DER formatted.
 * @param {Buffer} key
 * @returns {Boolean}
 */

ec.verify = function verify(msg, sig, key) {
  assert(Buffer.isBuffer(msg));
  assert(Buffer.isBuffer(sig));
  assert(Buffer.isBuffer(key));

  if (sig.length === 0)
    return false;

  if (key.length === 0)
    return false;

  // Attempt to normalize the signature
  // length before passing to elliptic.
  // https://github.com/indutny/elliptic/issues/78
  sig = normalizeLength(sig);

  try {
    return secp256k1.verify(msg, sig, key);
  } catch (e) {
    return false;
  }
};

/**
 * Validate a public key.
 * @param {Buffer} key
 * @returns {Boolean} True if buffer is a valid public key.
 */

ec.publicKeyVerify = function publicKeyVerify(key) {
  try {
    const pub = secp256k1.keyPair({ pub: key });
    return pub.validate();
  } catch (e) {
    return false;
  }
};

/**
 * Validate a private key.
 * @param {Buffer} key
 * @returns {Boolean} True if buffer is a valid private key.
 */

ec.privateKeyVerify = function privateKeyVerify(key) {
  if (key.length !== 32)
    return false;

  key = new BN(key);

  return key.cmpn(0) !== 0 && key.cmp(curve.n) < 0;
};

/**
 * Sign a message.
 * @param {Buffer} msg
 * @param {Buffer} key - Private key.
 * @returns {Buffer} DER-formatted signature.
 */

ec.sign = function sign(msg, key) {
  assert(Buffer.isBuffer(msg));
  assert(Buffer.isBuffer(key));

  // Sign message and ensure low S value
  const sig = secp256k1.sign(msg, key, { canonical: true });

  // Convert to DER
  return Buffer.from(sig.toDER());
};

/**
 * Convert DER signature to R/S.
 * @param {Buffer} raw
 * @returns {Buffer} R/S-formatted signature.
 */

ec.fromDER = function fromDER(raw) {
  assert(Buffer.isBuffer(raw));

  const sig = new Signature(raw);
  const out = Buffer.allocUnsafe(64);

  sig.r.toArrayLike(Buffer, 'be', 32).copy(out, 0);
  sig.s.toArrayLike(Buffer, 'be', 32).copy(out, 32);

  return out;
};

/**
 * Convert R/S signature to DER.
 * @param {Buffer} sig
 * @returns {Buffer} DER-formatted signature.
 */

ec.toDER = function toDER(raw) {
  assert(Buffer.isBuffer(raw));

  const sig = new Signature({
    r: new BN(raw.slice(0, 32), 'be'),
    s: new BN(raw.slice(32, 64), 'be')
  });

  return Buffer.from(sig.toDER());
};

/**
 * Test whether a signature has a low S value.
 * @param {Buffer} sig
 * @returns {Boolean}
 */

ec.isLowS = function isLowS(raw) {
  let sig;
  try {
    sig = new Signature(raw);
  } catch (e) {
    return false;
  }

  if (sig.s.cmpn(0) === 0)
    return false;

  // If S is greater than half the order,
  // it's too high.
  if (sig.s.cmp(secp256k1.nh) > 0)
    return false;

  return true;
};

/*
 * Helpers
 */

function normalizeLength(sig) {
  let data = sig;
  let pos = 0;
  let len;

  if (data[pos++] !== 0x30)
    return sig;

  [len, pos] = getLength(data, pos);

  if (data.length > len + pos)
    data = data.slice(0, len + pos);

  if (data[pos++] !== 0x02)
    return sig;

  // R length.
  [len, pos] = getLength(data, pos);

  pos += len;

  if (data[pos++] !== 0x02)
    return sig;

  // S length.
  [len, pos] = getLength(data, pos);

  if (data.length > len + pos)
    data = data.slice(0, len + pos);

  return data;
}

function getLength(buf, pos) {
  const initial = buf[pos++];

  if (!(initial & 0x80))
    return [initial, pos];

  const len = initial & 0xf;
  let val = 0;

  for (let i = 0; i < len; i++) {
    val <<= 8;
    val |= buf[pos++];
  }

  return [val, pos];
}
