// Game_Music_Emu $vers. http://www.slack.net/~ant/

#include "Ay_Core.h"

/* Copyright (C) 2006-2009 Shay Green. This module is free software; you
can redistribute it and/or modify it under the terms of the GNU Lesser
General Public License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. This
module 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 Lesser General Public License for more
details. You should have received a copy of the GNU Lesser General Public
License along with this module; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */

#include "blargg_source.h"

inline void Ay_Core::disable_beeper()
{
	beeper_mask = 0;
	last_beeper = 0;
}

Ay_Core::Ay_Core()
{
	cpc_mode = false;
	beeper_output = NULL;
	disable_beeper();
}

Ay_Core::~Ay_Core() { }

void Ay_Core::set_beeper_output( Blip_Buffer* b )
{
	beeper_output = b;
	if ( b && !cpc_mode )
		beeper_mask = 0x10;
	else
		disable_beeper();
}

void Ay_Core::start_track( registers_t const& r, addr_t play )
{
	play_addr = play;
	
	memset( mem_.padding1, 0xFF, sizeof mem_.padding1 );
	
	int const mirrored = 0x80; // this much is mirrored after end of memory
	memset( mem_.ram + mem_size + mirrored, 0xFF, sizeof mem_.ram - mem_size - mirrored );
	memcpy( mem_.ram + mem_size, mem_.ram, mirrored ); // some code wraps around (ugh)
	
	cpu.reset( mem_.padding1, mem_.padding1 );
	cpu.map_mem( 0, mem_size, mem_.ram, mem_.ram );
	cpu.r = r;
	
	beeper_delta   = (int) (apu_.amp_range * 0.8);
	last_beeper    = 0;
	next_play      = play_period;
	spectrum_mode  = false;
	cpc_mode       = false;
	cpc_latch      = 0;
	set_beeper_output( beeper_output );
	apu_.reset();
	
	// a few tunes rely on channels having tone enabled at the beginning
	apu_.write_addr( 7 );
	apu_.write_data( 0, 0x38 );
	
}

// Emulation

void Ay_Core::cpu_out_( time_t time, addr_t addr, int data )
{
	// Spectrum
	if ( !cpc_mode )
	{
		switch ( addr & 0xFEFF )
		{
		case 0xFEFD:
			spectrum_mode = true;
			apu_.write_addr( data );
			return;
		
		case 0xBEFD:
			spectrum_mode = true;
			apu_.write_data( time, data );
			return;
		}
	}
	
	// CPC
	if ( !spectrum_mode )
	{
		switch ( addr >> 8 )
		{
		case 0xF6:
			switch ( data & 0xC0 )
			{
			case 0xC0:
				apu_.write_addr( cpc_latch );
				goto enable_cpc;
			
			case 0x80:
				apu_.write_data( time, cpc_latch );
				goto enable_cpc;
			}
			break;
		
		case 0xF4:
			cpc_latch = data;
			goto enable_cpc;
		}
	}
	
	dprintf( "Unmapped OUT: $%04X <- $%02X\n", addr, data );
	return;
	
enable_cpc:
	if ( !cpc_mode )
	{
		cpc_mode = true;
		disable_beeper();
		set_cpc_callback.f( set_cpc_callback.data );
	}
}

int Ay_Core::cpu_in( addr_t addr )
{
	// keyboard read and other things
	if ( (addr & 0xFF) == 0xFE )
		return 0xFF; // other values break some beeper tunes
	
	dprintf( "Unmapped IN : $%04X\n", addr );
	return 0xFF;
}

void Ay_Core::end_frame( time_t* end )
{
	cpu.set_time( 0 );
	
	// Since detection of CPC mode will halve clock rate during the frame
	// and thus generate up to twice as much sound, we must generate half
	// as much until mode is known.
	if ( !(spectrum_mode | cpc_mode) )
		*end /= 2;
	
	while ( cpu.time() < *end )
	{
		run_cpu( min( *end, next_play ) );
		
		if ( cpu.time() >= next_play )
		{
			// next frame
			next_play += play_period;
			
			if ( cpu.r.iff1 )
			{
				// interrupt enabled
				
				if ( mem_.ram [cpu.r.pc] == 0x76 )
					cpu.r.pc++; // advance past HALT instruction
				
				cpu.r.iff1 = 0;
				cpu.r.iff2 = 0;
				
				mem_.ram [--cpu.r.sp] = byte (cpu.r.pc >> 8);
				mem_.ram [--cpu.r.sp] = byte (cpu.r.pc);
				
				// fixed interrupt
				cpu.r.pc = 0x38;
				cpu.adjust_time( 12 );
				
				if ( cpu.r.im == 2 )
				{
					// vectored interrupt
					addr_t addr = cpu.r.i * 0x100 + 0xFF;
					cpu.r.pc = mem_.ram [(addr + 1) & 0xFFFF] * 0x100 + mem_.ram [addr];
					cpu.adjust_time( 6 );
				}
			}
		}
	}
	
	// End time frame
	*end = cpu.time();
	next_play -= *end;
	check( next_play >= 0 );
	cpu.adjust_time( -*end );
	apu_.end_frame( *end );
}
