/**
 * 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_GCMSTREAMBASE_HPP)
#define LIBSECRECY_GCMSTREAMBASE_HPP

#include <cstdlib>
#include <climits>
#include <cassert>
#include <stdexcept>
#include <sstream>

namespace libsecrecy
{
	struct GCMStreamBase
	{
		static std::size_t getMask(unsigned int const numbits)
		{
			std::size_t bytemask = 0;

			for ( unsigned int i = 0; i < numbits; ++i  )
			{
				bytemask <<= 1;
				bytemask |= 1;
			}

			return bytemask;
		}

		static unsigned int numChar(std::size_t v)
		{
			unsigned int l = 0;
			std::size_t const bytemask = getMask(CHAR_BIT);

			while ( v )
			{
				v &= (~bytemask);
				v >>= CHAR_BIT;
				l += 1;
			}

			return l;
		}

		static unsigned int numBits(std::size_t v)
		{
			std::size_t const one = 1;
			std::size_t const notone = ~one;
			unsigned int l = 0;

			while ( v )
			{
				v &= notone;
				v >>= 1;
				l += 1;
			}

			return l;
		}

		static unsigned int writeNumber(std::ostream & out, std::size_t const v)
		{
			unsigned int const cb1 = CHAR_BIT-1;
			unsigned char topbit = 1ul << cb1;
			unsigned int r = 0;

			if ( ! v )
			{
				out.put(topbit);
				r += 1;
			}
			else
			{
				unsigned int const nb = numBits(v);
				unsigned int const l  = (nb + cb1 - 1) / cb1;
				assert ( l );
				std::size_t const mask = getMask(cb1);

				unsigned int shift = (l-1) * cb1;

				for ( unsigned int i = 1; i < l; ++i )
				{
					out.put((v >> shift) & mask);
					r += 1;
					shift -= cb1;
				}

				out.put(topbit | ((v >> shift) & mask));
				r += 1;
			}

			if ( ! out )
			{
				throw std::runtime_error("libsecrecy::GCMStreamBase::writeNumber: failed write data to stream");
			}

			return r;
		}

		static std::size_t writeNumberFixedSize(std::ostream & out, std::size_t const num, unsigned int const l)
		{
			unsigned int shift = l*CHAR_BIT;
			std::size_t const mask = getMask(CHAR_BIT);

			for ( unsigned int i = 0; i < l; ++i )
			{
				shift -= CHAR_BIT;
				out.put((num >> shift)&mask);
			}

			if ( ! out )
			{
				throw std::runtime_error("libsecrecy::GCMStreamBase::writeNumberFixedSize: failed write data to stream");
			}

			return l;
		}

		static unsigned char getChecked(std::istream & in)
		{
			int const l = in.get();

			if ( !in || l == std::istream::traits_type::eof() )
			{
				throw std::runtime_error("libsecrecy::GCMStreamBase::getChecked: failed to read data from stream");
			}

			return l;
		}

		static unsigned char getChecked(char const * & ca, char const * ce)
		{
			if ( ca == ce )
			{
				throw std::runtime_error("libsecrecy::GCMStreamBase::getChecked: failed to read data from stream");
			}

			return *(ca++);
		}


		static std::size_t readNumber(std::istream & in)
		{
			unsigned int const cb1 = CHAR_BIT-1;
			unsigned char topbit = 1ul << cb1;
			unsigned char mask = getMask(cb1);
			std::size_t v = 0;

			while ( true )
			{
				v <<= cb1;

				unsigned char const c = getChecked(in);

				v |= (c & mask);

				if ( (c & topbit) != 0 )
					break;
			}

			return v;
		}

		static std::size_t readNumber(char const * & ca, char const * ce)
		{
			unsigned int const cb1 = CHAR_BIT-1;
			unsigned char topbit = 1ul << cb1;
			unsigned char mask = getMask(cb1);
			std::size_t v = 0;

			while ( true )
			{
				v <<= cb1;

				unsigned char const c = getChecked(ca,ce);

				v |= (c & mask);

				if ( (c & topbit) != 0 )
					break;
			}

			return v;
		}

		static bool testEncodingSingle(std::size_t const n)
		{
			std::ostringstream ostr;
			writeNumber(ostr,n);
			std::istringstream istr(ostr.str());
			std::size_t const m = readNumber(istr);
			return n == m;
		}

		static bool testEncoding(std::size_t const n)
		{
			bool ok = true;

			for ( unsigned int i = 0; ok && i < n; ++i )
				ok = ok && testEncodingSingle(i);

			return ok;
		}

		static std::string readString(std::istream & in)
		{
			std::size_t l = readNumber(in);
			std::ostringstream ostr;

			for ( std::size_t i = 0; i < l; ++i )
				ostr.put(getChecked(in));

			return ostr.str();
		}

		static std::string readString(char const * & ca, char const * ce)
		{
			std::size_t l = readNumber(ca,ce);
			std::ostringstream ostr;

			for ( std::size_t i = 0; i < l; ++i )
				ostr.put(getChecked(ca,ce));

			return ostr.str();
		}

		static std::size_t writeString(std::ostream & out, std::string const & s)
		{
			std::size_t r = 0;
			r += writeNumber(out,s.size());
			out.write(s.c_str(),s.size());
			r += s.size();
			return r;
		}
	};
}
#endif
