////////////////////////////////////////////////////////////////////////////////////////
//
// 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 <cstdlib>
#include <cstring>
#include "NstLog.hpp"
#include "NstChecksum.hpp"
#include "NstImageDatabase.hpp"
#include "board/NstBoard.hpp"
#include "NstCartridge.hpp"
#include "NstCartridgeRomset.hpp"
#include "NstCartridgeInes.hpp"
#include "NstCartridgeUnif.hpp"
#include "vssystem/NstVsSystem.hpp"
#include "api/NstApiUser.hpp"

namespace Nes
{
	namespace Core
	{
		#ifdef NST_MSVC_OPTIMIZE
		#pragma optimize("s", on)
		#endif

		Cartridge::ProfileEx::ProfileEx()
		: nmt(NMT_DEFAULT), battery(false), wramAuto(false) {}

		Cartridge::Cartridge(Context& context)
		: Image(CARTRIDGE), board(NULL), vs(NULL), favoredSystem(context.favoredSystem)
		{
			try
			{
				ProfileEx profileEx;

				switch (Stream::In(&context.stream).Peek32())
				{
					case INES_ID:

						Ines::Load
						(
							context.stream,
							context.patch,
							context.patchBypassChecksum,
							context.patchResult,
							prg,
							chr,
							context.favoredSystem,
							profile,
							profileEx,
							context.database
						);
						break;

					case UNIF_ID:

						Unif::Load
						(
							context.stream,
							context.patch,
							context.patchBypassChecksum,
							context.patchResult,
							prg,
							chr,
							context.favoredSystem,
							profile,
							profileEx,
							context.database
						);
						break;

					case 0x0000:
						// Check for VC/NES Remix container for cartridge-based games
						// Just load the game for now, until more is known about the container format.
						//if (((Stream::In(&context.stream).Peek64() >> 32) & 0xFF) & 0x60)
						//{
							Ines::Load
							(
								context.stream,
								context.patch,
								context.patchBypassChecksum,
								context.patchResult,
								prg,
								chr,
								context.favoredSystem,
								profile,
								profileEx,
								context.database
							);
						//}
						break;

					default:

						Romset::Load
						(
							context.stream,
							context.patch,
							context.patchBypassChecksum,
							context.patchResult,
							prg,
							chr,
							context.favoredSystem,
							context.askProfile,
							profile
						);
						break;
				}

				if (profile.dump.state == Profile::Dump::BAD)
					context.result = RESULT_WARN_BAD_DUMP;
				else
					context.result = RESULT_OK;

				const Result result = SetupBoard( prg, chr, &board, &context, profile, profileEx, &prgCrc );

				if (NES_FAILED(result))
					throw result;

				board->Load( savefile );

				if ((profile.system.type) == Profile::System::VS_UNISYSTEM)
				{
					vs = VsSystem::Create
					(
						context.cpu,
						context.ppu,
						static_cast<PpuModel>(profile.system.ppu),
						prgCrc
					);

					profile.system.ppu = static_cast<Profile::System::Ppu>(vs->GetPpuModel());
				}
				else if ((profile.system.type) == Profile::System::VS_DUALSYSTEM)
				{
					throw RESULT_ERR_UNSUPPORTED_VSSYSTEM;
				}

				if (Cartridge::QueryExternalDevice( EXT_DIP_SWITCHES ))
					Log::Flush( "Cartridge: DIP Switches present" NST_LINEBREAK );
			}
			catch (...)
			{
				Destroy();
				throw;
			}
		}

		void Cartridge::Destroy()
		{
			VsSystem::Destroy( vs );
			Boards::Board::Destroy( board );
		}

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

		void Cartridge::ReadRomset(std::istream& stream,FavoredSystem favoredSystem,bool askSystem,Profile& profile)
		{
			Log::Suppressor logSupressor;
			Ram prg, chr;
			ProfileEx profileEx;
			Romset::Load( stream, NULL, false, NULL, prg, chr, favoredSystem, askSystem, profile, true );
			SetupBoard( prg, chr, NULL, NULL, profile, profileEx, NULL, true );
		}

		void Cartridge::ReadInes(std::istream& stream,FavoredSystem favoredSystem,Profile& profile)
		{
			Log::Suppressor logSupressor;
			Ram prg, chr;
			ProfileEx profileEx;
			Ines::Load( stream, NULL, false, NULL, prg, chr, favoredSystem, profile, profileEx, NULL );
			SetupBoard( prg, chr, NULL, NULL, profile, profileEx, NULL );
		}

		void Cartridge::ReadUnif(std::istream& stream,FavoredSystem favoredSystem,Profile& profile)
		{
			Log::Suppressor logSupressor;
			Ram prg, chr;
			ProfileEx profileEx;
			Unif::Load( stream, NULL, false, NULL, prg, chr, favoredSystem, profile, profileEx, NULL );
			SetupBoard( prg, chr, NULL, NULL, profile, profileEx, NULL );
		}

		uint Cartridge::GetDesiredController(uint port) const
		{
			NST_ASSERT( port < Api::Input::NUM_CONTROLLERS );
			return profile.game.controllers[port];
		}

		uint Cartridge::GetDesiredAdapter() const
		{
			return profile.game.adapter;
		}

		Cartridge::ExternalDevice Cartridge::QueryExternalDevice(ExternalDeviceType deviceType)
		{
			switch (deviceType)
			{
				case EXT_DIP_SWITCHES:

					if (vs)
						return &vs->GetDipSwiches();
					else
						return board->QueryDevice( Boards::Board::DEVICE_DIP_SWITCHES );

				case EXT_BARCODE_READER:

					return board->QueryDevice( Boards::Board::DEVICE_BARCODE_READER );

				default:

					return Image::QueryExternalDevice( deviceType );
			}
		}

		Result Cartridge::SetupBoard
		(
			Ram& prg,
			Ram& chr,
			Boards::Board** board,
			const Context* const context,
			Profile& profile,
			const ProfileEx& profileEx,
			dword* const prgCrc,
			const bool readOnly
		)
		{
			NST_ASSERT( bool(board) == bool(context) );

			Boards::Board::Type::Nmt nmt;

			if (profile.board.solderPads & (Profile::Board::SOLDERPAD_H|Profile::Board::SOLDERPAD_V))
			{
				if (profile.board.solderPads & Profile::Board::SOLDERPAD_H)
					nmt = Boards::Board::Type::NMT_VERTICAL;
				else
					nmt = Boards::Board::Type::NMT_HORIZONTAL;
			}
			else switch (profileEx.nmt)
			{
				case ProfileEx::NMT_HORIZONTAL:   nmt = Boards::Board::Type::NMT_HORIZONTAL;   break;
				case ProfileEx::NMT_VERTICAL:     nmt = Boards::Board::Type::NMT_VERTICAL;     break;
				case ProfileEx::NMT_SINGLESCREEN: nmt = Boards::Board::Type::NMT_SINGLESCREEN; break;
				case ProfileEx::NMT_FOURSCREEN:   nmt = Boards::Board::Type::NMT_FOURSCREEN;   break;
				default:                          nmt = Boards::Board::Type::NMT_CONTROLLED;   break;
			}

			Chips chips;

			for (Profile::Board::Chips::const_iterator i(profile.board.chips.begin()), end(profile.board.chips.end()); i != end; ++i)
			{
				Chips::Type& type = chips.Add( i->type.c_str() );

				for (Profile::Board::Pins::const_iterator j(i->pins.begin()), end(i->pins.end()); j != end; ++j)
					type.Pin(j->number) = j->function.c_str();

				for (Profile::Board::Samples::const_iterator j(i->samples.begin()), end(i->samples.end()); j != end; ++j)
					type.Sample(j->id) = j->file.c_str();
			}

			Boards::Board::Context b
			(
				context ? &context->cpu : NULL,
				context ? &context->apu : NULL,
				context ? &context->ppu : NULL,
				prg,
				chr,
				profileEx.trainer,
				nmt,
				profileEx.battery || profile.board.HasWramBattery(),
				profile.board.HasMmcBattery(),
				chips
			);

			if (profile.board.type.empty() || !b.DetectBoard( profile.board.type.c_str(), profile.board.GetWram() ))
			{
				if (profile.board.mapper == Profile::Board::NO_MAPPER || (!b.DetectBoard( profile.board.mapper, profile.board.subMapper, profile.board.chrRam, profile.board.GetWram(), profileEx.wramAuto ) && board))
					return RESULT_ERR_UNSUPPORTED_MAPPER;

				if (profile.board.type.empty())
					profile.board.type = std::wstring( b.name, b.name + std::strlen(b.name) );
			}

			if (profile.board.mapper == Profile::Board::NO_MAPPER && b.type.GetMapper() != Boards::Board::Type::NMPR)
				profile.board.mapper = b.type.GetMapper();

			for (uint i=0; i < 2; ++i)
			{
				dword size = (i ? b.chr : b.prg).Size();

				if (size != (i ? profile.board.GetChr() : profile.board.GetPrg()))
				{
					Profile::Board::Roms& roms = (i ? profile.board.chr : profile.board.prg);
					roms.clear();

					if (size)
					{
						Profile::Board::Rom rom;
						rom.size = size;
						roms.push_back( rom );
					}
				}

				size = (i ? b.type.GetVram() : b.type.GetWram());

				if (size != (i ? profile.board.GetVram() : profile.board.GetWram()))
				{
					Profile::Board::Rams& rams = (i ? profile.board.vram : profile.board.wram);
					rams.clear();

					for (uint j=0; j < 2; ++j)
					{
						size = i ? (j ? b.type.GetNonSavableVram() : b.type.GetSavableVram()) :
                                   (j ? b.type.GetNonSavableWram() : b.type.GetSavableWram());

						if (size)
						{
							Profile::Board::Ram ram;
							ram.size = size;
							ram.battery = (i == 0 && j == 0 && b.wramBattery);
							rams.push_back( ram );
						}
					}
				}

				Profile::Board::Roms& roms = (i ? profile.board.chr : profile.board.prg);

				for (dword j=0, k=0, n=roms.size(); j < n; k += roms[j].size, ++j)
					roms[j].hash.Compute( (i ? chr : prg).Mem(k), roms[j].size );
			}

			if (!readOnly)
			{
				Checksum checksum;

				checksum.Compute( prg.Mem(), prg.Size() );

				if (prgCrc)
					*prgCrc = checksum.GetCrc();

				checksum.Compute( chr.Mem(), chr.Size() );
				profile.hash.Assign( checksum.GetSha1(), checksum.GetCrc() );
			}

			if (board)
				*board = Boards::Board::Create( b );

			return RESULT_OK;
		}

		void Cartridge::Reset(const bool hard)
		{
			board->Reset( hard );

			if (vs)
				vs->Reset( hard );
		}

		bool Cartridge::PowerOff()
		{
			try
			{
				if (board)
				{
					board->Sync( Boards::Board::EVENT_POWER_OFF, NULL );
					board->Save( savefile );
				}

				return true;
			}
			catch (...)
			{
				return false;
			}
		}

		void Cartridge::SaveState(State::Saver& state,const dword baseChunk) const
		{
			state.Begin( baseChunk );

			board->SaveState( state, AsciiId<'M','P','R'>::V );

			if (vs)
				vs->SaveState( state, AsciiId<'V','S','S'>::V );

			state.End();
		}

		void Cartridge::LoadState(State::Loader& state)
		{
			while (const dword chunk = state.Begin())
			{
				switch (chunk)
				{
					case AsciiId<'M','P','R'>::V:

						board->LoadState( state );
						break;

					case AsciiId<'V','S','S'>::V:

						NST_VERIFY( vs );

						if (vs)
							vs->LoadState( state );

						break;
				}

				state.End();
			}
		}

		Region Cartridge::GetDesiredRegion() const
		{
			switch (profile.system.type)
			{
				case Profile::System::NES_PAL:
				case Profile::System::NES_PAL_A:
				case Profile::System::NES_PAL_B:
				case Profile::System::DENDY:

					return REGION_PAL;

				case Profile::System::NES_NTSC:
				case Profile::System::FAMICOM:

					if (favoredSystem == FAVORED_DENDY)
						return REGION_PAL;
					// fallthrough

				default:

					return REGION_NTSC;
			}
		}

		System Cartridge::GetDesiredSystem(Region region,CpuModel* cpu,PpuModel* ppu) const
		{
			if (region == Cartridge::GetDesiredRegion())
			{
				if (favoredSystem == FAVORED_DENDY && region == REGION_PAL)
				{
					switch (profile.system.type)
					{
						case Profile::System::NES_NTSC:
						case Profile::System::NES_PAL:
						case Profile::System::NES_PAL_A:
						case Profile::System::NES_PAL_B:
						case Profile::System::FAMICOM:
						case Profile::System::DENDY:

							if (cpu)
								*cpu = CPU_DENDY;

							if (ppu)
								*ppu = PPU_DENDY;

							return SYSTEM_DENDY;

						case Profile::System::VS_UNISYSTEM:
						case Profile::System::VS_DUALSYSTEM:
						case Profile::System::PLAYCHOICE_10:

							default:
							break;
					}
				}

				if (cpu)
					*cpu = static_cast<CpuModel>(profile.system.cpu);

				if (ppu)
					*ppu = static_cast<PpuModel>(profile.system.ppu);

				return static_cast<System>(profile.system.type);
			}
			else
			{
				return Image::GetDesiredSystem( region, cpu, ppu );
			}
		}

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

		void Cartridge::BeginFrame(const Api::Input& input,Input::Controllers* controllers)
		{
			board->Sync( Boards::Board::EVENT_BEGIN_FRAME, controllers );

			if (vs)
				vs->BeginFrame( input, controllers );
		}

		void Cartridge::VSync()
		{
			board->Sync( Boards::Board::EVENT_END_FRAME, NULL );

			if (vs)
				vs->VSync();
		}
	}
}
