// license:BSD-3-Clause
// copyright-holders:Curt Coder
/**********************************************************************

    Commodore 8050 floppy disk controller emulation

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

/*

    TODO:

    - write protect
    - 75,format speed error,01,00,0

*/

#include "emu.h"
#include "c8050fdc.h"



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

#define LOG 0
#define LOG_MORE 0
#define LOG_BITS 0

#define GCR_DECODE(_e, _i) \
	((BIT(_e, 6) << 7) | (BIT(_i, 7) << 6) | (_e & 0x33) | (BIT(_e, 2) << 3) | (_i & 0x04))

#define GCR_ENCODE(_e, _i) \
	((_e & 0xc0) << 2 | (_i & 0x80) | (_e & 0x3c) << 1 | (_i & 0x04) | (_e & 0x03))



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

DEFINE_DEVICE_TYPE(C8050_FDC, c8050_fdc_device, "c8050fdc", "Commodore 8050 FDC")


//-------------------------------------------------
//  ROM( c8050_fdc )
//-------------------------------------------------

ROM_START( c8050_fdc )
	ROM_REGION( 0x800, "gcr", 0)
	ROM_LOAD( "901467.uk6", 0x000, 0x800, CRC(a23337eb) SHA1(97df576397608455616331f8e837cb3404363fa2) )
ROM_END


//-------------------------------------------------
//  rom_region - device-specific ROM region
//-------------------------------------------------

const tiny_rom_entry *c8050_fdc_device::device_rom_region() const
{
	return ROM_NAME( c8050_fdc );
}



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

//-------------------------------------------------
//  c8050_fdc_device - constructor
//-------------------------------------------------

c8050_fdc_device::c8050_fdc_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) :
	device_t(mconfig, C8050_FDC, tag, owner, clock),
	m_write_sync(*this),
	m_write_ready(*this),
	m_write_brdy(*this),
	m_write_error(*this),
	m_gcr_rom(*this, "gcr"),
	m_floppy0(nullptr),
	m_floppy1(nullptr),
	m_mtr0(1),
	m_mtr1(1),
	m_stp0(0),
	m_stp1(0),
	m_ds(0),
	m_ds0(0),
	m_ds1(0),
	m_drv_sel(0),
	m_mode_sel(0),
	m_rw_sel(1), m_odd_hd(0), m_pi(0), t_gen(nullptr)
{
	cur_live.tm = attotime::never;
	cur_live.state = IDLE;
	cur_live.next_state = -1;
	cur_live.drv_sel = m_drv_sel;
}


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

void c8050_fdc_device::device_start()
{
	// resolve callbacks
	m_write_sync.resolve_safe();
	m_write_ready.resolve_safe();
	m_write_brdy.resolve_safe();
	m_write_error.resolve_safe();

	// allocate timer
	t_gen = timer_alloc(0);

	// register for state saving
	save_item(NAME(m_mtr0));
	save_item(NAME(m_mtr1));
	save_item(NAME(m_stp0));
	save_item(NAME(m_stp1));
	save_item(NAME(m_ds));
	save_item(NAME(m_ds0));
	save_item(NAME(m_ds1));
	save_item(NAME(m_drv_sel));
	save_item(NAME(m_mode_sel));
	save_item(NAME(m_rw_sel));
	save_item(NAME(m_odd_hd));
	save_item(NAME(m_pi));
}


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

void c8050_fdc_device::device_reset()
{
	live_abort();
}


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

void c8050_fdc_device::device_timer(emu_timer &timer, device_timer_id id, int param)
{
	live_sync();
	live_run();
}

floppy_image_device* c8050_fdc_device::get_floppy()
{
	return cur_live.drv_sel ? m_floppy1 : m_floppy0;
}

void c8050_fdc_device::stp_w(floppy_image_device *floppy, int mtr, int &old_stp, int stp)
{
	if (mtr) return;

	int tracks = 0;

	switch (old_stp)
	{
	case 0: if (stp == 1) tracks++; else if (stp == 2) tracks--; break;
	case 1: if (stp == 3) tracks++; else if (stp == 0) tracks--; break;
	case 2: if (stp == 0) tracks++; else if (stp == 3) tracks--; break;
	case 3: if (stp == 2) tracks++; else if (stp == 1) tracks--; break;
	}

	if (tracks == -1)
	{
		floppy->dir_w(1);
		floppy->stp_w(1);
		floppy->stp_w(0);
	}
	else if (tracks == 1)
	{
		floppy->dir_w(0);
		floppy->stp_w(1);
		floppy->stp_w(0);
	}

	old_stp = stp;
}

void c8050_fdc_device::stp0_w(int stp)
{
	if (m_stp0 != stp)
	{
		live_sync();
		stp_w(m_floppy0, m_mtr0, m_stp0, stp);
		checkpoint();
		live_run();
	}
}

void c8050_fdc_device::stp1_w(int stp)
{
	if (m_stp1 != stp)
	{
		live_sync();
		if (m_floppy1) stp_w(m_floppy1, m_mtr1, m_stp1, stp);
		checkpoint();
		live_run();
	}
}

void c8050_fdc_device::ds_w(int ds)
{
	if (m_ds != ds)
	{
		live_sync();
		m_ds = cur_live.ds = ds;
		pll_reset(cur_live.tm);
		if (LOG) logerror("%s %s DS %u\n", machine().time().as_string(), machine().describe_context(), ds);
		checkpoint();
		live_run();
	}
}

void c8050_fdc_device::set_floppy(floppy_connector *floppy0, floppy_connector *floppy1)
{
	m_floppy0 = floppy0->get_device();

	if (floppy1) {
		m_floppy1 = floppy1->get_device();
	}
}

void c8050_fdc_device::live_start()
{
	cur_live.tm = machine().time();
	cur_live.state = RUNNING;
	cur_live.next_state = -1;

	cur_live.shift_reg = 0;
	cur_live.shift_reg_write = 0;
	cur_live.bit_counter = 0;
	cur_live.ds = m_ds;
	cur_live.drv_sel = m_drv_sel;
	cur_live.mode_sel = m_mode_sel;
	cur_live.rw_sel = m_rw_sel;
	cur_live.pi = m_pi;

	pll_reset(cur_live.tm);
	checkpoint_live = cur_live;
	pll_save_checkpoint();

	live_run();
}

void c8050_fdc_device::pll_reset(const attotime &when)
{
	cur_pll.reset(when);
	cur_pll.set_clock(attotime::from_hz(clock() / (16 - m_ds)));
}

void c8050_fdc_device::pll_start_writing(const attotime &tm)
{
	cur_pll.start_writing(tm);
	pll_reset(cur_live.tm);
}

void c8050_fdc_device::pll_commit(floppy_image_device *floppy, const attotime &tm)
{
	cur_pll.commit(floppy, tm);
}

void c8050_fdc_device::pll_stop_writing(floppy_image_device *floppy, const attotime &tm)
{
	cur_pll.stop_writing(floppy, tm);
	pll_reset(cur_live.tm);
}

void c8050_fdc_device::pll_save_checkpoint()
{
	checkpoint_pll = cur_pll;
}

void c8050_fdc_device::pll_retrieve_checkpoint()
{
	cur_pll = checkpoint_pll;
}

int c8050_fdc_device::pll_get_next_bit(attotime &tm, floppy_image_device *floppy, const attotime &limit)
{
	return cur_pll.get_next_bit(tm, floppy, limit);
}

bool c8050_fdc_device::pll_write_next_bit(bool bit, attotime &tm, floppy_image_device *floppy, const attotime &limit)
{
	return cur_pll.write_next_bit(bit, tm, floppy, limit);
}

void c8050_fdc_device::checkpoint()
{
	pll_commit(get_floppy(), cur_live.tm);
	checkpoint_live = cur_live;
	pll_save_checkpoint();
}

void c8050_fdc_device::rollback()
{
	cur_live = checkpoint_live;
	pll_retrieve_checkpoint();
}

void c8050_fdc_device::live_delay(int state)
{
	cur_live.next_state = state;
	if(cur_live.tm != machine().time())
		t_gen->adjust(cur_live.tm - machine().time());
	else
		live_sync();
}

void c8050_fdc_device::live_sync()
{
	if(!cur_live.tm.is_never()) {
		if(cur_live.tm > machine().time()) {
			rollback();
			live_run(machine().time());
			pll_commit(get_floppy(), cur_live.tm);
		} else {
			pll_commit(get_floppy(), cur_live.tm);
			if(cur_live.next_state != -1) {
				cur_live.state = cur_live.next_state;
				cur_live.next_state = -1;
			}
			if(cur_live.state == IDLE) {
				pll_stop_writing(get_floppy(), cur_live.tm);
				cur_live.tm = attotime::never;
			}
		}
		cur_live.next_state = -1;
		checkpoint();
	}
}

void c8050_fdc_device::live_abort()
{
	if(!cur_live.tm.is_never() && cur_live.tm > machine().time()) {
		rollback();
		live_run(machine().time());
	}

	pll_stop_writing(get_floppy(), cur_live.tm);

	cur_live.tm = attotime::never;
	cur_live.state = IDLE;
	cur_live.next_state = -1;

	cur_live.ready = 1;
	cur_live.brdy = 1;
	cur_live.sync = 1;
	cur_live.error = 1;
}

void c8050_fdc_device::live_run(const attotime &limit)
{
	if(cur_live.state == IDLE || cur_live.next_state != -1)
		return;

	for(;;) {
		switch(cur_live.state) {
		case RUNNING: {
			bool syncpoint = false;

			if (cur_live.tm > limit)
				return;

			// read bit
			int bit = 0;
			if (cur_live.rw_sel) {
				bit = pll_get_next_bit(cur_live.tm, get_floppy(), limit);
				if(bit < 0)
					return;
			}

			// write bit
			int write_bit = BIT(cur_live.shift_reg_write, 9);
			if (!cur_live.rw_sel) { // TODO WPS
				/*
				write precompensation

				UA5.A = UM6.Qc
				UA5.B = !(!(!BRDY && UM6.Qa) && !(BRDY && E7))
				UA5.C0 = UA4.Qb = bit clock delayed 333ns
				UA5.C1 = UA4.Qa = bit clock delayed 166ns
				UA5.C2 = UA4.Qc = bit clock delayed 499ns
				UA5.C3 = UA5.Qb = bit clock delayed 333ns

				DATA OUT = !(!BITCLK || !(UA5.Y && !(WRITE_ENABLE && !UM6.Qb)))
				*/
				if (pll_write_next_bit(write_bit, cur_live.tm, get_floppy(), limit))
					return;
			}

			// clock read shift register
			cur_live.shift_reg <<= 1;
			cur_live.shift_reg |= bit;
			cur_live.shift_reg &= 0x3ff;

			// sync
			int sync = !((cur_live.shift_reg == 0x3ff) && cur_live.rw_sel);

			// bit counter
			if (!sync) {
				cur_live.bit_counter = 0;
			} else if (cur_live.sync) {
				cur_live.bit_counter++;
				if (cur_live.bit_counter == 10) {
					cur_live.bit_counter = 0;
				}
			}

			// GCR decoder
			if (cur_live.rw_sel) {
				cur_live.i = (cur_live.rw_sel << 10) | cur_live.shift_reg;
			} else {
				cur_live.i = (cur_live.rw_sel << 10) | ((cur_live.pi & 0xf0) << 1) | (cur_live.mode_sel << 4) | (cur_live.pi & 0x0f);
			}

			cur_live.e = m_gcr_rom->base()[cur_live.i];

			// byte ready
			int ready = !(cur_live.bit_counter == 9); // 74190 _RC, should be triggered on the falling edge of the clock
			int brdy = ready; // 74190 TC

			// GCR error
			int error = !(ready || BIT(cur_live.e, 3));

			if (LOG_BITS) {
				if (cur_live.rw_sel) {
					logerror("%s cyl %u bit %u sync %u bc %u sr %03x i %03x e %02x\n",cur_live.tm.as_string(),get_floppy()->get_cyl(),bit,sync,cur_live.bit_counter,cur_live.shift_reg,cur_live.i,cur_live.e);
				} else {
					logerror("%s cyl %u writing bit %u bc %u sr %03x i %03x e %02x\n",cur_live.tm.as_string(),get_floppy()->get_cyl(),write_bit,cur_live.bit_counter,cur_live.shift_reg_write,cur_live.i,cur_live.e);
				}
			}

			if (!ready) {
				// load write shift register
				cur_live.shift_reg_write = GCR_ENCODE(cur_live.e, cur_live.i);

				if (LOG_BITS) logerror("%s load write shift register %03x\n",cur_live.tm.as_string(),cur_live.shift_reg_write);
			} else {
				// clock write shift register
				cur_live.shift_reg_write <<= 1;
				cur_live.shift_reg_write &= 0x3ff;
			}

			if (ready != cur_live.ready) {
				if (cur_live.rw_sel && !ready)
					if (LOG) logerror("%s READY %u : %02x\n", cur_live.tm.as_string(),ready,GCR_DECODE(cur_live.e, cur_live.i));
				cur_live.ready = ready;
				syncpoint = true;
			}

			if (brdy != cur_live.brdy) {
				if (LOG_MORE) logerror("%s BRDY %u\n", cur_live.tm.as_string(), brdy);
				cur_live.brdy = brdy;
				syncpoint = true;
			}

			if (sync != cur_live.sync) {
				if (LOG) logerror("%s SYNC %u\n", cur_live.tm.as_string(), sync);
				cur_live.sync = sync;
				syncpoint = true;
			}

			if (error != cur_live.error) {
				if (LOG_MORE) logerror("%s ERROR %u\n", cur_live.tm.as_string(), error);
				cur_live.error = error;
				syncpoint = true;
			}

			if (syncpoint) {
				live_delay(RUNNING_SYNCPOINT);
				return;
			}
			break;
		}

		case RUNNING_SYNCPOINT: {
			m_write_ready(cur_live.ready);
			m_write_brdy(cur_live.brdy);
			m_write_sync(cur_live.sync);
			m_write_error(cur_live.error);

			cur_live.state = RUNNING;
			checkpoint();
			break;
		}
		}
	}
}

uint8_t c8050_fdc_device::read()
{
	uint8_t e = checkpoint_live.e;
	offs_t i = checkpoint_live.i;

	return GCR_DECODE(e, i);
}

void c8050_fdc_device::write(uint8_t data)
{
	if (LOG) logerror("%s %s PI %02x\n", machine().time().as_string(), machine().describe_context(), data);

	if (m_pi != data)
	{
		live_sync();
		m_pi = cur_live.pi = data;
		checkpoint();
		live_run();
	}
}

WRITE_LINE_MEMBER( c8050_fdc_device::ds0_w )
{
	m_ds0 = state;
}

WRITE_LINE_MEMBER( c8050_fdc_device::ds1_w )
{
	m_ds1 = state;

	ds_w(m_ds1 << 1 | m_ds0);
}

WRITE_LINE_MEMBER( c8050_fdc_device::drv_sel_w )
{
	if (m_drv_sel != state)
	{
		live_sync();
		m_drv_sel = cur_live.drv_sel = state;
		checkpoint();
		if (LOG) logerror("%s %s DRV SEL %u\n", machine().time().as_string(), machine().describe_context(), state);
		live_run();
	}
}

WRITE_LINE_MEMBER( c8050_fdc_device::mode_sel_w )
{
	if (m_mode_sel != state)
	{
		live_sync();
		m_mode_sel = cur_live.mode_sel = state;
		checkpoint();
		if (LOG) logerror("%s %s MODE SEL %u\n", machine().time().as_string(), machine().describe_context(), state);
		live_run();
	}
}

WRITE_LINE_MEMBER( c8050_fdc_device::rw_sel_w )
{
	if (m_rw_sel != state)
	{
		live_sync();
		m_rw_sel = cur_live.rw_sel = state;
		checkpoint();
		if (LOG) logerror("%s %s RW SEL %u\n", machine().time().as_string(), machine().describe_context(), state);
		if (m_rw_sel) {
			pll_stop_writing(get_floppy(), cur_live.tm);
		} else {
			pll_start_writing(cur_live.tm);
		}
		live_run();
	}
}

WRITE_LINE_MEMBER( c8050_fdc_device::mtr0_w )
{
	if (m_mtr0 != state)
	{
		live_sync();
		m_mtr0 = state;
		if (LOG) logerror("%s %s MTR0 %u\n", machine().time().as_string(), machine().describe_context(), state);
		m_floppy0->mon_w(state);
		checkpoint();

		if (!m_mtr0 || !m_mtr1) {
			if(cur_live.state == IDLE) {
				live_start();
			}
		} else {
			live_abort();
		}

		live_run();
	}
}

WRITE_LINE_MEMBER( c8050_fdc_device::mtr1_w )
{
	if (m_mtr1 != state)
	{
		live_sync();
		m_mtr1 = state;
		if (LOG) logerror("%s %s MTR1 %u\n", machine().time().as_string(), machine().describe_context(), state);
		if (m_floppy1) m_floppy1->mon_w(state);
		checkpoint();

		if (!m_mtr0 || !m_mtr1) {
			if(cur_live.state == IDLE) {
				live_start();
			}
		} else {
			live_abort();
		}

		live_run();
	}
}

WRITE_LINE_MEMBER( c8050_fdc_device::odd_hd_w )
{
	if (m_odd_hd != state)
	{
		live_sync();
		m_odd_hd = cur_live.odd_hd = state;
		if (LOG) logerror("%s %s ODD HD %u\n", machine().time().as_string(), machine().describe_context(), state);
		m_floppy0->ss_w(!state);
		if (m_floppy1) m_floppy1->ss_w(!state);
		checkpoint();
		live_run();
	}
}

WRITE_LINE_MEMBER( c8050_fdc_device::pull_sync_w )
{
	if (LOG_MORE) logerror("%s %s PULL SYNC %u\n", machine().time().as_string(), machine().describe_context(), state);
}
