// license:BSD-3-Clause
// copyright-holders:smf
/*
 * PlayStation DMA emulator
 *
 * Copyright 2003-2011 smf
 *
 */

#include "emu.h"
#include "dma.h"

#define VERBOSE_LEVEL ( 0 )

static inline void ATTR_PRINTF(3,4) verboselog( device_t& device, int n_level, const char *s_fmt, ... )
{
	if( VERBOSE_LEVEL >= n_level )
	{
		va_list v;
		char buf[ 32768 ];
		va_start( v, s_fmt );
		vsprintf( buf, s_fmt, v );
		va_end( v );
		device.logerror( "%s: %s", device.machine().describe_context(), buf );
	}
}

DEFINE_DEVICE_TYPE(PSX_DMA, psxdma_device, "psxdma", "Sony PSX DMA")

psxdma_device::psxdma_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) :
	device_t(mconfig, PSX_DMA, tag, owner, clock), m_ram(), m_ramsize(0), m_dpcp(0), m_dicr(0),
	m_irq_handler(*this)
{
}

void psxdma_device::device_reset()
{
	int n;

	m_dpcp = 0;
	m_dicr = 0;

	for( n = 0; n < 7; n++ )
	{
		dma_stop_timer( n );
	}
}

void psxdma_device::device_post_load()
{
	int n;

	for( n = 0; n < 7; n++ )
	{
		dma_timer_adjust( n );
	}
}

void psxdma_device::device_start()
{
	m_irq_handler.resolve_safe();

	for( int index = 0; index < 7; index++ )
	{
		psx_dma_channel *dma = &m_channel[ index ];

		dma->timer = timer_alloc(index);

		save_item( NAME( dma->n_base ), index );
		save_item( NAME( dma->n_blockcontrol ), index );
		save_item( NAME( dma->n_channelcontrol ), index );
		save_item( NAME( dma->n_ticks ), index );
		save_item( NAME( dma->b_running ), index );
	}

	save_item( NAME(m_dpcp) );
	save_item( NAME(m_dicr) );
}

void psxdma_device::dma_start_timer( int index, uint32_t n_ticks )
{
	psx_dma_channel *dma = &m_channel[ index ];

	dma->timer->adjust( attotime::from_hz(33868800) * n_ticks, index);
	dma->n_ticks = n_ticks;
	dma->b_running = 1;
}

void psxdma_device::dma_stop_timer( int index )
{
	psx_dma_channel *dma = &m_channel[ index ];

	dma->timer->adjust( attotime::never);
	dma->b_running = 0;
}

void psxdma_device::dma_timer_adjust( int index )
{
	psx_dma_channel *dma = &m_channel[ index ];

	if( dma->b_running )
	{
		dma_start_timer( index, dma->n_ticks );
	}
	else
	{
		dma_stop_timer( index );
	}
}

void psxdma_device::dma_interrupt_update()
{
	int n_int;
	int n_mask;

	n_int = ( m_dicr >> 24 ) & 0x7f;
	n_mask = ( m_dicr >> 16 ) & 0xff;

	if( ( n_mask & 0x80 ) != 0 && ( n_int & n_mask ) != 0 )
	{
		verboselog( *this, 2, "dma_interrupt_update( %02x, %02x ) interrupt triggered\n", n_int, n_mask );
		m_dicr |= 0x80000000;
		m_irq_handler(1);
	}
	else if( n_int != 0 )
	{
		verboselog( *this, 2, "dma_interrupt_update( %02x, %02x ) interrupt not enabled\n", n_int, n_mask );
	}
	m_dicr &= 0x00ffffff | ( m_dicr << 8 );
}

void psxdma_device::dma_finished( int index )
{
	psx_dma_channel *dma = &m_channel[ index ];

	if( dma->n_channelcontrol == 0x01000401 && index == 2 )
	{
		uint32_t n_size;
		uint32_t n_total;
		uint32_t n_address = ( dma->n_base & 0xffffff );
		uint32_t n_adrmask = m_ramsize - 1;
		uint32_t n_nextaddress;

		if( n_address != 0xffffff )
		{
			n_total = 0;
			for( ;; )
			{
				if( n_address == 0xffffff )
				{
					dma->n_base = n_address;
					//HACK: fixes pse bios 2.x & other texture uploading issues, breaks kdeadeye test mode, gtrfrk7m & gtrkfrk8m loading
					//dma_start_timer( index, 19000 );
					dma_start_timer( index, 500 );
					return;
				}
				if( n_total > 65535 )
				{
					dma->n_base = n_address;
					//FIXME:
					// 16000 below is based on try and error.
					// Mametesters.org: sfex20103red
					//dma_start_timer( index, 16 );
					dma_start_timer( index, 16000 );
					return;
				}
				n_address &= n_adrmask;
				n_nextaddress = m_ram[ n_address / 4 ];
				n_size = n_nextaddress >> 24;
				dma->fn_write( m_ram, n_address + 4, n_size );
				//FIXME:
				// The following conditions will cause an endless loop.
				// If stopping the transfer is correct I cannot judge
				// The patch is meant as a hint for somebody who knows
				// the hardware.
				// Mametesters.org: psyforce0105u5red, raystorm0111u1red
				if ((n_nextaddress & 0xffffff) != 0xffffff)
				{
					if (n_address == m_ram[ (n_nextaddress & n_adrmask) / 4] ||
						n_address == (n_nextaddress & n_adrmask) )
					{
						break;
					}
				}
				n_address = ( n_nextaddress & 0xffffff );

				n_total += ( n_size + 1 );
			}
		}
	}

	dma->n_channelcontrol &= ~( ( 1L << 0x18 ) | ( 1L << 0x1c ) );

	m_dicr |= 1 << ( 24 + index );
	dma_interrupt_update();
	dma_stop_timer( index );
}

void psxdma_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
	dma_finished(id);
}

void psxdma_device::install_read_handler( int index, read_delegate p_fn_dma_read )
{
	m_channel[ index ].fn_read = p_fn_dma_read;
}

void psxdma_device::install_write_handler( int index, write_delegate p_fn_dma_write )
{
	m_channel[ index ].fn_write = p_fn_dma_write;
}

void psxdma_device::write(offs_t offset, uint32_t data, uint32_t mem_mask)
{
	int index = offset / 4;
	psx_dma_channel *dma = &m_channel[ index ];

	if( index < 7 )
	{
		switch( offset % 4 )
		{
		case 0:
			verboselog( *this, 2, "dmabase( %d ) = %08x\n", index, data );
			dma->n_base = data;
			break;
		case 1:
			verboselog( *this, 2, "dmablockcontrol( %d ) = %08x\n", index, data );
			dma->n_blockcontrol = data;
			break;
		case 2:
			verboselog( *this, 2, "dmachannelcontrol( %d ) = %08x\n", index, data );
			dma->n_channelcontrol = data;
			if( ( dma->n_channelcontrol & ( 1L << 0x18 ) ) != 0 && ( m_dpcp & ( 1 << ( 3 + ( index * 4 ) ) ) ) != 0 )
			{
				int32_t n_size;
				uint32_t n_address;
				uint32_t n_nextaddress;
				uint32_t n_adrmask;

				n_adrmask = m_ramsize - 1;

				n_address = ( dma->n_base & n_adrmask );
				n_size = dma->n_blockcontrol;
				if( ( dma->n_channelcontrol & 0x200 ) != 0 )
				{
					uint32_t n_ba;
					n_ba = dma->n_blockcontrol >> 16;
					if( n_ba == 0 )
					{
						n_ba = 0x10000;
					}
					n_size = ( n_size & 0xffff ) * n_ba;
				}

				if( dma->n_channelcontrol == 0x01000000 &&
					!dma->fn_read.isnull() )
				{
					verboselog( *this, 1, "dma %d read block %08x %08x\n", index, n_address, n_size );
					dma->fn_read( m_ram, n_address, n_size );
					dma_finished( index );
				}
				else if ((dma->n_channelcontrol & 0xffbffeff) == 0x11000000 && // CD DMA
					!dma->fn_read.isnull() )
				{
					verboselog( *this, 1, "dma %d read block %08x %08x\n", index, n_address, n_size );

					// pSX's CD DMA size calc formula
					int oursize = (dma->n_blockcontrol>>16);
					oursize = (oursize > 1) ? oursize : 1;
					oursize *= (dma->n_blockcontrol&0xffff);

					dma->fn_read( m_ram, n_address, oursize );
					dma_finished( index );
				}
				else if( dma->n_channelcontrol == 0x01000200 &&
					!dma->fn_read.isnull() )
				{
					verboselog( *this, 1, "dma %d read block %08x %08x\n", index, n_address, n_size );
					dma->fn_read( m_ram, n_address, n_size );
					if( index == 1 )
					{
						dma_start_timer( index, 26000 );
					}
					else
					{
						dma_finished( index );
					}
				}
				else if( dma->n_channelcontrol == 0x01000201 &&
					!dma->fn_write.isnull() )
				{
					verboselog( *this, 1, "dma %d write block %08x %08x\n", index, n_address, n_size );
					dma->fn_write( m_ram, n_address, n_size );
					dma_finished( index );
				}
				else if( dma->n_channelcontrol == 0x11050100 &&
					!dma->fn_write.isnull() )
				{
					/* todo: check this is a write not a read... */
					verboselog( *this, 1, "dma %d write block %08x %08x\n", index, n_address, n_size );
					dma->fn_write( m_ram, n_address, n_size );
					dma_finished( index );
				}
				else if( dma->n_channelcontrol == 0x11150100 &&
					!dma->fn_write.isnull() )
				{
					/* todo: check this is a write not a read... */
					verboselog( *this, 1, "dma %d write block %08x %08x\n", index, n_address, n_size );
					dma->fn_write( m_ram, n_address, n_size );
					dma_finished( index );
				}
				else if( dma->n_channelcontrol == 0x01000401 &&
					index == 2 &&
					!dma->fn_write.isnull() )
				{
					verboselog( *this, 1, "dma %d write linked list %08x\n",
						index, dma->n_base );

					dma_finished( index );
				}
				else if( dma->n_channelcontrol == 0x11000002 &&
					index == 6 )
				{
					verboselog( *this, 1, "dma 6 reverse clear %08x %08x\n",
						dma->n_base, dma->n_blockcontrol );
					if( n_size > 0 )
					{
						n_size--;
						while( n_size > 0 )
						{
							n_nextaddress = ( n_address - 4 ) & 0xffffff;
							m_ram[ n_address / 4 ] = n_nextaddress;
							n_address = n_nextaddress;
							n_size--;
						}
						m_ram[ n_address / 4 ] = 0xffffff;
					}
					dma_start_timer( index, 2150 );
				}
				else
				{
					verboselog( *this, 1, "dma %d unknown mode %08x\n", index, dma->n_channelcontrol );
				}
			}
			else if( dma->n_channelcontrol != 0 )
			{
				verboselog( *this, 1, "psx_dma_w( %04x, %08x, %08x ) channel not enabled\n", offset, dma->n_channelcontrol, mem_mask );
			}
			break;
		default:
			verboselog( *this, 1, "psx_dma_w( %04x, %08x, %08x ) Unknown dma channel register\n", offset, data, mem_mask );
			break;
		}
	}
	else
	{
		switch( offset % 4 )
		{
		case 0x0:
			verboselog( *this, 1, "psx_dma_w( %04x, %08x, %08x ) dpcp\n", offset, data, mem_mask );
			m_dpcp = ( m_dpcp & ~mem_mask ) | data;
			break;
		case 0x1:

			m_dicr = ( m_dicr & ( 0x80000000 | ~mem_mask ) ) |
				( m_dicr & ~data & 0x7f000000 & mem_mask ) |
				( data & 0x00ffffff & mem_mask );

			if( ( m_dicr & 0x80000000 ) != 0 && ( m_dicr & 0x7f000000 ) == 0 )
			{
				verboselog( *this, 2, "dma interrupt cleared\n" );
				m_dicr &= ~0x80000000;
			}

			verboselog( *this, 1, "psx_dma_w( %04x, %08x, %08x ) dicr -> %08x\n", offset, data, mem_mask, m_dicr );
			break;
		default:
			verboselog( *this, 0, "psx_dma_w( %04x, %08x, %08x ) Unknown dma control register\n", offset, data, mem_mask );
			break;
		}
	}
}

uint32_t psxdma_device::read(offs_t offset, uint32_t mem_mask)
{
	int index = offset / 4;
	psx_dma_channel *dma = &m_channel[ index ];

	if( index < 7 )
	{
		switch( offset % 4 )
		{
		case 0:
			verboselog( *this, 1, "psx_dma_r dmabase[ %d ] ( %08x )\n", index, dma->n_base );
			return dma->n_base;
		case 1:
			verboselog( *this, 1, "psx_dma_r dmablockcontrol[ %d ] ( %08x )\n", index, dma->n_blockcontrol );
			return dma->n_blockcontrol;
		case 2:
			verboselog( *this, 1, "psx_dma_r dmachannelcontrol[ %d ] ( %08x )\n", index, dma->n_channelcontrol );
			return dma->n_channelcontrol;
		default:
			verboselog( *this, 0, "psx_dma_r( %08x, %08x ) Unknown dma channel register\n", offset, mem_mask );
			break;
		}
	}
	else
	{
		switch( offset % 4 )
		{
		case 0x0:
			verboselog( *this, 1, "psx_dma_r dpcp ( %08x )\n", m_dpcp );
			return m_dpcp;
		case 0x1:
			verboselog( *this, 1, "psx_dma_r dicr ( %08x )\n", m_dicr );
			return m_dicr;
		default:
			verboselog( *this, 0, "psx_dma_r( %08x, %08x ) Unknown dma control register\n", offset, mem_mask );
			break;
		}
	}
	return 0;
}
