//----------------------------------------------------------------------------
//
// File:        ti994a.cpp
// Date:        23-Feb-1998
// Programmer:  Marc Rousseau
//
// Description:
//
// Copyright (c) 1998-2004 Marc Rousseau, All Rights Reserved.
//
// This program 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.
//
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
//
// Revision History:
//
//----------------------------------------------------------------------------

#include <exception>
#include <stdexcept>
#include <cstdio>
#include <cstring>
#include "common.hpp"
#include "logger.hpp"
#include "compress.hpp"
#include "ti994a.hpp"
#include "icartridge.hpp"
#include "memory.hpp"
#include "tms9900.hpp"
#include "tms9901.hpp"
#include "tms9918a.hpp"
#include "tms9919.hpp"
#include "tms5220.hpp"
#include "support.hpp"

DBG_REGISTER( __FILE__ );

extern cTI994A *CRU_Object;
extern void   (*TimerHook)( );

static cTI994A *pTimerObj;

cTI994A::cTI994A( iCartridge *console, iTMS9918A *vdp, iTMS9919 *sound, iTMS5220 *speech ) :
	cBaseObject( "cTI994A" ),
	m_CPU( new cTMS9900 ),
	m_PIC( new cTMS9901( m_CPU )),
	m_VDP( vdp ? vdp : new cTMS9918A ),
	m_SoundGenerator( sound ? sound : new cTMS9919 ),
	m_SpeechSynthesizer( speech ),
	m_ClockSpeed( CPU_SPEED_HZ ),
	m_RetraceInterval( 0 ),
	m_LastRetrace( 0 ),
	m_Console( console ),
	m_Cartridge( nullptr ),
	m_ActiveCRU( 0 ),
	m_Device( ),
	m_GromAddress( 0 ),
	m_GromLastInstruction( 0 ),
	m_GromReadShift( 8 ),
	m_GromWriteShift( 8 ),
	m_GromCounter( 0 ),
	m_Scratchpad( ),
	m_DefaultBank( ),
	m_CpuMemoryInfo( ),
	m_GromMemoryInfo( ),
	m_VideoMemory( new UINT8[ 0x4000 ] )
{
	FUNCTION_ENTRY( this, "cTI994A ctor", true );

	// Store a copy of our 'this' pointer for the TimerHook function
	pTimerObj = this;

	// Define CRU_Object for _OpCodes.asm
	CRU_Object = this;

	memset( m_VideoMemory, 0, 0x4000 );

	m_VDP->SetMemory( m_VideoMemory );
	m_VDP->SetPIC( m_PIC, 2 );

	m_SoundGenerator->SetSpeechSynthesizer( m_SpeechSynthesizer );

	if( m_SpeechSynthesizer != nullptr )
	{
		m_SpeechSynthesizer->SetSoundChip( m_SoundGenerator );
		m_SpeechSynthesizer->SetComputer( this );
	}

	// Add the TMS9901 programmable timer
	m_Device[ 0 ] = static_cast< iDevice * >( m_PIC->GetInterface( "iDevice" ));

	TimerHook = _TimerHookProc;

	// Register the bank swap trap function here - used in UpdateBreakpoint
	m_CPU->RegisterTrapHandler( TrapFunction, this, TRAP_BANK_SWITCH );

	// Default to a system with ROM in every bank
	m_DefaultBank.CurBank = m_DefaultBank.Bank;

	for( auto &bank : m_CpuMemoryInfo )
	{
		bank.push_back( &m_DefaultBank );
	}
	for( auto &bank : m_GromMemoryInfo )
	{
		bank.push_back( &m_DefaultBank );
	}

	// Add the console ROM if one was provided
	if( m_Console != nullptr )
	{
		AddCartridge( m_Console, 0x00FFFFFF );

		// Do a quick sanity check on the system ROM before starting
		UINT16 wp = cpuMemory.ReadWord( 0x0000 );
		UINT16 pc = cpuMemory.ReadWord( 0x0002 );

		// Make sure that the level 0 interrupt vector is valid:
		//  1) The workspace pointer is in scratchpad RAM
		//  2) The startup code is in system ROM
		if((( wp & 0xFF00 ) != 0x8300 ) || (( pc & 0xE000 ) != 0x0000 ))
		{
			fprintf( stderr, "WARNING: System ROM appears to be invalid!\n" );
		}

		if( UINT8 MHz = cpuMemory.ReadByte( 0x000C ))
		{
			m_ClockSpeed = 1000000 * MHz / 16;
		}

		Reset( );
	}

	m_RetraceInterval = m_ClockSpeed / dynamic_cast< cTMS9918A * >( m_VDP.get( ))->GetRefreshRate( );

	// Mark the scratchpad RAM area so that we alias it correctly
	cpuMemory.SetMemory( 0x8000, 0x0100, m_Scratchpad, false );
	cpuMemory.SetMemory( 0x8100, 0x0100, m_Scratchpad, false );
	cpuMemory.SetMemory( 0x8200, 0x0100, m_Scratchpad, false );
	cpuMemory.SetMemory( 0x8300, 0x0100, m_Scratchpad, false );

	UINT8 index;

	index = m_CPU->RegisterTrapHandler( TrapFunction, this, TRAP_SOUND );
	for( UINT16 address = 0x8400; address < 0x8800; address += ( UINT16 ) 1 )
	{
		m_CPU->SetTrap( address, MEMFLG_TRAP_WRITE, index );									// Sound chip Port
	}

	index = m_CPU->RegisterTrapHandler( TrapFunction, this, TRAP_VIDEO );
	for( UINT16 address = 0x8800; address < 0x8C00; address += ( UINT16 ) 4 )
	{
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0000 ), MEMFLG_TRAP_READ, index );				// VDP Read Byte Port
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0002 ), MEMFLG_TRAP_READ, index );				// VDP Read Status Port
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0400 ), MEMFLG_TRAP_WRITE, index );				// VDP Write Byte Port
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0402 ), MEMFLG_TRAP_WRITE, index );				// VDP Write (set) Address Port
	}

	// Make this bank look like ROM (writes aren't stored)
	cpuMemory.SetMemory( 0x9000, ROM_BANK_SIZE, nullptr, true );

	index = m_CPU->RegisterTrapHandler( TrapFunction, this, TRAP_SPEECH );
	for( UINT16 address = 0x9000; address < 0x9400; address += ( UINT16 ) 2 )
	{
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0000 ), ( UINT8 ) MEMFLG_TRAP_READ, index );	// Speech Read Port
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0400 ), ( UINT8 ) MEMFLG_TRAP_WRITE, index );	// Speech Write Port
	}

	index = m_CPU->RegisterTrapHandler( TrapFunction, this, TRAP_GROM );
	for( UINT16 address = 0x9800; address < 0x9C00; address += ( UINT16 ) 2 )
	{
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0000 ), ( UINT8 ) MEMFLG_TRAP_READ, index );	// GROM Read Port
		m_CPU->SetTrap(( UINT16 ) ( address | 0x0400 ), ( UINT8 ) MEMFLG_TRAP_WRITE, index );	// GROM Write Port
	}
}

cTI994A::~cTI994A( )
{
	FUNCTION_ENTRY( this, "cTI994A dtor", true );

	delete [] m_VideoMemory;
}

//----------------------------------------------------------------------------
// iBaseObject Methods
//----------------------------------------------------------------------------

const void *cTI994A::GetInterface( const std::string &iName ) const
{
	if( iName == "iComputer" )
	{
		return static_cast< const iComputer * >( this );
	}

	return cBaseObject::GetInterface( iName );
}

//----------------------------------------------------------------------------
// iComputer Methods
//----------------------------------------------------------------------------

iTMS9900 *cTI994A::GetCPU( ) const
{
	FUNCTION_ENTRY( this, "cTI994A::GetCPU", false );

	return m_CPU;
}

iTMS9918A *cTI994A::GetVDP( ) const
{
	FUNCTION_ENTRY( this, "cTI994A::GetVDP", false );

	return m_VDP;
}

iTMS9919 *cTI994A::GetSoundGenerator( ) const
{
	FUNCTION_ENTRY( this, "cTI994A::GetSoundGenerator", false );

	return m_SoundGenerator;
}

iTMS5220 *cTI994A::GetSynthesizer( ) const
{
	FUNCTION_ENTRY( this, "cTI994A::GetSynthesizer", false );

	return m_SpeechSynthesizer;
}

UINT8 *cTI994A::GetVideoMemory( ) const
{
	FUNCTION_ENTRY( this, "cTI994A::GetVideoMemory", false );

	return m_VideoMemory;
}

void cTI994A::SetGromAddress( UINT16 addr )
{
	FUNCTION_ENTRY( this, "cTI994A::SetGromAddress", false );

	m_GromAddress = addr;
}

UINT16 cTI994A::GetGromAddress( ) const
{
	FUNCTION_ENTRY( this, "cTI994A::GetGromAddress", false );

	return m_GromAddress;
}

iDevice *cTI994A::GetDevice( ADDRESS address ) const
{
	FUNCTION_ENTRY( this, "cTI994A::GetDevice", false );

	// CRU Allocations:
	//  0000-0FFE       System Console
	//  1000-10FE       Unassigned
	//  1100-11FE       Disk Controller Card
	//  1200-12FE       Reserved
	//  1300-13FE       RS-232 (primary)
	//  1400-14FE       Unassigned
	//  1500-15FE       RS-232 (secondary)
	//  1600-16FE       Unassigned
	//  1700-17FE       HEX-BUS™ Interface
	//  1800-18FE       Thermal Printer
	//  1900-10FE       Reserved
	//  1A00-1AFE       Unassigned
	//  1B00-1BFE       Unassigned
	//  1C00-1CFE       Video Controler Card
	//  1D00-1DFE       IEE-488 Bus Controler Card
	//  1E00-1EFE       Unassigned
	//  1F00-1FFE       P-Code Card

	return m_Device[ ( address >> 8 ) & 0x1F ];
}

void cTI994A::_TimerHookProc( )
{
	FUNCTION_ENTRY( nullptr, "cTI994A::_TimerHookProc", false );

	UINT32 clockCycles = pTimerObj->m_CPU->GetClocks( );

	pTimerObj->TimerHookProc( clockCycles );
}

void cTI994A::TimerHookProc( UINT32 clockCycles )
{
	FUNCTION_ENTRY( this, "cTI994A::TimerHookProc", false );

	// Simulate a 50/60Hz VDP interrupt
	if( clockCycles - m_LastRetrace > m_RetraceInterval )
	{
		m_LastRetrace += m_RetraceInterval;
		VideoRetrace( );
	}
}

bool cTI994A::VideoRetrace( )
{
	FUNCTION_ENTRY( this, "cTI994A::VideoRetrace", false );

	return m_VDP->Retrace( );
}

UINT8 cTI994A::TrapFunction( void *ptr, int type, bool read, ADDRESS address, UINT8 value )
{
	FUNCTION_ENTRY( ptr, "cTI994A::TrapFunction", false );

	cTI994A *pThis = static_cast< cTI994A * >( ptr );
	UINT8 retVal = value;

	if(( address & 1 ) == 0 )
	{
		if( read == true )
		{
			switch( type )
			{
				case TRAP_SOUND :
					retVal = pThis->SoundBreakPoint( address, value );
					break;
				case TRAP_SPEECH :
					retVal = pThis->SpeechReadBreakPoint( address, value );
					break;
				case TRAP_VIDEO :
					retVal = pThis->VideoReadBreakPoint( address, value );
					break;
				case TRAP_GROM :
					retVal = pThis->GromReadBreakPoint( address, value );
					break;
				default :
					fprintf( stderr, "Invalid index %d for read access in TrapFunction\n", type );
					break;
			}
		}
		else
		{
			switch( type )
			{
				case TRAP_BANK_SWITCH :
					retVal = pThis->BankSwitch( address, value );
					break;
				case TRAP_SOUND :
					retVal = pThis->SoundBreakPoint( address, value );
					break;
				case TRAP_SPEECH :
					retVal = pThis->SpeechWriteBreakPoint( address, value );
					break;
				case TRAP_VIDEO :
					retVal = pThis->VideoWriteBreakPoint( address, value );
					break;
				case TRAP_GROM :
					retVal = pThis->GromWriteBreakPoint( address, value );
					break;
				default :
					fprintf( stderr, "Invalid index %d for write access in TrapFunction\n", type );
					break;
			}
		}
	}

	return retVal;
}

UINT8 cTI994A::BankSwitch( ADDRESS address, UINT8 )
{
	FUNCTION_ENTRY( this, "cTI994A::BankSwitch", false );

	ADDRESS baseAddress = ( ADDRESS ) ( address & 0xE000 );

	sMemoryRegion *region = m_CpuMemoryInfo[ baseAddress / ROM_BANK_SIZE ].back( );
	int newBank = ( address >> 1 ) % region->NumBanks;

	DBG_TRACE( "Switching to bank " << newBank << " (write to address 0x" << hex << address << ")" );

	region[ 0 ].CurBank = &region[ 0 ].Bank[ newBank ];
	region[ 1 ].CurBank = &region[ 1 ].Bank[ newBank ];

	cpuMemory.SetMemory( baseAddress + 0 * ROM_BANK_SIZE, ROM_BANK_SIZE, region[ 0 ].CurBank->Data, true );
	cpuMemory.SetMemory( baseAddress + 1 * ROM_BANK_SIZE, ROM_BANK_SIZE, region[ 1 ].CurBank->Data, true );

	return cpuMemory.ReadByte( address );
}

UINT8 cTI994A::SoundBreakPoint( ADDRESS, UINT8 data )
{
	FUNCTION_ENTRY( this, "cTI994A::SoundBreakPoint", false );

	m_SoundGenerator->WriteData( data );

	return data;
}

UINT8 cTI994A::SpeechWriteBreakPoint( ADDRESS, UINT8 data )
{
	FUNCTION_ENTRY( this, "cTI994A::SpeechWriteBreakPoint", false );

	if( m_SpeechSynthesizer != nullptr )
	{
		m_SpeechSynthesizer->WriteData( data );
	}

	return data;
}

UINT8 cTI994A::SpeechReadBreakPoint( ADDRESS, UINT8 data )
{
	FUNCTION_ENTRY( this, "cTI994A::SpeechReadBreakPoint", false );

	if( m_SpeechSynthesizer != nullptr )
	{
		data = m_SpeechSynthesizer->ReadData( data );
	}

	return data;
}

UINT8 cTI994A::VideoReadBreakPoint( ADDRESS address, UINT8 data )
{
	FUNCTION_ENTRY( this, "cTI994A::VideoReadBreakPoint", false );

	switch( address & 0x0002 )
	{
		case 0x0000 :
			data = m_VDP->ReadData( );
			break;
		case 0x0002 :
			data = m_VDP->ReadStatus( );
			break;
	}

	return data;
}

UINT8 cTI994A::VideoWriteBreakPoint( ADDRESS address, UINT8 data )
{
	FUNCTION_ENTRY( this, "cTI994A::VideoWriteBreakPoint", false );

	switch( address & 0x0002 )
	{
		case 0x0000 :
			m_VDP->WriteData( data );
			break;
		case 0x0002 :
			m_VDP->SetAddress( data );
			break;
	}

	return data;
}

UINT8 cTI994A::GromReadBreakPoint( ADDRESS address, UINT8 data )
{
	FUNCTION_ENTRY( this, "cTI994A::GromReadBreakPoint", false );

	m_GromWriteShift = 8;

	switch( address & 0x0002 )
	{
		case 0x0000 :					// GROM/GRAM Read Byte Port
			data = gplMemory.ReadByte( m_GromAddress );
			m_GromAddress = ( UINT16 ) (( m_GromAddress & 0xE000 ) | (( m_GromAddress + 1 ) & 0x1FFF ));
			break;
		case 0x0002 :					// GROM/GRAM Read Address Port
			data = ( UINT8 ) ((( m_GromAddress + 1 ) >> m_GromReadShift ) & 0x00FF );
			m_GromReadShift  = 8 - m_GromReadShift;
			break;
	}

	return data;
}

UINT8 cTI994A::GromWriteBreakPoint( ADDRESS address, UINT8 data )
{
	FUNCTION_ENTRY( this, "cTI994A::GromWriteBreakPoint", false );

	switch( address & 0x0002 )
	{
		case 0x0000 :					// GROM/GRAM Write Byte Port
			gplMemory.WriteByte( m_GromAddress, data );
			m_GromAddress = ( UINT16 ) (( m_GromAddress & 0xE000 ) | (( m_GromAddress + 1 ) & 0x1FFF ));
			m_GromWriteShift = 8;
			break;
		case 0x0002 :					// GROM/GRAM Write (set) Address Port
			m_GromAddress &= ( ADDRESS ) ( 0xFF00 >> m_GromWriteShift );
			m_GromAddress |= ( ADDRESS ) ( data << m_GromWriteShift );
			m_GromWriteShift = 8 - m_GromWriteShift;
			m_GromReadShift  = 8;
			break;
	}

	return data;
}

void WriteCRU( cTI994A *ti, ADDRESS address, int count, UINT16 value )
{
	FUNCTION_ENTRY( nullptr, "cTI994A::WriteCRU", false );

	while( count-- )
	{
		ti->WriteCRU(( ADDRESS ) ( address++ & 0x1FFF ), ( UINT16 ) ( value & 1 ));
		value >>= 1;
	}
}

int ReadCRU( cTI994A *ti, ADDRESS address, int count )
{
	FUNCTION_ENTRY( nullptr, "cTI994A::ReadCRU", false );

	int value = 0;
	address += ( UINT16 ) count;
	while( count-- )
	{
		value <<= 1;
		value |= ti->ReadCRU(( ADDRESS ) ( --address & 0x1FFF ));
	}
	return value;
}

int cTI994A::ReadCRU( ADDRESS address )
{
	FUNCTION_ENTRY( this, "cTI994A::ReadCRU", false );

	address <<= 1;
	iDevice *dev = GetDevice( address );

	int val = ( dev != nullptr ) ? dev->ReadCRU(( ADDRESS ) (( address - dev->GetCRU( )) >> 1 )) : 1;

	if( address > 0x07FF )
	{
		DBG_TRACE( "CRU read from address 0x" << hex << address << " - " << ( UINT8 ) val );
	}

	return val;
}

void cTI994A::WriteCRU( ADDRESS address, UINT16 val )
{
	FUNCTION_ENTRY( this, "cTI994A::WriteCRU", false );

	address <<= 1;
	iDevice *dev = GetDevice( address );

	if( address > 0x07FF )
	{
		DBG_TRACE( "CRU write to address 0x" << hex << address << " - " << ( UINT8 ) val );
	}

	if( dev == nullptr )
	{
		return;
	}

	// See if we need to swap in/out the DSR ROM routines
	if(( address != 0 ) && ( dev->GetCRU( ) == address ))
	{
		if( val == 1 )
		{
			if( m_ActiveCRU != address )
			{
				m_ActiveCRU = address;
				AddCartridge( dev->GetROM( ), INFO_MASK_DSR );
				dev->Activate( );
			}
		}
		else
		{
			m_ActiveCRU = 0;
			dev->DeActivate( );
			RemoveCartridge( dev->GetROM( ), INFO_MASK_DSR );
		}
	}
	else
	{
		dev->WriteCRU(( UINT16 ) (( address - dev->GetCRU( )) >> 1 ), val );
	}
}

void cTI994A::Run( )
{
	FUNCTION_ENTRY( this, "cTI994A::Run", true );

	m_CPU->Run( );
}

bool cTI994A::Step( )
{
	FUNCTION_ENTRY( this, "cTI994A::Step", true );

	return m_CPU->Step( );
}

void cTI994A::Stop( )
{
	FUNCTION_ENTRY( this, "cTI994A::Stop", true );

	m_CPU->Stop( );
}

bool cTI994A::IsRunning( )
{
	FUNCTION_ENTRY( this, "cTI994A::IsRunning", true );

	return m_CPU->IsRunning( );
}

static const char ImageFileHeader[ ] = "TI-994/A Memory Image File\n\x1A";

bool cTI994A::OpenImageFile( const char *filename, sImageFileState *info )
{
	FUNCTION_ENTRY( nullptr, "cTI994A::OpenImageFile", true );

	if( filename == nullptr )
	{
		return false;
	}

	info->file = fopen( filename, "rb" );
	if( info->file == nullptr )
	{
		return false;
	}

	char buffer[ sizeof( ImageFileHeader ) - 1 ];
	if( fread( buffer, sizeof( buffer ), 1, info->file ) != 1 )
	{
		DBG_ERROR( "Invalid memory image file" );
		fclose( info->file );
		return false;
	}

	// Make sure it's a proper memory image file
	if( memcmp( buffer, ImageFileHeader, sizeof( buffer )) != 0 )
	{
		DBG_ERROR( "Invalid memory image file" );
		fclose( info->file );
		return false;
	}

	info->start = info->next = ftell( info->file );

	return true;
}

bool cTI994A::FindHeader( sImageFileState *info, HEADER_SECTION_E section )
{
	FUNCTION_ENTRY( nullptr, "cTI994A::FindHeader", true );

	// Do a simple sanity check
	long offset = ftell( info->file );
	if( offset != info->next )
	{
		DBG_WARNING( "Incorrect offset " << hex << offset << " - expected " << info->next << " loading section " << section );
	}

	// Go to the 1st 'unclaimed' section
	fseek( info->file, info->start, SEEK_SET );

	sStateHeader header;
	memset( &header, -1, sizeof( header ));

	do
	{
		offset = ftell( info->file );
		if( fread( &header, sizeof( header ), 1, info->file ) != 1 )
		{
			break;
		}
		if( header.id == section )
		{
			info->next = offset + sizeof( header ) + header.length;
			if( offset == info->start )
			{
				info->start = info->next;
			}
			return true;
		}
		fseek( info->file, header.length, SEEK_CUR );
	}
	while( !feof( info->file ));

	DBG_ERROR( "Header " << section << " not found" );

	fseek( info->file, info->next, SEEK_SET );

	return false;
}

void cTI994A::MarkHeader( FILE *file, HEADER_SECTION_E section, sStateHeaderInfo *info )
{
	FUNCTION_ENTRY( nullptr, "cTI994A::MarkHeader", true );

	info->header.id     = ( UINT16 ) section;
	info->header.length = ( UINT16 ) -section;
	info->offset        = ( UINT32 ) ftell( file );

	fwrite( &info->header, sizeof( info->header ), 1, file );
}

bool cTI994A::SaveHeader( FILE *file, sStateHeaderInfo *info )
{
	FUNCTION_ENTRY( nullptr, "cTI994A::SaveHeader", true );

	if(( UINT8 ) info->header.id != ( UINT8 ) -info->header.length )
	{
		DBG_ERROR( "Invalid header" );
		return false;
	}

	long offset = ftell( file );
	info->header.length = ( UINT16 ) ( offset - info->offset - sizeof( info->header ));

	fseek( file, info->offset, SEEK_SET );
	if( fwrite( &info->header, sizeof( info->header ), 1, file ) != 1 )
	{
		DBG_ERROR( "Unable to write file header to file" );
		return false;
	}
	fseek( file, offset, SEEK_SET );

	return true;
}

bool cTI994A::SaveImage( const char *filename )
{
	FUNCTION_ENTRY( this, "cTI994A::SaveImage", true );

	CreateHomePath( );

	char buffer[ 256 ];
	snprintf( buffer, sizeof( buffer ), "%s%c%s", GetHomePath( ), FILE_SEPERATOR, filename );

	FILE *file = fopen( buffer, "wb" );
	if( file == nullptr )
	{
		DBG_ERROR( "Unable to open file '"<< buffer << "'" );
		return false;
	}

	fwrite( ImageFileHeader, 1, sizeof( ImageFileHeader ) - 1, file );

	sStateHeaderInfo info;
	MarkHeader( file, SECTION_BASE, &info );

	// Name of inserted Cartridge
	if( m_Cartridge && ( m_Cartridge->GetTitle( ) != nullptr ))
	{
		short len = ( short ) strlen( m_Cartridge->GetTitle( ));
		fwrite( &len, sizeof( len ), 1, file );
		fwrite( m_Cartridge->GetTitle( ), len, 1, file );
	}
	else
	{
		short len = 0;
		fwrite( &len, sizeof( len ), 1, file );
	}

	SaveHeader( file, &info );
	MarkHeader( file, SECTION_CPU, &info );

	{
		iStateObject *pState = static_cast< iStateObject * >( m_CPU->GetInterface( "iStateObject" ));
		pState->SaveState( file );
	}

	SaveHeader( file, &info );
	MarkHeader( file, SECTION_VDP, &info );

	{
		iStateObject *pState = static_cast< iStateObject * >( m_VDP->GetInterface( "iStateObject" ));
		pState->SaveState( file );
	}

	UINT32 LastRetrace = m_CPU->GetClocks( ) - m_LastRetrace;
	fwrite( &LastRetrace, sizeof( LastRetrace ), 1, file );

	SaveHeader( file, &info );
	MarkHeader( file, SECTION_GROM, &info );

	fwrite( &m_GromAddress, sizeof( m_GromAddress ), 1, file );
	fwrite( &m_GromLastInstruction, sizeof( m_GromLastInstruction ), 1, file );
	fwrite( &m_GromReadShift, sizeof( m_GromReadShift ), 1, file );
	fwrite( &m_GromWriteShift, sizeof( m_GromWriteShift ), 1, file );
	fwrite( &m_GromCounter, sizeof( m_GromCounter ), 1, file );

	for( unsigned i = 0; i < 8; i++ )
	{
		sMemoryRegion *memory = m_GromMemoryInfo[ i ].back( );
		fputc( memory->CurBank - memory->Bank, file );
		// Save any banks of RAM
		for( int j = 0; j < memory->NumBanks; j++ )
		{
			if( memory->Bank[ j ].Type != BANK_ROM )
			{
				SaveBuffer( GROM_BANK_SIZE, memory->Bank[ j ].Data, file );
			}
		}
	}

	SaveHeader( file, &info );
	MarkHeader( file, SECTION_CRU, &info );

	for( unsigned i = 0; i < SIZE( m_Device ); i++ )
	{
		if( m_Device[ i ] != nullptr )
		{
			fputc( i, file );
			long oldOffset = ftell( file );
			UINT16 size = 0;
			fwrite( &size, sizeof( size ), 1, file );
			iStateObject *pState = static_cast< iStateObject * >( m_Device[ i ]->GetInterface( "iStateObject" ));
			pState->SaveState( file );
			long newOffset = ftell( file );
			size = ( UINT16 ) ( newOffset - oldOffset - sizeof( size ));
			fseek( file, oldOffset, SEEK_SET );
			fwrite( &size, sizeof( size ), 1, file );
			fseek( file, newOffset, SEEK_SET );
		}
	}
	fputc(( UINT8 ) -1, file );

	SaveHeader( file, &info );
	MarkHeader( file, SECTION_DSR, &info );

	fwrite( &m_ActiveCRU, sizeof( m_ActiveCRU ), 1, file );
	if( m_ActiveCRU != 0 )
	{
		iStateObject *pState = static_cast< iStateObject * >( GetDevice( m_ActiveCRU )->GetInterface( "iStateObject" ));
		pState->SaveState( file );
	}

	SaveHeader( file, &info );
	MarkHeader( file, SECTION_ROM, &info );

	for( unsigned i = 0; i < 16; i++ )
	{
		sMemoryRegion *memory = m_CpuMemoryInfo[ i ].back( );
		fputc( memory->CurBank - memory->Bank, file );
		// Save any banks of RAM
		for( int j = 0; j < memory->NumBanks; j++ )
		{
			if( memory->Bank[ j ].Type != BANK_ROM )
			{
				SaveBuffer( ROM_BANK_SIZE, memory->Bank[ j ].Data, file );
			}
		}
	}

	SaveHeader( file, &info );

	fclose( file );

	return true;
}

bool cTI994A::LoadImage( const char *filename )
{
	FUNCTION_ENTRY( this, "cTI994A::LoadImage", true );

	char buffer[ 256 ];
	snprintf( buffer, sizeof( buffer ), "%s%c%s", GetHomePath( ), FILE_SEPERATOR, filename );

	sImageFileState info;
	if( OpenImageFile( buffer, &info ) == false )
	{
		return false;
	}

	bool retVal = true;
	bool reset = false;

	try
	{
		if( FindHeader( &info, SECTION_BASE ) != true )
		{
			throw std::runtime_error( "Error finding header for SECTION_BASE in image" );
		}

		// Make sure cartridge(s) match those currently loaded
		short len;
		if( fread( &len, sizeof( len ), 1, info.file ) != 1 )
		{
			throw std::runtime_error( "Error reading length" );
		}
		bool ok = true;
		bool haveCartridge = ( m_Cartridge && ( m_Cartridge->GetTitle( ) != nullptr )) ? true : false;
		if( len != 0 )
		{
			char *nameBuffer = new char[ len + 1 ];
			if( fread( nameBuffer, len, 1, info.file ) != 1 )
			{
				delete [] nameBuffer;
				throw std::runtime_error( "Error reading cartridge name" );
			}
			nameBuffer[ len ] = '\0';
			if(( haveCartridge == false ) || strnicmp( nameBuffer, m_Cartridge->GetTitle( ), len ))
			{
				ok = false;
			}
			DBG_STATUS( "Cartridge: " << nameBuffer );
			delete [] nameBuffer;
		}
		else
		{
			if( haveCartridge == true )
			{
				ok = false;
			}
		}
		if( !ok )
		{
			DBG_ERROR( "The image's cartridges don't match current system" );
			fclose( info.file );
			return false;
		}

		// From here on, the system state may be compromised if an error occurs
		reset = true;

		{
			if( FindHeader( &info, SECTION_CPU ) != true )
			{
			throw std::runtime_error( "Error finding header for SECTION_CPU in image" );
			}
			iStateObject *pState = static_cast< iStateObject * >( m_CPU->GetInterface( "iStateObject" ));
			if( pState->LoadState( info.file ) != true )
			{
				throw std::runtime_error( "Error loading state for CPU" );
			}
		}

		{
			if( FindHeader( &info, SECTION_VDP ) != true )
			{
			throw std::runtime_error( "Error finding header for SECTION_VDP in image" );
			}
			iStateObject *pState = static_cast< iStateObject * >( m_VDP->GetInterface( "iStateObject" ));
			if( pState->LoadState( info.file ) != true )
			{
				throw std::runtime_error( "Error loading state for VDP" );
			}
		}

		UINT32 LastRetrace = 0;
		if( fread( &LastRetrace, sizeof( LastRetrace ), 1, info.file ) != 1 )
		{
			throw std::runtime_error( "Error loading LastRetrace" );
		}
		m_LastRetrace = m_CPU->GetClocks( ) - LastRetrace;

		if( FindHeader( &info, SECTION_GROM ) != true )
		{
			throw std::runtime_error( "Error finding header for SECTION_GROM in image" );
		}

		if(( fread( &m_GromAddress, sizeof( m_GromAddress ), 1, info.file ) != 1 )                 ||
		   ( fread( &m_GromLastInstruction, sizeof( m_GromLastInstruction ), 1, info.file ) != 1 ) ||
		   ( fread( &m_GromReadShift, sizeof( m_GromReadShift ), 1, info.file ) != 1 )             ||
		   ( fread( &m_GromWriteShift, sizeof( m_GromWriteShift ), 1, info.file ) != 1 )           ||
		   ( fread( &m_GromCounter, sizeof( m_GromCounter ), 1, info.file ) != 1 ))
		{
			throw std::runtime_error( "Error loading GROM state" );
		}

		for( unsigned i = 0; i < 8; i++ )
		{
			sMemoryRegion *memory = m_GromMemoryInfo[ i ].back( );
			UINT8 bank = ( UINT8 ) fgetc( info.file );
			memory->CurBank = &memory->Bank[ bank ];
			for( int j = 0; j < memory->NumBanks; j++ )
			{
				if( memory->Bank[ j ].Type != BANK_ROM )
				{
					LoadBuffer( GROM_BANK_SIZE, memory->Bank[ j ].Data, info.file );
				}
			}
		}

		if( FindHeader( &info, SECTION_CRU ) != true )
		{
			throw std::runtime_error( "Error finding header for SECTION_CRU in image" );
		}

		for( EVER )
		{
			int i = fgetc( info.file );
			if( i == ( UINT8 ) -1 )
			{
				break;
			}
			if(( i < 0 ) || ( i >= 32 ))
			{
				throw std::runtime_error( "Invalid device index" );
			}
			UINT16 size;
			if( fread( &size, sizeof( size ), 1, info.file ) != 1 )
			{
				throw std::runtime_error( "Unable to read size" );
			}
			long offset = ftell( info.file );
			iDevice *dev = m_Device[ i ];
			if( dev != nullptr )
			{
				iStateObject *pState = static_cast< iStateObject * >( dev->GetInterface( "iStateObject" ));
				if( pState->LoadState( info.file ) != true )
				{
					throw std::runtime_error( "Error loading state for device" );
				}
			}
			fseek( info.file, offset + size, SEEK_SET );
		}

		if( FindHeader( &info, SECTION_DSR ) != true )
		{
			throw std::runtime_error( "Error finding header for SECTION_DSR in image" );
		}

		if( m_ActiveCRU )
		{
			iDevice *dev = GetDevice( m_ActiveCRU );
			dev->DeActivate( );
			RemoveCartridge( dev->GetROM( ), INFO_MASK_DSR );
		}
		if( fread( &m_ActiveCRU, sizeof( m_ActiveCRU ), 1, info.file ) != 1 )
		{
			throw std::runtime_error( "Error loading ActiveCRU" );
		}
		if( m_ActiveCRU )
		{
			iDevice *dev = GetDevice( m_ActiveCRU );
			iStateObject *pState = static_cast< iStateObject * >( dev->GetInterface( "iStateObject" ));
			if( pState->LoadState( info.file ) != true )
			{
				throw std::runtime_error( "Error loading state for active device" );
			}
			AddCartridge( dev->GetROM( ), INFO_MASK_DSR );
			dev->Activate( );
		}

		if( FindHeader( &info, SECTION_ROM ) != true )
		{
			throw std::runtime_error( "Error finding header for SECTION_ROM in image" );
		}

		for( unsigned i = 0; i < 16; i++ )
		{
			fgetc( info.file );
			sMemoryRegion *memory = m_CpuMemoryInfo[ i ].back( );
			UINT8 bank = ( UINT8 ) fgetc( info.file );
			memory->CurBank = &memory->Bank[ bank ];
			for( int j = 0; j < memory->NumBanks; j++ )
			{
				if( memory->Bank[ j ].Type != BANK_ROM )
				{
					LoadBuffer( ROM_BANK_SIZE, memory->Bank[ j ].Data, info.file );
				}
			}
		}
	}
	catch( const std::exception &error )
	{
		if( reset )
		{
			DBG_ERROR( "EXCEPTION: '" << error.what( ) << "' while loading the image - reseting the system" );
			Reset( );
		}
		retVal = false;
	}

	fclose( info.file );

	return retVal;
}

void cTI994A::Reset( )
{
	FUNCTION_ENTRY( this, "cTI994A::Reset", true );

	if( m_CPU != nullptr )
	{
		m_CPU->Reset( );
	}
	if( m_VDP != nullptr )
	{
		m_VDP->Reset( );
	}
	if( m_SpeechSynthesizer != nullptr )
	{
		m_SpeechSynthesizer->Reset( );
	}
}

void cTI994A::Sleep( int cycles, UINT32 timeout )
{
	FUNCTION_ENTRY( this, "cTI994A::Sleep", false );

	m_CPU->AddClocks( cycles );
}

void cTI994A::WakeCPU( UINT32 )
{
	FUNCTION_ENTRY( this, "cTI994A::WakeCPU", false );
}

bool cTI994A::AddDevice( iDevice *dev )
{
	FUNCTION_ENTRY( this, "cTI994A::AddDevice", true );

	int index = ( dev->GetCRU( ) >> 8 ) & 0x1F;
	if( m_Device[ index ] != nullptr )
	{
		DBG_WARNING( "A CRU device already exists at address " << index );
		return false;
	}

	m_Device[ index ] = dev;

	dev->SetCPU( m_CPU );

	return true;
}

bool cTI994A::InsertCartridge( iCartridge *cartridge )
{
	FUNCTION_ENTRY( this, "cTI994A::InsertCartridge", true );

	DBG_ASSERT( cartridge != nullptr );

	if( m_Cartridge != nullptr )
	{
		DBG_WARNING( "A cartridge is already inserted - removing" );

		return false;
	}

	m_Cartridge = cartridge;

	for( unsigned i = 0; i < SIZE( m_CpuMemoryInfo ); i++ )
	{
		sMemoryRegion *region = cartridge->GetCpuMemory( i );
		region->CurBank = region->Bank;
	}

	for( unsigned i = 0; i < SIZE( m_GromMemoryInfo ); i++ )
	{
		sMemoryRegion *region = cartridge->GetGromMemory( i );
		region->CurBank = region->Bank;
	}

	AddCartridge( cartridge, INFO_MASK_CARTRIDGE );

	Reset( );

	return true;
}

void cTI994A::RemoveCartridge( )
{
	FUNCTION_ENTRY( this, "cTI994A::RemoveCartridge", true );

	if( m_Cartridge != nullptr )
	{
		RemoveCartridge( m_Cartridge, INFO_MASK_CARTRIDGE );

		m_Cartridge = nullptr;

		Reset( );
	}
}

void cTI994A::AddCartridge( iCartridge *cartridge, int mask )
{
	FUNCTION_ENTRY( this, "cTI994A::AddCartridge", true );

	DBG_ASSERT( cartridge != nullptr );

	int changed = 0;

	for( unsigned i = 0; i < SIZE( m_CpuMemoryInfo ); i++ )
	{
		if( mask & ( 0x00001 << i ))
		{
			sMemoryRegion *region = cartridge->GetCpuMemory( i );
			if( region->NumBanks > 0 )
			{
				m_CpuMemoryInfo[ i ].push_back( region );
				changed |= ( 0x00001 << i );
			}
		}
	}

	for( unsigned i = 0; i < SIZE( m_GromMemoryInfo ); i++ )
	{
		if( mask & ( 0x10000 << i ))
		{
			sMemoryRegion *region = cartridge->GetGromMemory( i );
			if( region->NumBanks > 0 )
			{
				m_GromMemoryInfo[ i ].push_back( region );
				changed |= ( 0x10000 << i );
			}
		}
	}

	UpdateMemory( changed );
}

void cTI994A::RemoveCartridge( iCartridge *cartridge, int mask )
{
	FUNCTION_ENTRY( this, "cTI994A::RemoveCartridge", true );

	int changed = 0;

	for( unsigned i = 0; i < SIZE( m_CpuMemoryInfo ); i++ )
	{
		if( mask & ( 0x00001 << i ))
		{
			sMemoryRegion *region = cartridge->GetCpuMemory( i );
			if( m_CpuMemoryInfo[ i ].back( ) == region )
			{
				m_CpuMemoryInfo[ i ].pop_back( );
				changed |= ( 0x00001 << i );
			}
		}
	}

	for( unsigned i = 0; i < SIZE( m_GromMemoryInfo ); i++ )
	{
		if( mask & ( 0x10000 << i ))
		{
			sMemoryRegion *region = cartridge->GetGromMemory( i );
			if( m_GromMemoryInfo[ i ].back( ) == region )
			{
				m_GromMemoryInfo[ i ].pop_back( );
				changed |= ( 0x10000 << i );
			}
		}
	}

	UpdateMemory( changed );
}

void cTI994A::UpdateMemory( int mask )
{
	FUNCTION_ENTRY( this, "cTI994A::UpdateMemory", true );

	for( unsigned i = 0; i < SIZE( m_CpuMemoryInfo ); i++ )
	{
		if( mask & ( 0x00001 << i ))
		{
			sMemoryRegion *region = m_CpuMemoryInfo[ i ].back( );
			cpuMemory.SetMemory( i * ROM_BANK_SIZE, ROM_BANK_SIZE, region->CurBank->Data, region->CurBank->Flags & FLAG_READ_ONLY );
			UpdateBreakpoint( i * ROM_BANK_SIZE, region->NumBanks > 1 );
		}
	}

	for( unsigned i = 0; i < SIZE( m_GromMemoryInfo ); i++ )
	{
		if( mask & ( 0x10000 << i ))
		{
			sMemoryRegion *region = m_GromMemoryInfo[ i ].back( );
			gplMemory.SetMemory( i * GROM_BANK_SIZE, GROM_BANK_SIZE, region->CurBank->Data, region->CurBank->Flags & FLAG_READ_ONLY );
		}
	}
}

void cTI994A::UpdateBreakpoint( int address, bool set )
{
	FUNCTION_ENTRY( this, "cTI994A::UpdateBreakpoint", true );

	UINT8 index = m_CPU->GetTrapIndex( TrapFunction, TRAP_BANK_SWITCH );

	if( set )
	{
		for( int j = 0; j < ROM_BANK_SIZE; j++ )
		{
			m_CPU->SetTrap( address++, MEMFLG_TRAP_WRITE, index );
		}
	}
	else
	{
		m_CPU->ClearTrap( index, address, ROM_BANK_SIZE );
	}
}
