# frozen_string_literal: true
# rubocop:todo all

# Copyright (C) 2019-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

unless ENV['LIBMONGOCRYPT_PATH']
  begin
    require 'libmongocrypt_helper'
  rescue LoadError => e
    # It seems that MRI maintains autoload configuration for a module until
    # that module is defined, but JRuby removes autoload configuration as soon
    # as the referenced file is attempted to be loaded, even if the module
    # never ends up being defined.
    if BSON::Environment.jruby?
      module Mongo
        module Crypt
          autoload :Binding, 'mongo/crypt/binding'
        end
      end
    end

    # JRuby 9.3.2.0 replaces a LoadError with our custom message with a
    # generic NameError, when this load is attempted as part of autoloading
    # process. JRuby 9.2.20.0 propagates LoadError as expected.
    raise LoadError, "Cannot load Mongo::Crypt::Binding because there is no path " +
        "to libmongocrypt specified in the LIBMONGOCRYPT_PATH environment variable " +
        "and libmongocrypt-helper is not installed: #{e.class}: #{e}"
  end
end

require 'ffi'

module Mongo
  module Crypt

    # @api private
    def reset_autoload
      remove_const(:Binding)
      autoload(:Binding, 'mongo/crypt/binding')
    end
    module_function :reset_autoload

    # A Ruby binding for the libmongocrypt C library
    #
    # @api private
    class Binding
      extend FFI::Library

      if ENV['LIBMONGOCRYPT_PATH']
        begin
          ffi_lib ENV['LIBMONGOCRYPT_PATH']
        rescue LoadError => e
          Crypt.reset_autoload
          raise LoadError, "Cannot load Mongo::Crypt::Binding because the path to " +
            "libmongocrypt specified in the LIBMONGOCRYPT_PATH environment variable " +
            "is invalid: #{ENV['LIBMONGOCRYPT_PATH']}\n\n#{e.class}: #{e.message}"
        end
      else
        begin
          ffi_lib LibmongocryptHelper.libmongocrypt_path
        rescue LoadError => e
          Crypt.reset_autoload
          raise LoadError, "Cannot load Mongo::Crypt::Binding because the path to " +
            "libmongocrypt specified in libmongocrypt-helper " +
            "is invalid: #{LibmongocryptHelper.libmongocrypt_path}\n\n#{e.class}: #{e.message}"
        end
      end

      # Minimum version of libmongocrypt required by this version of the driver.
      # An attempt to use the driver with any previous version of libmongocrypt
      # will cause a `LoadError`.
      #
      # @api private
      MIN_LIBMONGOCRYPT_VERSION = Gem::Version.new("1.12.0")

      # @!method self.mongocrypt_version(len)
      #   @api private
      #
      #   Returns the version string of the libmongocrypt library.
      #   @param [ FFI::Pointer | nil ] len (out param) An optional pointer to a
      #     uint8 that will reference the length of the returned string.
      #   @return [ String ] A version string for libmongocrypt.
      attach_function :mongocrypt_version, [:pointer], :string

      # Given a string representing a version number, parses it into a
      # Gem::Version object. This handles the case where the string is not
      # in a format supported by Gem::Version by doing some custom parsing.
      #
      # @param [ String ] version String representing a version number.
      #
      # @return [ Gem::Version ] the version number
      #
      # @raise [ ArgumentError ] if the string cannot be parsed.
      #
      # @api private
      def self.parse_version(version)
        Gem::Version.new(version)
      rescue ArgumentError
        match = version.match(/\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?(-[A-Za-z\+\d]+)?\z/)
        raise ArgumentError.new("Malformed version number string #{version}") if match.nil?

        Gem::Version.new(
          [
            match[:major],
            match[:minor],
            match[:patch]
          ].join('.')
        )
      end

      # Validates if provided version of libmongocrypt is valid, i.e. equal or
      # greater than minimum required version. Raises a LoadError if not.
      #
      # @param [ String ] lmc_version String representing libmongocrypt version.
      #
      # @raise [ LoadError ] if given version is lesser than minimum required version.
      #
      # @api private
      def self.validate_version(lmc_version)
        if (actual_version = parse_version(lmc_version)) < MIN_LIBMONGOCRYPT_VERSION
          raise LoadError, "libmongocrypt version #{MIN_LIBMONGOCRYPT_VERSION} or above is required, " +
            "but version #{actual_version} was found."
        end
      end

      validate_version(mongocrypt_version(nil))

      # @!method self.mongocrypt_binary_new
      #   @api private
      #
      #   Creates a new mongocrypt_binary_t object (a non-owning view of a byte
      #     array).
      #   @return [ FFI::Pointer ] A pointer to the newly-created
      #     mongocrypt_binary_t object.
      attach_function :mongocrypt_binary_new, [], :pointer

      # @!method self.mongocrypt_binary_new_from_data(data, len)
      #   @api private
      #
      #   Create a new mongocrypt_binary_t object that maintains a pointer to
      #     the specified byte array.
      #   @param [ FFI::Pointer ] data A pointer to an array of bytes; the data
      #     is not copied and must outlive the mongocrypt_binary_t object.
      #   @param [ Integer ] len The length of the array argument.
      #   @return [ FFI::Pointer ] A pointer to the newly-created
      #     mongocrypt_binary_t object.
      attach_function(
        :mongocrypt_binary_new_from_data,
        [:pointer, :int],
        :pointer
      )

      # @!method self.mongocrypt_binary_data(binary)
      #   @api private
      #
      #   Get the pointer to the underlying data for the mongocrypt_binary_t.
      #   @param [ FFI::Pointer ] binary A pointer to a mongocrypt_binary_t object.
      #   @return [ FFI::Pointer ] A pointer to the data array.
      attach_function :mongocrypt_binary_data, [:pointer], :pointer

      # @!method self.mongocrypt_binary_len(binary)
      #   @api private
      #
      #   Get the length of the underlying data array.
      #   @param [ FFI::Pointer ] binary A pointer to a mongocrypt_binary_t object.
      #   @return [ Integer ] The length of the data array.
      attach_function :mongocrypt_binary_len, [:pointer], :int

      def self.get_binary_data_direct(mongocrypt_binary_t)
        mongocrypt_binary_t.get_pointer(0)
      end

      def self.get_binary_len_direct(mongocrypt_binary_t)
        mongocrypt_binary_t.get_uint32(FFI::NativeType::POINTER.size)
      end

      # @!method self.mongocrypt_binary_destroy(binary)
      #   @api private
      #
      #   Destroy the mongocrypt_binary_t object.
      #   @param [ FFI::Pointer ] binary A pointer to a mongocrypt_binary_t object.
      #   @return [ nil ] Always nil.
      attach_function :mongocrypt_binary_destroy, [:pointer], :void

      # Enum labeling different status types
      enum :status_type, [
        :ok,            0,
        :error_client,  1,
        :error_kms,     2,
      ]

      # @!method self.mongocrypt_status_new
      #   @api private
      #
      #   Create a new mongocrypt_status_t object.
      #   @return [ FFI::Pointer ] A pointer to the new mongocrypt_status_ts.
      attach_function :mongocrypt_status_new, [], :pointer

      # @!method self.mongocrypt_status_set(status, type, code, message, len)
      #   @api private
      #
      #   Set a message, type, and code on an existing status.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t.
      #   @param [ Symbol ] type The status type; possible values are defined
      #     by the status_type enum.
      #   @param [ Integer ] code The status code.
      #   @param [ String ] message The status message.
      #   @param [ Integer ] len The length of the message argument (or -1 for a
      #     null-terminated string).
      #   @return [ nil ] Always nil.
      attach_function(
        :mongocrypt_status_set,
        [:pointer, :status_type, :int, :string, :int],
        :void
      )

      # @!method self.mongocrypt_status_type(status)
      #   @api private
      #
      #   Indicates the status type.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t.
      #   @return [ Symbol ] The status type (as defined by the status_type enum).
      attach_function :mongocrypt_status_type, [:pointer], :status_type

      # @!method self.mongocrypt_status_code(status)
      #   @api private
      #
      #   Return the status error code.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t.
      #   @return [ Integer ] The status code.
      attach_function :mongocrypt_status_code, [:pointer], :int

      # @!method self.mongocrypt_status_message(status, len=nil)
      #   @api private
      #
      #   Returns the status message.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t.
      #   @param [ FFI::Pointer | nil ] len (out param) An optional pointer to a
      #     uint32, where the length of the retun string will be written.
      #   @return [ String ] The status message.
      attach_function :mongocrypt_status_message, [:pointer, :pointer], :string

      # @!method self.mongocrypt_status_ok(status)
      #   @api private
      #
      #   Returns whether the status is ok or an error.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t.
      #   @return [ Boolean ] Whether the status is ok.
      attach_function :mongocrypt_status_ok, [:pointer], :bool

      # @!method self.mongocrypt_status_destroy(status)
      #   @api private
      #
      #   Destroys the reference to the mongocrypt_status_t object.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t.
      #   @return [ nil ] Always nil.
      attach_function :mongocrypt_status_destroy, [:pointer], :void

      # Enum labeling the various log levels
      enum :log_level, [
        :fatal,   0,
        :error,   1,
        :warn,    2,
        :info,    3,
        :debug,   4,
      ]

      # @!method mongocrypt_log_fn_t(level, message, len, ctx)
      #   @api private
      #
      #   A callback to the mongocrypt log function. Set a custom log callback
      #     with the mongocrypt_setopt_log_handler method
      #   @param [ Symbol ] level The log level; possible values defined by the
      #     log_level enum
      #   @param [ String ] message The log message
      #   @param [ Integer ] len The length of the message param, or -1 if the
      #     string is null terminated
      #   @param [ FFI::Pointer | nil ] ctx An optional pointer to a context
      #     object when this callback was set
      #   @return [ nil ] Always nil.
      #
      #   @note This defines a method signature for an FFI callback; it is not
      #     an instance method on the Binding class.
      callback :mongocrypt_log_fn_t, [:log_level, :string, :int, :pointer], :void

      # @!method self.ongocrypt_new
      #   @api private
      #
      #   Creates a new mongocrypt_t object.
      #   @return [ FFI::Pointer ] A pointer to a new mongocrypt_t object.
      attach_function :mongocrypt_new, [], :pointer

      # @!method self.mongocrypt_setopt_log_handler(crypt, log_fn, log_ctx=nil)
      #   @api private
      #
      #   Set the handler on the mongocrypt_t object to be called every time
      #     libmongocrypt logs a message.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @param [ Method ] log_fn A logging callback method.
      #   @param [ FFI::Pointer | nil ] log_ctx An optional pointer to a context
      #     to be passed into the log callback on every invocation.
      #   @return [ Boolean ] Whether setting the callback was successful.
      attach_function(
        :mongocrypt_setopt_log_handler,
        [:pointer, :mongocrypt_log_fn_t, :pointer],
        :bool
      )

      # Set the logger callback function on the Mongo::Crypt::Handle object
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ Method ] log_callback
      #
      # @raise [ Mongo::Error::CryptError ] If the callback is not set successfully
      def self.setopt_log_handler(handle, log_callback)
        check_status(handle) do
          mongocrypt_setopt_log_handler(handle, log_callback, nil)
        end
      end

      # @!method self.mongocrypt_setopt_kms_providers(crypt, kms_providers)
      #   @api private
      #
      #   Configure KMS providers with a BSON document.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @param [ FFI::Pointer ] kms_providers A pointer to a
      #     mongocrypt_binary_t object that references a BSON document mapping
      #     the KMS provider names to credentials.
      #   @note Do not initialize ctx before calling this method.
      #
      #   @returns [ true | false ] Returns whether the options was set successfully.
      attach_function(
        :mongocrypt_setopt_kms_providers,
        [:pointer, :pointer],
        :bool
      )

      # Set KMS providers options on the Mongo::Crypt::Handle object
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ BSON::Document ] kms_providers BSON document mapping
      #   the KMS provider names to credentials.
      #
      # @raise [ Mongo::Error::CryptError ] If the option is not set successfully
      def self.setopt_kms_providers(handle, kms_providers)
        validate_document(kms_providers)
        data = kms_providers.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_status(handle) do
            mongocrypt_setopt_kms_providers(handle.ref, data_p)
          end
        end
      end

      # @!method self.mongocrypt_setopt_schema_map(crypt, schema_map)
      #   @api private
      #
      #   Sets a local schema map for encryption.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @param [ FFI::Pointer ] schema_map A pointer to a mongocrypt_binary_t.
      #     object that references the schema map as a BSON binary string.
      #   @return [ Boolean ] Returns whether the option was set successfully.
      attach_function :mongocrypt_setopt_schema_map, [:pointer, :pointer], :bool

      # Set schema map on the Mongo::Crypt::Handle object
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ BSON::Document ] schema_map_doc The schema map as a
      #   BSON::Document object
      #
      # @raise [ Mongo::Error::CryptError ] If the schema map is not set successfully
      def self.setopt_schema_map(handle, schema_map_doc)
        validate_document(schema_map_doc)
        data = schema_map_doc.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_status(handle) do
            mongocrypt_setopt_schema_map(handle.ref, data_p)
          end
        end
      end

      # @!method self.mongocrypt_init(crypt)
      #   @api private
      #
      #   Initialize the mongocrypt_t object.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @return [ Boolean ] Returns whether the crypt was initialized successfully.
      attach_function :mongocrypt_init, [:pointer], :bool

      # Initialize the Mongo::Crypt::Handle object
      #
      # @param [ Mongo::Crypt::Handle ] handle
      #
      # @raise [ Mongo::Error::CryptError ] If initialization fails
      def self.init(handle)
        check_status(handle) do
          mongocrypt_init(handle.ref)
        end
      end

      # @!method self.mongocrypt_status(crypt, status)
      #   @api private
      #
      #   Set the status information from the mongocrypt_t object on the
      #     mongocrypt_status_t object.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t object.
      #   @return [ Boolean ] Whether the status was successfully set.
      attach_function :mongocrypt_status, [:pointer, :pointer], :bool

      # @!method self.mongocrypt_destroy(crypt)
      #   @api private
      #
      #   Destroy the reference the mongocrypt_t object.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @return [ nil ] Always nil.
      attach_function :mongocrypt_destroy, [:pointer], :void

      # @!method self.mongocrypt_ctx_new(crypt)
      #   @api private
      #
      #   Create a new mongocrypt_ctx_t object (a wrapper for the libmongocrypt
      #     state machine).
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @return [ FFI::Pointer ] A new mongocrypt_ctx_t object.
      attach_function :mongocrypt_ctx_new, [:pointer], :pointer

      # @!method self.mongocrypt_ctx_status(ctx, status)
      #   @api private
      #
      #   Set the status information from the mongocrypt_ctx_t object on the
      #     mongocrypt_status_t object.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t object.
      #   @return [ Boolean ] Whether the status was successfully set.
      attach_function :mongocrypt_ctx_status, [:pointer, :pointer], :bool

      # @!method self.mongocrypt_ctx_setopt_key_id(ctx, key_id)
      #   @api private
      #
      #   Set the key id used for explicit encryption.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] key_id A pointer to a mongocrypt_binary_t object
      #     that references the 16-byte key-id.
      #   @note Do not initialize ctx before calling this method.
      #   @return [ Boolean ] Whether the option was successfully set.
      attach_function :mongocrypt_ctx_setopt_key_id, [:pointer, :pointer], :bool

      # Sets the key id option on an explicit encryption context.
      #
      # @param [ Mongo::Crypt::Context ] context Explicit encryption context
      # @param [ String ] key_id The key id
      #
      # @raise [ Mongo::Error::CryptError ] If the operation failed
      def self.ctx_setopt_key_id(context, key_id)
        Binary.wrap_string(key_id) do |key_id_p|
          check_ctx_status(context) do
            mongocrypt_ctx_setopt_key_id(context.ctx_p, key_id_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_setopt_key_alt_name(ctx, binary)
      #   @api private
      #
      #   When creating a data key, set an alternate name on that key. When
      #     performing explicit encryption, specifying which data key to use for
      #     encryption based on its keyAltName field.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] binary A pointer to a mongocrypt_binary_t
      #     object that references a BSON document in the format
      #     { "keyAltName": <BSON UTF8 value> }.
      #   @return [ Boolean ] Whether the alternative name was successfully set.
      #   @note Do not initialize ctx before calling this method.
      attach_function(
        :mongocrypt_ctx_setopt_key_alt_name,
        [:pointer, :pointer],
        :bool
      )

      # Set multiple alternate key names on data key creation
      #
      # @param [ Mongo::Crypt::Context ] context A DataKeyContext
      # @param [ Array ] key_alt_names An array of alternate key names as strings
      #
      # @raise [ Mongo::Error::CryptError ] If any of the alternate names are
      #   not valid UTF8 strings
      def self.ctx_setopt_key_alt_names(context, key_alt_names)
        key_alt_names.each do |key_alt_name|
          key_alt_name_bson = { :keyAltName => key_alt_name }.to_bson.to_s

          Binary.wrap_string(key_alt_name_bson) do |key_alt_name_p|
            check_ctx_status(context) do
              mongocrypt_ctx_setopt_key_alt_name(context.ctx_p, key_alt_name_p)
            end
          end
        end
      end

      # @!method self.mongocrypt_ctx_setopt_key_material(ctx, binary)
      #   @api private
      #
      #   When creating a data key, set a custom key material to use for
      #     encrypting data.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] binary A pointer to a mongocrypt_binary_t
      #     object that references the data encryption key to use.
      #   @return [ Boolean ] Whether the custom key material was successfully set.
      #   @note Do not initialize ctx before calling this method.
      attach_function(
        :mongocrypt_ctx_setopt_key_material,
        [:pointer, :pointer],
        :bool
      )

      # Set set a custom key material to use for
      #     encrypting data.
      #
      # @param [ Mongo::Crypt::Context ] context A DataKeyContext
      # @param [ BSON::Binary ] key_material 96 bytes of custom key material
      #
      # @raise [ Mongo::Error::CryptError ] If the key material is not 96 bytes.
      def self.ctx_setopt_key_material(context, key_material)
        data = {'keyMaterial' => key_material}.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_setopt_key_material(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_setopt_algorithm(ctx, algorithm, len)
      #   @api private
      #
      #   Set the algorithm used for explicit encryption.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ String ] algorithm The algorithm name. Valid values are:
      #     - "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
      #     - "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
      #   @param [ Integer ] len The length of the algorithm string.
      #   @note Do not initialize ctx before calling this method.
      #   @return [ Boolean ] Whether the option was successfully set.
      attach_function(
        :mongocrypt_ctx_setopt_algorithm,
        [:pointer, :string, :int],
        :bool
      )

      # Set the algorithm on the context
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ String ] name The algorithm name. Valid values are:
      #   - "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
      #   - "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
      #
      # @raise [ Mongo::Error::CryptError ] If the operation failed
      def self.ctx_setopt_algorithm(context, name)
        check_ctx_status(context) do
          mongocrypt_ctx_setopt_algorithm(context.ctx_p, name, -1)
        end
      end

      # @!method self.mongocrypt_ctx_setopt_key_encryption_key(ctx)
      #   @api private
      #
      #   Set key encryption key document for creating a data key.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] bin A pointer to a mongocrypt_binary_t
      #     object that references a BSON document representing the key
      #     encryption key document with an additional "provider" field.
      #   @note Do not initialize ctx before calling this method.
      #   @return [ Boolean ] Whether the option was successfully set.
      attach_function(
        :mongocrypt_ctx_setopt_key_encryption_key,
        [:pointer, :pointer],
        :bool
      )

      # Set key encryption key document for creating a data key.
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ BSON::Document ] key_document BSON document representing the key
      #     encryption key document with an additional "provider" field.
      #
      # @raise [ Mongo::Error::CryptError ] If the operation failed
      def self.ctx_setopt_key_encryption_key(context, key_document)
        validate_document(key_document)
        data = key_document.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_setopt_key_encryption_key(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_datakey_init(ctx)
      #   @api private
      #
      #   Initializes the ctx to create a data key.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @note Before calling this method, master key options must be set.
      #     Set AWS master key by calling mongocrypt_ctx_setopt_masterkey_aws
      #     and mongocrypt_ctx_setopt_masterkey_aws_endpoint. Set local master
      #     key by calling mongocrypt_ctx_setopt_masterkey_local.
      #   @return [ Boolean ] Whether the initialization was successful.
      attach_function :mongocrypt_ctx_datakey_init, [:pointer], :bool

      # Initialize the Context to create a data key
      #
      # @param [ Mongo::Crypt::Context ] context
      #
      # @raise [ Mongo::Error::CryptError ] If initialization fails
      def self.ctx_datakey_init(context)
        check_ctx_status(context) do
          mongocrypt_ctx_datakey_init(context.ctx_p)
        end
      end

      # @!method self.mongocrypt_ctx_datakey_init(ctx, filter)
      #   @api private
      #
      # Initialize a context to rewrap datakeys.
      #
      # @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      # @param [ FFI::Pointer ] filter A pointer to a  mongocrypt_binary_t object
      #   that represents filter to use for the find command on the key vault
      #   collection to retrieve datakeys to rewrap.
      #
      # @return [ Boolean ] Whether the initialization was successful.
      attach_function(
        :mongocrypt_ctx_rewrap_many_datakey_init,
        [:pointer, :pointer],
        :bool
      )

      # Initialize a context to rewrap datakeys.
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ BSON::Document ] filter BSON Document
      #   that represents filter to use for the find command on the key vault
      #   collection to retrieve datakeys to rewrap.
      #
      # @return [ Boolean ] Whether the initialization was successful.
      def self.ctx_rewrap_many_datakey_init(context, filter)
        filter_data = filter.to_bson.to_s
        Binary.wrap_string(filter_data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_rewrap_many_datakey_init(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_encrypt_init(ctx, db, db_len, cmd)
      #   @api private
      #
      #   Initializes the ctx for auto-encryption.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ String ] db The database name.
      #   @param [ Integer ] db_len The length of the database name argument
      #     (or -1 for a null-terminated string).
      #   @param [ FFI::Pointer ] cmd A pointer to a mongocrypt_binary_t object
      #     that references the database command as a binary string.
      #   @note This method expects the passed-in BSON to be in the format:
      #     { "v": BSON value to decrypt }.
      #   @return [ Boolean ] Whether the initialization was successful.
      attach_function(
        :mongocrypt_ctx_encrypt_init,
        [:pointer, :string, :int, :pointer],
        :bool
      )

      # Initialize the Context for auto-encryption
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ String ] db_name The name of the database against which the
      #   encrypted command is being performed
      # @param [ Hash ] command The command to be encrypted
      #
      # @raise [ Mongo::Error::CryptError ] If initialization fails
      def self.ctx_encrypt_init(context, db_name, command)
        validate_document(command)
        data = command.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_encrypt_init(context.ctx_p, db_name, -1, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_explicit_encrypt_init(ctx, msg)
      #   @api private
      #
      #   Initializes the ctx for explicit encryption.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] msg A pointer to a mongocrypt_binary_t object
      #     that references the message to be encrypted as a binary string.
      #   @note Before calling this method, set a key_id, key_alt_name (optional),
      #     and encryption algorithm using the following methods:
      #     mongocrypt_ctx_setopt_key_id, mongocrypt_ctx_setopt_key_alt_name,
      #     and mongocrypt_ctx_setopt_algorithm.
      #   @return [ Boolean ] Whether the initialization was successful.
      attach_function(
        :mongocrypt_ctx_explicit_encrypt_init,
        [:pointer, :pointer],
        :bool
      )

      # Initialize the Context for explicit encryption
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ Hash ] doc A BSON document to encrypt
      #
      # @raise [ Mongo::Error::CryptError ] If initialization fails
      def self.ctx_explicit_encrypt_init(context, doc)
        validate_document(doc)
        data = doc.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_explicit_encrypt_init(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_explicit_encrypt_init(ctx, msg)
      #   @api private
      #
      #   Initializes the ctx for explicit expression encryption.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] msg A pointer to a mongocrypt_binary_t object
      #     that references the message to be encrypted as a binary string.
      #   @note Before calling this method, set a key_id, key_alt_name (optional),
      #     and encryption algorithm using the following methods:
      #     mongocrypt_ctx_setopt_key_id, mongocrypt_ctx_setopt_key_alt_name,
      #     and mongocrypt_ctx_setopt_algorithm.
      #   @return [ Boolean ] Whether the initialization was successful.
      attach_function(
        :mongocrypt_ctx_explicit_encrypt_expression_init,
        [:pointer, :pointer],
        :bool
      )

      # Initialize the Context for explicit expression encryption.
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ Hash ] doc A BSON document to encrypt
      #
      # @raise [ Mongo::Error::CryptError ] If initialization fails
      def self.ctx_explicit_encrypt_expression_init(context, doc)
        validate_document(doc)
        data = doc.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_explicit_encrypt_expression_init(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_decrypt_init(ctx, doc)
      #   @api private
      #
      #   Initializes the ctx for auto-decryption.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] doc A pointer to a mongocrypt_binary_t object
      #     that references the document to be decrypted as a BSON binary string.
      #   @return [ Boolean ] Whether the initialization was successful.
      attach_function :mongocrypt_ctx_decrypt_init, [:pointer, :pointer], :bool

      # Initialize the Context for auto-decryption
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ BSON::Document ] command A BSON document to decrypt
      #
      # @raise [ Mongo::Error::CryptError ] If initialization fails
      def self.ctx_decrypt_init(context, command)
        validate_document(command)
        data = command.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_decrypt_init(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_explicit_decrypt_init(ctx, msg)
      #   @api private
      #
      #   Initializes the ctx for explicit decryption.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] msg A pointer to a mongocrypt_binary_t object
      #     that references the message to be decrypted as a BSON binary string.
      #   @return [ Boolean ] Whether the initialization was successful.
      attach_function(
        :mongocrypt_ctx_explicit_decrypt_init,
        [:pointer, :pointer],
        :bool
      )

      # Initialize the Context for explicit decryption
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ Hash ] doc A BSON document to decrypt
      #
      # @raise [ Mongo::Error::CryptError ] If initialization fails
      def self.ctx_explicit_decrypt_init(context, doc)
        validate_document(doc)
        data = doc.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_explicit_decrypt_init(context.ctx_p, data_p)
          end
        end
      end

      # An enum labeling different libmognocrypt state machine states
      enum :mongocrypt_ctx_state, [
        :error,                 0,
        :need_mongo_collinfo,   1,
        :need_mongo_markings,   2,
        :need_mongo_keys,       3,
        :need_kms,              4,
        :ready,                 5,
        :done,                  6,
        :need_kms_credentials,  7,
      ]

      # @!method self.mongocrypt_ctx_state(ctx)
      #   @api private
      #
      #   Get the current state of the ctx.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @return [ Symbol ] The current state, will be one of the values defined
      #     by the mongocrypt_ctx_state enum.
      attach_function :mongocrypt_ctx_state, [:pointer], :mongocrypt_ctx_state

      # @!method self.mongocrypt_ctx_mongo_op(ctx, op_bson)
      #   @api private
      #
      #   Get a BSON operation for the driver to run against the MongoDB
      #     collection, the key vault database, or mongocryptd.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] op_bson (out param) A pointer to a
      #     mongocrypt_binary_t object that will have a reference to the
      #     BSON operation written to it by libmongocrypt.
      #   @return [ Boolean ] A boolean indicating the success of the operation.
      attach_function :mongocrypt_ctx_mongo_op, [:pointer, :pointer], :bool

      # Returns a BSON::Document representing an operation that the
      # driver must perform on behalf of libmongocrypt to get the
      # information it needs in order to continue with
      # encryption/decryption (for example, a filter for a key vault query).
      #
      # @param [ Mongo::Crypt::Context ] context
      #
      # @raise [ Mongo::Crypt ] If there is an error getting the operation
      # @return [ BSON::Document ] The operation that the driver must perform
      def self.ctx_mongo_op(context)
        binary = Binary.new

        check_ctx_status(context) do
          mongocrypt_ctx_mongo_op(context.ctx_p, binary.ref)
        end

        # TODO since the binary references a C pointer, and ByteBuffer is
        # written in C in MRI, we could omit a copy of the data by making
        # ByteBuffer reference the string that is owned by libmongocrypt.
        BSON::Document.from_bson(BSON::ByteBuffer.new(binary.to_s), mode: :bson)
      end

      # @!method self.mongocrypt_ctx_mongo_feed(ctx, reply)
      #   @api private
      #
      #   Feed a BSON reply to libmongocrypt.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] reply A mongocrypt_binary_t object that
      #     references the BSON reply to feed to libmongocrypt.
      #   @return [ Boolean ] A boolean indicating the success of the operation.
      attach_function :mongocrypt_ctx_mongo_feed, [:pointer, :pointer], :bool

      # Feed a response from the driver back to libmongocrypt
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ BSON::Document ] doc The document representing the response
      #
      # @raise [ Mongo::Error::CryptError ] If the response is not fed successfully
      def self.ctx_mongo_feed(context, doc)
        validate_document(doc)
        data = doc.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_mongo_feed(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_mongo_done(ctx)
      #   @api private
      #
      #   Indicate to libmongocrypt that the driver is done feeding replies.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @return [ Boolean ] A boolean indicating the success of the operation.
      attach_function :mongocrypt_ctx_mongo_done, [:pointer], :bool

      # @!method self.mongocrypt_ctx_mongo_next_kms_ctx(ctx)
      #   @api private
      #
      #   Return a pointer to a mongocrypt_kms_ctx_t object or NULL.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @return [ FFI::Pointer ] A pointer to a mongocrypt_kms_ctx_t object.
      attach_function :mongocrypt_ctx_next_kms_ctx, [:pointer], :pointer

      # Return a new KmsContext object needed by a Context object.
      #
      # @param [ Mongo::Crypt::Context ] context
      #
      # @return [ Mongo::Crypt::KmsContext | nil ] The KmsContext needed to
      #   fetch an AWS master key or nil, if no KmsContext is needed
      def self.ctx_next_kms_ctx(context)
        kms_ctx_p = mongocrypt_ctx_next_kms_ctx(context.ctx_p)

        if kms_ctx_p.null?
          nil
        else
          KmsContext.new(kms_ctx_p)
        end
      end

      # @!method self.mongocrypt_kms_ctx_get_kms_provider(crypt, kms_providers)
      #   @api private
      #
      # Get the KMS provider identifier associated with this KMS request.
      #
      # This is used to conditionally configure TLS connections based on the KMS
      # request. It is useful for KMIP, which authenticates with a client
      # certificate.
      #
      # @param [ FFI::Pointer ] kms Pointer mongocrypt_kms_ctx_t object.
      # @param [ FFI::Pointer ] len (outparam) Receives the length of the
      #   returned string. It may be NULL. If it is not NULL, it is set to
      #   the length of the returned string without the NULL terminator.
      #
      # @returns [ FFI::Pointer ] One of the NULL terminated static strings: "aws", "azure", "gcp", or
      # "kmip".
      attach_function(
        :mongocrypt_kms_ctx_get_kms_provider,
        [:pointer, :pointer],
        :pointer
      )

      # Get the KMS provider identifier associated with this KMS request.
      #
      # This is used to conditionally configure TLS connections based on the KMS
      # request. It is useful for KMIP, which authenticates with a client
      # certificate.
      #
      # @param [ FFI::Pointer ] kms Pointer mongocrypt_kms_ctx_t object.
      #
      # @returns [ Symbol | nil ] KMS provider identifier.
      def self.kms_ctx_get_kms_provider(kms_context)
        len_ptr = FFI::MemoryPointer.new(:uint32, 1)
        provider = mongocrypt_kms_ctx_get_kms_provider(
          kms_context.kms_ctx_p,
          len_ptr
        )
        if len_ptr.nil?
          nil
        else
          len = if BSON::Environment.jruby?
            # JRuby FFI implementation does not have `read(type)` method, but it
            # has this `get_uint32`.
            len_ptr.get_uint32
          else
            # For MRI we use a documented `read` method - https://www.rubydoc.info/github/ffi/ffi/FFI%2FPointer:read
            len_ptr.read(:uint32)
          end
          provider.read_string(len).to_sym
        end
      end

      # @!method self.mongocrypt_kms_ctx_message(kms, msg)
      #   @api private
      #
      #   Get the message needed to fetch the AWS KMS master key.
      #   @param [ FFI::Pointer ] kms Pointer to the mongocrypt_kms_ctx_t object
      #   @param [ FFI::Pointer ] msg (outparam) Pointer to a mongocrypt_binary_t
      #     object that will have the location of the message written to it by
      #     libmongocrypt.
      #   @return [ Boolean ] Whether the operation is successful.
      attach_function :mongocrypt_kms_ctx_message, [:pointer, :pointer], :bool

      # Get the HTTP message needed to fetch the AWS KMS master key from a
      # KmsContext object.
      #
      # @param [ Mongo::Crypt::KmsContext ] kms_context
      #
      # @raise [ Mongo::Error::CryptError ] If the response is not fed successfully
      #
      # @return [ String ] The HTTP message
      def self.kms_ctx_message(kms_context)
        binary = Binary.new

        check_kms_ctx_status(kms_context) do
          mongocrypt_kms_ctx_message(kms_context.kms_ctx_p, binary.ref)
        end

        return binary.to_s
      end

      # @!method self.mongocrypt_kms_ctx_endpoint(kms, endpoint)
      #   @api private
      #
      #   Get the hostname with which to connect over TLS to get information about
      #     the AWS master key.
      #   @param [ FFI::Pointer ] kms A pointer to a mongocrypt_kms_ctx_t object.
      #   @param [ FFI::Pointer ] endpoint (out param) A pointer to which the
      #     endpoint string will be written by libmongocrypt.
      #   @return [ Boolean ] Whether the operation was successful.
      attach_function :mongocrypt_kms_ctx_endpoint, [:pointer, :pointer], :bool

      # Get the hostname with which to connect over TLS to get information
      # about the AWS master key.
      #
      # @param [ Mongo::Crypt::KmsContext ] kms_context
      #
      # @raise [ Mongo::Error::CryptError ] If the response is not fed successfully
      #
      # @return [ String | nil ] The hostname, or nil if none exists
      def self.kms_ctx_endpoint(kms_context)
        ptr = FFI::MemoryPointer.new(:pointer, 1)

        check_kms_ctx_status(kms_context) do
          mongocrypt_kms_ctx_endpoint(kms_context.kms_ctx_p, ptr)
        end

        str_ptr = ptr.read_pointer
        str_ptr.null? ? nil : str_ptr.read_string.force_encoding('UTF-8')
      end

      # @!method self.mongocrypt_kms_ctx_bytes_needed(kms)
      #   @api private
      #
      #   Get the number of bytes needed by the KMS context.
      #   @param [ FFI::Pointer ] kms The mongocrypt_kms_ctx_t object.
      #   @return [ Integer ] The number of bytes needed.
      attach_function :mongocrypt_kms_ctx_bytes_needed, [:pointer], :int

      # Get the number of bytes needed by the KmsContext.
      #
      # @param [ Mongo::Crypt::KmsContext ] kms_context
      #
      # @return [ Integer ] The number of bytes needed
      def self.kms_ctx_bytes_needed(kms_context)
        mongocrypt_kms_ctx_bytes_needed(kms_context.kms_ctx_p)
      end

      # @!method self.mongocrypt_kms_ctx_feed(kms, bytes)
      #   @api private
      #
      #   Feed replies from the KMS back to libmongocrypt.
      #   @param [ FFI::Pointer ] kms A pointer to the mongocrypt_kms_ctx_t object.
      #   @param [ FFI::Pointer ] bytes A pointer to a mongocrypt_binary_t
      #     object that references the response from the KMS.
      #   @return [ Boolean ] Whether the operation was successful.
      attach_function :mongocrypt_kms_ctx_feed, [:pointer, :pointer], :bool

      # Feed replies from the KMS back to libmongocrypt.
      #
      # @param [ Mongo::Crypt::KmsContext ] kms_context
      # @param [ String ] bytes The data to feed to libmongocrypt
      #
      # @raise [ Mongo::Error::CryptError ] If the response is not fed successfully
      def self.kms_ctx_feed(kms_context, bytes)
        check_kms_ctx_status(kms_context) do
          Binary.wrap_string(bytes) do |bytes_p|
            mongocrypt_kms_ctx_feed(kms_context.kms_ctx_p, bytes_p)
          end
        end
      end

      # @!method self.mongocrypt_kms_ctx_status(kms, status)
      #   @api private
      #
      #   Write status information about the mongocrypt_kms_ctx_t object
      #     to the mongocrypt_status_t object.
      #   @param [ FFI::Pointer ] kms A pointer to the mongocrypt_kms_ctx_t object.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t object.
      #   @return [ Boolean ] Whether the operation was successful.
      attach_function :mongocrypt_kms_ctx_status, [:pointer, :pointer], :bool

      # If the provided block returns false, raise a CryptError with the
      # status information from the provided KmsContext object.
      #
      # @param [ Mongo::Crypt::KmsContext ] kms_context
      #
      # @raise [ Mongo::Error::CryptError ] If the provided block returns false
      def self.check_kms_ctx_status(kms_context)
        unless yield
          status = Status.new

          mongocrypt_kms_ctx_status(kms_context.kms_ctx_p, status.ref)
          status.raise_crypt_error(kms: true)
        end
      end

      # @!method self.mongocrypt_kms_ctx_usleep(ctx)
      #   @api private
      #
      #   Indicates how long to sleep before sending KMS request.
      #
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @return [ int64 ] A 64-bit encoded number of microseconds of how long to sleep.
      attach_function :mongocrypt_kms_ctx_usleep, [:pointer], :int64

      # Returns number of milliseconds to sleep before sending KMS request
      # for the given KMS context.
      #
      # @param [ Mongo::Crypt::KmsContext ] kms_context KMS Context we are going
      #   to send KMS request for.
      # @return [ Integer ] A 64-bit encoded number of microseconds to sleep.
      def self.kms_ctx_usleep(kms_context)
        mongocrypt_kms_ctx_usleep(kms_context.kms_ctx_p)
      end

      # @!method self.mongocrypt_kms_ctx_fail(ctx)
      #   @api private
      #
      # Indicate a network-level failure.
      #
      # @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      # @return [ Boolean ] whether the failed request may be retried.
      attach_function :mongocrypt_kms_ctx_fail, [:pointer], :bool

      # Check whether the last failed request for the KMS context may be retried.
      #
      # @param [ Mongo::Crypt::KmsContext ] kms_context KMS Context
      # @return [ true, false ] whether the failed request may be retried.
      def self.kms_ctx_fail(kms_context)
        mongocrypt_kms_ctx_fail(kms_context.kms_ctx_p)
      end

      # @!method self.mongocrypt_setopt_retry_kms(crypt, enable)
      #   @api private
      #
      # Enable or disable KMS retry behavior.
      #
      # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object
      # @param [ Boolean ] enable A boolean indicating whether to retry operations.
      # @return [ Boolean ] indicating success.
      attach_function :mongocrypt_setopt_retry_kms, [:pointer, :bool], :bool

      # Enable or disable KMS retry behavior.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ true, false ] value whether to retry operations.
      # @return [ true, fale ] true is the option was set, otherwise false.
      def self.kms_ctx_setopt_retry_kms(handle, value)
        mongocrypt_setopt_retry_kms(handle.ref, value)
      end


      # @!method self.mongocrypt_kms_ctx_done(ctx)
      #   @api private
      #
      #   Indicate to libmongocrypt that it will receive no more replies from
      #     mongocrypt_kms_ctx_t objects.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @return [ Boolean ] Whether the operation was successful.
      attach_function :mongocrypt_ctx_kms_done, [:pointer], :bool

      # Indicate to libmongocrypt that it will receive no more KMS replies.
      #
      # @param [ Mongo::Crypt::Context ] context
      #
      # @raise [ Mongo::Error::CryptError ] If the operation is unsuccessful
      def self.ctx_kms_done(context)
        check_ctx_status(context) do
          mongocrypt_ctx_kms_done(context.ctx_p)
        end
      end

      # @!method self.mongocrypt_ctx_finalize(ctx, op_bson)
      #   @api private
      #
      #   Perform the final encryption or decryption and return a BSON document.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] op_bson (out param) A pointer to a
      #     mongocrypt_binary_t object that will have a reference to the
      #     final encrypted BSON document.
      #   @return [ Boolean ] A boolean indicating the success of the operation.
      attach_function :mongocrypt_ctx_finalize, [:pointer, :pointer], :void

      # Finalize the state machine represented by the Context
      #
      # @param [ Mongo::Crypt::Context ] context
      #
      # @raise [ Mongo::Error::CryptError ] If the state machine is not successfully
      #   finalized
      def self.ctx_finalize(context)
        binary = Binary.new

        check_ctx_status(context) do
          mongocrypt_ctx_finalize(context.ctx_p, binary.ref)
        end

        # TODO since the binary references a C pointer, and ByteBuffer is
        # written in C in MRI, we could omit a copy of the data by making
        # ByteBuffer reference the string that is owned by libmongocrypt.
        BSON::Document.from_bson(BSON::ByteBuffer.new(binary.to_s), mode: :bson)
      end

      # @!method self.mongocrypt_ctx_destroy(ctx)
      #   @api private
      #
      #   Destroy the reference to the mongocrypt_ctx_t object.
      #   @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      #   @return [ nil ] Always nil.
      attach_function :mongocrypt_ctx_destroy, [:pointer], :void

      # @!method mongocrypt_crypto_fn(ctx, key, iv, input, output, status)
      #   @api private
      #
      #   A callback to a function that performs AES encryption or decryption.
      #   @param [ FFI::Pointer | nil] ctx An optional pointer to a context object
      #     that may have been set when hooks were enabled.
      #   @param [ FFI::Pointer ] key A pointer to a mongocrypt_binary_t object
      #     that references the 32-byte AES encryption key.
      #   @param [ FFI::Pointer ] iv A pointer to a mongocrypt_binary_t object
      #     that references the 16-byte AES IV.
      #   @param [ FFI::Pointer ] input A pointer to a mongocrypt_binary_t object
      #     that references the value to be encrypted/decrypted.
      #   @param [ FFI::Pointer ] output (out param) A pointer to a
      #     mongocrypt_binary_t object will have a reference to the encrypted/
      #     decrypted value written to it by libmongocrypt.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t
      #     object to which an error message will be written if encryption fails.
      #   @return [ Bool ] Whether encryption/decryption was successful.
      #
      #   @note This defines a method signature for an FFI callback; it is not
      #     an instance method on the Binding class.
      callback(
        :mongocrypt_crypto_fn,
        [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer],
        :bool
      )

      # @!method mongocrypt_hmac_fn(ctx, key, input, output, status)
      #   @api private
      #
      #   A callback to a function that performs HMAC SHA-512 or SHA-256.
      #   @param [ FFI::Pointer | nil ] ctx An optional pointer to a context object
      #     that may have been set when hooks were enabled.
      #   @param [ FFI::Pointer ] key A pointer to a mongocrypt_binary_t object
      #     that references the 32-byte HMAC SHA encryption key.
      #   @param [ FFI::Pointer ] input A pointer to a mongocrypt_binary_t object
      #     that references the input value.
      #   @param [ FFI::Pointer ] output (out param) A pointer to a
      #     mongocrypt_binary_t object will have a reference to the output value
      #     written to it by libmongocrypt.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t
      #     object to which an error message will be written if encryption fails.
      #   @return [ Bool ] Whether HMAC-SHA was successful.
      #
      #   @note This defines a method signature for an FFI callback; it is not
      #     an instance method on the Binding class.
      callback(
        :mongocrypt_hmac_fn,
        [:pointer, :pointer, :pointer, :pointer, :pointer],
        :bool
      )

      # @!method mongocrypt_hash_fn(ctx, input, output, status)
      #   @api private
      #
      #   A callback to a SHA-256 hash function.
      #   @param [ FFI::Pointer | nil ] ctx An optional pointer to a context object
      #     that may have been set when hooks were enabled.
      #   @param [ FFI::Pointer ] input A pointer to a mongocrypt_binary_t object
      #     that references the value to be hashed.
      #   @param [ FFI::Pointer ] output (out param) A pointer to a
      #     mongocrypt_binary_t object will have a reference to the output value
      #     written to it by libmongocrypt.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t
      #     object to which an error message will be written if encryption fails.
      #   @return [ Bool ] Whether hashing was successful.
      #
      #   @note This defines a method signature for an FFI callback; it is not
      #     an instance method on the Binding class.
      callback :mongocrypt_hash_fn, [:pointer, :pointer, :pointer, :pointer], :bool

      # @!method mongocrypt_random_fn(ctx, output, count, status)
      #   @api private
      #
      #   A callback to a crypto secure random function.
      #   @param [ FFI::Pointer | nil ] ctx An optional pointer to a context object
      #     that may have been set when hooks were enabled.
      #   @param [ FFI::Pointer ] output (out param) A pointer to a
      #     mongocrypt_binary_t object will have a reference to the output value
      #     written to it by libmongocrypt.
      #   @param [ Integer ] count The number of random bytes to return.
      #   @param [ FFI::Pointer ] status A pointer to a mongocrypt_status_t
      #     object to which an error message will be written if encryption fails.
      #   @return [ Bool ] Whether hashing was successful.
      #
      #   @note This defines a method signature for an FFI callback; it is not
      #     an instance method on the Binding class.
      callback :mongocrypt_random_fn, [:pointer, :pointer, :int, :pointer], :bool

      # @!method self.mongocrypt_setopt_crypto_hooks(crypt, aes_enc_fn, aes_dec_fn, random_fn, sha_512_fn, sha_256_fn, hash_fn, ctx=nil)
      #   @api private
      #
      #   Set crypto hooks on the provided mongocrypt object.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @param [ Proc ] aes_enc_fn An AES encryption method.
      #   @param [ Proc ] aes_dec_fn An AES decryption method.
      #   @param [ Proc ] random_fn A random method.
      #   @param [ Proc ] sha_512_fn A HMAC SHA-512 method.
      #   @param [ Proc ] sha_256_fn A HMAC SHA-256 method.
      #   @param [ Proc ] hash_fn A SHA-256 hash method.
      #   @param [ FFI::Pointer | nil ] ctx An optional pointer to a context object
      #     that may have been set when hooks were enabled.
      #   @return [ Boolean ] Whether setting this option succeeded.
      attach_function(
        :mongocrypt_setopt_crypto_hooks,
        [
          :pointer,
          :mongocrypt_crypto_fn,
          :mongocrypt_crypto_fn,
          :mongocrypt_random_fn,
          :mongocrypt_hmac_fn,
          :mongocrypt_hmac_fn,
          :mongocrypt_hash_fn,
          :pointer
        ],
        :bool
      )

      # Set crypto callbacks on the Handle
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ Method ] aes_encrypt_cb An AES encryption method
      # @param [ Method ] aes_decrypt_cb A AES decryption method
      # @param [ Method ] random_cb A method that returns a string of random bytes
      # @param [ Method ] hmac_sha_512_cb A HMAC SHA-512 method
      # @param [ Method ] hmac_sha_256_cb A HMAC SHA-256 method
      # @param [ Method ] hmac_hash_cb A SHA-256 hash method
      #
      # @raise [ Mongo::Error::CryptError ] If the callbacks aren't set successfully
      def self.setopt_crypto_hooks(handle,
        aes_encrypt_cb, aes_decrypt_cb, random_cb,
        hmac_sha_512_cb, hmac_sha_256_cb, hmac_hash_cb
      )
        check_status(handle) do
          mongocrypt_setopt_crypto_hooks(handle.ref,
            aes_encrypt_cb, aes_decrypt_cb, random_cb,
            hmac_sha_512_cb, hmac_sha_256_cb, hmac_hash_cb, nil
          )
        end
      end

      # @!method self.mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5(crypt, sign_rsaes_pkcs1_v1_5, ctx=nil)
      #   @api private
      #
      #   Set a crypto hook for the RSASSA-PKCS1-v1_5 algorithm with a SHA-256 hash.
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @param [ Proc ] sign_rsaes_pkcs1_v1_5 A RSASSA-PKCS1-v1_5 signing method.
      #   @param [ FFI::Pointer | nil ] ctx An optional pointer to a context object
      #     that may have been set when hooks were enabled.
      #   @return [ Boolean ] Whether setting this option succeeded.
      attach_function(
        :mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5,
        [
          :pointer,
          :mongocrypt_hmac_fn,
          :pointer
        ],
        :bool
      )

      # Set a crypto hook for the RSASSA-PKCS1-v1_5 algorithm with
      #   a SHA-256 hash oh the Handle.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ Method ] rsaes_pkcs_signature_cb A RSASSA-PKCS1-v1_5 signing method.
      #
      # @raise [ Mongo::Error::CryptError ] If the callbacks aren't set successfully
      def self.setopt_crypto_hook_sign_rsaes_pkcs1_v1_5(
        handle,
        rsaes_pkcs_signature_cb
      )
        check_status(handle) do
          mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5(
            handle.ref,
            rsaes_pkcs_signature_cb,
            nil
          )
        end
      end

      # @!method self.mongocrypt_setopt_encrypted_field_config_map(crypt, efc_map)
      #   @api private
      #
      # Set a local EncryptedFieldConfigMap for encryption.
      #
      # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      # @param [ FFI::Pointer ] efc_map A pointer to mongocrypt_binary_t object that
      # references a BSON document representing the EncryptedFieldConfigMap
      # supplied by the user. The keys are collection namespaces and values are
      # EncryptedFieldConfigMap documents.
      #
      # @return [ Boolean ] Whether the operation succeeded.
      attach_function(
        :mongocrypt_setopt_encrypted_field_config_map,
        [
          :pointer,
          :pointer
        ],
        :bool
      )

      # Set a local EncryptedFieldConfigMap for encryption.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ BSON::Document ] efc_map A BSON document representing
      #   the EncryptedFieldConfigMap supplied by the user.
      #   The keys are collection namespaces and values are
      #   EncryptedFieldConfigMap documents.
      #
      # @raise [ Mongo::Error::CryptError ] If the operation failed.
      def self.setopt_encrypted_field_config_map(handle, efc_map)
        validate_document(efc_map)
        data = efc_map.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_status(handle) do
            mongocrypt_setopt_encrypted_field_config_map(
              handle.ref,
              data_p
            )
          end
        end
      end

      # @!method self.mongocrypt_setopt_bypass_query_analysis(crypt)
      #   @api private
      #
      # Opt into skipping query analysis.
      #
      # If opted in:
      # - The csfle shared library will not attempt to be loaded.
      # - A mongocrypt_ctx_t will never enter the MONGOCRYPT_CTX_NEED_MARKINGS state.
      #
      # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      attach_function(:mongocrypt_setopt_bypass_query_analysis, [:pointer], :void)

      # Opt-into skipping query analysis.
      #
      # If opted in:
      # - The csfle shared library will not attempt to be loaded.
      # - A mongocrypt_ctx_t will never enter the MONGOCRYPT_CTX_NEED_MARKINGS state.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      def self.setopt_bypass_query_analysis(handle)
        mongocrypt_setopt_bypass_query_analysis(handle.ref)
      end

      # @!method self.mongocrypt_setopt_aes_256_ctr(crypt, aes_256_ctr_encrypt, aes_256_ctr_decrypt, ctx)
      #   @api private
      #
      #   Set a crypto hook for the AES256-CTR operations.
      #
      #   @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #   @param [ Proc ] aes_enc_fn An AES-CTR encryption method.
      #   @param [ Proc ] aes_dec_fn An AES-CTR decryption method.
      #   @param [ FFI::Pointer | nil ] ctx An optional pointer to a context object
      #     that may have been set when hooks were enabled.
      #   @return [ Boolean ] Whether setting this option succeeded.
      attach_function(
        :mongocrypt_setopt_aes_256_ctr,
        [
          :pointer,
          :mongocrypt_crypto_fn,
          :mongocrypt_crypto_fn,
          :pointer
        ],
        :bool
      )

      # Set a crypto hook for the AES256-CTR operations.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ Method ] aes_encrypt_cb An AES-CTR encryption method
      # @param [ Method ] aes_decrypt_cb A AES-CTR decryption method
      #
      # @raise [ Mongo::Error::CryptError ] If the callbacks aren't set successfully
      def self.setopt_aes_256_ctr(handle, aes_ctr_encrypt_cb, aes_ctr_decrypt_cb)
        check_status(handle) do
          mongocrypt_setopt_aes_256_ctr(handle.ref,
            aes_ctr_encrypt_cb, aes_ctr_decrypt_cb, nil
          )
        end
      end

      # @!method self.mongocrypt_setopt_append_crypt_shared_lib_search_path(crypt, path)
      #   @api private
      #
      # Append an additional search directory to the search path for loading
      #   the crypt_shared dynamic library.
      #
      # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      # @param [ String ] path A path to search for the crypt shared library. If the leading element of
      #   the path is the literal string "$ORIGIN", that substring will be replaced
      #   with the directory path containing the executable libmongocrypt module. If
      #   the path string is literal "$SYSTEM", then libmongocrypt will defer to the
      #   system's library resolution mechanism to find the crypt_shared library.
      attach_function(
        :mongocrypt_setopt_append_crypt_shared_lib_search_path,
        [
          :pointer,
          :string,
        ],
        :void
      )

      # Append an additional search directory to the search path for loading
      #   the crypt_shared dynamic library.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ String ] path A search path for the crypt shared library.
      def self.setopt_append_crypt_shared_lib_search_path(handle, path)
        check_status(handle) do
          mongocrypt_setopt_append_crypt_shared_lib_search_path(handle.ref, path)
        end
      end

      # @!method self.mongocrypt_setopt_set_crypt_shared_lib_path_override(crypt, path)
      #   @api private
      #
      # Set a single override path for loading the crypt shared library.
      #
      # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      # @param [ String ] path A path to crypt shared library file. If the leading element of
      #   the path is the literal string "$ORIGIN", that substring will be replaced
      #   with the directory path containing the executable libmongocrypt module.
      attach_function(
        :mongocrypt_setopt_set_crypt_shared_lib_path_override,
        [
          :pointer,
          :string,
        ],
        :void
      )

      # Set a single override path for loading the crypt shared library.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      # @param [ String ] path A path to crypt shared library file.
      def self.setopt_set_crypt_shared_lib_path_override(handle, path)
        check_status(handle) do
          mongocrypt_setopt_set_crypt_shared_lib_path_override(handle.ref, path)
        end
      end

      # @!method self.mongocrypt_crypt_shared_lib_version(crypt)
      #   @api private
      #
      # Obtain a 64-bit constant encoding the version of the loaded
      # crypt_shared library, if available.
      #
      # The version is encoded as four 16-bit numbers, from high to low:
      #
      # - Major version
      # - Minor version
      # - Revision
      # - Reserved
      #
      # For example, version 6.2.1 would be encoded as: 0x0006'0002'0001'0000
      #
      # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      #
      # @return [int64] A 64-bit encoded version number, with the version encoded as four
      #   sixteen-bit integers, or zero if no crypt_shared library was loaded.
      attach_function(
        :mongocrypt_crypt_shared_lib_version,
        [ :pointer ],
        :uint64
      )

      # Obtain a 64-bit constant encoding the version of the loaded
      # crypt_shared library, if available.
      #
      # The version is encoded as four 16-bit numbers, from high to low:
      #
      # - Major version
      # - Minor version
      # - Revision
      # - Reserved
      #
      # For example, version 6.2.1 would be encoded as: 0x0006'0002'0001'0000
      #
      # @param [ Mongo::Crypt::Handle ] handle
      #
      # @return [ Integer ] A 64-bit encoded version number, with the version encoded as four
      #   sixteen-bit integers, or zero if no crypt_shared library was loaded.
      def self.crypt_shared_lib_version(handle)
        mongocrypt_crypt_shared_lib_version(handle.ref)
      end

      # @!method self.mongocrypt_setopt_use_need_kms_credentials_state(crypt)
      #   @api private
      #
      # Opt-into handling the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state.
      #
      # If set, before entering the MONGOCRYPT_CTX_NEED_KMS state,
      # contexts may enter the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state
      # and then wait for credentials to be supplied through
      # `mongocrypt_ctx_provide_kms_providers`.
      #
      # A context will only enter MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS
      # if an empty document was set for a KMS provider in
      # `mongocrypt_setopt_kms_providers`.
      #
      # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
      attach_function(
        :mongocrypt_setopt_use_need_kms_credentials_state,
        [ :pointer ],
        :void
      )

      # Opt-into handling the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state.
      #
      # If set, before entering the MONGOCRYPT_CTX_NEED_KMS state,
      # contexts may enter the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state
      # and then wait for credentials to be supplied through
      # `mongocrypt_ctx_provide_kms_providers`.
      #
      # A context will only enter MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS
      # if an empty document was set for a KMS provider in
      # `mongocrypt_setopt_kms_providers`.
      #
      # @param [ Mongo::Crypt::Handle ] handle
      def self.setopt_use_need_kms_credentials_state(handle)
        mongocrypt_setopt_use_need_kms_credentials_state(handle.ref)
      end

      # @!method self.mongocrypt_ctx_provide_kms_providers(ctx, kms_providers)
      #   @api private
      #
      # Call in response to the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state
      # to set per-context KMS provider settings. These follow the same format
      # as `mongocrypt_setopt_kms_providers``. If no keys are present in the
      # BSON input, the KMS provider settings configured for the mongocrypt_t
      # at initialization are used.
      #
      # @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      # @param [ FFI::Pointer ] kms_providers A pointer to a
      #   mongocrypt_binary_t object that references a BSON document mapping
      #   the KMS provider names to credentials.
      #
      # @returns [ true | false ] Returns whether the options was set successfully.
      attach_function(
        :mongocrypt_ctx_provide_kms_providers,
        [ :pointer, :pointer ],
        :bool
      )

      # Call in response to the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state
      # to set per-context KMS provider settings. These follow the same format
      # as `mongocrypt_setopt_kms_providers``. If no keys are present in the
      # BSON input, the KMS provider settings configured for the mongocrypt_t
      # at initialization are used.
      #
      # @param [ Mongo::Crypt::Context ] context Encryption context.
      # @param [ BSON::Document ] kms_providers BSON document mapping
      #   the KMS provider names to credentials.
      #
      # @raise [ Mongo::Error::CryptError ] If the option is not set successfully.
      def self.ctx_provide_kms_providers(context, kms_providers)
        validate_document(kms_providers)
        data = kms_providers.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_provide_kms_providers(context.ctx_p, data_p)
          end
        end
      end

      # @!method self.mongocrypt_ctx_setopt_query_type(ctx, mongocrypt_query_type)
      #   @api private
      #
      # Set the query type to use for FLE 2 explicit encryption.
      # The query type is only used for indexed FLE 2 encryption.
      #
      # @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      # @param [ String ] query_type Type of the query.
      # @param [ Integer ] len The length of the query type string.
      #
      # @return [ Boolean ] Whether setting this option succeeded.
      attach_function(
        :mongocrypt_ctx_setopt_query_type,
        [
          :pointer,
          :string,
          :int
        ],
        :bool
      )

      # Set the query type to use for FLE 2 explicit encryption.
      # The query type is only used for indexed FLE 2 encryption.
      #
      # @param [ Mongo::Crypt::Context ] context Explicit encryption context.
      # @param [ String ] :mongocrypt_query_type query_type Type of the query.
      #
      # @raise [ Mongo::Error::CryptError ] If the operation failed.
      def self.ctx_setopt_query_type(context, query_type)
        check_ctx_status(context) do
          mongocrypt_ctx_setopt_query_type(context.ctx_p, query_type, -1)
        end
      end

      # @!method self.mongocrypt_ctx_setopt_contention_factor(ctx, contention_factor)
      #   @api private
      #
      # Set the contention factor used for explicit encryption.
      # The contention factor is only used for indexed FLE 2 encryption.
      #
      # @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      # @param [ int64 ] contention_factor
      #
      # @return [ Boolean ] Whether setting this option succeeded.
      attach_function(
        :mongocrypt_ctx_setopt_contention_factor,
        [
          :pointer,
          :int64
        ],
        :bool
      )

      # Set the contention factor used for explicit encryption.
      # The contention factor is only used for indexed FLE 2 encryption.
      #
      # @param [ Mongo::Crypt::Context ] context Explicit encryption context.
      # @param [ Integer ] factor Contention factor used for explicit encryption.
      #
      # @raise [ Mongo::Error::CryptError ] If the operation failed.
      def self.ctx_setopt_contention_factor(context, factor)
        check_ctx_status(context) do
          mongocrypt_ctx_setopt_contention_factor(context.ctx_p, factor)
        end
      end

      # @!method self.mongocrypt_ctx_setopt_algorithm_range(ctx, opts)
      #   @api private
      #
      # Set options for explicit encryption with the "range" algorithm.
      #
      # @note The Range algorithm is experimental only. It is not intended for
      # public use.
      #
      # @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
      # @param [ FFI::Pointer ] opts opts A pointer to range
      #   options document.
      #
      # @return [ Boolean ] Whether setting this option succeeded.
      attach_function(
        :mongocrypt_ctx_setopt_algorithm_range,
        [
          :pointer,
          :pointer
        ],
        :bool
      )

      # Set options for explicit encryption with the "range" algorithm.
      #
      # @note The Range algorithm is experimental only. It is not intended for
      # public use.
      #
      # @param [ Mongo::Crypt::Context ] context
      # @param [ Hash ] opts options
      #
      # @raise [ Mongo::Error::CryptError ] If the operation failed
      def self.ctx_setopt_algorithm_range(context, opts)
        validate_document(opts)
        data = opts.to_bson.to_s
        Binary.wrap_string(data) do |data_p|
          check_ctx_status(context) do
            mongocrypt_ctx_setopt_algorithm_range(context.ctx_p, data_p)
          end
        end
      end

      # Raise a Mongo::Error::CryptError based on the status of the underlying
      # mongocrypt_t object.
      #
      # @return [ nil ] Always nil.
      def self.check_status(handle)
        unless yield
          status = Status.new

          mongocrypt_status(handle.ref, status.ref)
          status.raise_crypt_error
        end
      end

      # Raise a Mongo::Error::CryptError based on the status of the underlying
      # mongocrypt_ctx_t object.
      #
      # @return [ nil ] Always nil.
      def self.check_ctx_status(context)
        if block_given?
          do_raise = !yield
        else
          do_raise = true
        end

        if do_raise
          status = Status.new

          mongocrypt_ctx_status(context.ctx_p, status.ref)
          status.raise_crypt_error
        end
      end

      # Checks that the specified data is a Hash before serializing
      # it to BSON to prevent errors from libmongocrypt
      #
      # @note All BSON::Document instances are also Hash instances
      #
      # @param [ Object ] data The data to be passed to libmongocrypt
      #
      # @raise [ Mongo::Error::CryptError ] If the data is not a Hash
      def self.validate_document(data)
        return if data.is_a?(Hash)

        if data.nil?
          message = "Attempted to pass nil data to libmongocrypt. " +
            "Data must be a Hash"
        else
          message = "Attempted to pass invalid data to libmongocrypt: #{data} " +
            "Data must be a Hash"
        end

        raise Error::CryptError.new(message)
      end
    end
  end
end
