module Crypto.Gpgme.Crypto (

      encrypt
    , encryptSign
    , encryptFd
    , encryptSignFd
    , encrypt'
    , encryptSign'
    , decrypt
    , decryptFd
    , decryptVerifyFd
    , decrypt'
    , decryptVerify
    , decryptVerify'
    , verify
    , verify'
    , verifyDetached
    , verifyDetached'
    , verifyPlain
    , verifyPlain'
    , sign

) where

import System.Posix.Types (Fd(Fd))
import Bindings.Gpgme
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C8
import Control.Monad (liftM)
import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT, mapExceptT)
import Foreign
import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
import GHC.Ptr

import Crypto.Gpgme.Ctx
import Crypto.Gpgme.Internal
import Crypto.Gpgme.Key
import Crypto.Gpgme.Types

locale :: String
locale = "C"

-- | Convenience wrapper around 'withCtx' and 'withKey' to
--   encrypt a single plaintext for a single recipient with
--   its homedirectory.
encrypt' :: String -> Fpr -> Plain -> IO (Either String Encrypted)
encrypt' = encryptIntern' encrypt

-- | Convenience wrapper around 'withCtx' and 'withKey' to
--   encrypt and sign a single plaintext for a single recipient
--   with its homedirectory.
encryptSign' :: String -> Fpr -> Plain -> IO (Either String Encrypted)
encryptSign' = encryptIntern' encryptSign

orElse :: Monad m => m (Maybe a) -> e -> ExceptT e m a
orElse action err = ExceptT $ maybe (Left err) return `liftM` action

bimapExceptT :: Functor m => (x -> y) -> (a -> b) -> ExceptT x m a -> ExceptT y m b
bimapExceptT f g = mapExceptT (fmap h)
  where
    h (Left  e) = Left  (f e)
    h (Right a) = Right (g a)

encryptIntern' :: (Ctx -> [Key] -> Flag -> Plain
                        -> IO (Either [InvalidKey] Encrypted)
                    ) -> String -> Fpr -> Plain -> IO (Either String Encrypted)
encryptIntern' encrFun gpgDir recFpr plain =
    withCtx gpgDir locale OpenPGP $ \ctx -> runExceptT $
        do pubKey <- getKey ctx recFpr NoSecret `orElse` ("no such key: " ++ show recFpr)
           bimapExceptT show id $ ExceptT $ encrFun ctx [pubKey] NoFlag plain

-- | encrypt for a list of recipients
encrypt :: Ctx -> [Key] -> Flag -> Plain -> IO (Either [InvalidKey] Encrypted)
encrypt = encryptIntern c'gpgme_op_encrypt

-- | encrypt and sign for a list of recipients
encryptSign :: Ctx -> [Key] -> Flag -> Plain -> IO (Either [InvalidKey] Encrypted) 
encryptSign = encryptIntern c'gpgme_op_encrypt_sign

encryptIntern :: (C'gpgme_ctx_t
                    -> GHC.Ptr.Ptr C'gpgme_key_t
                    -> C'gpgme_encrypt_flags_t
                    -> C'gpgme_data_t
                    -> C'gpgme_data_t
                    -> IO C'gpgme_error_t
                  )
                  -> Ctx
                  -> [Key]
                  -> Flag
                  -> Plain
                  -> IO (Either [InvalidKey] Encrypted) 
encryptIntern enc_op (Ctx {_ctx=ctxPtr}) recPtrs flag plain = do
    -- init buffer with plaintext
    plainBufPtr <- malloc
    BS.useAsCString plain $ \bs -> do
        let copyData = 1 -- gpgme shall copy data, as bytestring will free it
        let plainlen = fromIntegral (BS.length plain)
        ret <- c'gpgme_data_new_from_mem plainBufPtr bs plainlen copyData
        checkError "data_new_from_mem" ret
    plainBuf <- peek plainBufPtr

    -- init buffer for result
    resultBufPtr <- newDataBuffer
    resultBuf <- peek resultBufPtr

    ctx <- peek ctxPtr

    -- encrypt
    withKeyPtrArray recPtrs $ \recArray -> 
        checkError "op_encrypt" =<< enc_op ctx recArray (fromFlag flag)
                                        plainBuf resultBuf
    free plainBufPtr

    -- check whether all keys could be used for encryption
    encResPtr <- c'gpgme_op_encrypt_result ctx
    encRes <- peek encResPtr
    let recPtr = c'_gpgme_op_encrypt_result'invalid_recipients encRes

    let res = if recPtr /= nullPtr
                then Left (collectFprs recPtr)
                else Right (collectResult resultBuf)

    free resultBufPtr

    return res

-- | Encrypt plaintext
encryptFd :: Ctx -> [Key] -> Flag -> Fd -> Fd -> IO (Either [InvalidKey] ())
encryptFd = encryptFdIntern c'gpgme_op_encrypt

-- | Encrypt and sign plaintext
encryptSignFd :: Ctx -> [Key] -> Flag -> Fd -> Fd -> IO (Either [InvalidKey] ())
encryptSignFd = encryptFdIntern c'gpgme_op_encrypt_sign

encryptFdIntern :: (C'gpgme_ctx_t
                 -> GHC.Ptr.Ptr C'gpgme_key_t
                 -> C'gpgme_encrypt_flags_t
                 -> C'gpgme_data_t
                 -> C'gpgme_data_t
                 -> IO C'gpgme_error_t
               )
               -> Ctx
               -> [Key]
               -> Flag
               -> Fd  -- ^ Plaintext data
               -> Fd  -- ^ Ciphertext data
               -> IO (Either [InvalidKey] ())
encryptFdIntern enc_op (Ctx {_ctx=ctxPtr}) recPtrs flag (Fd plainCInt) (Fd cipherCInt) = do
  -- Initialize plaintext buffer
  plainBufPtr <- malloc
  _ <- c'gpgme_data_new_from_fd plainBufPtr plainCInt
  plainBuf <- peek plainBufPtr

  -- Initialize ciphertext buffer
  cipherBufPtr <- malloc
  _ <- c'gpgme_data_new_from_fd cipherBufPtr cipherCInt
  cipherBuf <- peek cipherBufPtr

  ctx <- peek ctxPtr

  -- encrypt
  withKeyPtrArray recPtrs $ \recArray ->
      checkError "op_encrypt" =<< enc_op ctx recArray (fromFlag flag)
                                      plainBuf cipherBuf
  free plainBufPtr

  -- check whether all keys could be used for encryption
  encResPtr <- c'gpgme_op_encrypt_result ctx
  encRes <- peek encResPtr
  let recPtr = c'_gpgme_op_encrypt_result'invalid_recipients encRes

  let res = if recPtr /= nullPtr
              then Left (collectFprs recPtr)
              else Right (())

  free cipherBufPtr

  return res

-- | Build a null-terminated array of pointers from a list of 'Key's
withKeyPtrArray :: [Key] -> (Ptr C'gpgme_key_t -> IO a) -> IO a
withKeyPtrArray [] f   = f nullPtr
withKeyPtrArray keys f = do
    arr <- newArray0 nullPtr =<< mapM (peek . unsafeForeignPtrToPtr . unKey) keys
    f arr

-- | Convenience wrapper around 'withCtx' and 'withKey' to
--   decrypt a single ciphertext with its homedirectory.
decrypt' :: String -> Encrypted -> IO (Either DecryptError Plain)
decrypt' = decryptInternal' decrypt

-- | Convenience wrapper around 'withCtx' and 'withKey' to
--   decrypt and verify a single ciphertext with its homedirectory.
decryptVerify' :: String -> Encrypted -> IO (Either DecryptError Plain)
decryptVerify' = decryptInternal' decryptVerify

decryptInternal' :: (Ctx -> Encrypted -> IO (Either DecryptError Plain))
                  -> String
                  -> Encrypted
                  -> IO (Either DecryptError Plain)
decryptInternal' decrFun gpgDir cipher =
    withCtx gpgDir locale OpenPGP $ \ctx ->
        decrFun ctx cipher

-- | Decrypts a ciphertext
decrypt :: Ctx -> Encrypted -> IO (Either DecryptError Plain)
decrypt = decryptIntern c'gpgme_op_decrypt

-- | Decrypts and verifies a ciphertext
decryptVerify :: Ctx -> Encrypted -> IO (Either DecryptError Plain)
decryptVerify = decryptIntern c'gpgme_op_decrypt_verify

decryptIntern :: (C'gpgme_ctx_t
                    -> C'gpgme_data_t
                    -> C'gpgme_data_t
                    -> IO C'gpgme_error_t
                  )
                  -> Ctx
                  -> Encrypted
                  -> IO (Either DecryptError Plain)
decryptIntern dec_op (Ctx {_ctx=ctxPtr}) cipher = do
    -- init buffer with cipher
    cipherBufPtr <- malloc
    BS.useAsCString cipher $ \bs -> do
        let copyData = 1 -- gpgme shall copy data, as bytestring will free it
        let cipherlen = fromIntegral (BS.length cipher)
        ret <- c'gpgme_data_new_from_mem cipherBufPtr bs cipherlen copyData
        checkError "data_new_from_mem" ret
    cipherBuf <- peek cipherBufPtr

    -- init buffer for result
    resultBufPtr <- newDataBuffer
    resultBuf <- peek resultBufPtr

    ctx <- peek ctxPtr

    -- decrypt
    errcode <- dec_op ctx cipherBuf resultBuf

    let res = if errcode /= noError
                then Left  (toDecryptError errcode)
                else Right (collectResult resultBuf)

    free cipherBufPtr
    free resultBufPtr

    return res

-- | Decrypt a ciphertext
decryptFd :: Ctx -> Fd -> Fd -> IO (Either DecryptError ())
decryptFd = decryptFdIntern c'gpgme_op_decrypt

-- | Decrypt and verify ciphertext
decryptVerifyFd :: Ctx -> Fd -> Fd -> IO (Either DecryptError ())
decryptVerifyFd = decryptFdIntern c'gpgme_op_decrypt_verify

decryptFdIntern :: (C'gpgme_ctx_t
                    -> C'gpgme_data_t
                    -> C'gpgme_data_t
                    -> IO C'gpgme_error_t
                  )
                  -> Ctx
                  -> Fd
                  -> Fd
                  -> IO (Either DecryptError ())
decryptFdIntern dec_op (Ctx {_ctx=ctxPtr}) (Fd cipherCInt) (Fd plainCInt)= do
  -- Initialize ciphertext buffer
  cipherBufPtr <- malloc
  _ <- c'gpgme_data_new_from_fd cipherBufPtr cipherCInt
  cipherBuf <- peek cipherBufPtr

  -- Initialize plaintext buffer
  plainBufPtr <- malloc
  _ <- c'gpgme_data_new_from_fd plainBufPtr plainCInt
  plainBuf <- peek plainBufPtr

  ctx <- peek ctxPtr

  -- decrypt
  errcode <- dec_op ctx cipherBuf plainBuf

  let res = if errcode /= noError
              then Left  (toDecryptError errcode)
              else Right (())

  free cipherBufPtr
  free plainBufPtr

  return res

-- | Sign plaintext for a list of signers
sign :: Ctx      -- ^ Context to sign
     -> [Key]    -- ^ Keys to used for signing. An empty list will use context's default key.
     -> SignMode -- ^ Signing mode
     -> Plain    -- ^ Plain text to sign
     -> IO (Either [InvalidKey] Plain)
sign = signIntern c'gpgme_op_sign

signIntern :: (    C'gpgme_ctx_t
                -> C'gpgme_data_t
                -> C'gpgme_data_t
                -> C'gpgme_sig_mode_t
                -> IO C'gpgme_error_t
              ) -- ^ c'gpgme_op_sign type signature
              -> Ctx
              -> [Key]
              -> SignMode
              -> Plain
              -> IO (Either [InvalidKey] Encrypted)
signIntern sign_op (Ctx {_ctx=ctxPtr}) signPtrs mode plain = do
    -- init buffer with plaintext
    plainBufPtr <- malloc
    BS.useAsCString plain $ \bs -> do
        let copyData = 1 -- gpgme shall copy data, as bytestring will free it
        let plainlen = fromIntegral (BS.length plain)
        ret <- c'gpgme_data_new_from_mem plainBufPtr bs plainlen copyData
        checkError "data_new_from_mem" ret
    plainBuf <- peek plainBufPtr

    -- init buffer for result
    resultBufPtr <- newDataBuffer
    resultBuf <- peek resultBufPtr

    ctx <- peek ctxPtr

    -- add signing keys
    _ <- mapM ( \kForPtr -> withForeignPtr (unKey kForPtr)
           (\kPtr -> do
             k <- peek kPtr
             c'gpgme_signers_add ctx k
           )
         ) signPtrs

    -- sign
    let modeCode = case mode of
                     Normal -> c'GPGME_SIG_MODE_NORMAL
                     Detach -> c'GPGME_SIG_MODE_DETACH
                     Clear  -> c'GPGME_SIG_MODE_CLEAR

    checkError "op_sign" =<< sign_op ctx plainBuf resultBuf modeCode
    free plainBufPtr

    -- check whether all keys could be used for signingi
    signResPtr <- c'gpgme_op_sign_result ctx
    signRes <- peek signResPtr
    let recPtr = c'_gpgme_op_sign_result'invalid_signers signRes

    let res = if recPtr /= nullPtr
                then Left (collectFprs recPtr)
                else Right (collectResult resultBuf)

    free resultBufPtr

    return res


-- | Verify a payload with a detached signature
verifyDetached :: Ctx           -- ^ GPG context
               -> Signature     -- ^ Detached signature
               -> BS.ByteString -- ^ Signed text
               -> IO (Either GpgmeError VerificationResult)
verifyDetached ctx sig dat = do
    res <- verifyInternal go ctx sig dat
    return $ fmap fst res
    where
        go ctx' sig' dat' = do
            errcode <- c'gpgme_op_verify ctx' sig' dat' 0
            return (errcode, ())

-- | Convenience wrapper around 'withCtx' to
--   verify a single detached signature with its homedirectory.
verifyDetached' :: String        -- ^ GPG context home directory
                -> Signature     -- ^ Detached signature
                -> BS.ByteString -- ^ Signed text
                -> IO (Either GpgmeError VerificationResult)
verifyDetached' gpgDir sig dat =
    withCtx gpgDir locale OpenPGP $ \ctx ->
        verifyDetached ctx sig dat

{-# DEPRECATED verifyPlain "Use verify" #-}
verifyPlain :: Ctx -> Signature -> BS.ByteString -> IO (Either GpgmeError (VerificationResult, BS.ByteString))
verifyPlain c s _ = verify c s
{-# DEPRECATED verifyPlain' "Use verify'" #-}
verifyPlain' :: String -> Signature -> BS.ByteString -> IO (Either GpgmeError (VerificationResult, BS.ByteString))
verifyPlain' str sig _ = verify' str sig

-- | Verify a payload with a plain signature
verify :: Ctx -> Signature -> IO (Either GpgmeError (VerificationResult, BS.ByteString))
verify c s = verifyInternal go c s (C8.pack "")
    where
        go ctx sig _ = do
            -- init buffer for result
            resultBufPtr <- newDataBuffer
            resultBuf <- peek resultBufPtr

            errcode <- c'gpgme_op_verify ctx sig 0 resultBuf

            let res = if errcode /= noError
                        then mempty
                        else collectResult resultBuf

            free resultBufPtr

            return (errcode, res)

-- | Convenience wrapper around 'withCtx' to
--   verify a single plain signature with its homedirectory.
verify' :: String -> Signature -> IO (Either GpgmeError (VerificationResult, BS.ByteString))
verify' gpgDir sig =
    withCtx gpgDir locale OpenPGP $ \ctx ->
        verify ctx sig

verifyInternal :: (    C'gpgme_ctx_t
                    -> C'gpgme_data_t
                    -> C'gpgme_data_t
                    -> IO (C'gpgme_error_t, a)
                  )
                  -> Ctx
                  -> Signature
                  -> BS.ByteString
                  -> IO (Either GpgmeError (VerificationResult, a))
verifyInternal ver_op (Ctx {_ctx=ctxPtr}) sig dat = do
    -- init buffer with signature
    sigBufPtr <- malloc
    BS.useAsCString sig $ \bs -> do
        let copyData = 1 -- gpgme shall copy data, as bytestring will free it
        let siglen = fromIntegral (BS.length sig)
        ret <- c'gpgme_data_new_from_mem sigBufPtr bs siglen copyData
        checkError "data_new_from_mem" ret
    sigBuf <- peek sigBufPtr

    -- init buffer with data
    datBufPtr <- malloc
    BS.useAsCString dat $ \bs -> do
        let copyData = 1 -- gpgme shall copy data, as bytestring will free it
        let datlen = fromIntegral (BS.length dat)
        ret <- c'gpgme_data_new_from_mem datBufPtr bs datlen copyData
        checkError "data_new_from_mem" ret
    datBuf <- peek datBufPtr

    ctx <- peek ctxPtr

    -- verify
    (errcode, res) <- ver_op ctx sigBuf datBuf

    sigs <- collectSignatures' ctx
    let res' = if errcode /= noError
                then Left  (GpgmeError errcode)
                else Right (sigs, res)

    free sigBufPtr
    free datBufPtr

    return res'

newDataBuffer :: IO (Ptr C'gpgme_data_t)
newDataBuffer = do
    resultBufPtr <- malloc
    checkError "data_new" =<< c'gpgme_data_new resultBufPtr
    return resultBufPtr
