// license:BSD-3-Clause
// copyright-holders:Patrick Mackinlay

/*
 * Opus 100PM (Personal Mainframe) UNIX processor subsystem
 *
 * Sources:
 *  - http://bitsavers.org/pdf/opusSystems/32k/800-00237-000_Opus_100pm_User_Manual_1987.pdf
 *
 * TODO
 *  - 32032-based 110PM
 */

 /*
 * WIP
 * ---
 * Using IBM 5160 as host avoids boot problem with 5170 and issues with
 * shadowing in ct486.
 *
 *  - opus ibm5160 -window -nomax -hard1 opus_c.chd -hard2 opus_d.chd -isa5 opus108pm
 *
 * System configuration and software installation from boot and kernel floppies
 * appears successful. Valid Systems hard disk images appear to expect a card
 * with different registers?
 *
 * Booting Opus5 from swap hangs with WAIT instruction at 0xf0087c, presume
 * requires an interrupt from PC which never arrives. Maybe clock device isn't
 * working properly?
 */

#include "emu.h"
#include "opus100pm.h"

#define LOG_STAT    (1U << 1)
#define LOG_REG     (1U << 2)

//#define VERBOSE (LOG_GENERAL|LOG_REG)
#include "logmacro.h"

DEFINE_DEVICE_TYPE(ISA8_OPUS108PM, isa8_opus108pm_device, "opus108pm", "Opus 108PM")

// CPU bit definition here matches software, but differs from documentation
enum card_stat_mask : u8
{
	CARD_STAT_CPU  = 0x01, // CPU type (0=32016, 1=32032)
	CARD_STAT_EIRQ = 0x04, // IRQ enabled
	CARD_STAT_OPT  = 0x08, // option switch open
	CARD_STAT_DMA  = 0x10, // DMA abort
	CARD_STAT_PAR  = 0x20, // parity error
	CARD_STAT_IRQ  = 0x40, // interrupt to host active
	CARD_STAT_INT  = 0x80, // interrupt to card active
};

// the three low bits in the host status register probably correspond to
// unemulated CTTL, /TSO and /CWAIT signals from the TCU
enum host_stat_mask : u8
{
	HOST_STAT_CTTL = 0x01, // CTTL (diagnostic only)
	HOST_STAT_TSO  = 0x02, // TSO* (diagnostic only)
	HOST_STAT_CWT  = 0x04, // CWT* (diagnostic only)
	HOST_STAT_RUN  = 0x08, // RUN* (diagnostic only)
	HOST_STAT_DMA  = 0x10, // DMA abort
	HOST_STAT_PAR  = 0x20, // parity error
	HOST_STAT_IRQ  = 0x40, // interrupt to host active
	HOST_STAT_INT  = 0x80, // interrupt to card active
};

static INPUT_PORTS_START(opus108pm)
	PORT_START("BASE")
	PORT_DIPNAME(0xf0000, 0xa0000, "Base Address")
	PORT_DIPSETTING(      0x00000, "00000")
	PORT_DIPSETTING(      0x10000, "10000")
	PORT_DIPSETTING(      0x20000, "20000")
	PORT_DIPSETTING(      0x30000, "30000")
	PORT_DIPSETTING(      0x40000, "40000")
	PORT_DIPSETTING(      0x50000, "50000")
	PORT_DIPSETTING(      0x60000, "60000")
	PORT_DIPSETTING(      0x70000, "70000")
	PORT_DIPSETTING(      0x80000, "80000")
	PORT_DIPSETTING(      0x90000, "90000")
	PORT_DIPSETTING(      0xa0000, "A0000")
	PORT_DIPSETTING(      0xb0000, "B0000")
	PORT_DIPSETTING(      0xc0000, "C0000")
	PORT_DIPSETTING(      0xd0000, "D0000")
	PORT_DIPSETTING(      0xe0000, "E0000")
	PORT_DIPSETTING(      0xf0000, "F0000")

	PORT_START("IRQ")
	PORT_DIPNAME(0x0f, 0x07, "IRQ")
	PORT_DIPSETTING(   0x02, "IRQ 2")
	PORT_DIPSETTING(   0x03, "IRQ 3")
	PORT_DIPSETTING(   0x07, "IRQ 7")
INPUT_PORTS_END

isa8_opus108pm_device::isa8_opus108pm_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock)
	: device_t(mconfig, ISA8_OPUS108PM, tag, owner, clock)
	, device_isa8_card_interface(mconfig, *this)
	, m_cpu(*this, "cpu")
	, m_fpu(*this, "fpu")
	, m_mmu(*this, "mmu")
	, m_ram(*this, "ram")
	, m_base(*this, "BASE")
	, m_irq(*this, "IRQ")
	, m_installed(false)
{
}

void isa8_opus108pm_device::device_add_mconfig(machine_config &config)
{
	NS32016(config, m_cpu, 20_MHz_XTAL / 2);
	m_cpu->set_addrmap(0, &isa8_opus108pm_device::map_cpu);

	NS32081(config, m_fpu, 20_MHz_XTAL / 2);
	m_cpu->set_fpu(m_fpu);

	NS32082(config, m_mmu, 20_MHz_XTAL / 2);
	m_cpu->set_mmu(m_mmu);

	RAM(config, m_ram);
	m_ram->set_default_size("1MiB");
	m_ram->set_extra_options("2MiB");
}

ioport_constructor isa8_opus108pm_device::device_input_ports() const
{
	return INPUT_PORTS_NAME(opus108pm);
}

void isa8_opus108pm_device::device_start()
{
	set_isa_device();

	if (!m_ram->started())
		throw device_missing_dependencies();

	save_item(NAME(m_card_stat));
	save_item(NAME(m_host_stat));

	m_cpu->space(0).install_ram(0, m_ram->mask(), m_ram->pointer());
}

void isa8_opus108pm_device::device_reset()
{
	if (!m_installed)
	{
		u32 const base = m_base->read();

		m_isa->install_memory(base, base | 0xffff, *this, &isa8_opus108pm_device::map_isa);

		m_installed = true;
	}

	m_card_stat = CARD_STAT_OPT;
	m_host_stat = HOST_STAT_RUN;
}

void isa8_opus108pm_device::device_reset_after_children()
{
	m_cpu->suspend(SUSPEND_REASON_HALT, true);
	m_cpu->suspend(SUSPEND_REASON_RESET, false);
}

void isa8_opus108pm_device::map_cpu(address_map &map)
{
	// TODO: should unmapped memory accesses cause parity errors and NMI?

	// silence double-word access to byte-sized registers
	map(0x800000, 0x800003).noprw().mirror(0x030000);

	map(0x800000, 0x800000).r(FUNC(isa8_opus108pm_device::card_wait_r));
	map(0x810000, 0x810000).r(FUNC(isa8_opus108pm_device::card_stat_r));
	map(0x810000, 0x810000).w(FUNC(isa8_opus108pm_device::card_rste_w));
	map(0x820000, 0x820000).w(FUNC(isa8_opus108pm_device::card_ack_w));
	map(0x830000, 0x830000).w(FUNC(isa8_opus108pm_device::card_irq_w));
}

void isa8_opus108pm_device::map_isa(address_map &map)
{
	map(0x0000, 0xefff).lrw8(
		[this](offs_t offset)
		{
			if (!(m_host_stat & HOST_STAT_RUN))
				offset |= 0xff0000;

			// FIXME: invalid translation probably triggers "DMA abort" status
			// and generates an NMI; unsure what else is reported to the host
			if (m_mmu->translate(m_cpu->space(0), 0xa, offset, false, false) != ns32000_mmu_interface::COMPLETE)
				fatalerror("%s: host address translation failed (%s)\n", tag(), machine().describe_context());

			return m_ram->read(offset);
		}, "ram_r",
		[this](offs_t offset, u8 data)
		{
			if (!(m_host_stat & HOST_STAT_RUN))
				offset |= 0xff0000;

			// FIXME: invalid translation probably triggers "DMA abort" status
			// and generates an NMI; unsure what else is reported to the host
			if (m_mmu->translate(m_cpu->space(0), 0xa, offset, false, true) != ns32000_mmu_interface::COMPLETE)
				fatalerror("%s: host address translation failed (%s)\n", tag(), machine().describe_context());

			m_ram->write(offset, data);
		}, "ram_w");

	// software expects these registers at 0xfffN, not 0xf00N per documentation,
	// assume they are mirrored throughout the whole 4k range
	map(0xf000, 0xf000).mirror(0x0ff0).r(FUNC(isa8_opus108pm_device::host_stat_r));
	map(0xf001, 0xf001).mirror(0x0ff0).w(FUNC(isa8_opus108pm_device::host_eirq_w));
	map(0xf002, 0xf002).mirror(0x0ff0).w(FUNC(isa8_opus108pm_device::host_ack_w));
	map(0xf003, 0xf003).mirror(0x0ff0).w(FUNC(isa8_opus108pm_device::host_int_w));
	map(0xf004, 0xf004).mirror(0x0ff0).w(FUNC(isa8_opus108pm_device::host_nmi_w));
	map(0xf005, 0xf005).mirror(0x0ff0).w(FUNC(isa8_opus108pm_device::host_go_w));
	map(0xf006, 0xf006).mirror(0x0ff0).w(FUNC(isa8_opus108pm_device::host_run_w));
	map(0xf007, 0xf007).mirror(0x0ff0).w(FUNC(isa8_opus108pm_device::host_rst_w));
}

void isa8_opus108pm_device::update_isa_irq(int state)
{
	LOG("update_isa_irq %d\n", state);

	switch (m_irq->read())
	{
	case 2: m_isa->irq2_w(state); break;
	case 3: m_isa->irq3_w(state); break;
	case 7: m_isa->irq7_w(state); break;

	default:
		fatalerror("%s: invalid isa irq %d\n", tag(), m_irq->read());
	}
}

u8 isa8_opus108pm_device::card_stat_r()
{
	LOGMASKED(LOG_STAT, "card_stat_r 0x%02x (%s)\n", m_card_stat, machine().describe_context());

	return m_card_stat;
}

void isa8_opus108pm_device::card_rste_w(u8 data)
{
	LOG("card_rste_w (%s)\n", machine().describe_context());

	m_card_stat &= ~(CARD_STAT_PAR | CARD_STAT_DMA);
	m_host_stat &= ~(HOST_STAT_PAR | HOST_STAT_DMA);
}

void isa8_opus108pm_device::card_ack_w(u8 data)
{
	if (m_card_stat & CARD_STAT_INT)
	{
		LOG("card_ack_w (%s)\n", machine().describe_context());
		m_card_stat &= ~CARD_STAT_INT;
		m_host_stat &= ~HOST_STAT_INT;
		m_cpu->set_input_line(INPUT_LINE_IRQ0, CLEAR_LINE);
	}
}

void isa8_opus108pm_device::card_irq_w(u8 data)
{
	if (!(m_card_stat & CARD_STAT_IRQ))
	{
		LOG("card_irq_w (%s)\n", machine().describe_context());
		m_card_stat |= CARD_STAT_IRQ;
		m_host_stat |= HOST_STAT_IRQ;

		if (m_card_stat & CARD_STAT_EIRQ)
			update_isa_irq(1);
	}
}

u8 isa8_opus108pm_device::host_stat_r()
{
	// HACK: diagnostic test requires changes in this host status bit, this
	// arbitrary toggle satisfies the test but is not accurate
	m_host_stat ^= HOST_STAT_CWT;

	LOGMASKED(LOG_STAT, "host_stat_r 0x%02x (%s)\n", m_host_stat, machine().describe_context());
	return m_host_stat;
}

void isa8_opus108pm_device::host_eirq_w(u8 data)
{
	if (!(m_card_stat & CARD_STAT_EIRQ))
	{
		LOG("host_eirq_w (%s)\n", machine().describe_context());
		m_card_stat |= CARD_STAT_EIRQ;

		if (m_card_stat & CARD_STAT_IRQ)
			update_isa_irq(1);
	}
}

void isa8_opus108pm_device::host_ack_w(u8 data)
{
	if (m_host_stat & HOST_STAT_IRQ)
	{
		LOG("host_ack_w (%s)\n", machine().describe_context());
		m_card_stat &= ~CARD_STAT_IRQ;
		m_host_stat &= ~HOST_STAT_IRQ;

		update_isa_irq(0);
	}
}

void isa8_opus108pm_device::host_int_w(u8 data)
{
	if (!(m_host_stat & HOST_STAT_INT))
	{
		LOG("host_int_w (%s)\n", machine().describe_context());
		m_card_stat |= CARD_STAT_INT;
		m_host_stat |= HOST_STAT_INT;
		m_cpu->set_input_line(INPUT_LINE_IRQ0, ASSERT_LINE);
	}
}

void isa8_opus108pm_device::host_nmi_w(u8 data)
{
	LOG("host_nmi_w (%s)\n", machine().describe_context());
	m_cpu->set_input_line(INPUT_LINE_NMI, ASSERT_LINE);
	m_cpu->set_input_line(INPUT_LINE_NMI, CLEAR_LINE);
}

void isa8_opus108pm_device::host_go_w(u8 data)
{
	LOG("host_go_w (%s)\n", machine().describe_context());

	m_cpu->resume(SUSPEND_REASON_RESET);
}

void isa8_opus108pm_device::host_run_w(u8 data)
{
	LOG("host_run_w (%s)\n", machine().describe_context());

	m_host_stat &= ~HOST_STAT_RUN;
	m_cpu->resume(SUSPEND_REASON_HALT);
}

void isa8_opus108pm_device::host_rst_w(u8 data)
{
	LOG("host_rst_w (%s)\n", machine().describe_context());

	m_card_stat &= (CARD_STAT_OPT | CARD_STAT_CPU);
	m_host_stat = HOST_STAT_RUN;

	m_cpu->reset();
	m_fpu->reset();
	m_mmu->reset();

	m_cpu->suspend(SUSPEND_REASON_HALT, true);
	m_cpu->suspend(SUSPEND_REASON_RESET, false);
}
