unction asyncAesCtrCipher(mode, key, data, { counter, length }) {
  counter = getArrayBufferOrView(counter, 'algorithm.counter');
  validateByteLength(counter, 'algorithm.counter', 16);
  // The length must specify an integer between 1 and 128. While
  // there is no default, this should typically be 64.
  if (typeof length !== 'number' ||
      length <= 0 ||
      length > kMaxCounterLength) {
    throw lazyDOMException(
      'AES-CTR algorithm.length must be between 1 and 128',
      'OperationError');
  }

  return jobPromise(new AESCipherJob(
    kCryptoJobAsync,
    mode,
    key[kKeyObject][kHandle],
    data,
    getVariant('AES-CTR', key.algorithm.length),
    counter,
    length));
}

function asyncAesCbcCipher(mode, key, data, { iv }) {
  iv = getArrayBufferOrView(iv, 'algorithm.iv');
  validateByteLength(iv, 'algorithm.iv', 16);
  return jobPromise(new AESCipherJob(
    kCryptoJobAsync,
    mode,
    key[kKeyObject][kHandle],
    data,
    getVariant('AES-CBC', key.algorithm.length),
    iv));
}

function asyncAesKwCipher(mode, key, data) {
  return jobPromise(new AESCipherJob(
    kCryptoJobAsync,
    mode,
    key[kKeyObject][kHandle],
    data,
    getVariant('AES-KW', key.algorithm.length)));
}

function asyncAesGcmCipher(
  mode,
  key,
  data,
  { iv, additionalData, tagLength = 128 }) {
  if (!ArrayPrototypeIncludes(kTagLengths, tagLength)) {
    return PromiseReject(lazyDOMException(
      `${tagLength} is not a valid AES-GCM tag length`,
      'OperationError'));
  }

  iv = getArrayBufferOrView(iv, 'algorithm.iv');
  validateMaxBufferLength(iv, 'algorithm.iv');

  if (additionalData !== undefined) {
    additionalData =
      getArrayBufferOrView(additionalData, 'algorithm.additionalData');
    validateMaxBufferLength(additionalData, 'algorithm.additionalData');
  }

  const tagByteLength = MathFloor(tagLength / 8);
  let tag;
  switch (mode) {
    case kWebCryptoCipherDecrypt: {
      const slice = ArrayBufferIsView(data) ?
        TypedArrayPrototypeSlice : ArrayBufferPrototypeSlice;
      tag = slice(data, -tagByteLength);

      // Refs: https://www.w3.org/TR/WebCryptoAPI/#aes-gcm-operations
      //
      // > If *plaintext* has a length less than *tagLength* bits, then `throw`
      // > an `OperationError`.
      if (tagByteLength > tag.byteLength) {
        return PromiseReject(lazyDOMException(
          'The provided data is too small.',
          'OperationError'));
      }

      data = slice(data, 0, -tagByteLength);
      break;
    }
    case kWebCryptoCipherEncrypt:
      tag = tagByteLength;
      break;
  }

  return jobPromise(new AESCipherJob(
    kCryptoJobAsync,
    mode,
    key[kKeyObject][kHandle],
    data,
    getVariant('AES-GCM', key.algorithm.length),
    iv,
    tag,
    additionalData));
}

function aesCipher(mode, key, data, algorithm) {
  switch (algorithm.name) {
    case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm);
    case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm);
    case 'AES-GCM': return asyncAesGcmCipher(mode, key, data, algorithm);
    case 'AES-KW': return asyncAesKwCipher(mode, key, data);
  }
}

async function aesGenerateKey(algorithm, extractable, keyUsages) {
  const { name, length } = algorithm;
  validateInteger(length, 'algorithm.length');
  validateOneOf(length, 'algorithm.length', kAesKeyLengths);

  const usageSet = new SafeSet(keyUsages);

  if (hasAnyNotIn(usageSet, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'])) {
    throw lazyDOMException(
      'Unsupported key usage for an AES key',
      'SyntaxError');
  }
  return new Promise((resolve, reject) => {
    generateKey('aes', { length }, (err, key) => {
      if (err) {
        return reject(lazyDOMException(
          'The operation failed for an operation-specific reason ' +
          `[${err.message}]`,
          'OperationError'));
      }

      resolve(new InternalCryptoKey(
        key,
        { name, length },
        ArrayFrom(usageSet),
        extractable));
    });
  });
}

async function aesImportKey(
  algorithm,
  format,
  keyData,
  extractable,
  keyUsages) {
  const { name } = algorithm;
  const checkUsages = ['wrapKey', 'unwrapKey'];
  if (name !== 'AES-KW')
    ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt');

  const usagesSet = new SafeSet(keyUsages);
  if (hasAnyNotIn(usagesSet, checkUsages)) {
    throw lazyDOMException(
      'Unsupported key usage for an AES key',
      'SyntaxError');
  }

  let keyObject;
  let length;
  switch (format) {
    case 'node.keyObject': {
      if (!isKeyObject(keyData))
        throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData);

      if (keyData.type !== 'secret') {
        throw lazyDOMException(
          `Unable to import AES key with format ${format}`,
          'NotSupportedError');
      }

      keyObject = keyData;
      break;
    }
    case 'raw': {
      validateKeyLength(keyData.byteLength * 8);
      keyObject = createSecretKey(keyData);
      break;
    }
    case 'jwk': {
      if (keyData == null || typeof keyData !== 'object')
        throw lazyDOMException('Invalid JWK keyData', 'DataError');

      if (keyData.kty !== 'oct')
        throw lazyDOMException('Invalid key type', 'DataError');

      if (usagesSet.size > 0 &&
          keyData.use !== undefined &&
          keyData.use !== 'enc') {
        throw lazyDOMException('Invalid use type', 'DataError');
      }

      validateKeyOps(keyData.key_ops, usagesSet);

      if (keyData.ext !== undefined &&
          keyData.ext === false &&
          extractable === true) {
        throw lazyDOMException('JWK is not extractable', 'DataError');
      }

      const handle = new KeyObjectHandle();
      handle.initJwk(keyData);

      ({ length } = handle.keyDetail({ }));
      validateKeyLength(length);

      if (keyData.alg !== undefined) {
        if (typeof keyData.alg !== 'string')
          throw lazyDOMException('Invalid alg', 'DataError');
        if (keyData.alg !== getAlgorithmName(algorithm.name, length))
          throw lazyDOMException('Algorithm mismatch', 'DataError');
      }

      keyObject = new SecretKeyObject(handle);
      break;
    }
    default:
      throw lazyDOMException(
        `Unable to import AES key with format ${format}`,
        'NotSupportedError');
  }

  if (length === undefined) {
    ({ length } = keyObject[kHandle].keyDetail({ }));
    validateKeyLength(length);
  }

  return new InternalCryptoKey(
    keyObject,
    { name, length },
    keyUsages,
    extractable);
}

module.exports = {
  aesCipher,
  aesGenerateKey,
  aesImportKey,
  getAlgorithmName,
};
