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

    Sinclair ZX8302 emulation

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

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

/*

    TODO:

    - set time
    - read from microdrive
    - write to microdrive
    - DTR/CTS handling
    - network

*/

#include "emu.h"
#include <time.h>
#include "devices/microdrv.h"
#include "machine/devhelpr.h"
#include "zx8302.h"



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

#define LOG 0


// IPC serial state
enum
{
	IPC_START,
	IPC_DATA,
	IPC_STOP
};


// baud rate
enum
{
	 BAUD_19200				= 0x00,
	 BAUD_9600				= 0x01,
	 BAUD_4800				= 0x02,
	 BAUD_2400				= 0x03,
	 BAUD_1200				= 0x04,
	 BAUD_600				= 0x05,
	 BAUD_300				= 0x06,
	 BAUD_75				= 0x07,
	 BAUD_MASK				= 0x07,
};

// transmit mode
enum
{
	 MODE_SER1				= 0x00,
	 MODE_SER2				= 0x08,
	 MODE_MDV				= 0x10,
	 MODE_NET				= 0x18,
	 MODE_MASK				= 0x18,
};

// interrupts
enum
{
	 INT_GAP				= 0x01,
	 INT_INTERFACE			= 0x02,
	 INT_TRANSMIT			= 0x04,
	 INT_FRAME				= 0x08,
	 INT_EXTERNAL			= 0x10,
};

// status register
enum
{
	 STATUS_NETWORK_PORT	= 0x01,
	 STATUS_TX_BUFFER_FULL	= 0x02,
	 STATUS_RX_BUFFER_FULL	= 0x04,
	 STATUS_MICRODRIVE_GAP	= 0x08,
};

// transmit bits
enum
{
	 TXD_START				= 0,
	 TXD_STOP				= 9,
	 TXD_STOP2				= 10,
};


// Monday 1st January 1979 00:00:00 UTC
enum
{
	 RTC_BASE_ADJUST		= 283996800,
};


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

const device_type ZX8302 = &device_creator<zx8302_device>;

//-------------------------------------------------
//  device_config_complete - perform any
//  operations now that the configuration is
//  complete
//-------------------------------------------------

void zx8302_device::device_config_complete()
{
	// inherit a copy of the static data
	const zx8302_interface *intf = reinterpret_cast<const zx8302_interface *>(static_config());
	if (intf != NULL)
		*static_cast<zx8302_interface *>(this) = *intf;

	// or initialize to defaults if none provided
	else
	{
		memset(&out_ipl1l_cb, 0, sizeof(out_ipl1l_cb));
		memset(&out_baudx4_cb, 0, sizeof(out_baudx4_cb));
		memset(&out_comdata_cb, 0, sizeof(out_comdata_cb));
		memset(&out_txd1_cb, 0, sizeof(out_txd1_cb));
		memset(&out_txd2_cb, 0, sizeof(out_txd2_cb));
		memset(&in_dtr1_cb, 0, sizeof(in_dtr1_cb));
		memset(&in_cts2_cb, 0, sizeof(in_cts2_cb));
		memset(&out_netout_cb, 0, sizeof(out_netout_cb));
		memset(&in_netin_cb, 0, sizeof(in_netin_cb));
		memset(&out_mdselck_cb, 0, sizeof(out_mdselck_cb));
		memset(&out_mdseld_cb, 0, sizeof(out_mdseld_cb));
		memset(&out_mdrdw_cb, 0, sizeof(out_mdrdw_cb));
		memset(&out_erase_cb, 0, sizeof(out_erase_cb));
		memset(&out_raw1_cb, 0, sizeof(out_raw1_cb));
		memset(&in_raw1_cb, 0, sizeof(in_raw1_cb));
		memset(&out_raw2_cb, 0, sizeof(out_raw2_cb));
		memset(&in_raw2_cb, 0, sizeof(in_raw2_cb));
	}
}



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

//-------------------------------------------------
//  trigger_interrupt -
//-------------------------------------------------

inline void zx8302_device::trigger_interrupt(UINT8 line)
{
	m_irq |= line;

	m_out_ipl1l_func(ASSERT_LINE);
}


//-------------------------------------------------
//  transmit_ipc_data - transmit IPC data
//-------------------------------------------------

inline void zx8302_device::transmit_ipc_data()
{
	/*

        IPC <-> ZX8302 serial link protocol
        ***********************************

        Send bit to IPC
        ---------------

        1. ZX start bit (COMDATA = 0)
        2. IPC clock (COMCTL = 0, COMTL = 1)
        3. ZX data bit (COMDATA = 0/1)
        4. IPC clock (COMCTL = 0, COMTL = 1)
        5. ZX stop bit (COMDATA = 1)

        Receive bit from IPC
        --------------------

        1. ZX start bit (COMDATA = 0)
        2. IPC clock (COMCTL = 0, COMTL = 1)
        3. IPC data bit (COMDATA = 0/1)
        4. IPC clock (COMCTL = 0, COMTL = 1)
        5. IPC stop bit (COMDATA = 1)

    */

	switch (m_ipc_state)
	{
	case IPC_START:
		if (LOG) logerror("ZX8302 '%s' COMDATA Start Bit\n", tag());

		m_out_comdata_func(0);
		m_ipc_busy = 1;
		m_ipc_state = IPC_DATA;
		break;

	case IPC_DATA:
		if (LOG) logerror("ZX8302 '%s' COMDATA Data Bit: %x\n", tag(), BIT(m_idr, 1));

		m_comdata = BIT(m_idr, 1);
		m_out_comdata_func(m_comdata);
		m_ipc_state = IPC_STOP;
		break;

	case IPC_STOP:
		if (!m_ipc_rx)
		{
			if (LOG) logerror("ZX8302 '%s' COMDATA Stop Bit\n", tag());

			m_out_comdata_func(1);
			m_ipc_busy = 0;
			m_ipc_state = IPC_START;
		}
		break;
	}
}


//-------------------------------------------------
//  transmit_bit - transmit serial bit
//-------------------------------------------------

inline void zx8302_device::transmit_bit(int state)
{
	switch (m_tcr & MODE_MASK)
	{
	case MODE_SER1:
		m_out_txd1_func(state);
		break;

	case MODE_SER2:
		m_out_txd2_func(state);
		break;

	case MODE_MDV:
		// TODO
		break;

	case MODE_NET:
		m_out_netout_func(state);
		break;
	}
}


//-------------------------------------------------
//  transmit_data - transmit serial data
//-------------------------------------------------

inline void zx8302_device::transmit_serial_data()
{
	switch (m_tx_bits)
	{
	case TXD_START:
		if (!(m_irq & INT_TRANSMIT))
		{
			transmit_bit(0);
			m_tx_bits++;
		}
		break;

	default:
		transmit_bit(BIT(m_tdr, 0));
		m_tdr >>= 1;
		m_tx_bits++;
		break;

	case TXD_STOP:
		transmit_bit(1);
		m_tx_bits++;
		break;

	case TXD_STOP2:
		transmit_bit(1);
		m_tx_bits = TXD_START;
		m_status &= ~STATUS_TX_BUFFER_FULL;
		trigger_interrupt(INT_TRANSMIT);
		break;
	}
}



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

//-------------------------------------------------
//  zx8302_device - constructor
//-------------------------------------------------
zx8302_device::zx8302_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock)
    : device_t(mconfig, ZX8302, "Sinclair ZX8302", tag, owner, clock),
	  m_idr(1),
	  m_irq(0),
	  m_ctr(time(NULL) + RTC_BASE_ADJUST),
	  m_status(0),
	  m_comdata(1),
	  m_comctl(1),
	  m_ipc_state(0),
	  m_ipc_rx(0),
	  m_ipc_busy(0),
	  m_track(0)
{
}


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

void zx8302_device::device_start()
{
	// resolve callbacks
	m_out_ipl1l_func.resolve(out_ipl1l_cb, *this);
	m_out_baudx4_func.resolve(out_baudx4_cb, *this);
	m_out_comdata_func.resolve(out_comdata_cb, *this);
	m_out_txd1_func.resolve(out_txd1_cb, *this);
	m_out_txd2_func.resolve(out_txd2_cb, *this);
	m_in_dtr1_func.resolve(in_dtr1_cb, *this);
	m_in_cts2_func.resolve(in_cts2_cb, *this);
	m_out_netout_func.resolve(out_netout_cb, *this);
	m_in_netin_func.resolve(in_netin_cb, *this);
	m_out_mdselck_func.resolve(out_mdselck_cb, *this);
	m_out_mdseld_func.resolve(out_mdseld_cb, *this);
	m_out_mdrdw_func.resolve(out_mdrdw_cb, *this);
	m_out_erase_func.resolve(out_erase_cb, *this);
	m_out_raw1_func.resolve(out_raw1_cb, *this);
	m_in_raw1_func.resolve(in_raw1_cb, *this);
	m_out_raw2_func.resolve(out_raw2_cb, *this);
	m_in_raw2_func.resolve(in_raw2_cb, *this);

	// allocate timers
	m_txd_timer = timer_alloc(TIMER_TXD);
	m_baudx4_timer = timer_alloc(TIMER_BAUDX4);
	m_rtc_timer = timer_alloc(TIMER_RTC);
	m_gap_timer = timer_alloc(TIMER_GAP);
	m_ipc_timer = timer_alloc(TIMER_IPC);

	m_rtc_timer->adjust(attotime::zero, 0, attotime::from_hz(rtc_clock / 32768));
	m_gap_timer->adjust(attotime::zero, 0, attotime::from_msec(31));

	// register for state saving
	save_item(NAME(m_idr));
	save_item(NAME(m_tcr));
	save_item(NAME(m_tdr));
	save_item(NAME(m_irq));
	save_item(NAME(m_ctr));
	save_item(NAME(m_status));
	save_item(NAME(m_comdata));
	save_item(NAME(m_comctl));
	save_item(NAME(m_ipc_state));
	save_item(NAME(m_ipc_rx));
	save_item(NAME(m_ipc_busy));
	save_item(NAME(m_baudx4));
	save_item(NAME(m_tx_bits));
	save_item(NAME(m_mdv_data));
	save_item(NAME(m_track));
}


//-------------------------------------------------
//  device_timer - handler timer events
//-------------------------------------------------

void zx8302_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
	switch (id)
	{
	case TIMER_TXD:
		transmit_serial_data();
		break;

	case TIMER_BAUDX4:
		m_baudx4 = !m_baudx4;
		m_out_baudx4_func(m_baudx4);
		break;

	case TIMER_RTC:
		m_ctr++;
		break;

	case TIMER_GAP:
		trigger_interrupt(INT_GAP);
		break;

	case TIMER_IPC:
		m_idr = param;
		m_ipc_state = IPC_START;
		m_ipc_rx = 0;

		transmit_ipc_data();
		break;
	}
}


//-------------------------------------------------
//  rtc_r - real time clock read
//-------------------------------------------------

READ8_MEMBER( zx8302_device::rtc_r )
{
	UINT8 data = 0;

	switch (offset)
	{
	case 0:
		data = (m_ctr >> 24) & 0xff;
	case 1:
		data = (m_ctr >> 16) & 0xff;
	case 2:
		data = (m_ctr >> 8) & 0xff;
	case 3:
		data = m_ctr & 0xff;
	}

	return data;
}


//-------------------------------------------------
//  rtc_w - real time clock write
//-------------------------------------------------

WRITE8_MEMBER( zx8302_device::rtc_w )
{
	if (LOG) logerror("ZX8302 '%s' Set Real Time Clock: %02x\n", tag(), data);
}


//-------------------------------------------------
//  control_w - serial transmit clock
//-------------------------------------------------

WRITE8_MEMBER( zx8302_device::control_w )
{
	if (LOG) logerror("ZX8302 '%s' Transmit Control: %02x\n", tag(), data);

	int baud = (19200 >> (data & BAUD_MASK));
	int baudx4 = baud * 4;

	m_tcr = data;

	m_txd_timer->adjust(attotime::zero, 0, attotime::from_hz(baud));
	m_baudx4_timer->adjust(attotime::zero, 0, attotime::from_hz(baudx4));
}


//-------------------------------------------------
//  mdv_track_r - microdrive track data
//-------------------------------------------------

READ8_MEMBER( zx8302_device::mdv_track_r )
{
	if (LOG) logerror("ZX8302 '%s' Microdrive Track %u: %02x\n", tag(), m_track, m_mdv_data[m_track]);

	UINT8 data = m_mdv_data[m_track];

	m_track = !m_track;

	return data;
}


//-------------------------------------------------
//  status_r - status register
//-------------------------------------------------

READ8_MEMBER( zx8302_device::status_r )
{
	/*

        bit     description

        0       Network port
        1       Transmit buffer full
        2       Receive buffer full
        3       Microdrive GAP
        4       SER1 DTR
        5       SER2 CTS
        6       IPC busy
        7       COMDATA

    */

	UINT8 data = 0;

	// TODO network port

	// serial status
	data |= m_status;

	// TODO microdrive GAP

	// data terminal ready
	data |= m_in_dtr1_func() << 4;

	// clear to send
	data |= m_in_cts2_func() << 5;

	// IPC busy
	data |= m_ipc_busy << 6;

	// COMDATA
	data |= m_comdata << 7;

	if (LOG) logerror("ZX8302 '%s' Status: %02x\n", tag(), data);

	return data;
}


//-------------------------------------------------
//  ipc_command_w - IPC command
//-------------------------------------------------

WRITE8_MEMBER( zx8302_device::ipc_command_w )
{
	if (LOG) logerror("ZX8302 '%s' IPC Command: %02x\n", tag(), data);

	if (data != 0x01)
	{
		m_ipc_timer->adjust(attotime::from_nsec(480), data);
	}
}


//-------------------------------------------------
//  mdv_control_w - microdrive control
//-------------------------------------------------

WRITE8_MEMBER( zx8302_device::mdv_control_w )
{
	/*

        bit     description

        0       MDSELDH
        1       MDSELCKH
        2       MDRDWL
        3       ERASE
        4
        5
        6
        7

    */

	if (LOG) logerror("ZX8302 '%s' Microdrive Control: %02x\n", tag(), data);

	m_out_mdseld_func(BIT(data, 0));
	m_out_mdselck_func(BIT(data, 1));
	m_out_mdrdw_func(BIT(data, 2));
	m_out_erase_func(BIT(data, 3));

	if (BIT(data, 1))
	{
		m_status &= ~STATUS_RX_BUFFER_FULL;
	}
}


//-------------------------------------------------
//  irq_status_r - interrupt status
//-------------------------------------------------

READ8_MEMBER( zx8302_device::irq_status_r )
{
	if (LOG) logerror("ZX8302 '%s' Interrupt Status: %02x\n", tag(), m_irq);

	return m_irq;
}


//-------------------------------------------------
//  irq_acknowledge_w - interrupt acknowledge
//-------------------------------------------------

WRITE8_MEMBER( zx8302_device::irq_acknowledge_w )
{
	if (LOG) logerror("ZX8302 '%s' Interrupt Acknowledge: %02x\n", tag(), data);

	m_irq &= ~data;

	if (!m_irq)
	{
		m_out_ipl1l_func(CLEAR_LINE);
	}
}


//-------------------------------------------------
//  data_w - transmit buffer
//-------------------------------------------------

WRITE8_MEMBER( zx8302_device::data_w )
{
	if (LOG) logerror("ZX8302 '%s' Data Register: %02x\n", tag(), data);

	m_tdr = data;
	m_status |= STATUS_TX_BUFFER_FULL;
}


//-------------------------------------------------
//  vsync_w - vertical sync
//-------------------------------------------------

WRITE_LINE_MEMBER( zx8302_device::vsync_w )
{
	if (state)
	{
		if (LOG) logerror("ZX8302 '%s' Frame Interrupt\n", tag());

		trigger_interrupt(INT_FRAME);
	}
}


//-------------------------------------------------
//  comctl_w - IPC COMCTL
//-------------------------------------------------

WRITE_LINE_MEMBER( zx8302_device::comctl_w )
{
	if (LOG) logerror("ZX8302 '%s' COMCTL: %x\n", tag(), state);

	if (state)
	{
		transmit_ipc_data();
	}
}


//-------------------------------------------------
//  comdata_w - IPC COMDATA
//-------------------------------------------------

WRITE_LINE_MEMBER( zx8302_device::comdata_w )
{
	if (LOG) logerror("ZX8302 '%s' COMDATA: %x\n", tag(), state);

	if (m_ipc_state == IPC_DATA || m_ipc_state == IPC_STOP)
	{
		if (m_ipc_rx)
		{
			m_ipc_rx = 0;
			m_ipc_busy = 0;
			m_ipc_state = IPC_START;
		}
		else
		{
			m_ipc_rx = 1;
			m_comdata = state;
		}
	}
}


//-------------------------------------------------
//  extint_w - external interrupt
//-------------------------------------------------

WRITE_LINE_MEMBER( zx8302_device::extint_w )
{
	if (LOG) logerror("ZX8302 '%s' EXTINT: %x\n", tag(), state);

	if (state == ASSERT_LINE)
	{
		trigger_interrupt(INT_EXTERNAL);
	}
}
