// license:BSD-3-Clause
// copyright-holders:Barry Rodewald

/*

    am9517a.cpp is a more complete implementation of this, the uPD71071 appears to be a clone of it

    NEC uPD71071 DMA Controller
    Used on the Fujitsu FM-Towns

    Register description:

    0x00:   Initialise (Write-only)
            - bit 0: Reset
            - bit 1: 16-bit data bus

    0x01:   Channel Register
            On read:
            - bits 0-3: Selected channel
            - bit 4: Only base registers may be read or written
            On write:
            - bits 0-1: Select channel for programming count, address, and mode registers
            - bit 2: Only base registers can be read or written to

    0x02:
    0x03:   Count Register (16-bit)
            DMA Transfer counter

    0x04:
    0x05:
    0x06:
    0x07:   Address Register (32-bit)
            Self-explanatory, I hope. :)
            NOTE: Datasheet clearly shows this as 24-bit, with register 7 unused.
            But the FM-Towns definitely uses reg 7 as bits 24-31.
            The documentation on the V53A manual doesn't show these bits either, maybe it's
            an external connection on the FMT? might be worth checking overflow behavior etc.
            The Towns manual confirms the top 8 bits are external to the DMAC and there's no carry into them.

    0x08:
    0x09:   Device Control register (16-bit)
            bit 0: Enable memory-to-memory (MTM) transfers
            bit 1: Enable fixed address for channel 0 only (MTM only)
            bit 2: Disable DMA operation (stops HLDRQ signal to the CPU)
            bit 3: Use compressed timing
            bit 4: Rotational Priority
            bit 5: Extended Writing
            bit 6: DMARQ active level (1=active low)
            bit 7: DMAAK active level (1=active high)
            bit 8: Bus mode (0=bus release, 1=bus hold)
            bit 9: Wait Enable during Verify

    0x0a:   Mode Control register
            bit 0: Transfer size (1=16-bit, 0=8-bit,  16-bit data bus size only)
            bit 2-3: Transfer direction (ignored for MTM transfers)
                        00 = Verify
                        01 = I/O to memory
                        10 = memory to I/O
                        11 = invalid
            bit 4: Enable auto-initialise
            bit 5: Address direction (0=increment, 1=decrement, affects only current Address reg)
            bit 6-7: Transfer mode (ignored for MTM transfers)
                        00 = Demand
                        01 = Single
                        10 = Block
                        11 = Cascade

    0x0b:   Status register
            bit 0-3: Terminal count (per channel)
            bit 4-7: DMA request present (external hardware DMA only)

    0x0c:
    0x0d:   Temporary register (16-bit, read-only)
            Stores the last data transferred in an MTM transfer

    0x0e:   Request register
            bit 0-3: Software DMA request (1=set)
            bit 0 only in MTM transfers

    0x0f:   Mask register
            bit 0-3: DMARQ mask
            bits 1 and 0 only in MTM transfers

    Note, the uPD71071 compatible mode of the V53 CPU differs from a real uPD71071 in the following ways



*/

#include "emu.h"
#include "machine/upd71071.h"


DEFINE_DEVICE_TYPE(UPD71071, upd71071_device, "upd71071", "NEC uPD71071 DMA Controller")

upd71071_device::upd71071_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
	: device_t(mconfig, UPD71071, tag, owner, clock)
	, m_upd_clock(0)
	, m_out_hreq_cb(*this)
	, m_out_eop_cb(*this)
	, m_dma_read_cb(*this)
	, m_dma_write_cb(*this)
	, m_out_dack_cb(*this)
	, m_cpu(*this, finder_base::DUMMY_TAG)
{
}

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

void upd71071_device::device_start()
{
	m_out_hreq_cb.resolve_safe();
	m_out_eop_cb.resolve_safe();
	m_dma_read_cb.resolve_all_safe(0);
	m_dma_write_cb.resolve_all_safe();
	m_out_dack_cb.resolve_all_safe();
	for (auto &elem : m_timer)
		elem = machine().scheduler().timer_alloc(timer_expired_delegate(FUNC(upd71071_device::dma_transfer_timer), this));
	m_selected_channel = 0;

	m_reg.device_control = 0;
	m_reg.mask = 0x0f;  // mask all channels
	for (int x = 0; x < 4; x++)
		m_reg.mode_control[x] = 0;

	save_item(NAME(m_reg.initialise));
	save_item(NAME(m_reg.channel));
	save_item(NAME(m_reg.count_current));
	save_item(NAME(m_reg.count_base));
	save_item(NAME(m_reg.address_current));
	save_item(NAME(m_reg.address_base));
	save_item(NAME(m_reg.device_control));
	save_item(NAME(m_reg.mode_control));
	save_item(NAME(m_reg.status));
	save_item(NAME(m_reg.temp_l));
	save_item(NAME(m_reg.temp_h));
	save_item(NAME(m_reg.request));
	save_item(NAME(m_reg.mask));

	save_item(NAME(m_selected_channel));
	save_item(NAME(m_buswidth));
	save_item(NAME(m_dmarq));
	save_item(NAME(m_base));
	save_item(NAME(m_hreq));
	save_item(NAME(m_eop));
}



TIMER_CALLBACK_MEMBER(upd71071_device::dma_transfer_timer)
{
	// single byte or word transfer
	int channel = param;
	uint16_t data = 0;  // data to transfer

	switch (m_reg.mode_control[channel] & 0x0c)
	{
	case 0x00:  // Verify
		break;
	case 0x04:  // I/O -> memory
		switch (channel)
		{
		case 0:
		case 1:
		case 2:
		case 3:
			if (!m_dma_read_cb[channel].isnull())
				data = m_dma_read_cb[channel](0);
			break;
		}

		if (m_cpu)
		{
			address_space& space = m_cpu->space(AS_PROGRAM);
			space.write_byte(m_reg.address_current[channel], data & 0xff);
		}
		else
		{
			printf("upd71071_device: dma_transfer_timer - write to memory, no dest space %02x\n", data & 0xff);
		}

		if (m_reg.mode_control[channel] & 0x20)  // Address direction
			m_reg.address_current[channel]--;
		else
			m_reg.address_current[channel]++;
		m_reg.count_current[channel]--;
		if(m_reg.count_current[channel] == 0xffff)
		{
			if (m_reg.mode_control[channel] & 0x10)  // auto-initialise
			{
				m_reg.address_current[channel] = m_reg.address_base[channel];
				m_reg.count_current[channel] = m_reg.count_base[channel];
			}
			else
			{
				m_timer[channel]->adjust(attotime::never);
				m_reg.mask |= (0x01 << channel);  // END or TC
			}
			// TODO: send terminal count
			set_eop(ASSERT_LINE);
			m_reg.status |= (0x01 << channel);  // END or TC
		}
		break;
	case 0x08:  // memory -> I/O
		if (m_cpu)
		{
			address_space& space = m_cpu->space(AS_PROGRAM);
			data = space.read_byte(m_reg.address_current[channel]);
		}
		else
		{
			printf("upd71071_device: dma_transfer_timer - read from memory, no src space\n");
			data = 0x00;
		}

		switch (channel)
		{
		case 0:
		case 1:
		case 2:
		case 3:
			if (!m_dma_write_cb[channel].isnull())
				m_dma_write_cb[channel](offs_t(0), data);
			break;
		}
		if (m_reg.mode_control[channel] & 0x20)  // Address direction
			m_reg.address_current[channel]--;
		else
			m_reg.address_current[channel]++;
		m_reg.count_current[channel]--;
		if(m_reg.count_current[channel] == 0xffff)
		{
			if (m_reg.mode_control[channel] & 0x10)  // auto-initialise
			{
				m_reg.address_current[channel] = m_reg.address_base[channel];
				m_reg.count_current[channel] = m_reg.count_base[channel];
			}
			else
			{
				m_timer[channel]->adjust(attotime::never);
				m_reg.mask |= (0x01 << channel);  // END or TC
			}
			// TODO: send terminal count
			set_eop(ASSERT_LINE);
			m_reg.status |= (0x01 << channel);  // END or TC
		}
		break;
	case 0x0c:  // Invalid
		break;
	}
}

void upd71071_device::soft_reset()
{
	// Does not change base/current address, count, or buswidth
	m_selected_channel = 0;
	m_base = 0;
	for (int x = 0; x < 4; x++)
		m_reg.mode_control[x] = 0;
	m_reg.device_control = 0;
	m_reg.temp_h = 0;
	m_reg.temp_l = 0;
	m_reg.mask = 0x0f;  // mask all channels
	m_reg.status &= ~0x0f;  // clears bits 0-3 only
	m_reg.request = 0;
}

int upd71071_device::dmarq(int state, int channel)
{
	if (state != 0)
	{
		if (m_reg.device_control & 0x0004)
			return 2;

		if (m_reg.mask & (1 << channel))  // is channel masked?
			return 1;

		m_dmarq[channel] = 1;  // DMARQ line is set
		m_reg.status |= (0x10 << channel);

		// start transfer
		switch (m_reg.mode_control[channel] & 0xc0)
		{
		case 0x00:  // Demand
			// TODO
			set_eop(CLEAR_LINE);
			m_timer[channel]->adjust(attotime::from_hz(m_upd_clock), channel);
			break;
		case 0x40:  // Single
			m_timer[channel]->adjust(attotime::from_hz(m_upd_clock), channel);
			break;
		case 0x80:  // Block
			// TODO
			break;
		case 0xc0:  // Cascade
			// TODO
			break;
		}
	}
	else
	{
		m_dmarq[channel] = 0;  // clear DMARQ line
		m_reg.status &= ~(0x10 << channel);
		m_timer[channel]->adjust(attotime::never);
	}
	return 0;
}

uint8_t upd71071_device::read(offs_t offset)
{
	uint8_t ret = 0;

	logerror("DMA: read from register %02x\n",offset);
	switch(offset)
	{
	case 0x01:  // Channel
		ret = (1 << m_selected_channel);
		if (m_base != 0)
			ret |= 0x10;
		break;
	case 0x02:  // Count (low)
		if (m_base != 0)
			ret = m_reg.count_base[m_selected_channel] & 0xff;
		else
			ret = m_reg.count_current[m_selected_channel] & 0xff;
		break;
	case 0x03:  // Count (high)
		if (m_base != 0)
			ret = (m_reg.count_base[m_selected_channel] >> 8) & 0xff;
		else
			ret = (m_reg.count_current[m_selected_channel] >> 8) & 0xff;
		break;
	case 0x04:  // Address (low)
		if (m_base != 0)
			ret = m_reg.address_base[m_selected_channel] & 0xff;
		else
			ret = m_reg.address_current[m_selected_channel] & 0xff;
		break;
	case 0x05:  // Address (mid)
		if (m_base != 0)
			ret = (m_reg.address_base[m_selected_channel] >> 8) & 0xff;
		else
			ret = (m_reg.address_current[m_selected_channel] >> 8) & 0xff;
		break;
	case 0x06:  // Address (high)
		if (m_base != 0)
			ret = (m_reg.address_base[m_selected_channel] >> 16) & 0xff;
		else
			ret = (m_reg.address_current[m_selected_channel] >> 16) & 0xff;
		break;
	case 0x07:  // Address (highest)
		if (m_base != 0)
			ret = (m_reg.address_base[m_selected_channel] >> 24) & 0xff;
		else
			ret = (m_reg.address_current[m_selected_channel] >> 24) & 0xff;
		break;
	case 0x08:  // Device control (low)
		ret = m_reg.device_control & 0xff;
		break;
	case 0x09:  // Device control (high)
		ret = (m_reg.device_control >> 8) & 0xff;
		break;
	case 0x0a:  // Mode control
		ret = m_reg.mode_control[m_selected_channel];
		break;
	case 0x0b:  // Status
		ret = m_reg.status;
		m_reg.status &= ~0x0f;  // resets END/TC?
		break;
	case 0x0c:  // Temporary (low)
		ret = m_reg.temp_h;
		break;
	case 0x0d:  // Temporary (high)
		ret = m_reg.temp_l;
		break;
	case 0x0e:  // Request
		ret = m_reg.request;
		break;
	case 0x0f:  // Mask
		ret = m_reg.mask;
		break;
	}
	return ret;
}

void upd71071_device::write(offs_t offset, uint8_t data)
{
	switch (offset)
	{
	case 0x00:  // Initialise
		// TODO: reset (bit 0)
		m_buswidth = data & 0x02;
		if (data & 0x01)
			soft_reset();
		logerror("DMA: Initialise [%02x]\n",data);
		break;
	case 0x01:  // Channel
		m_selected_channel = data & 0x03;
		m_base = data & 0x04;
		logerror("DMA: Channel selected [%02x]\n",data);
		break;
	case 0x02:  // Count (low)
		m_reg.count_base[m_selected_channel] =
			(m_reg.count_base[m_selected_channel] & 0xff00) | data;
		if (m_base == 0)
			m_reg.count_current[m_selected_channel] =
				(m_reg.count_current[m_selected_channel] & 0xff00) | data;
		logerror("DMA: Channel %i Counter set [%04x]\n",m_selected_channel,m_reg.count_base[m_selected_channel]);
		break;
	case 0x03:  // Count (high)
		m_reg.count_base[m_selected_channel] =
			(m_reg.count_base[m_selected_channel] & 0x00ff) | (data << 8);
		if (m_base == 0)
			m_reg.count_current[m_selected_channel] =
				(m_reg.count_current[m_selected_channel] & 0x00ff) | (data << 8);
		logerror("DMA: Channel %i Counter set [%04x]\n",m_selected_channel,m_reg.count_base[m_selected_channel]);
		break;
	case 0x04:  // Address (low)
		m_reg.address_base[m_selected_channel] =
			(m_reg.address_base[m_selected_channel] & 0xffffff00) | data;
		if (m_base == 0)
			m_reg.address_current[m_selected_channel] =
				(m_reg.address_current[m_selected_channel] & 0xffffff00) | data;
		logerror("DMA: Channel %i Address set [%08x]\n",m_selected_channel,m_reg.address_base[m_selected_channel]);
		break;
	case 0x05:  // Address (mid)
		m_reg.address_base[m_selected_channel] =
			(m_reg.address_base[m_selected_channel] & 0xffff00ff) | (data << 8);
		if (m_base == 0)
			m_reg.address_current[m_selected_channel] =
				(m_reg.address_current[m_selected_channel] & 0xffff00ff) | (data << 8);
		logerror("DMA: Channel %i Address set [%08x]\n",m_selected_channel,m_reg.address_base[m_selected_channel]);
		break;
	case 0x06:  // Address (high)
		m_reg.address_base[m_selected_channel] =
			(m_reg.address_base[m_selected_channel] & 0xff00ffff) | (data << 16);
		if (m_base == 0)
			m_reg.address_current[m_selected_channel] =
				(m_reg.address_current[m_selected_channel] & 0xff00ffff) | (data << 16);
		logerror("DMA: Channel %i Address set [%08x]\n",m_selected_channel,m_reg.address_base[m_selected_channel]);
		break;
	case 0x07:  // Address (highest)
		m_reg.address_base[m_selected_channel] =
			(m_reg.address_base[m_selected_channel] & 0x00ffffff) | (data << 24);
		if (m_base == 0)
			m_reg.address_current[m_selected_channel] =
				(m_reg.address_current[m_selected_channel] & 0x00ffffff) | (data << 24);
		logerror("DMA: Channel %i Address set [%08x]\n",m_selected_channel,m_reg.address_base[m_selected_channel]);
		break;
	case 0x08:  // Device control (low)
		m_reg.device_control = (m_reg.device_control & 0xff00) | data;
		logerror("DMA: Device control set [%04x]\n",m_reg.device_control);
		break;
	case 0x09:  // Device control (high)
		m_reg.device_control = (m_reg.device_control & 0x00ff) | (data << 8);
		logerror("DMA: Device control set [%04x]\n",m_reg.device_control);
		break;
	case 0x0a:  // Mode control
		m_reg.mode_control[m_selected_channel] = data;
		logerror("DMA: Channel %i Mode control set [%02x]\n",m_selected_channel,m_reg.mode_control[m_selected_channel]);
		break;
	case 0x0e:  // Request
		m_reg.request = data;
		logerror("DMA: Request set [%02x]\n",data);
		break;
	case 0x0f:  // Mask
		m_reg.mask = data;
		logerror("DMA: Mask set [%02x]\n",data);
		break;
	}
}

WRITE_LINE_MEMBER(upd71071_device::set_hreq)
{
	if (m_hreq != state)
	{
		m_out_hreq_cb(state);
		m_hreq = state;
	}
}

WRITE_LINE_MEMBER(upd71071_device::set_eop)
{
	if (m_eop != state)
	{
		m_out_eop_cb(state);
		m_eop = state;
	}
}
