////////////////////////////////////////////////////////////////////////////////////////
//
// 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 "NstInpDevice.hpp"
#include "NstInpFamilyKeyboard.hpp"
#include "../NstCpu.hpp"
#include "../NstHook.hpp"
#include "../NstFile.hpp"
#include "../api/NstApiTapeRecorder.hpp"

namespace Nes
{
	namespace Core
	{
		namespace Input
		{
			class FamilyKeyboard::DataRecorder
			{
			public:

				explicit DataRecorder(Cpu&);
				~DataRecorder();

				Result Record();
				Result Play();

				void SaveState(State::Saver&,dword) const;
				void LoadState(State::Loader&);

			private:

				NST_NO_INLINE void Start();
				NST_NO_INLINE Result Stop(bool);

				NES_DECL_HOOK( Tape );

				enum
				{
					MAX_LENGTH = SIZE_4096K,
					TAPE_CLOCK = 32000
				};

				enum Status
				{
					STOPPED,
					PLAYING,
					RECORDING
				};

				qaword cycles;
				Cpu& cpu;
				dword multiplier;
				dword clock;
				Status status;
				Vector<byte> stream;
				dword pos;
				uint in;
				uint out;
				File file;

			public:

				bool IsStopped() const
				{
					return status == STOPPED;
				}

				bool IsRecording() const
				{
					return status == RECORDING;
				}

				bool IsPlaying() const
				{
					return status == PLAYING;
				}

				bool Playable() const
				{
					return stream.Size();
				}

				Result Stop()
				{
					return Stop( false );
				}

				void Reset()
				{
					clock = 0;
					Stop( false );
				}

				void Poke(uint data)
				{
					out = data;
				}

				uint Peek() const
				{
					return in;
				}

				NST_SINGLE_CALL void EndFrame()
				{
					if (!clock)
						return;

					if (multiplier)
					{
						const qaword frame = qaword(cpu.GetFrameCycles()) * multiplier;
						NST_VERIFY( cycles >= frame );

						if (cycles > frame)
							cycles -= frame;
						else
							cycles = 0;
					}
					else
					{
						clock = 0;
						cpu.RemoveHook( Hook(this,&DataRecorder::Hook_Tape) );
					}
				}
			};

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

			FamilyKeyboard::FamilyKeyboard(Cpu& c,bool connectDataRecorder)
			:
			Device       (c,Api::Input::FAMILYKEYBOARD),
			dataRecorder (connectDataRecorder ? new DataRecorder(c) : NULL)
			{
				FamilyKeyboard::Reset();
			}

			FamilyKeyboard::DataRecorder::DataRecorder(Cpu& c)
			: cycles(0), cpu(c), multiplier(0), clock(0), status(STOPPED), pos(0), in(0), out(0)
			{
				file.Load( File::TAPE, stream, MAX_LENGTH );
			}

			FamilyKeyboard::~FamilyKeyboard()
			{
				delete dataRecorder;
			}

			FamilyKeyboard::DataRecorder::~DataRecorder()
			{
				Stop( true );

				if (stream.Size())
					file.Save( File::TAPE, stream.Begin(), stream.Size() );
			}

			void FamilyKeyboard::Reset()
			{
				scan = 0;
				mode = 0;

				if (dataRecorder)
					dataRecorder->Reset();
			}

			void FamilyKeyboard::SaveState(State::Saver& saver,const byte id) const
			{
				saver.Begin( AsciiId<'F','B'>::R(0,0,id) );
				saver.Begin( AsciiId<'K','B','D'>::V ).Write8( mode | (scan << 1) ).End();

				if (dataRecorder)
					dataRecorder->SaveState( saver, AsciiId<'D','T','R'>::V );

				saver.End();
			}

			void FamilyKeyboard::DataRecorder::SaveState(State::Saver& state,const dword baseChunk) const
			{
				if (stream.Size() || status != STOPPED)
				{
					state.Begin( baseChunk );

					if (status == PLAYING)
					{
						state.Begin( AsciiId<'P','L','Y'>::V ).Write32( pos ).Write8( in ).Write32( cycles ).Write32( multiplier ).End();
					}
					else if (status == RECORDING)
					{
						state.Begin( AsciiId<'R','E','C'>::V ).Write8( out ).Write32( cycles ).Write32( multiplier ).End();
					}

					if (stream.Size())
						state.Begin( AsciiId<'D','A','T'>::V ).Write32( stream.Size() ).Compress( stream.Begin(), stream.Size() ).End();

					state.End();
				}
			}

			void FamilyKeyboard::LoadState(State::Loader& loader,const dword id)
			{
				if (dataRecorder)
					dataRecorder->Stop();

				if (id == AsciiId<'F','B'>::V)
				{
					while (const dword chunk = loader.Begin())
					{
						switch (chunk)
						{
							case AsciiId<'K','B','D'>::V:
							{
								const uint data = loader.Read8();

								mode = data & 0x1;
								scan = data >> 1 & 0xF;

								if (scan > 9)
									scan = 0;

								break;
							}

							case AsciiId<'D','T','R'>::V:

								NST_VERIFY( dataRecorder );

								if (dataRecorder)
									dataRecorder->LoadState( loader );

								break;
						}

						loader.End();
					}
				}
			}

			void FamilyKeyboard::DataRecorder::LoadState(State::Loader& state)
			{
				Stop( true );

				while (const dword chunk = state.Begin())
				{
					switch (chunk)
					{
						case AsciiId<'P','L','Y'>::V:

							NST_VERIFY( status == STOPPED );

							if (status == STOPPED)
							{
								status = PLAYING;
								pos = state.Read32();
								in = state.Read8() & 0x2;

								cycles = state.Read32();

								if (const dword multiplier = state.Read32())
									cycles = cycles * (cpu.GetClockDivider() * TAPE_CLOCK) / multiplier;
								else
									cycles = 0;
							}
							break;

						case AsciiId<'R','E','C'>::V:

							NST_VERIFY( status == STOPPED );

							if (status == STOPPED)
							{
								status = RECORDING;
								out = state.Read8();

								cycles = state.Read32();

								if (const dword multiplier = state.Read32())
									cycles = cycles * (cpu.GetClockDivider() * TAPE_CLOCK) / multiplier;
								else
									cycles = 0;
							}
							break;

						case AsciiId<'D','A','T'>::V:
						{
							const dword size = state.Read32();
							NST_VERIFY( size > 0 && size <= MAX_LENGTH );

							if (size > 0 && size <= MAX_LENGTH)
							{
								stream.Resize( size );
								state.Uncompress( stream.Begin(), size );
							}

							break;
						}
					}

					state.End();
				}

				if (status == PLAYING)
				{
					NST_VERIFY( pos < stream.Size() );

					if (pos < stream.Size())
					{
						Start();
					}
					else
					{
						status = STOPPED;
						cycles = 0;
						pos = 0;
						in = 0;
					}
				}
				else if (status == RECORDING)
				{
					Start();
				}
			}

			Result FamilyKeyboard::PlayTape()
			{
				return dataRecorder ? dataRecorder->Play() : RESULT_ERR_NOT_READY;
			}

			Result FamilyKeyboard::RecordTape()
			{
				return dataRecorder ? dataRecorder->Record() : RESULT_ERR_NOT_READY;
			}

			Result FamilyKeyboard::StopTape()
			{
				return dataRecorder ? dataRecorder->Stop() : RESULT_NOP;
			}

			bool FamilyKeyboard::IsTapeRecording() const
			{
				return dataRecorder ? dataRecorder->IsRecording() : false;
			}

			bool FamilyKeyboard::IsTapePlaying() const
			{
				return dataRecorder ? dataRecorder->IsPlaying() : false;
			}

			bool FamilyKeyboard::IsTapeStopped() const
			{
				return dataRecorder ? dataRecorder->IsStopped() : false;
			}

			bool FamilyKeyboard::IsTapePlayable() const
			{
				return dataRecorder ? dataRecorder->Playable() : false;
			}

			Result FamilyKeyboard::DataRecorder::Record()
			{
				if (status == RECORDING)
					return RESULT_NOP;

				if (status == PLAYING)
					return RESULT_ERR_NOT_READY;

				status = RECORDING;
				stream.Destroy();

				Start();

				return RESULT_OK;
			}

			Result FamilyKeyboard::DataRecorder::Play()
			{
				if (status == PLAYING)
					return RESULT_NOP;

				if (status == RECORDING || !Playable())
					return RESULT_ERR_NOT_READY;

				status = PLAYING;

				Start();

				return RESULT_OK;
			}

			NST_NO_INLINE void FamilyKeyboard::DataRecorder::Start()
			{
				clock = cpu.GetClockBase();
				multiplier = cpu.GetClockDivider() * TAPE_CLOCK;

				cpu.AddHook( Hook(this,&DataRecorder::Hook_Tape) );

				Api::TapeRecorder::eventCallback( status == PLAYING ? Api::TapeRecorder::EVENT_PLAYING : Api::TapeRecorder::EVENT_RECORDING );
			}

			NST_NO_INLINE Result FamilyKeyboard::DataRecorder::Stop(const bool removeHook)
			{
				if (removeHook)
					cpu.RemoveHook( Hook(this,&DataRecorder::Hook_Tape) );

				if (status == STOPPED)
					return RESULT_NOP;

				status = STOPPED;
				cycles = 0;
				multiplier = 0;
				in = 0;
				out = 0;
				pos = 0;

				Api::TapeRecorder::eventCallback( Api::TapeRecorder::EVENT_STOPPED );

				return RESULT_OK;
			}

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

			void FamilyKeyboard::EndFrame()
			{
				if (dataRecorder)
					dataRecorder->EndFrame();
			}

			void FamilyKeyboard::Poke(const uint data)
			{
				if (dataRecorder)
					dataRecorder->Poke( data );

				if (data & COMMAND_KEY)
				{
					const uint out = data & COMMAND_SCAN;

					if (mode && !out && ++scan > 9)
						scan = 0;

					mode = out >> 1;

					if (data & COMMAND_RESET)
						scan = 0;
				}
			}

			uint FamilyKeyboard::Peek(uint port)
			{
				if (port == 0)
				{
					return dataRecorder ? dataRecorder->Peek() : 0;
				}
				else if (input && scan < 9)
				{
					Controllers::FamilyKeyboard::callback( input->familyKeyboard, scan, mode );
					return ~uint(input->familyKeyboard.parts[scan]) & 0x1E;
				}
				else
				{
					return 0x1E;
				}
			}

			NES_HOOK(FamilyKeyboard::DataRecorder,Tape)
			{
				for (const qaword next = qaword(cpu.GetCycles()) * multiplier; cycles < next; cycles += clock)
				{
					if (status == PLAYING)
					{
						if (pos < stream.Size())
						{
							const uint data = stream[pos++];

							if (data >= 0x8C)
							{
								in = 0x2;
							}
							else if (data <= 0x74)
							{
								in = 0x0;
							}
						}
						else
						{
							Stop( false );
							break;
						}
					}
					else
					{
						NST_ASSERT( status == RECORDING );

						if (stream.Size() < MAX_LENGTH)
						{
							stream.Append( (out & 0x7) == 0x7 ? 0x90 : 0x70 );
						}
						else
						{
							Stop( false );
							break;
						}
					}
				}
			}
		}
	}
}
