;(function (exports) {
  'use strict';

  var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;

  OAUTH3.crypto = {};
  try {
    OAUTH3.crypto.core = require('./oauth3.node.crypto');
  } catch (error) {
    OAUTH3.crypto.core = {};
    OAUTH3.crypto.core.ready = false;
    var finishBeforeReady = [];
    var deferedCalls = [];

    // We don't currently have a fallback method for this function, so we assign
    // it directly to the core object instead of the webCrypto object.
    OAUTH3.crypto.core.randomBytes = function (size) {
      var buf = OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(size));
      return OAUTH3.PromiseA.resolve(buf);
    };

    var webCrypto = {};

    var deferCryptoCall = function(name) {
      return function() {
        var args = arguments;
        return new OAUTH3.PromiseA(function(resolve, reject) {
          deferedCalls.push(function(){
            try {
              webCrypto[name].apply(webCrypto, args)
                .then(function(result){
                  resolve(result);
                });
            } catch(e) {
              reject(e);
            }
          });
        });
      };
    };

    OAUTH3.crypto.core.sha256 = deferCryptoCall("sha256");
    webCrypto.sha256 = function (buf) {
      return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf);
    };

    OAUTH3.crypto.core.pbkdf2 = deferCryptoCall("pbkdf2");
    webCrypto.pbkdf2 = function (password, salt) {
      return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey'])
        .then(function (key) {
          var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}};
          return OAUTH3._browser.window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']);
        })
        .then(function (key) {
          return OAUTH3._browser.window.crypto.subtle.exportKey('raw', key);
        });
    };

    OAUTH3.crypto.core.encrypt = deferCryptoCall("encrypt");
    webCrypto.encrypt = function (rawKey, iv, data) {
      return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt'])
        .then(function (key) {
          return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data);
        });
    };

    OAUTH3.crypto.core.decrypt = deferCryptoCall("decrypt");
    webCrypto.decrypt = function (rawKey, iv, data) {
      return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt'])
        .then(function (key) {
          return OAUTH3._browser.window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data);
        });
    };

    OAUTH3.crypto.core.genEcdsaKeyPair = deferCryptoCall("genEcdsaKeyPair");
    webCrypto.genEcdsaKeyPair = function () {
      return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
        .then(function (keyPair) {
          return OAUTH3.PromiseA.all([
            OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.privateKey)
          , OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.publicKey)
          ]);
        }).then(function (jwkPair) {
          return { privateKey: jwkPair[0], publicKey:  jwkPair[1] };
        });
    };

    OAUTH3.crypto.core.sign = deferCryptoCall("sign");
    webCrypto.sign = function (jwk, msg) {
      return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign'])
        .then(function (key) {
          return OAUTH3._browser.window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, msg);
        })
        .then(function (sig) {
          return new Uint8Array(sig);
        });
    };
    
    OAUTH3.crypto.core.verify = deferCryptoCall("verify");
    webCrypto.verify = function (jwk, msg, signature) {
      // If the JWK has properties that should only exist on the private key or is missing
      // "verify" in the key_ops, importing in as a public key won't work.
      if (jwk.hasOwnProperty('d') || jwk.hasOwnProperty('key_ops')) {
        jwk = JSON.parse(JSON.stringify(jwk));
        delete jwk.d;
        delete jwk.key_ops;
      }

      return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify'])
      .then(function (key) {
        return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg);
      });
    };

    function checkWebCrypto() {
      /* global OAUTH3_crypto_fallback */
      var loadFallback = function() {
        var prom;
        loadFallback = function () { return prom; };

        prom = new OAUTH3.PromiseA(function (resolve) {
          var body = document.getElementsByTagName('body')[0];
          var script = document.createElement('script');
          script.type = 'text/javascript';
          script.onload = resolve;
          script.onreadystatechange = function () {
            if (this.readyState === 'complete' || this.readyState === 'loaded') {
              resolve();
            }
          };
          script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js';
          body.appendChild(script);
        });
        return prom;
      };
      function checkException(name, func) {
        return OAUTH3.PromiseA.resolve().then(func)
          .then(function () {
            OAUTH3.crypto.core[name] = webCrypto[name];
          }, function (err) {
            console.warn('error with WebCrypto', name, '- using fallback', err);
            return loadFallback().then(function () {
              OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
            });
          });
      }
      function checkResult(name, expected, func) {
        
        finishBeforeReady.push(checkException(name, function () {
          return func()
            .then(function (result) {
              if (typeof expected === typeof result) {
                return result;
              }
              return OAUTH3._base64.bufferToUrlSafe(result);
            })
            .then(function (result) {
              if (result !== expected) {
                throw new Error("result ("+result+") doesn't match expectation ("+expected+")");
              }
            });
        }));
      }

      var zeroBuf = new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
      var dataBuf = OAUTH3._base64.urlSafeToBuffer('1234567890abcdefghijklmn');
      var keyBuf = OAUTH3._base64.urlSafeToBuffer('l_Aeoqk6ePjwjCYrlHrgrg');
      var encBuf = OAUTH3._base64.urlSafeToBuffer('Ji_gEtcNElUONSR4Mf9S75davXjh_6-oQN9AgO5UF8rERw');
      checkResult('sha256', 'BwMveUm2V1axuERvUoxM4dScgNl9yKhER9a6p80GXj4', function () {
        return webCrypto.sha256(dataBuf);
      });
      checkResult('pbkdf2', OAUTH3._base64.bufferToUrlSafe(keyBuf), function () {
        return webCrypto.pbkdf2('password', zeroBuf);
      });
      checkResult('encrypt', OAUTH3._base64.bufferToUrlSafe(encBuf), function () {
        return webCrypto.encrypt(keyBuf, zeroBuf.slice(0, 12), dataBuf);
      });
      checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () {
        return webCrypto.decrypt(keyBuf, zeroBuf.slice(0, 12), encBuf);
      });

      var jwk = {
        kty: "EC"
      , crv: "P-256"
      , d: "ChXx7ea5YtEltCufA8CVb0lQv3glcCfcSpEgdedgIP0"
      , x: "Akt5ZDbytcKS5UQMURvGb_UIMS4qFctDwrX8bX22ato"
      , y: "cV7nhpWNT1FeRIbdold4jLtgsEpZBFcNy3p2E5mqvto"
      };
      var sig = OAUTH3._base64.urlSafeToBuffer('nc3F8qeP8OXpfqPD9tTcFQg0Wfp37RTAppLPIKE1ZupR_8Aba64hNExwd1dOk802OFQxaECPDZCkKe7WA9RXAg');
      checkResult('verify', true, function() {
        return webCrypto.verify(jwk, dataBuf, sig);
      });
      // The results of these functions are less predictable, so we can't check their return value.
      finishBeforeReady.push(checkException('genEcdsaKeyPair', function () {
        return webCrypto.genEcdsaKeyPair();
      }));
      finishBeforeReady.push(checkException('sign', function () {
        return webCrypto.sign(jwk, dataBuf);
      }));
      
      OAUTH3.PromiseA.all(finishBeforeReady)
        .then(function(results) {
          OAUTH3.crypto.core.ready = true;
          deferedCalls.forEach(function(request) {
            request();
          });
        });
    }
    checkWebCrypto();
  }

  OAUTH3.crypto.thumbprintJwk = function (jwk) {
    var keys;
    if (jwk.kty === 'EC') {
      keys = ['crv', 'x', 'y'];
    } else if (jwk.kty === 'RSA') {
      keys = ['e', 'n'];
    } else if (jwk.kty === 'oct') {
      keys = ['k'];
    } else {
      return OAUTH3.PromiseA.reject(new Error('invalid JWK key type ' + jwk.kty));
    }
    keys.push('kty');
    keys.sort();

    var missing = keys.filter(function (name) { return !jwk.hasOwnProperty(name); });
    if (missing.length > 0) {
      return OAUTH3.PromiseA.reject(new Error('JWK of type '+jwk.kty+' missing fields ' + missing));
    }

    // I'm not actually 100% sure this behavior is guaranteed, but when we use an array as the
    // replacer argument the keys are always in the order they appeared in the array.
    var jwkStr = JSON.stringify(jwk, keys);
    return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr))
      .then(OAUTH3._base64.bufferToUrlSafe);
  };

  OAUTH3.crypto.createKeyPair = function () {
    // TODO: maybe support other types of key pairs, not just ECDSA P-256
    return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) {
      return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) {
        keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256';
        keyPair.privateKey.kid = keyPair.publicKey.kid = kid;
        return keyPair;
      });
    });
  };

  OAUTH3.crypto.encryptKeyPair = function (keyPair, password) {
    var saltProm = OAUTH3.crypto.core.randomBytes(16);
    var kekProm = saltProm.then(function (salt) {
      return OAUTH3.crypto.core.pbkdf2(password, salt);
    });

    return OAUTH3.PromiseA.all([
      kekProm
    , saltProm
    , OAUTH3.crypto.core.randomBytes(12)
  , ]).then(function (results) {
      var kek        = results[0];
      var salt       = results[1];
      var ecdsaIv    = results[2];

      var privKeyBuf = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey));
      return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) {
        return {
          publicKey:  keyPair.publicKey
        , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted)
        , salt:       OAUTH3._base64.bufferToUrlSafe(salt)
        , ecdsaIv:    OAUTH3._base64.bufferToUrlSafe(ecdsaIv)
      , };
      });
    });
  };

  OAUTH3.crypto.decryptKeyPair = function (storedObj, password) {
    var salt   = OAUTH3._base64.urlSafeToBuffer(storedObj.salt);
    var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey);
    var iv     = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv);

    return OAUTH3.crypto.core.pbkdf2(password, salt)
      .then(function (key) {
        return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
      })
      .then(OAUTH3._binStr.bufferToBinStr)
      .then(JSON.parse)
      .then(function (privateKey) {
        return {
          privateKey: privateKey
        , publicKey:  storedObj.publicKey
      , };
      });
  };

}('undefined' !== typeof exports ? exports : window));