/** @file bitstreamencoder.cpp
 *  An encoder that writes data in a compact bitstream format
 * 	@headerfile bitstreamencoder.h "ux_base/bitstreamencoder.h"
 *	@author Chris Burns (cburns@ea.com)
 *
 *	UX
 *	(c) 2012 Electronic Arts Inc. 
 */

#include "EABaseTypesPlus/bitstreamencoder.h"
#include "EABaseTypesPlus/bitstream.h"

#include "smaz.h"

namespace EA
{
	namespace Types
	{
		BitStreamEncoder::BitStreamEncoder(EA::Allocator::ICoreAllocator* allocator, BitOutputStream *out):
			mAllocator(allocator),
			mOut(out),
			mNextId(0),
			mInterned(allocator)
		{
		}

		void BitStreamEncoder::Reset()
		{
			mOut->Reset();
			mInterned.clear();
		}

		void BitStreamEncoder::BeginArray()
		{
			mOut->Serialize<unsigned char>(CODE_ARRAY, 3);
		}

		void BitStreamEncoder::EndArray()
		{
			mOut->Serialize<unsigned char>(0, 1);
		}

		void BitStreamEncoder::BeginArrayValue(size_t index)
		{
			mOut->Serialize<unsigned char>(1, 1);
		}

		void BitStreamEncoder::BeginObject()
		{
			mOut->Serialize<unsigned char>(CODE_OBJECT, 3);
		}

		void BitStreamEncoder::EndObject()
		{
			mOut->Serialize<unsigned char>(0, 1);
		}

		void BitStreamEncoder::BeginObjectValue(const char* key, size_t length)
		{
			mOut->Serialize<unsigned char>(1, 1);

			StringData(key, length);
		}

		void BitStreamEncoder::Boolean(bool boolean)
		{
			mOut->Serialize<unsigned char>(CODE_BOOLEAN, 3);

			mOut->Serialize<unsigned char>(boolean ? static_cast<unsigned char>(1) : static_cast<unsigned char>(0), 1);
		}

		void BitStreamEncoder::Number(EA::BaseTypeFloat number)
		{
			int integer = static_cast<int>(number);
			if(static_cast<EA::BaseTypeFloat>(integer) == number)
			{
				mOut->Serialize<unsigned char>(CODE_INTEGER, 3);

				Integer(integer);
			}
			else
			{
				mOut->Serialize<unsigned char>(CODE_FLOAT, 3);

				mOut->Serialize<EA::BaseTypeFloat>(number);
			}
		}

		void BitStreamEncoder::String(const char* str, size_t length)
		{
			mOut->Serialize<unsigned char>(CODE_STRING, 3);

			StringData(str, length);
		}

		void BitStreamEncoder::Function(EA::Types::FunctionIn function)
		{
			EA_FAIL_MESSAGE("Cannot encode a function into a bitstream!");
		}

		void BitStreamEncoder::UserData(EA::Types::UserDataIn function)
		{
			EA_FAIL_MESSAGE("Cannot encode a UserData into a bitstream!");
		}

		void BitStreamEncoder::Null()
		{
			mOut->Serialize<unsigned char>(CODE_NULL, 3);
		}

		/* private */ void BitStreamEncoder::Integer(int integer)
		{
			if(integer <= 0xF)
			{
				mOut->Serialize<unsigned char>(0, 2);

				mOut->Serialize<int>(integer, 4);
			}
			else if(integer <= 0xFF)
			{
				mOut->Serialize<unsigned char>(1, 2);

				mOut->Serialize<int>(integer, 8);
			}
			else if(integer <= 0xFFFF)
			{
				mOut->Serialize<unsigned char>(2, 2);

				mOut->Serialize<int>(integer, 16);
			}
			else
			{
				mOut->Serialize<unsigned char>(3, 2);

				mOut->Serialize<int>(integer, 32);
			}
		}

		/* private */ void BitStreamEncoder::StringData(const char* str, size_t length)
		{
			EA::RawString s(mAllocator);
			s.assign(str, static_cast<eastl_size_t>(length));

			StringMap::const_iterator it = mInterned.find(s);
			if(it != mInterned.end())
			{
				mOut->Serialize<unsigned char>(STRING_ENCODING_INTERN, 2);

				Integer((*it).second);
			}
			else
			{
				char compressed[256];
				unsigned int smazSize = static_cast<unsigned int>(smaz_compress(const_cast<char *>(str), static_cast<int>(length), compressed, static_cast<int>(sizeof(compressed) - 1)));
				EA_ASSERT(smazSize < sizeof(compressed));
				compressed[smazSize] = '\0';

				size_t utf8Bits = (length + 1) * 8;
				size_t alphanumericBits = IsAlphanumeric(s.c_str(), s.length()) ? (length + 1) * 6 : utf8Bits + 1;
				size_t smazBits = (smazSize + 1) * 8;

				if(smazBits < alphanumericBits) // better than Alphanumeric and as good as or better than UTF8 in this case
				{
					mOut->Serialize<unsigned char>(STRING_ENCODING_SMAZ, 2);
					UTF8(compressed, smazSize + 1);
				}
				else if(alphanumericBits < utf8Bits) // better than UTF8 and SMAZ
				{
					mOut->Serialize<unsigned char>(STRING_ENCODING_ALPHANUMERIC, 2);
					Alphanumeric(s.c_str(), s.length() + 1);
				}
				else // UTF8 wins sometimes
				{
					mOut->Serialize<unsigned char>(STRING_ENCODING_UTF8, 2);
					UTF8(s.c_str(), s.length() + 1);
				}

				mInterned.insert(s).first->second = mNextId++;
			}
		}

		/* private */ void BitStreamEncoder::UTF8(const char* str, size_t length)
		{
			BitInputStream in(str, 0, static_cast<int>(length * 8));

			mOut->Write(in);
		}

		static const unsigned char alphanumeric_codebook[256] =
		{
			  0,'~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			  1,  2,  3,  4,  5,  6,  7,  8,
			  9, 10,'~','~','~','~','~','~',
			'~', 11, 12, 13, 14, 15, 16, 17,
			 18, 19, 20, 21, 22, 23, 24, 25,
			 26, 27, 28, 29, 30, 31, 32, 33,
			 34, 35, 36,'~','~','~','~', 37,
			 '~',38, 39, 40, 41, 42, 43, 44,
			 45, 46, 47, 48, 49, 50, 51, 52, 
			 53, 54, 55, 56, 57, 58, 59, 60, 
			 61, 62, 63,'~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~',
			'~','~','~','~','~','~','~','~'
		};

		static const unsigned char alphanumeric_reversecodebook[64] =
		{
			  0,'0','1','2','3','4','5','6',
			'7','8','9','A','B','C','D','E',
			'F','G','H','I','J','K','L','M',
			'N','O','P','Q','R','S','T','U',
			'V','W','X','Y','Z','_','a','b',
			'c','d','e','f','g','h','i','j',
			'k','l','m','n','o','p','q','r',
			's','t','u','v','w','x','y','z'
		};

		/* private */ bool BitStreamEncoder::IsAlphanumeric(const char* str, size_t length)
		{
			bool result = true;
			for(size_t i = 0; i < length; i++)
			{
				if(alphanumeric_codebook[static_cast<unsigned char>(str[i])] == '~')
				{
					result = false;
					break;
				}
			}

			return result;
		}

		/* private */ void BitStreamEncoder::Alphanumeric(const char* str, size_t length)
		{
			for(size_t i = 0; i < length; i++)
			{
				mOut->Serialize<unsigned char>(alphanumeric_codebook[(unsigned char) str[i]], 6);
			}
		}

		void DecodeUTF8(BitInputStream* in, EA::RawString& str)
		{
			unsigned char c = in->Deserialize<unsigned char>();
			while(c != 0)
			{
				str.push_back(static_cast<char>(c));

				c = in->Deserialize<unsigned char>();
			}
		}

		void DecodeAlphanumeric(BitInputStream* in, EA::RawString& str)
		{
			unsigned char c = in->Deserialize<unsigned char>(6);
			while(c != 0)
			{
				str.push_back(static_cast<char>(alphanumeric_reversecodebook[c]));

				c = in->Deserialize<unsigned char>(6);
			}
		}

		void DecodeSMAZ(BitInputStream* in, EA::RawString& str)
		{
			DecodeUTF8(in, str);

			char decompressed[256];

			unsigned int size = static_cast<unsigned int>(smaz_decompress(const_cast<char*>(str.c_str()), static_cast<int>(str.size()), decompressed, static_cast<int>(sizeof(decompressed))));
			EA_ASSERT(size <= sizeof(decompressed));

			str.assign(decompressed, size);
		}
	}
}
