/**
 * 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_GCMINPUTSTREAMBUFFER_HPP)
#define LIBSECRECY_GCMINPUTSTREAMBUFFER_HPP

#include <streambuf>
#include <istream>
#include <libsecrecy/GCMDecrypter.hpp>
#include <libsecrecy/GPGMEVersionCheck.hpp>
#include <libsecrecy/GPGMEContext.hpp>
#include <libsecrecy/GCMStreamHeader.hpp>
#include <libsecrecy/GCMStreamBlockHeader.hpp>

namespace libsecrecy
{
	struct GCMInputStreamBuffer : public ::std::streambuf
	{
		private:
		std::istream & in;
		libsecrecy::GCMStreamHeader BSH;
		std::size_t const headersize;
		std::shared_ptr<libsecrecy::GCMIV> IV;
		std::pair < std::shared_ptr<uint8_t[]>, std::size_t > pauthdata;
		std::string const skeyhash;
		std::size_t const buffersize;
		std::size_t const blocksize;
		std::shared_ptr<GCMDecrypterInterface> interface;
		std::size_t const putbackbuffersize;
		std::shared_ptr<char[]> B;
		char * const be;

		std::streampos bufferoffset;
		std::streampos blockoffset;
		std::streampos currentblocksize;
		std::streamsize fs;
		bool fsvalid;

		GCMInputStreamBuffer(GCMInputStreamBuffer const &) = delete;
		GCMInputStreamBuffer & operator=(GCMInputStreamBuffer &) = delete;

		public:
		GCMInputStreamBuffer(
			std::istream & rin,
			std::size_t rputbackbuffersize,
			RawKeyBaseCacheInterface * cache
		)
		:
		  in(rin),
		  BSH(in),
		  headersize(BSH.size()),
		  IV(BSH.getIV()),
		  pauthdata(BSH.getAuthData()),
		  skeyhash(BSH.getStringValue("keyhash")),
		  buffersize(BSH.getParsedValue<std::size_t>("buffersize")),
		  blocksize(BSH.getParsedValue<std::size_t>("blocksize")),
		  interface(libsecrecy::GCMDecrypterFactory::construct(rin,skeyhash,IV,pauthdata.first,pauthdata.second,0 /* countershift */,1024 /* outblocksize */,cache)),
		  putbackbuffersize(rputbackbuffersize),
		  B(new char[putbackbuffersize + buffersize]),
		  be(B.get() + putbackbuffersize + buffersize),
		  bufferoffset(0),
		  blockoffset(0),
		  currentblocksize(0),
		  fs(0),
		  fsvalid(false)
		{
			setg(be,be,be);
		}

		/**
		 * return size of decrypted file
		 * this requires the underlying stream to be seekable
		 **/
		std::streamsize getFileSize()
		{
			if ( ! fsvalid )
			{
				in.clear();
				std::streampos const p = in.tellg();

				if ( p < 0 )
					throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: underlying stream does not support tellg");

				in.clear();
				in.seekg(0,std::ios::end);

				if ( ! in )
					throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: underlying stream does not support seeking to end");

				std::streampos const e = in.tellg();

				if ( e < 0 )
					throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: underlying stream does not support tellg after seeking to end");

				if ( e < static_cast<std::streampos>(headersize) )
					throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: file size report is smaller than headersize");

				// if file consist only of the file header
				if ( e == static_cast<std::streampos>(headersize) )
				{
					in.seekg(p);

					if ( ! in )
						throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: underlying stream failed to seek back to original position");

					fs = 0;
					fsvalid = true;

					return fs;
				}

				std::streamsize const cryptsize = static_cast<std::streamsize>(e) - headersize;
				std::streamsize const cryptunit = buffersize + static_cast<std::streamsize>(interface->getDigestSize());

				if ( cryptsize % cryptunit != 0 )
					throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: file size is inconsistent with format (file may be truncated)");

				std::streamsize const cryptblocks = cryptsize / cryptunit;

				assert ( cryptblocks > 0 );

				std::streampos const cryptblock = (cryptblocks - 1);
				in.clear();
				in.seekg(headersize + cryptblock * cryptunit);

				if ( ! in )
					throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: underlying stream failed to seek to last encrypted block");

				std::shared_ptr<char[]> LB(new char[buffersize]);
				interface->reset((cryptblock * buffersize) / interface->getBlockSize());
				interface->read(LB.get(),buffersize);
				interface->readDigest();

	                        libsecrecy::GCMStreamBlockHeader GSBH(LB.get());

	                        fs = (cryptblocks-1) * blocksize + GSBH.blocksize;
	                        fsvalid = true;

				in.clear();
				in.seekg(p);

				if ( ! in )
					throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::getFileSize: underlying stream failed to seek back to original position");
			}

			assert ( fsvalid );

			return fs;
		}


		private:
		// gptr as unsigned pointer
		uint8_t const * uptr() const
		{
			return reinterpret_cast<uint8_t const *>(gptr());
		}

		/**
		 * seek to absolute position
		 **/
		::std::streampos seekpos(::std::streampos sp, ::std::ios_base::openmode which = ::std::ios_base::in | ::std::ios_base::out)
		{
			if ( which & ::std::ios_base::in )
			{
				// current position
				assert ( static_cast<std::ptrdiff_t>(blockoffset) >= egptr() - gptr() );
				std::streampos const curpos = blockoffset - (egptr()-gptr());

				// start of buffer
				assert ( static_cast<std::ptrdiff_t>(blockoffset) >= (egptr() - eback()) );
				std::streampos bufstart = blockoffset - static_cast<std::streampos>(egptr()-eback());

				// start of buffer
				std::streampos bufend = blockoffset;

				assert ( curpos >= bufstart && curpos <= bufend );

				if ( ! (sp >= bufstart && sp <= bufend) )
				{
					std::streamsize const fs = getFileSize();

					if ( sp > fs )
					{
						std::ostringstream ostr;
						ostr << "libsecrecy::GCMInputStreamBuffer::seekpos: position " << sp << " is out of range";
						throw std::runtime_error(ostr.str());
					}

					std::streampos const nblock = sp / blocksize;
					std::streampos const tpos = headersize + nblock * (buffersize + interface->getDigestSize());

					in.clear();
					in.seekg(tpos);

					if ( ! in )
						throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::seekpos: failed to seek on underlying stream");

					blockoffset = std::min(static_cast<std::streamsize>(nblock * blocksize),fs);
					bufferoffset = nblock * buffersize;

					setg(be,be,be);

					underflow();

					bufstart = blockoffset - static_cast<std::streamsize>(egptr()-eback());
					bufend   = blockoffset;

					assert ( sp >= bufstart && sp <= bufend );
				}

				char * const a = eback();
				char * const e = egptr();
				setg(a,a + (sp-bufstart),e);
				return sp;
			}

			return -1;
		}

		::std::streampos seekoff(::std::streamoff off, ::std::ios_base::seekdir way, ::std::ios_base::openmode which = ::std::ios_base::in | ::std::ios_base::out)
		{
			if ( which & ::std::ios_base::in )
			{
				// current position
				assert ( static_cast<std::ptrdiff_t>(blockoffset) >= egptr() - gptr() );
				std::streampos const curpos = blockoffset - (egptr()-gptr());

				// start of buffer
				assert ( static_cast<std::ptrdiff_t>(blockoffset) >= (egptr() - eback()) );
				std::streampos bufstart = blockoffset - static_cast<std::streampos>(egptr()-eback());

				// start of buffer
				std::streampos bufend = blockoffset;
				assert ( curpos >= bufstart && curpos <= bufend );

				std::streampos abstarget = 0;

				if ( way == ::std::ios_base::cur )
					abstarget = curpos + off;
				else if ( way == ::std::ios_base::beg )
					abstarget = off;
				else // if ( way == ::std::ios_base::end )
					abstarget = getFileSize() + off;

				// no actual seek
				if ( abstarget - curpos == 0 )
				{
					return abstarget;
				}
				// move right inside current buffer
				else if ( (abstarget - curpos) > 0 && (abstarget - curpos) <= (egptr()-gptr()) )
				{
					setg(eback(),gptr()+(abstarget-curpos),egptr());
					return abstarget;
				}
				// move left inside current buffer
				else if ( (abstarget - curpos) < 0 && (curpos-abstarget) <= (gptr()-eback()) )
				{
					setg(eback(),gptr()-(curpos-abstarget),egptr());
					return abstarget;
				}
				// new position is outside current buffer, use absolute seek
				else
				{
					return seekpos(abstarget,which);
				}
			}

			return -1;
		}

		int_type underflow()
		{
			// if there is still data, then return it
			if ( gptr() < egptr() )
				return static_cast<int_type>(*uptr());

			assert ( gptr() == egptr() );

			if ( in.peek() == std::istream::traits_type::eof() )
				return traits_type::eof();

			// bytes available in buffer
			std::size_t const av = gptr() - eback();
			// bytes used for putback buffer
			std::size_t const use = std::min(av,putbackbuffersize);

			// copy last use bytes from current buffer to start of B
			std::copy(gptr() - use,gptr(),B.get());

			char * p = be-buffersize;

			interface->reset(bufferoffset / interface->getBlockSize());
			interface->read(p,buffersize);
			interface->readDigest();

                        libsecrecy::GCMStreamBlockHeader GSBH(p);
                        p += GSBH.size();

                        if ( ! GSBH.blocksize )
                        	throw std::runtime_error("libsecrecy::GCMInputStreamBuffer::underflow: invalid empty block");

                        // copy putback buffer to correct location
			std::copy_backward(B.get(),B.get() + use,p);

			currentblocksize = GSBH.blocksize;
			bufferoffset += buffersize;
			blockoffset += currentblocksize;

			// set new buffer
			setg(p-use,p,p+currentblocksize);

			return static_cast<int_type>(*uptr());
		}
	};
}
#endif
