// license:BSD-3-Clause
// copyright-holders:Curt Coder
/***************************************************************************

    Intel 8257 DMA Controller emulation

    Copyright the MESS Team.
    Visit http://mamedev.org for licensing and usage restrictions.

***************************************************************************/

#include "i8257.h"



//**************************************************************************
//  DEVICE DEFINITIONS
//**************************************************************************

const device_type I8257 = &device_creator<i8257_device>;



//**************************************************************************
//  MACROS / CONSTANTS
//**************************************************************************

#define LOG 0


enum
{
	REGISTER_ADDRESS = 0,
	REGISTER_WORD_COUNT,
	REGISTER_STATUS = 8,
	REGISTER_MODE = REGISTER_STATUS
};


#define MODE_CHAN_ENABLE(x)      BIT(m_transfer_mode, x)
#define MODE_ROTATING_PRIORITY   BIT(m_transfer_mode, 4)
#define MODE_EXTENDED_WRITE      BIT(m_transfer_mode, 5)
#define MODE_TC_STOP             BIT(m_transfer_mode, 6)
#define MODE_AUTOLOAD            BIT(m_transfer_mode, 7)
#define MODE_TRANSFER_MASK       (m_channel[m_current_channel].m_mode)
#define MODE_TRANSFER_VERIFY     0
#define MODE_TRANSFER_WRITE      1
#define MODE_TRANSFER_READ       2


enum
{
	STATE_SI,
	STATE_S0,
	STATE_S1,
	STATE_S2,
	STATE_S3,
	STATE_SW,
	STATE_S4,
};



//**************************************************************************
//  INLINE HELPERS
//**************************************************************************

//-------------------------------------------------
//  dma_request -
//-------------------------------------------------

inline void i8257_device::dma_request(int channel, int state)
{
	if (LOG) logerror("I8257 '%s' Channel %u DMA Request: %u\n", tag(), channel, state);

	if (state)
	{
		m_request |= 1 << channel;
	}
	else
	{
		m_request &= ~1 << channel;
	}
	trigger(1);
}


//-------------------------------------------------
//  is_request_active -
//-------------------------------------------------

inline bool i8257_device::is_request_active(int channel)
{
	return (BIT(m_request, channel) && MODE_CHAN_ENABLE(channel)) ? true : false;
}

//-------------------------------------------------
//  set_hreq
//-------------------------------------------------

inline void i8257_device::set_hreq(int state)
{
	if (m_hreq != state)
	{
		m_out_hrq_cb(state);
		m_hreq = state;
	}
}


//-------------------------------------------------
//  set_tc -
//-------------------------------------------------

inline void i8257_device::set_tc(int state)
{
	if (m_tc != state)
	{
		m_out_tc_cb(state);

		m_tc = state;
	}
}


//-------------------------------------------------
//  set_dack - dack is active low
//-------------------------------------------------

inline void i8257_device::set_dack()
{
	m_out_dack_0_cb(m_current_channel != 0);
	m_out_dack_1_cb(m_current_channel != 1);
	m_out_dack_2_cb(m_current_channel != 2);
	m_out_dack_3_cb(m_current_channel != 3);
}


//-------------------------------------------------
//  dma_read -
//-------------------------------------------------

inline void i8257_device::dma_read()
{
	offs_t offset = m_channel[m_current_channel].m_address;

	switch (MODE_TRANSFER_MASK)
	{
	case MODE_TRANSFER_VERIFY:
	case MODE_TRANSFER_WRITE:
		switch(m_current_channel)
		{
			case 0:
				m_temp = m_in_ior_0_cb(offset);
				break;
			case 1:
				m_temp = m_in_ior_1_cb(offset);
				break;
			case 2:
				m_temp = m_in_ior_2_cb(offset);
				break;
			case 3:
				m_temp = m_in_ior_3_cb(offset);
				break;
		}
		break;

	case MODE_TRANSFER_READ:
		m_temp = m_in_memr_cb(offset);
		break;
	}

}


//-------------------------------------------------
//  dma_write -
//-------------------------------------------------

inline void i8257_device::dma_write()
{
	offs_t offset = m_channel[m_current_channel].m_address;

	switch (MODE_TRANSFER_MASK)
	{
	case MODE_TRANSFER_VERIFY: {
		UINT8 v1 = m_in_memr_cb(offset);
		if(0 && m_temp != v1)
			logerror("%s: verify error %02x vs. %02x\n", tag(), m_temp, v1);
		break;
	}

	case MODE_TRANSFER_WRITE:
		m_out_memw_cb(offset, m_temp);
		break;

	case MODE_TRANSFER_READ:
		switch(m_current_channel)
		{
			case 0:
				m_out_iow_0_cb(offset, m_temp);
				break;
			case 1:
				m_out_iow_1_cb(offset, m_temp);
				break;
			case 2:
				m_out_iow_2_cb(offset, m_temp);
				break;
			case 3:
				m_out_iow_3_cb(offset, m_temp);
				break;
		}
		break;
	}
}


//-------------------------------------------------
//  end_of_process -
//-------------------------------------------------

inline void i8257_device::advance()
{
	bool tc = (m_channel[m_current_channel].m_count == 0);
	bool al = (MODE_AUTOLOAD && (m_current_channel == 2));

	if(tc)
	{
		m_status |= 1 << m_current_channel;
		m_request &= ~(1 << m_current_channel); // docs imply this isn't right but pc-8001 works better with it
		set_tc(1);

		if(al)
		{
			// autoinitialize
			m_channel[2].m_address = m_channel[3].m_address;
			m_channel[2].m_count = m_channel[3].m_count;
			m_channel[2].m_mode = m_channel[3].m_mode;
		}
		else if(MODE_TC_STOP)
			// disable channel
			m_transfer_mode &= ~(1 << m_current_channel);
	}

	if(!(al && tc))
	{
		m_channel[m_current_channel].m_count--;
		m_channel[m_current_channel].m_count &= 0x3fff;
		m_channel[m_current_channel].m_address++;
	}
}



//**************************************************************************
//  LIVE DEVICE
//**************************************************************************

//-------------------------------------------------
//  i8257_device - constructor
//-------------------------------------------------

i8257_device::i8257_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock)
	: device_t(mconfig, I8257, "Intel 8257", tag, owner, clock, "i8257", __FILE__),
		device_execute_interface(mconfig, *this),
		m_icount(0),
		m_reverse_rw(0),
		m_tc(false),
		m_msb(0),
		m_hreq(CLEAR_LINE),
		m_hack(0),
		m_ready(1),
		m_state(0),
		m_current_channel(0),
		m_last_channel(0),
		m_transfer_mode(0),
		m_status(0),
		m_request(0),
		m_temp(0),
		m_out_hrq_cb(*this),
		m_out_tc_cb(*this),
		m_in_memr_cb(*this),
		m_out_memw_cb(*this),
		m_in_ior_0_cb(*this),
		m_in_ior_1_cb(*this),
		m_in_ior_2_cb(*this),
		m_in_ior_3_cb(*this),
		m_out_iow_0_cb(*this),
		m_out_iow_1_cb(*this),
		m_out_iow_2_cb(*this),
		m_out_iow_3_cb(*this),
		m_out_dack_0_cb(*this),
		m_out_dack_1_cb(*this),
		m_out_dack_2_cb(*this),
		m_out_dack_3_cb(*this)
{
}


//-------------------------------------------------
//  device_start - device-specific startup
//-------------------------------------------------

void i8257_device::device_start()
{
	// set our instruction counter
	m_icountptr = &m_icount;

	// resolve callbacks
	m_out_hrq_cb.resolve_safe();
	m_out_tc_cb.resolve_safe();
	m_in_memr_cb.resolve_safe(0);
	m_out_memw_cb.resolve_safe();
	m_in_ior_0_cb.resolve_safe(0);
	m_in_ior_1_cb.resolve_safe(0);
	m_in_ior_2_cb.resolve_safe(0);
	m_in_ior_3_cb.resolve_safe(0);
	m_out_iow_0_cb.resolve_safe();
	m_out_iow_1_cb.resolve_safe();
	m_out_iow_2_cb.resolve_safe();
	m_out_iow_3_cb.resolve_safe();
	m_out_dack_0_cb.resolve_safe();
	m_out_dack_1_cb.resolve_safe();
	m_out_dack_2_cb.resolve_safe();
	m_out_dack_3_cb.resolve_safe();

	// state saving
	save_item(NAME(m_msb));
	save_item(NAME(m_hreq));
	save_item(NAME(m_hack));
	save_item(NAME(m_ready));
	save_item(NAME(m_state));
	save_item(NAME(m_current_channel));
	save_item(NAME(m_last_channel));
	save_item(NAME(m_transfer_mode));
	save_item(NAME(m_status));
	save_item(NAME(m_request));

	save_item(NAME(m_channel[0].m_address));
	save_item(NAME(m_channel[0].m_count));
	save_item(NAME(m_channel[0].m_mode));
	save_item(NAME(m_channel[1].m_address));
	save_item(NAME(m_channel[1].m_count));
	save_item(NAME(m_channel[1].m_mode));
	save_item(NAME(m_channel[2].m_address));
	save_item(NAME(m_channel[2].m_count));
	save_item(NAME(m_channel[2].m_mode));
	save_item(NAME(m_channel[3].m_address));
	save_item(NAME(m_channel[3].m_count));
	save_item(NAME(m_channel[3].m_mode));
}


//-------------------------------------------------
//  device_reset - device-specific reset
//-------------------------------------------------

void i8257_device::device_reset()
{
	m_state = STATE_SI;
	m_transfer_mode = 0;
	m_status = 0;
	m_request = 0;
	m_msb = 0;
	m_current_channel = -1;
	m_last_channel = 3;
	m_hreq = -1;
	m_tc = 0;

	for (int i = 0; i < 4; i++)
	{
		m_channel[i].m_address = 0;
		m_channel[i].m_count = 0;
		m_channel[i].m_mode = 0;
	}
	set_hreq(0);
	set_dack();
}

bool i8257_device::next_channel()
{
	int priority[] = { 0, 1, 2, 3 };

	if (MODE_ROTATING_PRIORITY)
	{
		int last_channel = m_last_channel;

		for (int channel = 3; channel >= 0; channel--)
		{
			priority[channel] = last_channel;
			last_channel--;
			if (last_channel < 0) last_channel = 3;
		}
	}

	for (int channel = 0; channel < 4; channel++)
	{
		if (is_request_active(priority[channel]))
		{
			m_current_channel = m_last_channel = priority[channel];
			return true;
		}
	}
	return false;
}


//-------------------------------------------------
//  execute_run -
//-------------------------------------------------

void i8257_device::execute_run()
{
	do
	{
		switch (m_state)
		{
		case STATE_SI:
			set_tc(0);
			if(next_channel())
				m_state = STATE_S0;
			else
			{
				suspend_until_trigger(1, true);
				m_icount = 0;
			}
			break;

		case STATE_S0:
			set_hreq(1);

			if (m_hack)
			{
				m_state = STATE_S1;
			}
			else
			{
				suspend_until_trigger(1, true);
				m_icount = 0;
			}
			break;

		case STATE_S1:
			set_tc(0);
			m_state = STATE_S2;
			break;

		case STATE_S2:
			set_dack();
			m_state = STATE_S3;
			break;

		case STATE_S3:
			dma_read();

			if (MODE_EXTENDED_WRITE)
			{
				dma_write();
			}

			m_state = m_ready ? STATE_S4 : STATE_SW;
			break;

		case STATE_SW:
			m_state = m_ready ? STATE_S4 : STATE_SW;
			break;

		case STATE_S4:
			if (!MODE_EXTENDED_WRITE)
			{
				dma_write();
			}
			advance();

			if(next_channel())
				m_state = STATE_S1;
			else
			{
				set_hreq(0);
				m_current_channel = -1;
				m_state = STATE_SI;
				set_dack();
			}
			break;
		}
		m_icount--;
	} while (m_icount > 0);
}


//-------------------------------------------------
//  read -
//-------------------------------------------------

READ8_MEMBER( i8257_device::read )
{
	UINT8 data = 0;

	if (!BIT(offset, 3))
	{
		int channel = (offset >> 1) & 0x03;

		switch (offset & 0x01)
		{
		case REGISTER_ADDRESS:
			if (m_msb)
			{
				data = m_channel[channel].m_address >> 8;
			}
			else
			{
				data = m_channel[channel].m_address & 0xff;
			}
			break;

		case REGISTER_WORD_COUNT:
			if (m_msb)
			{
				data = (m_channel[channel].m_count >> 8);
				if(m_reverse_rw && m_channel[channel].m_mode)
					data |= (m_channel[channel].m_mode == 1) ? 0x80 : 0x40;
				else
					data |= (m_channel[channel].m_mode << 6);
			}
			else
			{
				data = m_channel[channel].m_count & 0xff;
			}
			break;
		}

		m_msb = !m_msb;
	}
	else if(offset == REGISTER_STATUS)
	{
		data = m_status;

		// clear TC bits
		m_status &= 0xf0;
	}

	return data;
}


//-------------------------------------------------
//  write -
//-------------------------------------------------

WRITE8_MEMBER( i8257_device::write )
{
	if (!BIT(offset, 3))
	{
		int channel = (offset >> 1) & 0x03;

		switch (offset & 0x01)
		{
		case REGISTER_ADDRESS:
			if (m_msb)
			{
				m_channel[channel].m_address = (data << 8) | (m_channel[channel].m_address & 0xff);
				if(MODE_AUTOLOAD && (channel == 2))
					m_channel[3].m_address = (data << 8) | (m_channel[3].m_address & 0xff);
			}
			else
			{
				m_channel[channel].m_address = (m_channel[channel].m_address & 0xff00) | data;
				if(MODE_AUTOLOAD && (channel == 2))
					m_channel[3].m_address = (m_channel[3].m_address & 0xff00) | data;
			}
			break;

		case REGISTER_WORD_COUNT:
			if (m_msb)
			{
				m_channel[channel].m_count = ((data & 0x3f) << 8) | (m_channel[channel].m_count & 0xff);
				m_channel[channel].m_mode = (data >> 6);

				if(m_reverse_rw && m_channel[channel].m_mode)
					m_channel[channel].m_mode = (m_channel[channel].m_mode == 1) ? 2 : 1;

				if(MODE_AUTOLOAD && (channel == 2))
				{
					m_channel[3].m_count = ((data & 0x3f) << 8) | (m_channel[3].m_count & 0xff);
					m_channel[3].m_mode = m_channel[2].m_mode;
				}
			}
			else
			{
				m_channel[channel].m_count = (m_channel[channel].m_count & 0xff00) | data;
				if(MODE_AUTOLOAD && (channel == 2))
					m_channel[3].m_count = (m_channel[3].m_count & 0xff00) | data;
			}
			break;
		}

		m_msb = !m_msb;
	}
	else if(offset == REGISTER_MODE)
	{
		m_transfer_mode = data;

		if (LOG) logerror("I8257 '%s' Command Register: %02x\n", tag(), m_transfer_mode);
	}
	trigger(1);
}


//-------------------------------------------------
//  hlda_w - hold acknowledge
//-------------------------------------------------

WRITE_LINE_MEMBER( i8257_device::hlda_w )
{
	if (LOG) logerror("I8257 '%s' Hold Acknowledge: %u\n", tag(), state);

	m_hack = state;
	trigger(1);
}


//-------------------------------------------------
//  ready_w - ready
//-------------------------------------------------

WRITE_LINE_MEMBER( i8257_device::ready_w )
{
	if (LOG) logerror("I8257 '%s' Ready: %u\n", tag(), state);

	m_ready = state;
}


//-------------------------------------------------
//  dreq0_w - DMA request for channel 0
//-------------------------------------------------

WRITE_LINE_MEMBER( i8257_device::dreq0_w )
{
	dma_request(0, state);
}


//-------------------------------------------------
//  dreq0_w - DMA request for channel 1
//-------------------------------------------------

WRITE_LINE_MEMBER( i8257_device::dreq1_w )
{
	dma_request(1, state);
}


//-------------------------------------------------
//  dreq1_w - DMA request for channel 2
//-------------------------------------------------

WRITE_LINE_MEMBER( i8257_device::dreq2_w )
{
	dma_request(2, state);
}


//-------------------------------------------------
//  dreq3_w - DMA request for channel 3
//-------------------------------------------------

WRITE_LINE_MEMBER( i8257_device::dreq3_w )
{
	dma_request(3, state);
}
