// license:GPL-2.0+
// copyright-holders:Dirk Best, Olivier Galibert
/***************************************************************************

    VTech Laser/VZ Floppy Controller Cartridge

    Laser DD 20
    Dick Smith Electronics X-7304

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

#include "emu.h"
#include "floppy.h"

#include "formats/fs_vtech.h"
#include "formats/vt_dsk.h"

#include <algorithm>


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

DEFINE_DEVICE_TYPE(VTECH_FLOPPY_CONTROLLER, vtech_floppy_controller_device, "vtech_fdc", "Laser/VZ Floppy Disk Controller")

//-------------------------------------------------
//  mem_map - memory space address map
//-------------------------------------------------

void vtech_floppy_controller_device::mem_map(address_map &map)
{
	map.unmap_value_high();
	map(0x4000, 0x5fff).rom().region("software", 0);
}

//-------------------------------------------------
//  io_map - io space address map
//-------------------------------------------------

void vtech_floppy_controller_device::io_map(address_map &map)
{
	map.unmap_value_high();
	map(0x10, 0x10).w(FUNC(vtech_floppy_controller_device::latch_w));
	map(0x11, 0x11).r(FUNC(vtech_floppy_controller_device::shifter_r));
	map(0x12, 0x12).r(FUNC(vtech_floppy_controller_device::rd_r));
	map(0x13, 0x13).r(FUNC(vtech_floppy_controller_device::wpt_r));
}

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

ROM_START( floppy )
	ROM_REGION(0x2000, "software", 0)
	ROM_LOAD("vzdos.rom", 0x0000, 0x2000, CRC(b6ed6084) SHA1(59d1cbcfa6c5e1906a32704fbf0d9670f0d1fd8b))
ROM_END

const tiny_rom_entry *vtech_floppy_controller_device::device_rom_region() const
{
	return ROM_NAME( floppy );
}

//-------------------------------------------------
//  device_add_mconfig - add device configuration
//-------------------------------------------------

static void laser_floppies(device_slot_interface &device)
{
	device.option_add("525", FLOPPY_525_VTECH);
}

void vtech_floppy_controller_device::floppy_formats(format_registration &fr)
{
	fr.add(FLOPPY_VTECH_BIN_FORMAT);
	fr.add(FLOPPY_VTECH_DSK_FORMAT);
	fr.add(fs::VTECH);
}

void vtech_floppy_controller_device::device_add_mconfig(machine_config &config)
{
	vtech_memexp_device::device_add_mconfig(config);

	VTECH_MEMEXP_SLOT(config, m_memexp);
	m_memexp->set_memspace(m_mem, AS_PROGRAM);
	m_memexp->set_iospace(m_io, AS_PROGRAM);

	for (auto &floppy : m_floppy)
		FLOPPY_CONNECTOR(config, floppy, laser_floppies, "525", floppy_formats);
}


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

//-------------------------------------------------
//  vtech_floppy_controller_device - constructor
//-------------------------------------------------

vtech_floppy_controller_device::vtech_floppy_controller_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) :
	vtech_memexp_device(mconfig, VTECH_FLOPPY_CONTROLLER, tag, owner, clock),
	m_memexp(*this, "mem"),
	m_floppy(*this, "%u", 0U),
	m_selected_floppy(nullptr), m_latch(0), m_shifter(0), m_latching_inverter(false), m_current_cyl(0), m_write_position(0)
{
}

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

void vtech_floppy_controller_device::device_start()
{
	vtech_memexp_device::device_start();

	// register for save states
	save_item(NAME(m_latch));
	save_item(NAME(m_shifter));
	save_item(NAME(m_latching_inverter));
	save_item(NAME(m_current_cyl));
	save_item(NAME(m_last_latching_inverter_update_time));
	save_item(NAME(m_write_start_time));
	save_item(NAME(m_write_position));

	// TODO: save m_write_buffer and rebuild m_selected_floppy after load

	// Obvious bugs... must have worked by sheer luck and very subtle
	// timings.  Our current z80 is not subtle enough.
	uint8_t *rom = memregion("software")->base();
	rom[0x1678] = 0x75;
	rom[0x1688] = 0x85;
}

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

void vtech_floppy_controller_device::device_reset()
{
	m_latch = 0x00;
	m_selected_floppy = nullptr;
	m_current_cyl = 0;
	m_shifter = 0x00;
	m_latching_inverter = false;
	m_last_latching_inverter_update_time = machine().time();
	m_write_start_time = attotime::never;
	m_write_position = 0;
	std::fill(std::begin(m_write_buffer), std::end(m_write_buffer), attotime::zero);
}


//**************************************************************************
//  IMPLEMENTATION
//**************************************************************************

// latch at +0 is linked to:
//  bits 0-3: track step motor phases
//  bit  5:   write data (flux reversal on every level change)
//  bit  6:   !write request
//  bits 4,7: floppy select

void vtech_floppy_controller_device::latch_w(uint8_t data)
{
	uint8_t diff = m_latch ^ data;
	m_latch = data;

	floppy_image_device *newflop = nullptr;
	if(m_latch & 0x10)
		newflop = m_floppy[0]->get_device();
	else if(m_latch & 0x80)
		newflop = m_floppy[1]->get_device();

	if(newflop != m_selected_floppy) {
		update_latching_inverter();
		flush_writes();
		if(m_selected_floppy) {
			m_selected_floppy->mon_w(1);
			m_selected_floppy->setup_index_pulse_cb(floppy_image_device::index_pulse_cb());
		}
		if(newflop) {
			newflop->mon_w(0);
			newflop->setup_index_pulse_cb(floppy_image_device::index_pulse_cb(&vtech_floppy_controller_device::index_callback, this));
			m_current_cyl = newflop->get_cyl() << 1;
		}
		m_selected_floppy = newflop;
	}

	if(m_selected_floppy) {
		int cph = m_current_cyl & 3;
		int pcyl = m_current_cyl;
		if(!(m_latch & (1 << cph))) {
			if(m_current_cyl < 84*2 && (m_latch & (1 << ((cph+1) & 3))))
				m_current_cyl++;
			if(m_current_cyl && (m_latch & (1 << ((cph+3) & 3))))
				m_current_cyl--;
			if(m_current_cyl != pcyl && !(m_current_cyl & 1)) {
				m_selected_floppy->dir_w(m_current_cyl < pcyl);
				m_selected_floppy->stp_w(true);
				m_selected_floppy->stp_w(false);
				m_selected_floppy->stp_w(true);
			}
		}
	}

	if(diff & 0x40) {
		if(!(m_latch & 0x40)) {
			m_write_start_time = machine().time();
			m_write_position = 0;
			if(m_selected_floppy)
				m_selected_floppy->set_write_splice(m_write_start_time);

		} else {
			update_latching_inverter();
			flush_writes();
			m_write_start_time = attotime::never;
		}
	}
	if(!(m_latch & 0x40) && (diff & 0x20)) {
		if(m_write_position == std::size(m_write_buffer)) {
			update_latching_inverter();
			flush_writes(true);
		}
		m_write_buffer[m_write_position++] = machine().time();
	}
}


// The read data line is connected to a flip/flop with inverted input
// connected to the input.  That means it inverts its value on every
// floppy flux reversal.  We'll call it a latching inverter.
//
// The latching inverter is connected to a 8-bits shift register.  On
// reading the shifter address we get:
// - the inverted inverter output is shifted through the lsb of the shift register
// - the inverter is cleared

uint8_t vtech_floppy_controller_device::shifter_r()
{
	if (!machine().side_effects_disabled())
	{
		update_latching_inverter();
		m_shifter = (m_shifter << 1) | !m_latching_inverter;
		m_latching_inverter = false;
	}
	return m_shifter;
}


// Linked to the latching inverter on bit 7, rest is floating
uint8_t vtech_floppy_controller_device::rd_r()
{
	update_latching_inverter();
	return m_latching_inverter ? 0x80 : 0x00;
}


// Linked to wp signal on bit 7, rest is floating
uint8_t vtech_floppy_controller_device::wpt_r()
{
	return m_selected_floppy && m_selected_floppy->wpt_r() ? 0x80 : 0x00;
}

void vtech_floppy_controller_device::update_latching_inverter()
{
	attotime now = machine().time();
	if(!m_selected_floppy) {
		m_last_latching_inverter_update_time = now;
		return;
	}

	attotime when = m_last_latching_inverter_update_time;
	for(;;) {
		when = m_selected_floppy->get_next_transition(when);
		if(when == attotime::never || when > now)
			break;
		m_latching_inverter = !m_latching_inverter;
	}
	m_last_latching_inverter_update_time = now;
}

void vtech_floppy_controller_device::index_callback(floppy_image_device *floppy, int state)
{
	update_latching_inverter();
	flush_writes(true);
}

void vtech_floppy_controller_device::flush_writes(bool keep_margin)
{
	if(!m_selected_floppy || m_write_start_time == attotime::never)
		return;

	// Beware of time travel.  Index pulse callback (which flushes)
	// can be called with a machine().time() inferior to the last
	// m_write_buffer value if the calling cpu instructions are not
	// suspendable.

	attotime limit = machine().time();
	int kept_pos = m_write_position;
	int kept_count = 0;
	while(kept_pos > 0 && m_write_buffer[kept_pos-1] >= limit) {
		kept_pos--;
		kept_count++;
	}

	if(keep_margin) {
		attotime last = kept_pos ? m_write_buffer[kept_pos-1] : m_write_start_time;
		attotime delta = limit-last;
		delta = delta / 2;
		limit = limit - delta;
	}
	m_write_position -= kept_count;
	if(m_write_position && m_write_buffer[0] == m_write_start_time) {
		if(m_write_position)
			std::copy_n(m_write_buffer + 1, m_write_position - 1, m_write_buffer);
		m_write_position--;
	}
	m_selected_floppy->write_flux(m_write_start_time, limit, m_write_position, m_write_buffer);
	m_write_start_time = limit;

	if(kept_count != 0)
		std::copy_n(m_write_buffer + kept_pos, kept_count, m_write_buffer);
	m_write_position = kept_count;
}
