/**
 * Copyright 2020 German Tischler-Höhle
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 **/
#if ! defined(LIBSECRECY_GPGMECONTEXT_HPP)
#define LIBSECRECY_GPGMECONTEXT_HPP

#include <libsecrecy/LockedMemory.hpp>
#include <libsecrecy/GPGMEErrorBase.hpp>
#include <libsecrecy/GPGMEInputStringWrapper.hpp>
#include <libsecrecy/GPGMEInputStreamWrapper.hpp>
#include <libsecrecy/GPGMEOutputStreamWrapper.hpp>
#include <libsecrecy/GPGMEOutputStringWrapper.hpp>
#include <libsecrecy/GPGMEOutputLockedStringWrapper.hpp>

namespace libsecrecy
{
	struct GPGMEContext : public GPGMEErrorBase
	{
		libsecrecy::LockedMemory<gpgme_ctx_t> lctx;
		gpgme_ctx_t * ctx;
		libsecrecy::LockedMemory<gpgme_key_t> lkey;
		gpgme_key_t * key;

		GPGMEContext()
		: lctx(1), ctx(lctx.get()), lkey(2), key(lkey.get())
		{
			key[0] = nullptr;
			key[1] = nullptr;

			if ( ! gpgme_check_version(nullptr) )
				throw std::runtime_error("libsecrecy::GPGMEContext: gpgme_check_version failed");

			gpgme_error_t const err = gpgme_new(ctx);

			if ( err )
				throw std::runtime_error("libsecrecy::GPGMEContext: gpgme_new failed "+errorToString(err));
		}

		~GPGMEContext()
		{
			gpgme_release(*ctx);
		}

		/**
		 * load a key through gpgme using fptr as description (hash or email adress)
		 **/
		void loadKey(char const * fptr, int const secret)
		{
			gpgme_error_t const err = gpgme_get_key(*ctx,fptr,&key[0],secret);

			if ( err )
				throw std::runtime_error("libsecrecy::GPGMEKey::loadKey: gpgme_get_key failed "+errorToString(err));
		}

		/**
		 * call encrypt through gpgme
		 **/
		void callEncrypt(gpgme_encrypt_flags_t flags, gpgme_data_t plain, gpgme_data_t cipher)
		{
			gpgme_error_t const err = gpgme_op_encrypt (*ctx, key, flags, plain, cipher);

			if ( err )
				throw std::runtime_error("libsecrecy::GPGMEKey: gpgme_op_encrypt failed "+errorToString(err));
		}

		/**
		 * call decrypt through gpgme
		 **/
		void callDecrypt(gpgme_data_t cipher, gpgme_data_t plain)
		{
			gpgme_error_t const err = gpgme_op_decrypt (*ctx, cipher, plain);

			if ( err )
				throw std::runtime_error("libsecrecy::GPGMEKey: gpgme_op_decrypt failed "+errorToString(err));
		}

		std::pair<
			std::shared_ptr<LockedMemory<char> >,
			std::size_t
		> decrypt(std::istream & in)
		{
			GPGMEInputStreamWrapper gpgistr(in);
			GPGMEOutputLockedStringWrapper gpgostr;
			callDecrypt(gpgistr.data,gpgostr.data);

			auto P = std::make_pair(
				gpgostr.L,
				gpgostr.size()
			);

			return P;
		}

		/**
		 * decrypt data from stream in to stream out via gpgme
		 **/
		void decrypt(std::istream & in, std::ostream & out)
		{
			GPGMEInputStreamWrapper gpgistr(in);
			GPGMEOutputStreamWrapper gpgostr(out);
			callDecrypt(gpgistr.data, gpgostr.data);
		}

		/**
		 * decrypt data from stream in to fixed length character buffer
		 * returns number of characters decrypted
		 **/
		std::size_t decrypt(std::istream & in, char * const ca, char * const ce)
		{
			GPGMEInputStreamWrapper gpgistr(in);
			GPGMEOutputStringWrapper gpgostr(ca,ce);
			callDecrypt(gpgistr.data, gpgostr.data);
			std::size_t const wr = gpgostr.ca - ca;

			if (
				wr !=
				static_cast<std::size_t>(ce-ca)
			)
			{
				throw std::runtime_error("libsecrecy::GPGMEContext::decrypt: message is truncated");
			}

			return wr;
		}

		/**
		 * encrypt data from fixed length buffer to stream out
		 * using the key previously loaded via loadKey
		 **/
		void encrypt(char const * ca, char const * ce, std::ostream & out)
		{
			GPGMEInputStringWrapper gpgistr(ca,ce);
			GPGMEOutputStreamWrapper gpgostr(out);
			callEncrypt(GPGME_ENCRYPT_ALWAYS_TRUST, gpgistr.data, gpgostr.data);
		}

		void encrypt(uint8_t const * ca, uint8_t const * ce, std::ostream & out)
		{
			encrypt(
				reinterpret_cast<char const *>(ca),
				reinterpret_cast<char const *>(ce),
				out
			);
		}

		/**
		 * encrypt data from stream in to stream out
		 * using the key previously loaded via loadKey
		 **/
		void encrypt(std::istream & in, std::ostream & out)
		{
			GPGMEInputStreamWrapper gpgistr(in);
			GPGMEOutputStreamWrapper gpgostr(out);
			callEncrypt(GPGME_ENCRYPT_ALWAYS_TRUST, gpgistr.data, gpgostr.data);
		}

		/**
		 * encrypt data from string s to stream out
		 * using the key previously loaded via loadKey
		 **/
		void encrypt(std::string const & s, std::ostream & out)
		{
			char const * ca = s.c_str();
			char const * ce = ca + s.size();
			encrypt(ca,ce,out);
		}

		/**
		 * encrypt data from array L to stream out
		 * using the key previously loaded via loadKey
		 **/
		void encrypt(libsecrecy::LockedMemory<char> const & L, std::ostream & out)
		{
			encrypt(L.begin(),L.end(),out);
		}
	};
}
#endif
