/*!
 * jwk/rsa.js - RSA Key Representation
 *
 * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file.
 */
"use strict";

var ecutil = require("../algorithms/ec-util.js"),
    forge = require("../deps/forge"),
    depsecc = require("../deps/ecc");

var JWK = {
  BaseKey: require("./basekey.js"),
  helpers: require("./helpers.js")
};

var SIG_ALGS = [
  "ES256",
  "ES384",
  "ES512"
];
var WRAP_ALGS = [
  "ECDH-ES",
  "ECDH-ES+A128KW",
  "ECDH-ES+A192KW",
  "ECDH-ES+A256KW"
];

var EC_OID = ecutil.EC_OID;
function oidToCurveName(oid) {
  switch (oid) {
    case "1.2.840.10045.3.1.7":
      return "P-256";
    case "1.3.132.0.34":
      return "P-384";
    case "1.3.132.0.35":
      return "P-521";
    default:
      return null;
  }
}

var JWKEcCfg = {
  publicKey: function(props) {
    var fields = JWK.helpers.COMMON_PROPS.concat([
      {name: "crv", type: "string"},
      {name: "x", type: "binary"},
      {name: "y", type: "binary"}
    ]);
    var pk = JWK.helpers.unpackProps(props, fields);
    if (pk && pk.crv && pk.x && pk.y) {
      pk.length = ecutil.curveSize(pk.crv);
    } else {
      delete pk.crv;
      delete pk.x;
      delete pk.y;
    }

    return pk;
  },
  privateKey: function(props) {
    var fields = JWK.helpers.COMMON_PROPS.concat([
      {name: "crv", type: "string"},
      {name: "x", type: "binary"},
      {name: "y", type: "binary"},
      {name: "d", type: "binary"}
    ]);
    var pk = JWK.helpers.unpackProps(props, fields);
    if (pk && pk.crv && pk.x && pk.y && pk.d) {
      pk.length = ecutil.curveSize(pk.crv);
    } else {
      pk = undefined;
    }

    return pk;
  },
  thumbprint: function(json) {
    if (json.public) {
      json = json.public;
    }
    var fields = {
      crv: json.crv,
      kty: "EC",
      x: json.x,
      y: json.y
    };
    return fields;
  },
  algorithms: function(keys, mode) {
    var len = (keys.public && keys.public.length) ||
              (keys.private && keys.private.length) ||
              0;
    // NOTE: 521 is the actual, but 512 is the expected
    if (len === 521) {
        len = 512;
    }

    switch (mode) {
      case "encrypt":
      case "decrypt":
        return [];
      case "wrap":
        return (keys.public && WRAP_ALGS) || [];
      case "unwrap":
        return (keys.private && WRAP_ALGS) || [];
      case "sign":
        if (!keys.private) {
          return [];
        }
        return SIG_ALGS.filter(function(a) {
          return (a === ("ES" + len));
        });
      case "verify":
        if (!keys.public) {
          return [];
        }
        return SIG_ALGS.filter(function(a) {
          return (a === ("ES" + len));
        });
    }
  },

  encryptKey: function(alg, keys) {
    return keys.public;
  },
  decryptKey: function(alg, keys) {
    return keys.private;
  },

  wrapKey: function(alg, keys) {
    return keys.public;
  },
  unwrapKey: function(alg, keys) {
    return keys.private;
  },

  signKey: function(alg, keys) {
    return keys.private;
  },
  verifyKey: function(alg, keys) {
    return keys.public;
  }
};
JWKEcCfg.convertToPEM = ecutil.convertToPEM;

// Inspired by digitalbaazar/node-forge/js/rsa.js
var validators = {
  oid: EC_OID,
  privateKey: {
    // ECPrivateKey
    name: "ECPrivateKey",
    tagClass: forge.asn1.Class.UNIVERSAL,
    type: forge.asn1.Type.SEQUENCE,
    constructed: true,
    value: [
      {
        // EC version
        name: "ECPrivateKey.version",
        tagClass: forge.asn1.Class.UNIVERSAL,
        type: forge.asn1.Type.INTEGER,
        constructed: false
      },
      {
        // private value (d)
        name: "ECPrivateKey.private",
        tagClass: forge.asn1.Class.UNIVERSAL,
        type: forge.asn1.Type.OCTETSTRING,
        constructed: false,
        capture: "d"
      },
      {
        // EC parameters
        tagClass: forge.asn1.Class.CONTEXT_SPECIFIC,
        name: "ECPrivateKey.parameters",
        constructed: true,
        value: [
          {
            // namedCurve (crv)
            name: "ECPrivateKey.namedCurve",
            tagClass: forge.asn1.Class.UNIVERSAL,
            type: forge.asn1.Type.OID,
            constructed: false,
            capture: "crv"
          }
        ]
      },
      {
        // publicKey
        name: "ECPrivateKey.publicKey",
        tagClass: forge.asn1.Class.CONTEXT_SPECIFIC,
        constructed: true,
        value: [
          {
            name: "ECPrivateKey.point",
            tagClass: forge.asn1.Class.UNIVERSAL,
            type: forge.asn1.Type.BITSTRING,
            constructed: false,
            capture: "point"
          }
        ]
      }
    ]
  },
  embeddedPrivateKey: {
    // ECPrivateKey
    name: "ECPrivateKey",
    tagClass: forge.asn1.Class.UNIVERSAL,
    type: forge.asn1.Type.SEQUENCE,
    constructed: true,
    value: [
      {
        // EC version
        name: "ECPrivateKey.version",
        tagClass: forge.asn1.Class.UNIVERSAL,
        type: forge.asn1.Type.INTEGER,
        constructed: false
      },
      {
        // private value (d)
        name: "ECPrivateKey.private",
        tagClass: forge.asn1.Class.UNIVERSAL,
        type: forge.asn1.Type.OCTETSTRING,
        constructed: false,
        capture: "d"
      },
      {
        // publicKey
        name: "ECPrivateKey.publicKey",
        tagClass: forge.asn1.Class.CONTEXT_SPECIFIC,
        constructed: true,
        value: [
          {
            name: "ECPrivateKey.point",
            tagClass: forge.asn1.Class.UNIVERSAL,
            type: forge.asn1.Type.BITSTRING,
            constructed: false,
            capture: "point"
          }
        ]
      }
    ]
  }
};

var JWKEcFactory = {
  kty: "EC",
  validators: validators,
  prepare: function(props) {
    // TODO: validate key properties
    var cfg = JWKEcCfg;
    var p = Promise.resolve(props);
    p = p.then(function(json) {
      return JWK.helpers.thumbprint(cfg, json);
    });
    p = p.then(function(hash) {
      var prints = {};
      prints[JWK.helpers.INTERNALS.THUMBPRINT_HASH] = hash;
      props[JWK.helpers.INTERNALS.THUMBPRINT_KEY] = prints;
      return cfg;
    });
    return p;
  },
  generate: function(size) {
    var keypair = depsecc.generateKeyPair(size);
    var result = {
      "crv": size,
      "x": keypair.public.x,
      "y": keypair.public.y,
      "d": keypair.private.d
    };
    return Promise.resolve(result);
  },
  import: function(input) {
    if (validators.oid !== input.keyOid) {
      return null;
    }

    // coerce key params to OID
    var crv;
    if (input.keyParams && forge.asn1.Type.OID === input.keyParams.type) {
      crv = forge.asn1.derToOid(input.keyParams.value);
      crv = oidToCurveName(crv);
    } else if (input.crv) {
      crv = forge.asn1.derToOid(input.crv);
      crv = oidToCurveName(crv);
    }
    if (!crv) {
      return null;
    }

    if (!input.parsed) {
      var capture = {},
          errors = [];
      if ("private" === input.type) {
        // coerce capture.value to DER *iff* private
        if ("string" === typeof input.keyValue) {
          input.keyValue = forge.asn1.fromDer(input.keyValue);
        } else if (Array.isArray(input.keyValue)) {
          input.keyValue = input.keyValue[0];
        }

        if (!forge.asn1.validate(input.keyValue,
                                 validators.embeddedPrivateKey,
                                 capture,
                                 errors)) {
          return null;
        }
      } else {
        capture.point = input.keyValue;
      }
      input = capture;
    }

    // convert factors to Buffers
    var output = {
      kty: "EC",
      crv: crv
    };
    if (input.d) {
      output.d = Buffer.from(input.d, "binary");
    }
    if (input.point) {
      var pt = Buffer.from(input.point, "binary");
      // only support uncompressed
      if (4 !== pt.readUInt16BE(0)) {
        return null;
      }
      pt = pt.slice(2);
      var len = pt.length / 2;
      output.x = pt.slice(0, len);
      output.y = pt.slice(len);
    }
    return output;
  }
};
// public API
module.exports = Object.freeze({
  config: JWKEcCfg,
  factory: JWKEcFactory
});

// registration
(function(REGISTRY) {
  REGISTRY.register(JWKEcFactory);
})(require("./keystore").registry);
