////////////////////////////////////////////////////////////////////////////////////////
//
// 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 <new>
#include "NstMachine.hpp"
#include "NstTrackerMovie.hpp"
#include "NstTrackerRewinder.hpp"
#include "NstImage.hpp"
#include "api/NstApiMachine.hpp"

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

		Tracker::Tracker()
		:
		frame           (0),
		rewinderSound   (false),
		rewinderEnabled (NULL),
		rewinder        (NULL),
		movie           (NULL)
		{}

		Tracker::~Tracker()
		{
			delete rewinder;
			delete movie;
		}

		void Tracker::Unload()
		{
			frame = 0;

			if (rewinder)
				rewinder->Unload();
			else
				StopMovie();
		}

		void Tracker::Reset()
		{
			frame = 0;

			if (rewinder)
			{
				rewinder->Reset();
			}
			else if (movie)
			{
				movie->Reset();
			}
		}

		void Tracker::PowerOff()
		{
			StopMovie();
		}

		void Tracker::Resync(bool excludeFrame) const
		{
			if (rewinder)
			{
				rewinder->Reset();
			}
			else if (movie && !excludeFrame)
			{
				movie->Resync();
			}
		}

		Result Tracker::TryResync(Result lastResult,bool excludeFrame) const
		{
			NST_VERIFY( NES_SUCCEEDED(lastResult) );

			if (NES_SUCCEEDED(lastResult) && lastResult != RESULT_NOP)
				Resync( excludeFrame );

			return lastResult;
		}

		Result Tracker::EnableRewinder(Machine* const emulator)
		{
			if (rewinderEnabled == emulator)
				return RESULT_NOP;

			rewinderEnabled = emulator;
			UpdateRewinderState( true );

			return RESULT_OK;
		}

		void Tracker::EnableRewinderSound(bool enable)
		{
			rewinderSound = enable;

			if (rewinder)
				rewinder->EnableSound( enable );
		}

		void Tracker::ResetRewinder() const
		{
			if (rewinder)
				rewinder->Reset();
		}

		void Tracker::UpdateRewinderState(bool enable)
		{
			if (enable && rewinderEnabled && !movie)
			{
				if (!rewinder)
				{
					rewinder = new Rewinder
					(
						*rewinderEnabled,
						&Machine::Execute,
						&Machine::LoadState,
						&Machine::SaveState,
						rewinderEnabled->cpu,
						rewinderEnabled->cpu.GetApu(),
						rewinderEnabled->ppu,
						rewinderSound
					);
				}
			}
			else
			{
				delete rewinder;
				rewinder = NULL;
			}
		}

		Result Tracker::PlayMovie(Machine& emulator,std::istream& stream)
		{
			if (!emulator.Is(Api::Machine::GAME))
				return RESULT_ERR_NOT_READY;

			UpdateRewinderState( false );

			Result result;

			try
			{
				if (movie == NULL)
				{
					movie = new Movie
					(
						emulator,
						&Machine::LoadState,
						&Machine::SaveState,
						emulator.cpu,
						emulator.Is(Api::Machine::CARTRIDGE) ? emulator.image->GetPrgCrc() : 0
					);
				}

				if (movie->Play( stream ))
				{
					if (emulator.Is(Api::Machine::ON))
						emulator.Reset( true );

					return RESULT_OK;
				}
				else
				{
					return RESULT_NOP;
				}
			}
			catch (Result r)
			{
				result = r;
			}
			catch (const std::bad_alloc&)
			{
				result = RESULT_ERR_OUT_OF_MEMORY;
			}
			catch (...)
			{
				result = RESULT_ERR_GENERIC;
			}

			StopMovie();

			return result;
		}

		Result Tracker::RecordMovie(Machine& emulator,std::iostream& stream,const bool append)
		{
			if (!emulator.Is(Api::Machine::GAME))
				return RESULT_ERR_NOT_READY;

			UpdateRewinderState( false );

			Result result;

			try
			{
				if (movie == NULL)
				{
					movie = new Movie
					(
						emulator,
						&Machine::LoadState,
						&Machine::SaveState,
						emulator.cpu,
						emulator.image->GetPrgCrc()
					);
				}

				return movie->Record( stream, append ) ? RESULT_OK : RESULT_NOP;
			}
			catch (Result r)
			{
				result = r;
			}
			catch (const std::bad_alloc&)
			{
				result = RESULT_ERR_OUT_OF_MEMORY;
			}
			catch (...)
			{
				result = RESULT_ERR_GENERIC;
			}

			StopMovie();

			return result;
		}

		void Tracker::StopMovie()
		{
			delete movie;
			movie = NULL;

			UpdateRewinderState( true );
		}

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

		Result Tracker::StartRewinding() const
		{
			return rewinder ? rewinder->Start() : RESULT_ERR_NOT_READY;
		}

		Result Tracker::StopRewinding() const
		{
			return rewinder ? rewinder->Stop() : RESULT_NOP;
		}

		bool Tracker::IsRewinding() const
		{
			return rewinder && rewinder->IsRewinding();
		}

		bool Tracker::IsMoviePlaying() const
		{
			return movie && movie->IsPlaying();
		}

		bool Tracker::IsMovieRecording() const
		{
			return movie && movie->IsRecording();
		}

		bool Tracker::IsLocked(bool excludeFrame) const
		{
			return IsRewinding() || (!excludeFrame && IsMoviePlaying());
		}

		bool Tracker::IsActive() const
		{
			return IsRewinding() || movie;
		}

		Result Tracker::Execute
		(
			Machine& machine,
			Video::Output* const video,
			Sound::Output* const sound,
			Input::Controllers* input
		)
		{
			if (machine.Is(Api::Machine::ON))
			{
				++frame;

				try
				{
					if (machine.Is(Api::Machine::GAME))
					{
						if (rewinder)
						{
							rewinder->Execute( video, sound, input );
							return RESULT_OK;
						}
						else if (movie)
						{
							if (!movie->Execute())
							{
								StopMovie();
							}
							else if (movie->IsPlaying())
							{
								input = NULL;
							}
						}
					}

					machine.Execute( video, sound, input );
					return RESULT_OK;
				}
				catch (Result result)
				{
					return machine.PowerOff( result );
				}
				catch (const std::bad_alloc&)
				{
					return machine.PowerOff( RESULT_ERR_OUT_OF_MEMORY );
				}
				catch (...)
				{
					return machine.PowerOff( RESULT_ERR_GENERIC );
				}
			}
			else
			{
				return RESULT_ERR_NOT_READY;
			}
		}
	}
}
