/**
 * 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_GCMENCRYPTER_HPP)
#define LIBSECRECY_GCMENCRYPTER_HPP

#include <libsecrecy/GCMCryptBase.hpp>
#include <libsecrecy/GCMFactoryBase.hpp>
#include <libsecrecy/Yarrow.hpp>
#include <libsecrecy/AESEncryptKey.hpp>

namespace libsecrecy
{
	struct GCMEncrypterInterface
	{
		virtual ~GCMEncrypterInterface() {}

		virtual void write(char const * const cdata, ::std::size_t const len) = 0;
		virtual void write(uint8_t const * data, ::std::size_t len) = 0;
		virtual void writeDigest() = 0;
		virtual void reset(uint64_t const) = 0;
		virtual std::string getPrefix() const = 0;
		virtual std::size_t getBlockSize() const = 0;
		virtual std::size_t getDigestSize() const = 0;
		virtual std::string getIVHexString() const = 0;
		virtual std::string getKeyHashString() const = 0;
		virtual std::string getAuthHexString() const = 0;
	};
}

namespace libsecrecy
{
	template<typename _encrypt_key_type>
	struct GCMEncrypter : public GCMCryptBase<_encrypt_key_type>, public GCMEncrypterInterface
	{
		typedef _encrypt_key_type encrypt_key_type;
		typedef GCMCryptBase<encrypt_key_type> base_type;

		static ::std::size_t const blocksize = encrypt_key_type::blocksize;
		static ::std::size_t const digestsize = GCM_DIGEST_SIZE;

		private:
		std::ostream & out;
		::std::size_t const outblocksize;
		std::shared_ptr<uint8_t[]> B;

		public:
		GCMEncrypter(
			std::ostream & rout,
			std::shared_ptr<encrypt_key_type> & rpkey,
			std::shared_ptr<GCMIV> IV,
			std::shared_ptr<uint8_t[]> sauthdata,
			::std::size_t const l_authdata,
			uint64_t const countershift = 0,
			::std::size_t const routblocksize = 1024
		)
		:
		  GCMCryptBase<encrypt_key_type>(rpkey,IV,sauthdata,l_authdata,countershift),
		  out(rout),
		  outblocksize(routblocksize),
		  B(new uint8_t[outblocksize])
		{
		}

		void reset(uint64_t const countershift)
		{
			base_type::reset(countershift);
		}

		std::string getPrefix() const
		{
			return base_type::getPrefix();
		}

		std::string getIVHexString() const
		{
			return base_type::getIVHexString();
		}

		std::string getKeyHashString() const
		{
			return base_type::getKeyHashString();
		}

		std::string getAuthHexString() const
		{
			return base_type::getAuthHexString();
		}

		std::size_t getDigestSize() const
		{
			return digestsize;
		}

		void write(char const * const cdata, ::std::size_t const len)
		{
			static_assert (
				sizeof(char) == sizeof(uint8_t),
				"sizeof(char) != sizeof(uint8_t)"
			);
			write(
				reinterpret_cast<uint8_t const *>(cdata),
				len
			);
		}

		void write(uint8_t const * data, ::std::size_t len)
		{
			if ( len % blocksize != 0 )
			{
				std::ostringstream ostr;
				ostr << "libsecrecy::GCMEncrypter::write: input block of size " << len << " is not a multiple of blocksize " << blocksize;
				throw std::runtime_error(ostr.str());
			}

			while ( len )
			{
				std::size_t const use = std::min(len,outblocksize);
				gcm_encrypt(base_type::gcmctx,base_type::gcmkey,base_type::key.getContext(),encrypt_key_type::getCipherFunction(),use,B.get(),data);

				out.write(reinterpret_cast<char const *>(B.get()),use);

				if ( ! out )
				{
					std::ostringstream ostr;
					ostr << "libsecrecy::GCMEncrypter::write: failed to write " << use << " bytes to output stream";
					throw std::runtime_error(ostr.str());

				}

				data += use;
				len -= use;
			}
		}

		void writeDigest()
		{
			uint8_t digest[GCM_DIGEST_SIZE];
			gcm_digest(base_type::gcmctx,base_type::gcmkey,base_type::key.getContext(),encrypt_key_type::getCipherFunction(), GCM_DIGEST_SIZE, &digest[0]);

			out.write(reinterpret_cast<char const *>(digest),GCM_DIGEST_SIZE);

			if ( ! out )
			{
				std::ostringstream ostr;
				ostr << "libsecrecy::GCMEncrypter::writeDigest: failed to write " << GCM_DIGEST_SIZE << " bytes to output stream";
				throw std::runtime_error(ostr.str());
			}
		}

		std::size_t getBlockSize() const
		{
			return blocksize;
		}
	};
};

namespace libsecrecy
{
	struct GCMEncrypterFactory : public GCMFactoryBase
	{
		template<typename key_type>
		static std::shared_ptr<GCMEncrypterInterface> construct(
			std::ostream & rout,
			RawKeyObject const & RK,
			std::shared_ptr<GCMIV> IV,
			std::shared_ptr<uint8_t[]> sauthdata,
			::std::size_t const l_authdata,
			uint64_t const countershift = 0,
			::std::size_t const routblocksize = 1024
		)
		{
			typename key_type::raw_key_type const & rawkey = dynamic_cast<typename key_type::raw_key_type const &>(*(RK.ptr));
			std::shared_ptr<key_type> K(new key_type(rawkey));

			std::shared_ptr<GCMEncrypterInterface> tptr(
				new GCMEncrypter(rout,K,IV,sauthdata,l_authdata,countershift,routblocksize)
			);
			return tptr;
		}

		static std::shared_ptr<GCMEncrypterInterface> construct(
			std::ostream & rout,
			libsecrecy::GPGMEContext & context,
			std::string const & hexhash,
			std::shared_ptr<GCMIV> IV,
			std::shared_ptr<uint8_t[]> sauthdata,
			::std::size_t const l_authdata,
			uint64_t const countershift = 0,
			::std::size_t const routblocksize = 1024,
			RawKeyBaseCacheInterface * cache = nullptr
		)
		{
			RawKeyObject RK = obtainKey(hexhash,true,context,cache);
			std::string const scipher = RK.cipher;
			std::string const keyname = RK.name;

			#if 0
			std::string const keyfiledata = RawKeyBase::readKeyData(hexhash,true /* try to resolve name if hash is not given */);
			std::istringstream keydataistr(keyfiledata);
			RawKeyBase::expectMagic(keydataistr);
			// read cipher to be used for key
			std::string const scipher = GCMStreamBase::readString(keydataistr);
			std::string const skeyname = GCMStreamBase::readString(keydataistr);
			#endif

			if ( scipher == "AES128" )
			{
				return construct<AES128EncryptKey>(rout,RK,IV,sauthdata,l_authdata,countershift,routblocksize);
			}
			else if ( scipher == "AES192" )
			{
				return construct<AES192EncryptKey>(rout,RK,IV,sauthdata,l_authdata,countershift,routblocksize);
			}
			else if ( scipher == "AES256" )
			{
				return construct<AES256EncryptKey>(rout,RK,IV,sauthdata,l_authdata,countershift,routblocksize);
			}
			else
			{
				std::ostringstream ostr;
				ostr << "libsecrecy::GCMEncrypterFactory:: unsupported cipher " << scipher;
				throw std::runtime_error(ostr.str());
			}
		}

		static std::shared_ptr<GCMEncrypterInterface> construct(
			std::ostream & rout,
			libsecrecy::GPGMEContext & context,
			std::string const & hexhash,
			libsecrecy::Yarrow & Y,
			::std::size_t const l_authdata,
			uint64_t const countershift = 0,
			::std::size_t const routblocksize = 1024,
			RawKeyBaseCacheInterface * cache = nullptr
		)
		{
			std::shared_ptr<GCMIV> IV(new GCMIV(Y));
			std::shared_ptr<uint8_t[]> authdata(new uint8_t[l_authdata]);
			Y(authdata.get(),l_authdata);
			return construct(rout,context,hexhash,IV,authdata,l_authdata,countershift,routblocksize,cache);
		}

		static std::shared_ptr<GCMEncrypterInterface> construct(
			std::ostream & rout,
			libsecrecy::GPGMEContext & context,
			std::string const & hexhash,
			::std::size_t const l_authdata,
			uint64_t const countershift = 0,
			::std::size_t const routblocksize = 1024,
			RawKeyBaseCacheInterface * cache = nullptr
		)
		{
			libsecrecy::Yarrow Y;
			return construct(rout,context,hexhash,Y,l_authdata,countershift,routblocksize,cache);
		}

		static std::shared_ptr<GCMEncrypterInterface> construct(
			std::ostream & rout,
			std::string const & hexhash,
			::std::size_t const l_authdata,
			uint64_t const countershift = 0,
			::std::size_t const routblocksize = 1024,
			RawKeyBaseCacheInterface * cache = nullptr
		)
		{
			libsecrecy::Yarrow Y;
			libsecrecy::GPGMEContext context;
			return construct(rout,context,hexhash,Y,l_authdata,countershift,routblocksize,cache);
		}
	};
}
#endif
