// license:BSD-3-Clause
// copyright-holders:Barry Rodewald
/*
 * ym3802.c - Yamaha MCS MIDI Communication and Service Controller
 *
 * TODO:
 *   - Receive serial data
 *   - Transmit Idle detection
 *   - IRx/ITx (used for MIDI system messages)
 *   - FSK modulation
 *   - Timers (MIDI clock timer and Click counter are working but not guaranteed to be perfectly accurate)
 *   - Interrupts (except for Tx Buffer Empty, MIDI clock detect, Click Counter)
 */

#include "emu.h"
#include "ym3802.h"


DEFINE_DEVICE_TYPE(YM3802, ym3802_device, "ym3802", "Yamaha YM3802 MCS MIDI Communication and Service Controller")

ym3802_device::ym3802_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
	: device_t(mconfig, YM3802, tag, owner, clock)
	, device_serial_interface(mconfig, *this)
	, m_irq_handler(*this)
	, m_txd_handler(*this)
	, m_rxd_handler(*this)
	, m_reg(REG_MAX)
	, m_wdr(0)
	, m_irq_status(0)
	, m_vector(0)
	, m_clkm_rate(500000)  // TODO: make these configurable
	, m_clkf_rate(614400)
	{
	}

void ym3802_device::device_start()
{
	m_irq_handler.resolve_safe();
	m_txd_handler.resolve_safe();
	m_rxd_handler.resolve_safe(0xff);
	m_clock_timer = timer_alloc(TIMER_SYSTEM_CLOCK);
	m_midi_timer = timer_alloc(TIMER_TX_CLOCK);
	m_midi_counter_timer = timer_alloc(TIMER_MIDI_CLOCK);
	save_item(NAME(m_reg));
}

void ym3802_device::device_reset()
{
	m_reg.clear();
	reset_irq(0xff);
	transmit_register_reset();
	receive_register_reset();
	reset_midi_timer();
	set_comms_mode();
}

void ym3802_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
	// TODO: support clock and timers
	switch(id)
	{
		case TIMER_TX_CLOCK:
			transmit_clk();
			break;
		case TIMER_MIDI_CLOCK:
			midi_clk();
			break;
	}
}

void ym3802_device::set_irq(uint8_t irq)
{
	uint8_t x;

	m_irq_status |= (irq & m_reg[REG_IER]);
	for(x=0;x<8;x++)
	{
		if(m_irq_status & (1 << x))
			break;
	}
	m_vector = (m_reg[REG_IOR] & 0xe0) | (x << 1);
	if(m_irq_status != 0)
		m_irq_handler(ASSERT_LINE);
}

void ym3802_device::reset_irq(uint8_t irq)
{
	m_irq_status &= ~irq;
	if(m_irq_status == 0)
		m_irq_handler(CLEAR_LINE);
}

void ym3802_device::transmit_clk()
{
	if(m_reg[REG_TCR] & 0x01) // Tx Enable
	{
		if(!m_tx_fifo.empty())
		{
			if (is_transmit_register_empty())
			{
				transmit_register_setup(m_tx_fifo.front());  // start to send first byte in FIFO
				m_tx_fifo.pop();                                // and remove it from the FIFO
				if(m_tx_fifo.empty())
					set_irq(IRQ_FIFOTX_EMPTY);
			}
		}
		/* if diserial has bits to send, make them so */
		if (!is_transmit_register_empty())
		{
			uint8_t data = transmit_register_get_data_bit();
			m_tx_busy = true;
			m_txd_handler(data);
		}
		if (m_tx_fifo.empty() && is_transmit_register_empty())
			m_tx_busy = false;
	}
}

void ym3802_device::midi_clk()
{
	if(m_midi_counter_base > 1)  // counter is not guaranteed to work if set to 0 or 1.
	{
		if(m_midi_counter == 0)
		{
			m_midi_counter = m_midi_counter_base;  // reload timer
			if(m_reg[REG_IMR] & 0x08)  // if IRQ1 is set to MIDI clock detect
				set_irq(IRQ_MIDI_CLK);
			if(m_click_counter_base != 0)
			{
				m_click_counter--;
				if(m_click_counter == 0)
				{
					m_click_counter = m_click_counter_base;
					if(!(m_reg[REG_IMR] & 0x08))  // if IRQ1 is set to click counter
						set_irq(IRQ_CLICK);
				}
			}
		}
		else
			m_midi_counter--;
	}
}

void ym3802_device::reset_midi_timer()
{
	uint32_t rate;
	uint8_t divisor = m_reg[REG_TRR] & 0x1f;

	if(!(divisor & 0x10))
	{
		if(divisor & 0x08)
			rate = m_clkm_rate / 32;
		else
			rate = m_clkm_rate / 16;
	}
	else
	{
		if(!(divisor & 0x08))
			rate = m_clkf_rate / 32;
		else
			rate = m_clkf_rate / (64 << (divisor & 0x07));
	}

	if(rate != m_prev_rate)
		m_midi_timer->adjust(attotime::from_hz(rate),0,attotime::from_hz(rate));
	m_prev_rate = rate;
	logerror("MIDI Timer rate set to %iHz\n",rate);
}

void ym3802_device::set_comms_mode()
{
	uint8_t data_bits = (m_reg[REG_TMR] & 0x20) ? 7 : 8;
	parity_t parity;
	stop_bits_t stop_bits = (m_reg[REG_TMR] & 0x02) ? STOP_BITS_2 : STOP_BITS_1;

	if(!(m_reg[REG_TMR] & 0x10))  // parity enable
		parity = PARITY_NONE;
	else
	{
		if(m_reg[REG_TMR] & 0x04)
			parity = PARITY_ODD;
		else
			parity = PARITY_EVEN;
		// TODO: 4-bit parity
	}

	set_data_frame(1, data_bits, parity, stop_bits);
	logerror("MIDI comms set to 1 start bit, %i data bits, %s, parity = %i\n",data_bits, (stop_bits == STOP_BITS_2) ? "2 stop bits" : "1 stop bit", parity);
}

READ8_MEMBER(ym3802_device::read)
{
	if(offset < 4)
	{
		if(offset == 3)
			return m_wdr;
		if(offset == 2)
			return m_irq_status;
		if(offset == 0)
			return m_vector;
		return m_reg[offset];
	}
	else
	{
		uint8_t bank = m_reg[REG_RGR] & 0x0f;
		uint8_t ret = 0;

		if(bank > 9)
			return m_wdr;

		switch(offset + (bank * 10))
		{
			case REG_TSR:
				if(m_tx_fifo.empty())
					ret |= 0x80;
				if(m_tx_fifo.size() < 16)
					ret |= 0x40;
				if(m_tx_busy)
					ret |= 0x01;
				break;
			default:
				ret = m_reg[offset + (bank * 10)];
		}
		return ret;
	}
}

WRITE8_MEMBER(ym3802_device::write)
{
	m_wdr = data;
	if(offset == 1)
	{
		m_reg[REG_RGR] = data & 0x0f;
		if(data & 0x80)
			device_reset();
		logerror("MIDI: writing %02x to reg %i\n",data,offset);
	}
	if(offset == 3)
		reset_irq(data);
	if(offset > 4)
	{
		uint8_t bank = m_reg[REG_RGR] & 0x0f;

		if(bank > 9)
			return;

		m_reg[offset + (bank * 10)] = data;
		logerror("MIDI: writing %02x to reg %i\n",data,offset + (bank * 10));

		switch(offset + (bank * 10))
		{
			case REG_IOR:
				logerror("IOR vector write %02\n",data);
				break;
			case REG_IER:
				logerror("IER set to %02x\n",data);
				break;
			case REG_DCR:
				if(data & 0x20)
				{
					if((data & 0x07) == 2)
					{
						const double rate = (m_reg[REG_CCR] & 0x02) ? m_clkm_rate / 4 : m_clkm_rate / 8;

						// start message to click counter
						m_midi_counter_timer->adjust(attotime::from_hz(rate),0,attotime::from_hz(rate));
					}
					if((data & 0x07) == 3)
					{
						// stop message to click counter
						m_midi_counter_timer->adjust(attotime::zero,0,attotime::never);
					}
				}
				break;
			case REG_TMR:
				set_comms_mode();
				break;
			case REG_TCR:
				if(data & 0x01)
					reset_midi_timer();
				break;
			case REG_TDR:
				m_tx_fifo.push(data);
				reset_irq(IRQ_FIFOTX_EMPTY);
				break;
			case REG_GTR_LOW:
				m_general_counter = (m_general_counter & 0xff00) | data;
				//popmessage("General counter set to %i\n",m_general_counter);
				break;
			case REG_GTR_HIGH:
				m_general_counter = (m_general_counter & 0x00ff) | ((data & 0x3f) << 8);
				//popmessage("General counter set to %i\n",m_general_counter);
				break;
			case REG_MTR_LOW:
				m_midi_counter_base = (m_midi_counter & 0xff00) | data;
				m_midi_counter = m_midi_counter_base;
				//popmessage("MIDI counter set to %i\n",m_midi_counter);
				break;
			case REG_MTR_HIGH:
				m_midi_counter_base = (m_midi_counter & 0x00ff) | ((data & 0x3f) << 8);
				m_midi_counter = m_midi_counter_base;
				//popmessage("MIDI counter set to %i\n",m_midi_counter);
				break;
			case REG_CDR:
				m_click_counter_base = data & 0x7f;
				m_click_counter = m_click_counter_base;
				break;
		}
	}
}
