#include "YamahaSKW01.hh"

#include "CacheLine.hh"
#include "DummyPrinterPortDevice.hh"
#include "MSXException.hh"
#include "serialize.hh"

#include "checked_cast.hh"

// Implementation based on reverse-engineering of 'acet'/'uniabis', see:
//   https://www.msx.org/forum/msx-talk/hardware/trying-to-dump-yamaha-skw-01
//   https://github.com/uniabis/msxrawl/tree/main/dumpskw1
// The printer port is assumed to work the same as a standard MSX printer port.
// This seems to be OK, because when printing with the SKW-01 software and
// selecting the "HR-5X" printer and an emulated MSX Printer plugged in, sane
// output is produced.
// As the emulation is purely based on reverse engineering based on the built
// in software, there may be plenty of details wrong.
// Also note that the delay that is apparently present when writing the font
// address registers (other wise the READY bit wouldn't need to be read out) is
// not emulated. Because of that, the READY bit will always just be enabled.

// Addendum:
// It looks like the SKW-05-like device built in the Yamaha YIS-805/256 is very
// similar, although it does not have a printer port of course and also no font
// ROM. And the main ROM is 48kB instead. We don't know about the real SKW-05,
// so let's not make any assumptions on that, but the stuff in the YIS-805/256
// we do know about and that part is emulated, to a similar level as the
// SKW-01.

namespace openmsx {

// Printer port part of the SKW-01, as it needs to be optional in that class for the National CF-SM003

YamahaSKW01PrinterPort::YamahaSKW01PrinterPort(PluggingController& pluggingController_, const std::string& name_)
	: Connector(pluggingController_, name_, std::make_unique<DummyPrinterPortDevice>())
{
	reset(EmuTime::dummy());
}

void YamahaSKW01PrinterPort::reset(EmuTime time)
{
	writeData(0, time);    // TODO check this, see MSXPrinterPort
	setStrobe(true, time); // TODO check this, see MSXPrinterPort
}

bool YamahaSKW01PrinterPort::getStatus(EmuTime time) const
{
	return getPluggedPrintDev().getStatus(time);
}

void YamahaSKW01PrinterPort::setStrobe(bool newStrobe, EmuTime time)
{
	if (newStrobe != strobe) {
		strobe = newStrobe;
		getPluggedPrintDev().setStrobe(strobe, time);
	}
}

void YamahaSKW01PrinterPort::writeData(uint8_t newData, EmuTime time)
{
	if (newData != data) {
		data = newData;
		getPluggedPrintDev().writeData(data, time);
	}
}

zstring_view YamahaSKW01PrinterPort::getDescription() const
{
	return "Yamaha SKW-01 printer port";
}

zstring_view YamahaSKW01PrinterPort::getClass() const
{
	return "Printer Port";
}

void YamahaSKW01PrinterPort::plug(Pluggable& dev, EmuTime time)
{
	Connector::plug(dev, time);
	getPluggedPrintDev().writeData(data, time);
	getPluggedPrintDev().setStrobe(strobe, time);
}

PrinterPortDevice& YamahaSKW01PrinterPort::getPluggedPrintDev() const
{
	return *checked_cast<PrinterPortDevice*>(&getPlugged());
}

template<typename Archive>
void YamahaSKW01PrinterPort::serialize(Archive& ar, unsigned /*version*/)
{
	ar.template serializeBase<Connector>(*this);
	ar.serialize("strobe"     , strobe,
	             "data"       , data);
	// TODO force writing data to port?? (See MSXPrinterPort)
}

YamahaSKW01::YamahaSKW01(DeviceConfig& config)
	: MSXDevice(config)
	, mainRom(MSXDevice::getName() + " main"     , "rom", config, "main")
	, fontRom(config.getXML()->findChildWithAttribute("rom", "id", "kanjifont") ?
		 std::make_optional<Rom>(MSXDevice::getName() + " kanjifont", "rom", config, "kanjifont") : std::nullopt)
	, dataRom(MSXDevice::getName() + " data"     , "rom", config, "data")
	, sram(MSXDevice::getName() + " SRAM", 0x800, config)
{
	if (config.getChildDataAsBool("hasprinterport", true)) {
		printerPort.emplace(getPluggingController(), getName() + " printerport");
	}

	if (mainRom.size() != one_of(32 * 1024u, 48 * 1024u)) {
		throw MSXException("Main ROM must be exactly 32kB or 48kB in size.");
	}
	if (fontRom && fontRom->size() != 128 * 1024) {
		throw MSXException("Font ROM must be exactly 128kB in size.");
	}
	if (dataRom.size() != 32 * 1024) {
		throw MSXException("Data ROM must be exactly 32kB in size.");
	}

	reset(EmuTime::dummy());
}

void YamahaSKW01::reset(EmuTime time)
{
	fontAddress = {0, 0, 0, 0};
	dataAddress = 0;

	if (printerPort) printerPort->reset(time);
}

byte YamahaSKW01::readMem(uint16_t address, EmuTime time)
{
	return peekMem(address, time);
}

byte YamahaSKW01::peekMem(uint16_t address, EmuTime time) const
{
	if (address == one_of(0x7FC0, 0x7FC2, 0x7FC4, 0x7FC6)) {
		return fontRom ? 0x01 /* for now, always READY to read */ : 0x00;
	} else if (address == one_of(0x7FC1, 0x7FC3, 0x7FC5, 0x7FC7)) {
		if (!fontRom) return 0x00;
		unsigned group = (address - 0x7FC1) / 2;
		unsigned base = 0x8000 * group;
		unsigned offset = fontAddress[group] & 0x7FFF;
		return (*fontRom)[base + offset];
	} else if (address == 0x7FC8 || address == 0x7FC9) {
		return 0xFF;
	} else if (address == 0x7FCA || address == 0x7FCB) {
		if ((dataAddress & (1 << 15)) == 0) {
			return dataRom[dataAddress & 0x7FFF];
		} else {
			return sram[dataAddress & 0x7FF];
		}
	} else if (address == 0x7FCC && printerPort) {
		// bit 1 = status / other bits always 1
		return printerPort->getStatus(time)
		       ? 0xFF : 0xFD;
	} else if (address < mainRom.size()) {
		return mainRom[address];
	} else {
		return 0xFF;
	}
}

void YamahaSKW01::writeMem(uint16_t address, byte value, EmuTime time)
{
	if (0x7FC0 <= address && address <= 0x7FC7) {
		unsigned group = (address - 0x7FC0) / 2;
		if ((address & 1) == 0) {
			// LSB address
			fontAddress[group] = (fontAddress[group] & 0xFF00) | uint16_t(value << 0);
		} else {
			// MSB address
			fontAddress[group] = (fontAddress[group] & 0x00FF) | uint16_t(value << 8);
		}
	} else if (address == 0x7FC8) {
		dataAddress = (dataAddress & 0xFF00) | uint16_t(value << 0);
	} else if (address == 0x7FC9) {
		dataAddress = (dataAddress & 0x00FF) | uint16_t(value << 8);
	} else if (address == 0x7FCA || address == 0x7FCB) {
		if ((dataAddress & (1 << 15)) != 0) {
			sram.write(dataAddress & 0x7FF, value);
		}
	} else if (address == 0x7FCC && printerPort) {
		printerPort->setStrobe(value & 1, time);
	} else if (address == 0x7FCE && printerPort) {
		printerPort->writeData(value, time);
	}
}

const byte* YamahaSKW01::getReadCacheLine(uint16_t start) const
{
	if ((start & CacheLine::HIGH) == (0x7FC0 & CacheLine::HIGH)) {
		// 0x7FC0-0x7FCF memory mapped registers
		return nullptr; // not cacheable
	} else if (start < mainRom.size()) {
		return &mainRom[start];
	} else {
		return unmappedRead.data();
	}
}

byte* YamahaSKW01::getWriteCacheLine(uint16_t start)
{
	if ((start & CacheLine::HIGH) == (0x7FC0 & CacheLine::HIGH)) {
		// 0x7FC0-0x7FCF memory mapped registers
		return nullptr; // not cacheable
	} else {
		return unmappedWrite.data();
	}
}

template<typename Archive>
void YamahaSKW01::serialize(Archive& ar, unsigned version)
{
	ar.template serializeBase<MSXDevice>(*this);
	ar.serialize("fontAddress", fontAddress,
	             "dataAddress", dataAddress,
	             "SRAM"       , sram);
	if (printerPort) {
	             printerPort->serialize(ar, version);
	}
}
INSTANTIATE_SERIALIZE_METHODS(YamahaSKW01);
REGISTER_MSXDEVICE(YamahaSKW01, "YamahaSKW01");

} // namespace openmsx
