module GPGME

  ##
  # A context within which all cryptographic operations are performed.
  #
  # More operations can be done which are not available in the higher level
  # API. Note how to create a new instance of this class in {GPGME::Ctx.new}.
  #
  class Ctx

    ##
    # Create a new instance from the given +options+. Must be released either
    # executing the operations inside a block, or executing {GPGME::Ctx#release}
    # afterwards.
    #
    # @param [Hash] options
    #  The optional parameters are as follows:
    #  * +:protocol+ Either +PROTOCOL_OpenPGP+ or +PROTOCOL_CMS+.
    #  * +:armor+ will return ASCII armored outputs if specified true.
    #  * +:textmode+ if +true+, inform the recipient that the input is text.
    #  * +:keylist_mode+ One of: +KEYLIST_MODE_LOCAL+, +KEYLIST_MODE_EXTERN+,
    #    +KEYLIST_MODE_SIGS+ or +KEYLIST_MODE_VALIDATE+.
    #  * +:pinentry_mode+ One of: +PINENTRY_MODE_DEFAULT+,
    #    +PINENTRY_MODE_ASK+, +PINENTRY_MODE_CANCEL+,
    #    +PINENTRY_MODE_ERROR+, or +PINENTRY_MODE_LOOPBACK+.
    #  * +:offline+ if set to true, dirmngr will not contact external services
    #  * +:password+ password of the passphrased password being used.
    #  * +:passphrase_callback+ A callback function. See {#set_passphrase_callback}.
    #  * +:passphrase_callback_value+ An object passed to passphrase_callback.
    #  * +:progress_callback+  A callback function. See {#set_progress_callback}.
    #  * +:progress_callback_value+ An object passed to progress_callback.
    #  * +:status_callback+ A callback function. See {#set_status_callback}.
    #  * +:status_callback_value+ An object passed to status_callback.
    #
    # @example
    #   ctx = GPGME::Ctx.new
    #   # operate on ctx
    #   ctx.release
    #
    # @example
    #   GPGME::Ctx.new do |ctx|
    #     # operate on ctx
    #   end
    #
    def self.new(options = {})
      rctx = []
      err = GPGME::gpgme_new(rctx)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      ctx = rctx[0]

      ctx.protocol          = options[:protocol]          if options[:protocol]
      ctx.armor             = options[:armor]             if options[:armor]
      ctx.textmode          = options[:textmode]          if options[:textmode]
      ctx.keylist_mode      = options[:keylist_mode]      if options[:keylist_mode]
      ctx.pinentry_mode     = options[:pinentry_mode]     if options[:pinentry_mode]
      ctx.offline           = options[:offline]           if options[:offline]
      ctx.ignore_mdc_error  = options[:ignore_mdc_error]  if options[:ignore_mdc_error]

      if options[:password]
        ctx.set_passphrase_callback GPGME::Ctx.method(:pass_function),
          options[:password]
      else
        if options[:passphrase_callback]
          ctx.set_passphrase_callback options[:passphrase_callback],
            options[:passphrase_callback_value]
        end
      end
      if options[:progress_callback]
        ctx.set_progress_callback options[:progress_callback],
          options[:progress_callback_value]
      end
      if options[:status_callback]
        ctx.set_status_callback options[:status_callback],
          options[:status_callback_value]
      end

      if block_given?
        GPGME.synchronize do
          begin
            yield ctx
          ensure
            GPGME::gpgme_release(ctx)
          end
        end
      else
        ctx
      end
    end


    ##
    # Releases the Ctx instance. Must be called if it was initialized without
    # a block.
    #
    # @example
    #   ctx = GPGME::Ctx.new
    #   # operate on ctx
    #   ctx.release
    #
    def release
      GPGME::gpgme_release(self)
    end

    ##
    # Getters and setters
    ##

    # Get the value of the Ctx flag with the given name.
    #
    # Allowed flag names may include:
    # - 'redraw'
    # - 'full-status'
    # - 'raw-description'
    # - 'export-session-key'
    # - 'override-session-key'
    # - 'include-key-block'
    # - 'auto-key-import'
    # - 'auto-key-retrieve'
    # - 'request-origin'
    # - 'no-symkey-cache'
    # - 'ignore-mdc-error'
    # - 'auto-key-locate'
    # - 'trust-model'
    # - 'extended-edit'
    # - 'cert-expire'
    # - 'key-origin'
    # - 'import-filter'
    # - 'no-auto-check-trustdb'
    #
    # Please consult the GPGPME documentation for more details
    #
    def get_ctx_flag(flag_name)
      GPGME::gpgme_get_ctx_flag(self, flag_name.to_s)
    end

    # Set the Ctx flag with the given name
    # to the given value.
    def set_ctx_flag(flag_name, val)
      err = GPGME::gpgme_set_ctx_flag(self, flag_name.to_s, val.to_s)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      val
    end

    # Set the +protocol+ used within this context. See {GPGME::Ctx.new} for
    # possible values.
    def protocol=(proto)
      err = GPGME::gpgme_set_protocol(self, proto)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      proto
    end

    # Return the +protocol+ used within this context.
    def protocol
      GPGME::gpgme_get_protocol(self)
    end

    # Tell whether the output should be ASCII armored.
    def armor=(yes)
      GPGME::gpgme_set_armor(self, yes ? 1 : 0)
      yes
    end

    # Return true if the output is ASCII armored.
    def armor
      GPGME::gpgme_get_armor(self) == 1 ? true : false
    end

    # This option ignores a MDC integrity protection failure.
    # It is required to decrypt old messages which did not use an MDC.
    # It may also be useful if a message is partially garbled,
    # but it is necessary to get as much data as possible out of that garbled message.
    # Be aware that a missing or failed MDC can be an indication of an attack.
    # Use with great caution.
    def ignore_mdc_error=(yes)
      GPGME::gpgme_set_ignore_mdc_error(self, yes ? 1 : 0)
      yes
    end

    # Return true if the MDC integrity protection is disabled.
    def ignore_mdc_error
      GPGME::gpgme_get_ignore_mdc_error(self) == 1 ? true : false
    end

    # Tell whether canonical text mode should be used.
    def textmode=(yes)
      GPGME::gpgme_set_textmode(self, yes ? 1 : 0)
      yes
    end

    # Return true if canonical text mode is enabled.
    def textmode
      GPGME::gpgme_get_textmode(self) == 1 ? true : false
    end

    # Change the default behaviour of the key listing functions.
    def keylist_mode=(mode)
      GPGME::gpgme_set_keylist_mode(self, mode)
      mode
    end

    # Return the current key listing mode.
    def keylist_mode
      GPGME::gpgme_get_keylist_mode(self)
    end

    # Change the default behaviour of the pinentry invocation.
    def pinentry_mode=(mode)
      GPGME::gpgme_set_pinentry_mode(self, mode)
      mode
    end

    # Return the current pinentry mode.
    def pinentry_mode
      GPGME::gpgme_get_pinentry_mode(self)
    end

    # Change the default behaviour of the dirmngr that might require
    # connections to external services.
    def offline=(mode)
      GPGME::gpgme_set_offline(self, mode)
      mode
    end

    # Return the current offline mode.
    def offline
      GPGME::gpgme_get_offline(self)
    end

    ##
    # Passphrase and progress callbacks
    ##

    # Set the passphrase callback with given hook value.
    # +passfunc+ should respond to +call+ with 5 arguments.
    #
    # * +obj+ the parameter +:passphrase_callback_value+ passed when creating
    #   the {GPGME::Ctx} object.
    # * +uid_hint+ hint as to what key are we asking the password for. Ex:
    #
    #   +CFB3294A50C2CFD7 Albert Llop <mrsimo@example.com>+
    #
    # * +passphrase_info+
    # * +prev_was_bad+ 0 if it's the first time the password is being asked,
    #   1 otherwise.
    # * +fd+ file descriptor where the password must be written too.
    #
    # Expects a Method object which can be obtained by the +method+ method
    # (really..).
    #
    #  ctx.set_passphrase_callback(MyModule.method(:passfunc))
    #
    # @example this method will simply return +maria+ as password.
    #  def pass_function(obj, uid_hint, passphrase_info, prev_was_bad, fd)
    #    io = IO.for_fd(fd, 'w')
    #    io.puts "maria"
    #    io.flush
    #  end
    #
    # @example this will interactively ask for the password
    #  def passfunc(obj, uid_hint, passphrase_info, prev_was_bad, fd)
    #    $stderr.write("Passphrase for #{uid_hint}: ")
    #    $stderr.flush
    #    begin
    #      system('stty -echo')
    #      io = IO.for_fd(fd, 'w')
    #      io.puts(gets)
    #      io.flush
    #    ensure
    #      (0 ... $_.length).each do |i| $_[i] = ?0 end if $_
    #      system('stty echo')
    #    end
    #    $stderr.puts
    #  end
    #
    # Note that this function doesn't work with GnuPG 2.0.  You can
    # use either GnuPG 1.x, which can be installed in parallel with
    # GnuPG 2.0, or GnuPG 2.1, which has loopback pinentry feature (see
    # {#pinentry_mode}).
    def set_passphrase_callback(passfunc, hook_value = nil)
      GPGME::gpgme_set_passphrase_cb(self, passfunc, hook_value)
    end
    alias set_passphrase_cb set_passphrase_callback

    # Set the progress callback with given hook value.
    # <i>progfunc</i> should respond to <code>call</code> with 5 arguments.
    #
    #  def progfunc(hook, what, type, current, total)
    #    $stderr.write("#{what}: #{current}/#{total}\r")
    #    $stderr.flush
    #  end
    #
    #  ctx.set_progress_callback(method(:progfunc))
    #
    def set_progress_callback(progfunc, hook_value = nil)
      GPGME::gpgme_set_progress_cb(self, progfunc, hook_value)
    end
    alias set_progress_cb set_progress_callback

    # Set the status callback with given hook value.
    # +statusfunc+ should respond to +call+ with 3 arguments.
    #
    # * +obj+ the parameter +:status_callback_value+ passed when creating
    #   the {GPGME::Ctx} object.
    # * +keyword+ the name of the status message
    # * +args+ any arguments for the status message
    #
    #  def status_function(obj, keyword, args)
    #    $stderr.puts("#{keyword} #{args}")
    #    return 0
    #  end
    def set_status_callback(statusfunc, hook_value = nil)
      GPGME::gpgme_set_status_cb(self, statusfunc, hook_value)
    end
    alias set_status_cb set_status_callback

    ##
    # Searching and iterating through keys. Used by {GPGME::Key.find}
    ##

    # Initiate a key listing operation for given pattern. If +pattern+ is
    # +nil+, all available keys are returned. If +secret_only<+ is +true+,
    # only secret keys are returned.
    #
    # Used by {GPGME::Ctx#each_key}
    def keylist_start(pattern = nil, secret_only = false)
      err = GPGME::gpgme_op_keylist_start(self, pattern, secret_only ? 1 : 0)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end

    # Advance to the next key in the key listing operation.
    #
    # Used by {GPGME::Ctx#each_key}
    def keylist_next
      rkey = []
      err = GPGME::gpgme_op_keylist_next(self, rkey)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      rkey[0]
    end

    # End a pending key list operation.
    #
    # Used by {GPGME::Ctx#each_key}
    def keylist_end
      err = GPGME::gpgme_op_keylist_end(self)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end

    # Convenient method to iterate over keys.
    #
    # If +pattern+ is +nil+, all available keys are returned. If +secret_only+
    # is +true+, only secret keys are returned.
    #
    # See {GPGME::Key.find} for an example of how to use, or for an easier way
    # to use.
    def each_key(pattern = nil, secret_only = false, &block)
      keylist_start(pattern, secret_only)
      begin
        loop { yield keylist_next }
      rescue EOFError
        # The last key in the list has already been returned.
      ensure
        keylist_end
      end
    end
    alias each_keys each_key

    # Returns the keys that match the +pattern+, or all if +pattern+ is nil.
    # Returns only secret keys if +secret_only+ is true.
    def keys(pattern = nil, secret_only = nil)
      keys = []
      each_key(pattern, secret_only) do |key|
        keys << key
      end
      keys
    end

    # Get the key with the +fingerprint+.
    # If +secret+ is +true+, secret key is returned.
    def get_key(fingerprint, secret = false)
      rkey = []
      err = GPGME::gpgme_get_key(self, fingerprint, rkey, secret ? 1 : 0)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      rkey[0]
    end

    ##
    # Import/export and generation/deletion of keys
    ##

    # Generate a new key pair.
    # +parms+ is a string which looks like
    #
    #  <GnupgKeyParms format="internal">
    #  Key-Type: DSA
    #  Key-Length: 1024
    #  Subkey-Type: ELG-E
    #  Subkey-Length: 1024
    #  Name-Real: Joe Tester
    #  Name-Comment: with stupid passphrase
    #  Name-Email: joe@foo.bar
    #  Expire-Date: 0
    #  Passphrase: abc
    #  </GnupgKeyParms>
    #
    # If +pubkey+ and +seckey+ are both set to +nil+, it stores the generated
    # key pair into your key ring.
    def generate_key(parms, pubkey = nil, seckey = nil)
      err = GPGME::gpgme_op_genkey(self, parms, pubkey, seckey)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end
    alias genkey generate_key

    # Extract the public keys that match the +recipients+. Returns a
    # {GPGME::Data} object which is not rewinded (should do +seek(0)+
    # before reading).
    #
    # Private keys cannot be exported due to GPGME restrictions.
    #
    # If passed, the key will be exported to +keydata+, which must be
    # a {GPGME::Data} object.
    def export_keys(recipients, keydata = Data.new, mode=0)
      err = GPGME::gpgme_op_export(self, recipients, mode, keydata)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      keydata
    end
    alias export export_keys

    # Add the keys in the data buffer to the key ring.
    def import_keys(keydata)
      err = GPGME::gpgme_op_import(self, keydata)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end
    alias import import_keys

    def import_result
      GPGME::gpgme_op_import_result(self)
    end

    # Delete the key from the key ring.
    # If allow_secret is false, only public keys are deleted,
    # otherwise secret keys are deleted as well.
    # If force is true, the confirmation dialog will not be displayed.
    def delete_key(key, allow_secret = false, force = false)
      err = nil
      if defined?(GPGME::gpgme_op_delete_ext)
        flag = 0
        flag ^= GPGME::DELETE_ALLOW_SECRET if allow_secret
        flag ^= GPGME::DELETE_FORCE if force
        err = GPGME::gpgme_op_delete_ext(self, key, flag)
      else
        err = GPGME::gpgme_op_delete(self, key, allow_secret ? 1 : 0)
      end
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end
    alias delete delete_key

    # Edit attributes of the key in the local key ring.
    #
    # The callback receives (hook_value, status, args, fd) where status is
    # a numeric status code (e.g., GPGME::GPGME_STATUS_GET_BOOL).
    def edit_key(key, editfunc, hook_value = nil, out = Data.new)
      err = GPGME::gpgme_op_edit(self, key, editfunc, hook_value, out)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end
    alias edit edit_key

    # Edit attributes of the key on the card.
    #
    # The callback receives (hook_value, status, args, fd) where status is
    # a numeric status code (e.g., GPGME::GPGME_STATUS_GET_BOOL).
    def edit_card_key(key, editfunc, hook_value = nil, out = Data.new)
      err = GPGME::gpgme_op_card_edit(self, key, editfunc, hook_value, out)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end
    alias edit_card edit_card_key
    alias card_edit edit_card_key

    ##
    # Crypto operations
    ##

    # Decrypt the ciphertext and return the plaintext.
    def decrypt(cipher, plain = Data.new)
      err = GPGME::gpgme_op_decrypt(self, cipher, plain)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      plain
    end

    def decrypt_verify(cipher, plain = Data.new)
      err = GPGME::gpgme_op_decrypt_verify(self, cipher, plain)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      plain
    end

    def decrypt_result
      GPGME::gpgme_op_decrypt_result(self)
    end

    # Verify that the signature in the data object is a valid signature.
    def verify(sig, signed_text = nil, plain = Data.new)
      err = GPGME::gpgme_op_verify(self, sig, signed_text, plain)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      plain
    end

    def verify_result
      GPGME::gpgme_op_verify_result(self)
    end

    # Remove the list of signers from this object.
    def clear_signers
      GPGME::gpgme_signers_clear(self)
    end

    # Add _keys_ to the list of signers.
    def add_signer(*keys)
      keys.each do |key|
        err = GPGME::gpgme_signers_add(self, key)
        exc = GPGME::error_to_exception(err)
        raise exc if exc
      end
    end

    # Create a signature for the text.
    # +plain+ is a data object which contains the text.
    # +sig+ is a data object where the generated signature is stored.
    def sign(plain, sig = Data.new, mode = GPGME::SIG_MODE_NORMAL)
      err = GPGME::gpgme_op_sign(self, plain, sig, mode)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      sig
    end

    def sign_result
      GPGME::gpgme_op_sign_result(self)
    end

    # Encrypt the plaintext in the data object for the recipients and
    # return the ciphertext.
    def encrypt(recp, plain, cipher = Data.new, flags = 0)
      err = GPGME::gpgme_op_encrypt(self, recp, flags, plain, cipher)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      cipher
    end

    def encrypt_result
      GPGME::gpgme_op_encrypt_result(self)
    end

    def encrypt_sign(recp, plain, cipher = Data.new, flags = 0)
      err = GPGME::gpgme_op_encrypt_sign(self, recp, flags, plain, cipher)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
      cipher
    end

    def spawn(file, argv, datain, dataout, dataerr, flags = 0)
      err = GPGME::gpgme_op_spawn(self, file, argv, datain, dataout, dataerr,
                                  flags)
      exc = GPGME::error_to_exception(err)
      raise exc if exc
    end

    # Generate cryptographically strong random bytes.
    # Available since GPGME 2.0.0.
    #
    # @param [Integer] size Number of bytes to generate
    # @param [Integer] mode Random generation mode (RANDOM_MODE_NORMAL or RANDOM_MODE_ZBASE32)
    # @return [String] Random bytes as a binary string
    def random_bytes(size, mode = GPGME::RANDOM_MODE_NORMAL)
      result = GPGME::gpgme_op_random_bytes(self, size, mode)
      if result.is_a?(String)
        result
      else
        exc = GPGME::error_to_exception(result)
        raise exc if exc
        result
      end
    end

    # Generate a cryptographically strong random unsigned integer value.
    # Available since GPGME 2.0.0.
    #
    # @param [Integer] limit Upper limit for the random value (exclusive)
    # @return [Integer] Random unsigned integer value in range [0, limit)
    def random_value(limit)
      result = GPGME::gpgme_op_random_value(self, limit)
      if result.is_a?(Integer) && result >= 0
        result
      else
        exc = GPGME::error_to_exception(result)
        raise exc if exc
        result
      end
    end

    def inspect
      "#<#{self.class} protocol=#{PROTOCOL_NAMES[protocol] || protocol}, \
armor=#{armor}, textmode=#{textmode}, \
keylist_mode=#{KEYLIST_MODE_NAMES[keylist_mode]}>"
    end

    private

    def self.pass_function(pass, uid_hint, passphrase_info, prev_was_bad, fd)
      # Write the passphrase directly using IO.for_fd. We set autoclose=false
      # to prevent Ruby from closing the fd (which belongs to GPGME/gpg-agent).
      # The IO object is used only within this method scope and not stored,
      # so we also ensure it isn't prematurely collected by keeping a strong
      # reference until we're done.
      io = IO.for_fd(fd, 'w')
      io.autoclose = false
      begin
        io.write "#{pass}\n"
        io.flush
      rescue => e
        # If the fd has become invalid (e.g. agent communication error),
        # re-raise as a more descriptive error.
        raise e
      end
    end

  end
end
