/**
 * 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_GCMOUTPUTSTREAMBUFFER_HPP)
#define LIBSECRECY_GCMOUTPUTSTREAMBUFFER_HPP

#include <libsecrecy/LockedMemory.hpp>
#include <libsecrecy/GCMStreamBase.hpp>
#include <libsecrecy/GCMStreamBlockHeader.hpp>
#include <libsecrecy/GCMStreamHeader.hpp>
#include <libsecrecy/GCMEncrypter.hpp>

namespace libsecrecy
{
	struct GCMOutputStreamBuffer : public ::std::streambuf
	{
		private:
		// get default block size
		static std::size_t getDefaultBufferSize()
		{
			return 64*1024;
		}

		std::ostream & out;
		std::shared_ptr<GCMEncrypterInterface> interface;
		// size of buffer (meta+payload)
		std::size_t const buffersize;
		// size of payload block
		std::size_t const blocksize;
		// buffer
		std::shared_ptr<char[]> B;
		std::size_t counteroffset;
		bool seenshort;
		std::streamsize written;

		// get size of encrypted block
		std::size_t getEncryptedBlockSize() const
		{
			return buffersize + interface->getDigestSize();
		}

		// buffer size divided by encryption block size
		std::size_t getPrimitiveBufferSize() const
		{
			return buffersize / interface->getBlockSize();
		}

		// flush the buffer
		void doSync()
		{
			std::size_t n = pptr()-pbase();

			if ( n )
			{
				if ( n < blocksize )
				{
					if ( seenshort )
					{
						throw std::runtime_error("libsecrecy::GCMOutputStreamBuffer::doSync: second short block is not supported");
					}
					seenshort = true;
				}

				char * p = pbase();
				pbump(-n);

				// pad with zeros if block is short
				if ( n < blocksize )
					std::fill(p+n,p+blocksize,0);

				p -= GCMStreamBlockHeader::size();
				assert ( p == B.get() );

				GCMStreamBlockHeader const GSBH(n);
				GSBH.encode(p);

				assert ( buffersize % interface->getBlockSize() == 0 );
				interface->reset(counteroffset);
				interface->write(p,buffersize);
				interface->writeDigest();

				counteroffset += buffersize / interface->getBlockSize();
				written += n;
			}
		}

		std::size_t computeBlockSize(std::size_t const tblocksize) const
		{
			std::size_t const gcmblocksize = interface->getBlockSize();
			std::size_t const numblocks =
				(tblocksize + gcmblocksize - 1)
				/
				gcmblocksize;
			std::size_t const blocksize = numblocks * gcmblocksize;

			if ( blocksize > std::numeric_limits<uint32_t>::max() )
				throw std::runtime_error("libsecrecy::GCMOutputStreamBuffer::computeBlockSize: target block size is too large for file format");

			return blocksize;
		}

		public:
		void writeHeader()
		{
			GCMStreamHeader GSH;
			GSH.setStringValue("keyhash",interface->getKeyHashString());
			GSH.setStringValue("IV",interface->getIVHexString());
			GSH.setStringValue("authdata",interface->getAuthHexString());
			GSH.setNumericalValue("buffersize",buffersize);
			GSH.setNumericalValue("blocksize",blocksize);
			GSH.serialise(out);
		}

		GCMOutputStreamBuffer(
			std::ostream & rout,
			std::shared_ptr<GCMEncrypterInterface> rinterface,
			std::size_t const tbuffersize = 0
		)
		:
		  out(rout),
		  interface(rinterface),
		  buffersize(computeBlockSize(tbuffersize ? tbuffersize : getDefaultBufferSize())),
		  blocksize(buffersize - GCMStreamBlockHeader::size()),
		  B(new char[buffersize]),
		  counteroffset(0),
		  seenshort(false),
		  written(0)
		{
			setp(B.get()+GCMStreamBlockHeader::size(),B.get()+buffersize-1);
			writeHeader();
		}

		~GCMOutputStreamBuffer()
		{
			sync();
		}

		int_type overflow(int_type c = traits_type::eof())
		{
			if ( c != traits_type::eof() )
			{
				*pptr() = c;
				pbump(1);
				doSync();
			}

			return c;
		}

		int sync()
		{
			doSync();
			return 0; // no error, -1 for error
		}

		/**
		 * seek relative to beg, cur or end
		 **/
		::std::streampos seekoff(::std::streamoff offset, ::std::ios_base::seekdir rel, ::std::ios_base::openmode which)
		{
			// allow tellp to work (seek by 0 bytes relative to current position)
			if (
				rel == ::std::ios_base::cur
				&&
				offset == 0
				&&
				((which & ::std::ios_base::out) != 0)
			)
				return written + (pptr()-pbase());

			return -1;
		}
	};
}
#endif
