////////////////////////////////////////////////////////////////////////////////////////
//
// 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 "NstStream.hpp"
#include "NstVector.hpp"
#include "NstChecksum.hpp"
#include "NstPatcher.hpp"
#include "NstFile.hpp"
#include "api/NstApiUser.hpp"

namespace Nes
{
	namespace Core
	{

		struct File::Context
		{
			Checksum checksum;
			Vector<byte> data;
		};

		File::File()
		: context( *new Context )
		{
		}

		File::~File()
		{
			delete &context;
		}

		void File::Load(const byte* data,dword size) const
		{
			NST_ASSERT( data && size );

			context.checksum.Clear();
			context.checksum.Compute( data, size );
			context.data.Destroy();
		}

		void File::Load(byte* data,dword size,Type type) const
		{
			NST_ASSERT( data && size );

			context.data.Assign( data, size );

			bool altered = false;

			const LoadBlock loadBlock = {data,size};
			Load( type, &loadBlock, 1, &altered );

			if (altered)
				context.checksum.Clear();
		}

		void File::Load(Type type,byte* data,dword size) const
		{
			const LoadBlock loadBlock = {data,size};
			Load( type, &loadBlock, 1 );
		}

		void File::Load(const Type type,const LoadBlock* const loadBlock,const uint loadBlockCount,bool* const altered) const
		{
			class Loader : public Api::User::File
			{
				const Action action;
				const LoadBlock* const loadBlock;
				const uint loadBlockCount;
				bool* const altered;

				Action GetAction() const throw()
				{
					return action;
				}

				ulong GetMaxSize() const throw()
				{
					dword maxsize = 0;

					for (const LoadBlock* NST_RESTRICT it=loadBlock, *const end=loadBlock+loadBlockCount; it != end; ++it)
						maxsize += it->size;

					return maxsize;
				}

				Result SetContent(const void* data,ulong filesize) throw()
				{
					if (altered)
						*altered = true;

					if (!data || !filesize)
						return RESULT_ERR_INVALID_PARAM;

					const byte* NST_RESTRICT filedata = static_cast<const byte*>(data);

					for (const LoadBlock* NST_RESTRICT it=loadBlock, *const end=loadBlock+loadBlockCount; it != end; ++it)
					{
						if (const dword size = NST_MIN(filesize,it->size))
						{
							std::memcpy( it->data, filedata, size );
							filedata += size;
							filesize -= size;
						}
					}

					return RESULT_OK;
				}

				void GetRawStorage(void*& data, ulong& size) const throw()
				{
					if (loadBlockCount == 1)
					{
						data = loadBlock->data;
						size = loadBlock->size;
					}
					else
					{
						data = 0;
						size = 0;
					}
				}

				Result SetContent(std::istream& stdStream) throw()
				{
					if (altered)
						*altered = true;

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

						if (ulong length = stream.Length())
						{
							for (const LoadBlock* NST_RESTRICT it=loadBlock, *const end=loadBlock+loadBlockCount; it != end; ++it)
							{
								if (const dword size = NST_MIN(length,it->size))
								{
									stream.Read( it->data, size );
									length -= size;
								}
							}
						}
						else
						{
							return RESULT_ERR_INVALID_PARAM;
						}
					}
					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 SetPatchContent(std::istream& stream) throw()
				{
					if (altered)
						*altered = true;

					Patcher patcher;

					Result result = patcher.Load( stream );

					if (NES_FAILED(result))
						return result;

					if (loadBlockCount > 1)
					{
						Patcher::Block* const patchBlocks = new (std::nothrow) Patcher::Block [loadBlockCount];

						if (!patchBlocks)
							return RESULT_ERR_OUT_OF_MEMORY;

						for (uint i=0; i < loadBlockCount; ++i)
						{
							patchBlocks[i].data = loadBlock[i].data;
							patchBlocks[i].size = loadBlock[i].size;
						}

						result = patcher.Test( patchBlocks, loadBlockCount );

						delete [] patchBlocks;
					}
					else
					{
						result = patcher.Test( loadBlockCount ? loadBlock->data : NULL, loadBlockCount ? loadBlock->size : 0 );
					}

					if (NES_SUCCEEDED(result))
					{
						for (dword i=0, offset=0; i < loadBlockCount; offset += loadBlock[i].size, ++i)
							patcher.Patch( loadBlock[i].data, loadBlock[i].data, loadBlock[i].size, offset );
					}

					return result;
				}

			public:

				Loader(Type t,const LoadBlock* l,uint c,bool* a)
				:
				action
				(
					t == EEPROM    ? LOAD_EEPROM :
					t == TAPE      ? LOAD_TAPE :
					t == TURBOFILE ? LOAD_TURBOFILE :
					t == DISK      ? LOAD_FDS :
                                     LOAD_BATTERY
				),
				loadBlock      (l),
				loadBlockCount (c),
				altered        (a)
				{
					if (altered)
						*altered = false;
				}
			};

			{
				Loader loader( type, loadBlock, loadBlockCount, altered );
				Api::User::fileIoCallback( loader );
			}

			context.checksum.Clear();

			for (const LoadBlock* NST_RESTRICT it=loadBlock, *const end=loadBlock+loadBlockCount; it != end; ++it)
				context.checksum.Compute( it->data, it->size );
		}

		void File::Load(const Type type,Vector<byte>& buffer,const dword maxsize) const
		{
			NST_ASSERT( maxsize && type != DISK );

			class Loader : public Api::User::File
			{
				const Action action;
				Vector<byte>& buffer;
				const dword maxsize;

				Action GetAction() const throw()
				{
					return action;
				}

				ulong GetMaxSize() const throw()
				{
					return maxsize;
				}

				Result SetContent(const void* filedata,ulong filesize) throw()
				{
					if (!filedata || !filesize)
						return RESULT_ERR_INVALID_PARAM;

					try
					{
						buffer.Assign( static_cast<const byte*>(filedata), NST_MIN(filesize,maxsize) );
					}
					catch (const std::bad_alloc&)
					{
						return RESULT_ERR_OUT_OF_MEMORY;
					}
					catch (...)
					{
						return RESULT_ERR_GENERIC;
					}

					return RESULT_OK;
				}

				Result SetContent(std::istream& stdStream) throw()
				{
					try
					{
                  Nes::Core::Stream::In stream( &stdStream );

						if (const ulong length = stream.Length())
						{
							buffer.Resize( NST_MIN(length,maxsize) );

							try
							{
								stream.Read( buffer.Begin(), buffer.Size() );
							}
							catch (...)
							{
								buffer.Destroy();
								throw;
							}
						}
						else
						{
							return RESULT_ERR_INVALID_PARAM;
						}
					}
					catch (Result result)
					{
						return result;
					}
					catch (const std::bad_alloc&)
					{
						return RESULT_ERR_OUT_OF_MEMORY;
					}
					catch (...)
					{
						return RESULT_ERR_GENERIC;
					}

					return RESULT_OK;
				}

			public:

				Loader(Type t,Vector<byte>& b,dword m)
				:
				action
				(
					t == EEPROM    ? LOAD_EEPROM :
					t == TAPE      ? LOAD_TAPE :
					t == TURBOFILE ? LOAD_TURBOFILE :
                                     LOAD_BATTERY
				),
				buffer  (b),
				maxsize (m)
				{
				}
			};

			{
				Loader loader( type, buffer, maxsize );
				Api::User::fileIoCallback( loader );
			}

			if (buffer.Size())
				Load( buffer.Begin(), buffer.Size() );
		}

		void File::Save(Type type,const byte* data,dword size) const
		{
			const SaveBlock saveBlock = {data,size};
			Save( type, &saveBlock, 1 );
		}

		void File::Save(const Type type,const SaveBlock* const saveBlock,const uint saveBlockCount) const
		{
			NST_ASSERT( saveBlock && saveBlockCount );

			Checksum checksum;

			for (const SaveBlock *NST_RESTRICT it=saveBlock, *const end=saveBlock+saveBlockCount; it != end; ++it)
				checksum.Compute( it->data, it->size );

			if (checksum != context.checksum)
			{
				class Saver : public Api::User::File
				{
					const Action action;
					const SaveBlock* const saveBlock;
					const uint saveBlockCount;
					mutable Vector<byte> buffer;
					const Vector<byte> original;

					Action GetAction() const throw()
					{
						return action;
					}

					ulong GetMaxSize() const throw()
					{
						dword size = 0;

						for (const SaveBlock* NST_RESTRICT it=saveBlock, *const end=saveBlock+saveBlockCount; it != end; ++it)
							size += it->size;

						return size;
					}

					Result GetContent(const void*& filedata,ulong& filesize) const throw()
					{
						if (saveBlockCount <= 1)
						{
							filedata = saveBlock->data;
							filesize = saveBlock->size;
						}
						else
						{
							if (!buffer.Size())
							{
								try
								{
									buffer.Resize( Saver::GetMaxSize() );
								}
								catch (...)
								{
									filedata = NULL;
									filesize = 0;
									return RESULT_ERR_OUT_OF_MEMORY;
								}

								dword offset = 0;

								for (const SaveBlock* NST_RESTRICT it=saveBlock, *const end=saveBlock+saveBlockCount; it != end; ++it)
								{
									std::memcpy( &buffer[offset], it->data, it->size );
									offset += it->size;
								}
							}

							filedata = buffer.Begin();
							filesize = buffer.Size();
						}

						return RESULT_OK;
					}

					Result GetContent(std::ostream& stdStream) const throw()
					{
						try
						{
                     Nes::Core::Stream::Out stream( &stdStream );

							for (const SaveBlock* NST_RESTRICT it=saveBlock, *const end=saveBlock+saveBlockCount; it != end; ++it)
							{
								if (it->size)
									stream.Write( it->data, it->size );
							}
						}
						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 GetPatchContent(Patch format,std::ostream& stream) const throw()
					{
						if (!original.Size() || (format != PATCH_UPS && format != PATCH_IPS))
							return RESULT_ERR_UNSUPPORTED;

						const void* data;
						ulong size;

						Result result = Saver::GetContent( data, size );

						if (NES_FAILED(result))
							return result;

						if (size != original.Size())
							return RESULT_ERR_UNSUPPORTED;

						Patcher patcher;

						result = patcher.Create
						(
							format == PATCH_UPS ? Patcher::UPS : Patcher::IPS,
							original.Begin(),
							static_cast<const byte*>(data),
							size
						);

						if (NES_FAILED(result))
							return result;

						return patcher.Save( stream );
					}

				public:

					Saver(Type t,const SaveBlock* s,uint c,const Vector<byte>& o)
					:
					action
					(
						t == EEPROM    ? SAVE_EEPROM :
						t == TAPE      ? SAVE_TAPE :
						t == TURBOFILE ? SAVE_TURBOFILE :
						t == DISK      ? SAVE_FDS :
                                         SAVE_BATTERY
					),
					saveBlock      (s),
					saveBlockCount (c),
					original       (o)
					{
					}
				};

				Saver saver( type, saveBlock, saveBlockCount, context.data );
				Api::User::fileIoCallback( saver );
			}
		}
	}
}
