/*!
 * algorithms/ecdsa.js - Elliptic Curve Digitial Signature Algorithms
 *
 * Copyright (c) 2015 Cisco Systems, Inc.  See LICENSE file.
 */
"use strict";

var ecUtil = require("./ec-util.js"),
    helpers = require("./helpers.js"),
    sha = require("./sha.js");

function idealCurve(hash) {
  switch (hash) {
    case "SHA-256":
      return "P-256";
    case "SHA-384":
      return "P-384";
    case "SHA-512":
      return "P-521";
    default:
      throw new Error("unsupported hash: " + hash);
  }
}

function ecdsaSignFN(hash) {
  var curve = idealCurve(hash);

  // ### Fallback implementation -- uses forge
  var fallback = function(key, pdata /*, props */) {
    if (curve !== key.crv) {
      return Promise.reject(new Error("invalid curve"));
    }
    var pk = ecUtil.convertToForge(key, false);

    var promise;
    // generate hash
    promise = sha[hash].digest(pdata);
    // sign hash
    promise = promise.then(function(result) {
      result = pk.sign(result);
      result = Buffer.concat([result.r, result.s]);
      return {
        data: pdata,
        mac: result
      };
    });
    return promise;
  };

  // ### WebCrypto API implementation
  var webcrypto = function(key, pdata /*, props */) {
    if (curve !== key.crv) {
      return Promise.reject(new Error("invalid curve"));
    }
    var pk = ecUtil.convertToJWK(key, false);

    var promise;
    var alg = {
      name: "ECDSA",
      namedCurve: pk.crv,
      hash: {
        name: hash
      }
    };
    promise = helpers.subtleCrypto.importKey("jwk",
                                             pk,
                                             alg,
                                             true,
                                             [ "sign" ]);
    promise = promise.then(function(key) {
      return helpers.subtleCrypto.sign(alg, key, pdata);
    });
    promise = promise.then(function(result) {
      result = Buffer.from(result);
      return {
        data: pdata,
        mac: result
      };
    });
    return promise;
  };

  var nodejs;
  var nodeHash = hash.toLowerCase().replace("-", "");
  if (helpers.nodeCrypto && helpers.nodeCrypto.getHashes().indexOf(nodeHash) > -1) {
    nodejs = function(key, pdata) {
      if (curve !== key.crv) {
        return Promise.reject(new Error("invalid curve"));
      }

      var promise;
      promise = Promise.resolve(helpers.nodeCrypto.createSign(nodeHash));
      promise = promise.then(function (sign) {
        sign.update(pdata);
        return sign;
      });

      var size;

      switch (nodeHash.slice(-3)) {
        case "384":
          size = 48;
          break;
        case "512":
          size = 66;
          break;
        default:
          size = 32;
      }

      promise = promise.then(function (sign) {
        return ecUtil.derToConcat(sign.sign(ecUtil.convertToPEM(key, true)), size);
      });

      promise = promise.then(function (result) {
        return {
          data: pdata,
          mac: result
        };
      });

      return promise;
    };
  }

  return helpers.setupFallback(nodejs, webcrypto, fallback);
}

function ecdsaVerifyFN(hash) {
  var curve = idealCurve(hash);

  // ### Fallback implementation -- uses forge
  var fallback = function(key, pdata, mac /*, props */) {
    if (curve !== key.crv) {
      return Promise.reject(new Error("invalid curve"));
    }
    var pk = ecUtil.convertToForge(key, true);

    var promise;
    // generate hash
    promise = sha[hash].digest(pdata);
    // verify hash
    promise = promise.then(function(result) {
      var len = mac.length / 2;
      var rs = {
        r: mac.slice(0, len),
        s: mac.slice(len)
      };
      if (!pk.verify(result, rs)) {
        return Promise.reject(new Error("verification failed"));
      }
      return {
        data: pdata,
        mac: mac,
        valid: true
      };
    });
    return promise;
  };

  // ### WebCrypto API implementation
  var webcrypto = function(key, pdata, mac /* , props */) {
    if (curve !== key.crv) {
      return Promise.reject(new Error("invalid curve"));
    }
    var pk = ecUtil.convertToJWK(key, true);

    var promise;
    var alg = {
      name: "ECDSA",
      namedCurve: pk.crv,
      hash: {
        name: hash
      }
    };
    promise = helpers.subtleCrypto.importKey("jwk",
                                             pk,
                                             alg,
                                             true,
                                             ["verify"]);
    promise = promise.then(function(key) {
      return helpers.subtleCrypto.verify(alg, key, mac, pdata);
    });
    promise = promise.then(function(result) {
      if (!result) {
        return Promise.reject(new Error("verification failed"));
      }
      return {
        data: pdata,
        mac: mac,
        valid: true
      };
    });
    return promise;
  };

  var nodejs;
  var nodeHash = hash.toLowerCase().replace("-", "");
  if (helpers.nodeCrypto && helpers.nodeCrypto.getHashes().indexOf(nodeHash) > -1) {
    nodejs = function(key, pdata, mac /* , props */) {
      if (curve !== key.crv) {
        return Promise.reject(new Error("invalid curve"));
      }

      var size;
      switch (nodeHash.slice(-3)) {
        case "384":
          size = 48;
          break;
        case "512":
          size = 66;
          break;
        default:
          size = 32;
      }

      var promise;
      promise = Promise.resolve(helpers.nodeCrypto.createVerify(nodeHash));
      promise = promise.then(function (verify) {
        verify.update(pdata);
        verify.end();
        return verify.verify(ecUtil.convertToPEM(key, false), ecUtil.concatToDer(mac, size));
      });
      promise = promise.then(function (result) {
        if (!result) {
          throw new Error("verification failed");
        }
        return {
          data: pdata,
          mac: mac,
          valid: true
        };
      });

      return promise;
    };
  }

  return helpers.setupFallback(nodejs, webcrypto, fallback);
}

// ### Public API
var ecdsa = {};

// * [name].sign
// * [name].verify
[
  "ES256",
  "ES384",
  "ES512"
].forEach(function(name) {
  var hash = name.replace(/ES(\d+)/g, function(m, size) {
    return "SHA-" + size;
  });
  ecdsa[name] = {
    sign: ecdsaSignFN(hash),
    verify: ecdsaVerifyFN(hash)
  };
});

module.exports = ecdsa;
