#include "Nes_Vrc7_Apu.h"

extern "C" {
#include "ext/emu2413.h"
}

#include <string.h>

#include "blargg_source.h"

static int const period = 36; // NES CPU clocks per FM clock

Nes_Vrc7_Apu::Nes_Vrc7_Apu()
{
	opll = 0;
}

blargg_err_t Nes_Vrc7_Apu::init()
{
	CHECK_ALLOC( opll = OPLL_new( 3579545, 3579545 / 72 ) );
	OPLL_setChipType((OPLL *) opll, 1);
	OPLL_resetPatch((OPLL *) opll, 1);

	set_output( 0 );
	volume( 1.0 );
	reset();
	return 0;
}

Nes_Vrc7_Apu::~Nes_Vrc7_Apu()
{
	if ( opll )
		OPLL_delete( (OPLL *) opll );
}

void Nes_Vrc7_Apu::set_output( Blip_Buffer* buf )
{
	for ( int i = 0; i < osc_count; ++i )
		oscs [i].output = buf;
	output_changed();
}

void Nes_Vrc7_Apu::output_changed()
{
	mono.output = oscs [0].output;
	for ( int i = osc_count; --i; )
	{
		if ( mono.output != oscs [i].output )
		{
			mono.output = 0;
			break;
		}
	}

	if ( mono.output )
	{
		for ( int i = osc_count; --i; )
		{
			mono.last_amp += oscs [i].last_amp;
			oscs [i].last_amp = 0;
		}
	}
}

void Nes_Vrc7_Apu::reset()
{
	addr      = 0;
	next_time = 0;
	mono.last_amp = 0;

	for ( int i = osc_count; --i >= 0; )
	{
		Vrc7_Osc& osc = oscs [i];
		osc.last_amp = 0;
		for ( int j = 0; j < 3; ++j )
			osc.regs [j] = 0;
	}

	OPLL_reset( (OPLL *) opll );
}

void Nes_Vrc7_Apu::write_reg( int data )
{
	addr = data;
}

void Nes_Vrc7_Apu::write_data( blip_time_t time, int data )
{
	int type = (addr >> 4) - 1;
	int chan = addr & 15;
	if ( (unsigned) type < 3 && chan < osc_count )
		oscs [chan].regs [type] = data;

	if ( addr < 0x08 )
		inst [addr] = data;


	if ( time > next_time )
		run_until( time );

	OPLL_writeIO( (OPLL *) opll, 0, addr );
	OPLL_writeIO( (OPLL *) opll, 1, data );
}

void Nes_Vrc7_Apu::end_frame( blip_time_t time )
{
	if ( time > next_time )
		run_until( time );

	next_time -= time;
	assert( next_time >= 0 );

	for ( int i = osc_count; --i >= 0; )
	{
		Blip_Buffer* output = oscs [i].output;
		if ( output )
			output->set_modified();
	}
}

void Nes_Vrc7_Apu::save_snapshot( vrc7_snapshot_t* out ) const
{
	out->latch = addr;
	out->delay = next_time;
	for ( int i = osc_count; --i >= 0; )
	{
		for ( int j = 0; j < 3; ++j )
			out->regs [i] [j] = oscs [i].regs [j];
	}
	memcpy( out->inst, inst, 8 );
}

void Nes_Vrc7_Apu::load_snapshot( vrc7_snapshot_t const& in )
{
	assert( offsetof (vrc7_snapshot_t,delay) == 28 - 1 );

	reset();
	next_time = in.delay;
	write_reg( in.latch );
	int i;

	for ( i = 0; i < osc_count; ++i )
	{
		for ( int j = 0; j < 3; ++j )
			oscs [i].regs [j] = in.regs [i] [j];
	}

	memcpy( inst, in.inst, 8 );
	for ( i = 0; i < 8; ++i )
	{
		OPLL_writeIO( (OPLL *) opll, 0, i );
		OPLL_writeIO( (OPLL *) opll, 1, in.inst [i] );
	}

	for ( i = 0; i < 3; ++i )
	{
		for ( int j = 0; j < 6; ++j )
		{
			OPLL_writeIO( (OPLL *) opll, 0, 0x10 + i * 0x10 + j );
			OPLL_writeIO( (OPLL *) opll, 1, oscs [j].regs [i] );
		}
	}
}

void Nes_Vrc7_Apu::run_until( blip_time_t end_time )
{
	require( end_time > next_time );

	blip_time_t time = next_time;
	void* opll = this->opll; // cache
	Blip_Buffer* const mono_output = mono.output;
	int32_t buffer [2];

	if ( mono_output )
	{
		// optimal case
		do
		{
			OPLL_calc_stereo( (OPLL *) opll, buffer );
			int amp = buffer [0] + buffer [1];
			int delta = amp - mono.last_amp;
			if ( delta )
			{
				mono.last_amp = amp;
				synth.offset_inline( time, delta, mono_output );
			}
			time += period;
		}
		while ( time < end_time );
	}
	else
	{
		mono.last_amp = 0;
		do
		{
			OPLL_calc_stereo( (OPLL *) opll, buffer );
			for ( int i = 0; i < osc_count; ++i )
			{
				Vrc7_Osc& osc = oscs [i];
				if ( osc.output )
				{
					int amp = ((OPLL *) opll)->ch_out[i];
					int delta = amp - osc.last_amp;
					if ( delta )
					{
						osc.last_amp = amp;
						synth.offset( time, delta, osc.output );
					}
				}
			}
			time += period;
		}
		while ( time < end_time );
	}

	next_time = time;
}
