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

  ISA 8 bit IBM PC Music Feature Card

  TODO:
   - YM-2164
   - MIDI
   - IRQ/base address selection

  Notes:
   - Some software does not function correctly at higher CPU speeds
  (e.g. the Sierra games and Yamaha Compose/PlayRec)

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


#include "emu.h"
#include "isa_ibm_mfc.h"
#include "cpu/z80/z80.h"
#include "machine/pit8253.h"
#include "sound/speaker.h"


//-------------------------------------------------
//  Constants
//-------------------------------------------------

#define TCR_TAC                 0x01
#define TCR_TBC                 0x02
#define TCR_TAE                 0x04
#define TCR_TBE                 0x08
#define TCR_EXT8                0x10
#define TCR_TMSK                0x40
#define TCR_IBE                 0x80

#define TSR_TAS                 0x01
#define TSR_TBS                 0x02
#define TSR_TCS                 0x80

enum
{
	PC_IRQ_TIMERA,
	PC_IRQ_TIMERB,
	PC_IRQ_RXRDY,
	PC_IRQ_TXRDY,
};

enum
{
	Z80_IRQ_YM,
	Z80_IRQ_RXRDY,
	Z80_IRQ_TXRDY,
	Z80_IRQ_MIDI_RXRDY,
	Z80_IRQ_MIDI_TXRDY,
};


//-------------------------------------------------
//  Globals
//-------------------------------------------------

const device_type ISA8_IBM_MFC = &device_creator<isa8_ibm_mfc_device>;


//-------------------------------------------------
//  Interrupt handling
//-------------------------------------------------

void isa8_ibm_mfc_device::set_pc_interrupt(int src, int state)
{
	if (state)
		m_pc_irq_state |= 1 << src;
	else
		m_pc_irq_state &= ~(1 << src);

	update_pc_interrupts();
}

void isa8_ibm_mfc_device::update_pc_interrupts(void)
{
	// IRQs enabled?
	if (m_tcr & TCR_IBE)
	{
		// IRQs unmasked?
		if (m_tcr & TCR_TMSK)
		{
			m_isa->irq3_w(m_pc_irq_state ? ASSERT_LINE : CLEAR_LINE);
		}
	}
}

void isa8_ibm_mfc_device::set_z80_interrupt(int src, int state)
{
	if (state)
		m_z80_irq_state |= 1 << src;
	else
		m_z80_irq_state &= ~(1 << src);

	update_z80_interrupts();
}

void isa8_ibm_mfc_device::update_z80_interrupts(void)
{
	m_cpu->set_input_line(0, m_z80_irq_state ? ASSERT_LINE : CLEAR_LINE);
}


//-------------------------------------------------
//  Z80 memory map
//-------------------------------------------------

static ADDRESS_MAP_START( prg_map, AS_PROGRAM, 8, isa8_ibm_mfc_device )
	AM_RANGE(0x0000, 0x7fff) AM_ROM
	AM_RANGE(0x8000, 0x8000) AM_RAM // Unknown - tested on startup
	AM_RANGE(0xbfff, 0xbfff) AM_RAM // Unknown - tested on startup
	AM_RANGE(0xc000, 0xdfff) AM_RAM
	AM_RANGE(0xe000, 0xffff) AM_RAM
ADDRESS_MAP_END

static ADDRESS_MAP_START( io_map, AS_IO, 8, isa8_ibm_mfc_device )
	ADDRESS_MAP_UNMAP_HIGH
	ADDRESS_MAP_GLOBAL_MASK(0xff)
	AM_RANGE(0x00, 0x01) AM_DEVREADWRITE("ym2151", ym2151_device, read, write)
	AM_RANGE(0x10, 0x10) AM_DEVREADWRITE("d71051", i8251_device, data_r, data_w)
	AM_RANGE(0x11, 0x11) AM_DEVREADWRITE("d71051", i8251_device, status_r, control_w)
	AM_RANGE(0x20, 0x23) AM_DEVREADWRITE("d71055c_1", i8255_device, read, write)
ADDRESS_MAP_END


//-------------------------------------------------
//  Jumpers and DIP switches
//-------------------------------------------------

static INPUT_PORTS_START( ibm_mfc )
	PORT_START("J1")
	PORT_DIPNAME( 0x07, 0x03, "IBM MFC J1: IRQ" )
	PORT_DIPSETTING(    0x02, "2" )
	PORT_DIPSETTING(    0x03, "3" )
	PORT_DIPSETTING(    0x04, "4" )
	PORT_DIPSETTING(    0x05, "5" )
	PORT_DIPSETTING(    0x06, "6" )
	PORT_DIPSETTING(    0x07, "7" )

	PORT_START("SW1")
	PORT_DIPNAME( 0x03, 0x00, "IBM MFC SW1: Base Address" )
	PORT_DIPSETTING(    0x00, "2A00" )
	PORT_DIPSETTING(    0x01, "2A10" )
	PORT_DIPSETTING(    0x02, "2A20" )
	PORT_DIPSETTING(    0x03, "2A30" )
INPUT_PORTS_END

//-------------------------------------------------
//  D71055C PPI (PC)
//-------------------------------------------------

READ8_MEMBER( isa8_ibm_mfc_device::ppi0_i_a )
{
	// Read data from the Z80 PIU
	return m_d71055c_1->pa_r();
}

WRITE8_MEMBER( isa8_ibm_mfc_device::ppi0_o_b )
{
	// Write data to the Z80 PIU - no action required
}

WRITE8_MEMBER( isa8_ibm_mfc_device::ppi0_o_c )
{
	// PC Port B /OBF (C1) -> Z80 Port B /STB (C2)
	m_d71055c_1->pc2_w(BIT(data, 1));

	// PC Port A IBF (C5) -> Z80 Port A /ACK (C6)
#if 0 // TODO
	m_d71055c_1->pc6_w(!BIT(data, 5));
#else
	if (!BIT(data, 5) && BIT(m_pc_ppi_c, 5))
		m_d71055c_1->pc6_w(0);
#endif

	// Bit 0 (INTRB) is TxRDY
	set_pc_interrupt(PC_IRQ_TXRDY, BIT(data, 0));

	// Bit 3 (INTRA) is RxRDY
	set_pc_interrupt(PC_IRQ_RXRDY, BIT(data, 3));

	m_pc_ppi_c = data;
}

READ8_MEMBER( isa8_ibm_mfc_device::ppi0_i_c )
{
	// Receive data bit 8
	return BIT(m_z80_ppi_c, 5) << 7;
}

static I8255_INTERFACE( d71055c_0_intf )
{
	DEVCB_DEVICE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, ppi0_i_a),
	DEVCB_NULL,
	DEVCB_NULL,
	DEVCB_DEVICE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, ppi0_o_b),
	DEVCB_DEVICE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, ppi0_i_c),
	DEVCB_DEVICE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, ppi0_o_c),
};


//-------------------------------------------------
//  D71055C PPI (Z80)
//-------------------------------------------------

WRITE8_MEMBER( isa8_ibm_mfc_device::ppi1_o_a )
{
	// Write data to the PC PIU - no action required
}

READ8_MEMBER( isa8_ibm_mfc_device::ppi1_i_b )
{
	// Read data from the PC PIU
	return m_d71055c_0->pb_r();
}

WRITE8_MEMBER( isa8_ibm_mfc_device::ppi1_o_c )
{
	// PortA /OBF (C7) -> PortA /STB (C2)
	m_d71055c_0->pc4_w(BIT(data, 7));

	// PortB IBF (C1) -> PortB /ACK (C2)
#if 0 // TODO
	m_d71055c_0->pc2_w(!BIT(data, 1));
#else
	if (!BIT(data, 1) && BIT(m_z80_ppi_c, 1))
		m_d71055c_0->pc2_w(0);
#endif

	set_z80_interrupt(Z80_IRQ_TXRDY, BIT(data, 3));
	set_z80_interrupt(Z80_IRQ_RXRDY, BIT(data, 0));

	m_z80_ppi_c = data;
}

static I8255_INTERFACE( d71055c_1_intf )
{
	DEVCB_NULL,
	DEVCB_DEVICE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, ppi1_o_a),
	DEVCB_DEVICE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, ppi1_i_b),
	DEVCB_NULL,
	DEVCB_NULL,
	DEVCB_DEVICE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, ppi1_o_c),
};


//-------------------------------------------------
//  D8253 PIT
//-------------------------------------------------

WRITE_LINE_MEMBER( isa8_ibm_mfc_device::d8253_clk0_out )
{
	if (m_tcr & TCR_TAE)
		set_pc_interrupt(PC_IRQ_TIMERA, 1);
}

WRITE_LINE_MEMBER( isa8_ibm_mfc_device::d8253_clk1_out )
{
	if (m_tcr & TCR_TBE)
		set_pc_interrupt(PC_IRQ_TIMERB, 1);
}

WRITE_LINE_MEMBER( isa8_ibm_mfc_device::d8253_clk2_out )
{
	m_d8253->clk1_w(state);
}

static const struct pit8253_interface d8253_intf =
{
	{
		{
			XTAL_4MHz / 8,
			DEVCB_NULL,
			DEVCB_DEVICE_LINE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, d8253_clk0_out)
		},
		{
			0,
			DEVCB_NULL,
			DEVCB_DEVICE_LINE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, d8253_clk1_out)
		},
		{
			XTAL_4MHz / 2,
			DEVCB_NULL,
			DEVCB_DEVICE_LINE_MEMBER(DEVICE_SELF_OWNER, isa8_ibm_mfc_device, d8253_clk2_out)
		}
	}
};


//-------------------------------------------------
//  uPD71051 USART
//-------------------------------------------------

void isa8_ibm_mfc_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
	m_d71051->transmit_clock();
	m_d71051->receive_clock();
}

static const i8251_interface d71051_intf =
{
	DEVCB_NULL,
	DEVCB_NULL,
	DEVCB_NULL,
	DEVCB_NULL,
	DEVCB_NULL,
	DEVCB_NULL,
	DEVCB_NULL
};


//-------------------------------------------------
//  YM-2164
//-------------------------------------------------


WRITE_LINE_MEMBER(isa8_ibm_mfc_device::ibm_mfc_ym_irq)
{
	set_z80_interrupt(Z80_IRQ_YM, state);
}


//-------------------------------------------------
//  Machine config
//-------------------------------------------------

static MACHINE_CONFIG_FRAGMENT( ibm_mfc )
	MCFG_CPU_ADD("ibm_mfc", Z80, XTAL_11_8MHz / 2)
	MCFG_CPU_PROGRAM_MAP(prg_map)
	MCFG_CPU_IO_MAP(io_map)

	MCFG_I8255_ADD("d71055c_0", d71055c_0_intf)
	MCFG_I8255_ADD("d71055c_1", d71055c_1_intf)
	MCFG_I8251_ADD("d71051", d71051_intf)
	MCFG_PIT8253_ADD("d8253", d8253_intf)

	MCFG_SPEAKER_STANDARD_STEREO("ymleft", "ymright")
	MCFG_YM2151_ADD("ym2151", XTAL_4MHz)
	MCFG_YM2151_IRQ_HANDLER(WRITELINE(isa8_ibm_mfc_device, ibm_mfc_ym_irq))
	MCFG_SOUND_ROUTE(0, "ymleft", 1.00)
	MCFG_SOUND_ROUTE(1, "ymright", 1.00)
MACHINE_CONFIG_END


//-------------------------------------------------
//  ISA interface
//-------------------------------------------------

READ8_MEMBER( isa8_ibm_mfc_device::ibm_mfc_r )
{
	UINT8 val = 0xff;

	switch (offset)
	{
		case 0x0:
		case 0x1:
		case 0x2:
		case 0x3:
		{
			val = m_d71055c_0->read(space, offset);
			break;
		}

		case 0xc:
		case 0xd:
		case 0xe:
		case 0xf:
		{
			val = (m_pc_irq_state ? 0x80 : 0) | (m_pc_irq_state & 3);
			break;
		}

		default:
		{
			fatalerror("Unhandled IBM MFC read from %d\n", offset);
		}
	}

	return val;
}

WRITE8_MEMBER( isa8_ibm_mfc_device::ibm_mfc_w )
{
	switch (offset)
	{
		case 0x0:
		case 0x1:
		case 0x2:
		case 0x3:
		{
			machine().scheduler().boost_interleave(attotime::zero, attotime::from_usec(1000));
			m_d71055c_0->write(space, offset, data);
			break;
		}

		case 0x4:
		case 0x5:
		case 0x6:
		case 0x7:
		{
			m_d8253->write(space, offset & 3, data);
			break;
		}

		case 0x8:
		case 0x9:
		case 0xa:
		case 0xb:
		{
			m_tcr = data;

			if (~m_tcr & TCR_TAC)
				set_pc_interrupt(PC_IRQ_TIMERA, 0);

			if (~m_tcr & TCR_TBC)
				set_pc_interrupt(PC_IRQ_TIMERB, 0);

			m_d71051->write_dsr((m_tcr & TCR_EXT8) ? 1 : 0);

			break;
		}

		case 0xc:
		case 0xd:
		case 0xe:
		case 0xf:
		{
			// TSR is read-only but Yamaha software attempts to write to it
			break;
		}
	}
}


//-------------------------------------------------
//  ROM definition
//-------------------------------------------------

ROM_START( ibm_mfc )
	ROM_REGION( 0x8000, "ibm_mfc", 0 )
	ROM_LOAD( "xc215 c 0.bin", 0x0000, 0x8000, CRC(28c58a4f) SHA1(e7edf28d20e6c146e3144526c89cd6beea64663b) )
ROM_END


//-------------------------------------------------
//  machine_config_additions - device-specific
//  machine configurations
//-------------------------------------------------

machine_config_constructor isa8_ibm_mfc_device::device_mconfig_additions() const
{
	return MACHINE_CONFIG_NAME( ibm_mfc );
}


//-------------------------------------------------
//  input_ports - device-specific input ports
//-------------------------------------------------

ioport_constructor isa8_ibm_mfc_device::device_input_ports() const
{
	return INPUT_PORTS_NAME( ibm_mfc );
}


//-------------------------------------------------
//  rom_region - return a pointer to the device's
//  internal ROM region
//-------------------------------------------------

const rom_entry *isa8_ibm_mfc_device::device_rom_region() const
{
	return ROM_NAME( ibm_mfc );
}


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

//-------------------------------------------------
//  isa8_ibm_mfc_device - constructor
//-------------------------------------------------

isa8_ibm_mfc_device::isa8_ibm_mfc_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock) :
		device_t(mconfig, ISA8_IBM_MFC, "IBM PC Music Feature Card", tag, owner, clock, "ibm_mfc", __FILE__),
		device_isa8_card_interface(mconfig, *this),
		m_cpu(*this, "ibm_mfc"),
		m_ym2151(*this, "ym2151"),
		m_d8253(*this, "d8253"),
		m_d71051(*this, "d71051"),
		m_d71055c_0(*this, "d71055c_0"),
		m_d71055c_1(*this, "d71055c_1")
{
}


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

void isa8_ibm_mfc_device::device_start()
{
	set_isa_device();
	m_isa->install_device(0x2a20, 0x2a20 + 15, 0, 0, read8_delegate(FUNC(isa8_ibm_mfc_device::ibm_mfc_r), this), write8_delegate(FUNC(isa8_ibm_mfc_device::ibm_mfc_w), this));

	// Create a 500KHz timer for the USART
	m_serial_timer = timer_alloc();
	m_serial_timer->adjust(attotime::zero, 0, attotime::from_hz(XTAL_4MHz / 8));
}


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

void isa8_ibm_mfc_device::device_reset()
{
	m_tcr = 0;
	m_d71051->write_dsr(0);
	m_pc_irq_state = 0;
	m_z80_irq_state = 0;
}
