////////////////////////////////////////////////////////////////////////////////////////
//
// Nestopia - NES/Famicom emulator written in C++
//
// Copyright (C) 2003-2008 Martin Freij
//
// This file is part of Nestopia.
//
// Nestopia is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// Nestopia is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Nestopia; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
////////////////////////////////////////////////////////////////////////////////////////

#include <cstring>
#include <new>
#include <iosfwd>
#include "NstVector.hpp"
#include "NstCrc32.hpp"
#include "NstStream.hpp"
#include "NstPatcherUps.hpp"

namespace Nes
{
	namespace Core
	{
		class Ups::Reader
		{
		public:

			uint Read();
			dword ReadInt();
			dword ReadCrc();

		private:

			Stream::In stream;
			dword remaining;
			dword crc;

		public:

			explicit Reader(std::istream& stdStream)
			:
			stream    (&stdStream),
			remaining (stream.Length()),
			crc       (0)
			{}

			void ReadSignature()
			{
				if
				(
					Read() != Ascii<'U'>::V ||
					Read() != Ascii<'P'>::V ||
					Read() != Ascii<'S'>::V ||
					Read() != Ascii<'1'>::V
				)
					throw RESULT_ERR_INVALID_FILE;
			}

			dword Remaining() const
			{
				return remaining;
			}

			dword Crc() const
			{
				return crc;
			}
		};

		class Ups::Writer
		{
		public:

			void Write(uint);
			void Write(const byte* NST_RESTRICT,dword);
			void WriteInt(dword);
			void WriteCrc(dword);

		private:

			Stream::Out stream;
			dword crc;

		public:

			explicit Writer(std::ostream& stdStream)
			:
			stream (&stdStream),
			crc    (0)
			{}

			void WriteSignature()
			{
				const byte data[] =
				{
					Ascii<'U'>::V,
					Ascii<'P'>::V,
					Ascii<'S'>::V,
					Ascii<'1'>::V
				};

				Write( data, sizeof(data) );
			}

			dword Crc() const
			{
				return crc;
			}
		};

		#ifdef NST_MSVC_OPTIMIZE
		#pragma optimize("s", on)
		#endif

		Ups::Ups()
		:
		srcSize (0),
		srcCrc  (0),
		dstSize (0),
		dstCrc  (0),
		patch   (NULL)
		{
		}

		Ups::~Ups()
		{
			Destroy();
		}

		void Ups::Destroy()
		{
			srcSize = 0;
			srcCrc  = 0;
			dstSize = 0;
			dstCrc  = 0;

			delete [] patch;
			patch = NULL;
		}

		#ifdef NST_MSVC_OPTIMIZE
		#pragma optimize("", on)
		#endif

		bool Ups::IsUps(std::istream& stream)
		{
			try
			{
				return Stream::In(&stream).Peek32() == AsciiId<'U','P','S','1'>::V;
			}
			catch (...)
			{
				return false;
			}
		}

		Result Ups::Load(std::istream& stdStream,const bool bypassChecksum)
		{
			Destroy();

			try
			{
				Reader reader( stdStream );

				reader.ReadSignature();

				srcSize = reader.ReadInt();
				dstSize = reader.ReadInt();

				const dword size = NST_MAX(srcSize,dstSize) + 1;

				patch = new byte [size];
				std::memset( patch, 0, size );

				for (dword i=0; reader.Remaining() > 4+4+4; ++i)
				{
					i += reader.ReadInt();

					if (i > MAX_OFFSET)
						throw RESULT_ERR_OUT_OF_MEMORY;

					while (const uint data = reader.Read())
					{
						if (i < dstSize)
							patch[i++] = data;
						else
							throw RESULT_ERR_CORRUPT_FILE;
					}
				}

				srcCrc = reader.ReadCrc();
				dstCrc = reader.ReadCrc();

				const dword crc = reader.Crc();
				const dword fileCrc = reader.ReadCrc();

				if (!bypassChecksum && crc != fileCrc)
					throw RESULT_ERR_INVALID_CRC;
			}
			catch (Result result)
			{
				Destroy();
				return result;
			}
			catch (const std::bad_alloc&)
			{
				Destroy();
				return RESULT_ERR_OUT_OF_MEMORY;
			}
			catch (...)
			{
				Destroy();
				return RESULT_ERR_CORRUPT_FILE;
			}

			return RESULT_OK;
		}

		Result Ups::Save(std::ostream& stdStream) const
		{
			try
			{
				Writer writer( stdStream );

				writer.WriteSignature();

				writer.WriteInt( srcSize );
				writer.WriteInt( dstSize );

				for (dword i=0, offset=0, n=dstSize; i < n; ++i)
				{
					if (patch[i])
					{
						writer.WriteInt( i - offset );
						offset = i;

						while (patch[++i]);

						NST_ASSERT( i <= n );

						writer.Write( patch + offset, i - offset );
						writer.Write( 0 );

						offset = i + 1;
					}
				}

				writer.WriteCrc( srcCrc );
				writer.WriteCrc( dstCrc );
				writer.WriteCrc( writer.Crc() );
			}
			catch (Result result)
			{
				return result;
			}
			catch (const std::bad_alloc&)
			{
				return RESULT_ERR_OUT_OF_MEMORY;
			}
			catch (...)
			{
				return RESULT_ERR_GENERIC;
			}

			return RESULT_OK;
		}

		Result Ups::Test(std::istream& stdStream,const bool bypassChecksum) const
		{
			Vector<byte> buffer( srcSize );

			try
			{
				Stream::In stream( &stdStream );

				if (stream.Length() < buffer.Size())
					return RESULT_ERR_CORRUPT_FILE;

				if (buffer.Size())
					stream.Peek( buffer.Begin(), buffer.Size() );
			}
			catch (Result result)
			{
				return result;
			}
			catch (const std::bad_alloc&)
			{
				return RESULT_ERR_OUT_OF_MEMORY;
			}
			catch (...)
			{
				return RESULT_ERR_CORRUPT_FILE;
			}

			return Test( buffer.Begin(), buffer.Size(), bypassChecksum );
		}

		Result Ups::Test(const byte* const NST_RESTRICT src,const dword size,const bool bypassChecksum) const
		{
			NST_ASSERT( !size || src );

			if (size < srcSize)
				return RESULT_ERR_CORRUPT_FILE;

			if (!bypassChecksum)
			{
				if (Crc32::Compute( src, srcSize ) != srcCrc)
					return RESULT_ERR_INVALID_CRC;

				dword crc = 0;

				for (dword i=0, n=dstSize; i < n; ++i)
					crc = Crc32::Compute( (i < size ? src[i] : 0U) ^ patch[i], crc );

				if (crc != dstCrc)
					return RESULT_ERR_INVALID_CRC;
			}

			return RESULT_OK;
		}

		bool Ups::Patch(const byte* const src,byte* const dst,const dword size,dword offset) const
		{
			NST_ASSERT( !size || (src && dst) );

			uint patched = 0;

			if (dstSize || src != dst)
			{
				for (dword i=0, end=dstSize; i < size; ++i)
					dst[i] = src[i] ^ (offset < end ? patched |= patch[offset], patch[offset++] : 0U);
			}

			return patched;
		}

		Result Ups::Create(const byte* const src,const byte* const dst,const dword size)
		{
			NST_ASSERT( !size || (src && dst) );

			Destroy();

			if (size)
			{
				patch = new (std::nothrow) byte [size];

				if (!patch)
					return RESULT_ERR_OUT_OF_MEMORY;

				srcSize = size;
				dstSize = size;

				srcCrc = Crc32::Compute( src, size );
				dstCrc = Crc32::Compute( dst, size );

				for (dword i=0; i < size; ++i)
					patch[i] = uint(src[i]) ^ uint(dst[i]);
			}

			return RESULT_OK;
		}

		uint Ups::Reader::Read()
		{
			if (remaining)
				remaining--;
			else
				throw 1;

			const uint data = stream.Read8();
			crc = Crc32::Compute( data, crc );

			return data;
		}

		dword Ups::Reader::ReadInt()
		{
			dword v = 0, s = 0;

			for (;;)
			{
				uint b = Read();
				v += (b & 0x7F) << s;

				if (v > MAX_SIZE)
					throw 1;

				if (b & 0x80)
					break;

				s += 7;
				v += 0x1UL << s;
			}

			return v;
		}

		dword Ups::Reader::ReadCrc()
		{
			dword crc = 0;

			for (uint i=0; i < 32; i += 8)
				crc |= dword(Read()) << i;

			return crc;
		}

		void Ups::Writer::Write(const uint data)
		{
			crc = Crc32::Compute( data, crc );
			stream.Write8( data );
		}

		void Ups::Writer::Write(const byte* const NST_RESTRICT data,const dword size)
		{
			crc = Crc32::Compute( data, size, crc );
			stream.Write( data, size );
		}

		void Ups::Writer::WriteCrc(const dword crc)
		{
			for (uint i=0; i < 32; i += 8)
				Write( crc >> i & 0xFF );
		}

		void Ups::Writer::WriteInt(dword v)
		{
			for (;;)
			{
				uint x = v & 0x7F;
				v >>= 7;

				if (!v)
				{
					Write( 0x80 | x );
					break;
				}

				Write( x );
				v--;
			}
		}
	}
}
