#include "SMXWiFi.hh"

#include "Connector.hh"
#include "EmuDuration.hh"
#include "MSXCliComm.hh"
#include "MSXException.hh"
#include "RS232Device.hh"
#include "RS232Raw.hh"
#include "Timer.hh"
#include "serialize.hh"

#include <array>
#include <cassert>
#include <chrono>
#include <functional>
#include <memory>

namespace openmsx {

static constexpr auto baudRates = std::to_array<unsigned>({
	859372, 346520, 231014, 115200, 57600, 38400, 31250, 19200, 9600, 4800
});

SMXWiFi::SMXWiFi(DeviceConfig& config)
	: MSXDevice(config)
	, RS232Connector(MSXDevice::getPluggingController(), "smxwifi")
	, rom(MSXDevice::getName() + " ROM", "rom", config)
	, cpu(getCPU())
	, quickRcvSetting(config.getCommandController(),
		"smxwifi-emulatedesp32-quickrcv-enabled",
		"Use quick receive (assert WAIT when the UART FIFO is empty)", true)
{
	emulated = std::make_unique<EmulatedEsp32>(
		config, [this](std::span<const uint8_t> bytes) { pushToFifo(bytes); });
}

SMXWiFi::~SMXWiFi() = default;

// ====================================================================
//  Memory-mapped ROM
// ====================================================================

uint8_t SMXWiFi::peekMem(uint16_t address, EmuTime /*time*/) const
{
	return *getReadCacheLine(address);
}

uint8_t SMXWiFi::readMem(uint16_t address, EmuTime time)
{
	return peekMem(address, time);
}

const uint8_t* SMXWiFi::getReadCacheLine(uint16_t start) const
{
	if (0x4000 <= start && start < 0x8000) {
		return &rom[start & 0x3FFF];
	}
	return unmappedRead.data();
}

// powerDown: stop the emulated ESP32 while the MSX is powered off — it
// runs as a host thread with real network sockets, and on the real
// device the ESP32 is powered from the MSX bus, so it powers down with
// it. The destructor only covers device removal; power cycles happen
// without destruction. On power-up the default MSXDevice::powerUp()
// calls reset(), which resets and restarts the emulation.
void SMXWiFi::powerDown(EmuTime /*time*/)
{
	emulated->stop();
}

void SMXWiFi::reset(EmuTime time)
{
	uartSpeed = 0; // 859372
	resetFIFO();
	emulated->resetState();
	emulated->start();
	updateEndpoint(time);
	if (isRealEndpoint()) {
		// No endpoint switch happened, but the UART is back to its
		// default baud rate: re-apply the configuration to the port.
		applyParamsToReal(time);
	}
}

uint8_t SMXWiFi::readIO(uint16_t port, EmuTime time)
{
	switch (port & 0x01) {
	case 0: // UART data
		return readFIFO(time);
	case 1: // UART status
		return readStatus();
	default:
		return 0xFF;
	}
}

uint8_t SMXWiFi::peekIO(uint16_t port, EmuTime /*time*/) const
{
	switch (port & 0x01) {
	case 0: // UART data
		return peekFIFO();
	case 1: // UART status
		return peekStatus();
	default:
		return 0xFF;
	}
}

void SMXWiFi::writeIO(uint16_t port, uint8_t value, EmuTime time)
{
	switch (port & 0x01) {
	case 0: // UART command
		writeCommand(value, time);
		break;
	case 1: // UART data
		updateEndpoint(time);
		if (isRealEndpoint()) {
			getPluggedRS232Dev().recvByte(value, time);
		} else {
			emulated->recvByte(value, time);
		}
		break;
	}
}

void SMXWiFi::plug(Pluggable& device, EmuTime time)
{
	Connector::plug(device, time);
	// Configure the port right away when an RS232Raw is plugged, so the
	// first byte is always sent at the current UART speed (baud) and
	// format (8N1): on Windows this reopens the port before any traffic
	// flows, on POSIX the open handle is reconfigured live.
	if (dynamic_cast<RS232Raw*>(&getPluggedRS232Dev())) {
		applyParamsToReal(time);
	}
	updateEndpoint(time);
}

void SMXWiFi::unplug(EmuTime time)
{
	Connector::unplug(time);
	updateEndpoint(time);
}

void SMXWiFi::setDataBits(DataBits /*bits*/)
{
	// The SM-X WiFi UART has a fixed format (8 data bits, 1 stop bit, no
	// parity): only the baud rate is configurable (via the command
	// register), so these are no-ops. They are still required by the
	// RS232Connector interface.
}

void SMXWiFi::setStopBits(StopBits /*bits*/)
{
	// The SM-X WiFi UART has a fixed format (8 data bits, 1 stop bit, no
	// parity): only the baud rate is configurable (via the command
	// register), so these are no-ops. They are still required by the
	// RS232Connector interface.
}

void SMXWiFi::setParityBit(bool /*enable*/, Parity /*parity*/)
{
	// The SM-X WiFi UART has a fixed format (8 data bits, 1 stop bit, no
	// parity): only the baud rate is configurable (via the command
	// register), so these are no-ops. They are still required by the
	// RS232Connector interface.
}

void SMXWiFi::recvByte(uint8_t value, EmuTime /*time*/)
{
	// Only bytes from the active endpoint enter the FIFO: while a
	// plugged-but-inactive device (e.g. RS232Tester, or an RS232Raw whose
	// port is not open) is attached, the emulated ESP32 is the active
	// endpoint and bytes from the real side are dropped.
	if (!isRealEndpoint()) return;
	pushToFifo(std::span(&value, 1));
}

void SMXWiFi::pushToFifo(std::span<const uint8_t> data)
{
	std::scoped_lock lock(fifoMutex);
	fifo.push_back_range(data);
}

bool SMXWiFi::ready()
{
	return true;
}

bool SMXWiFi::acceptsData()
{
	return true;
}

bool SMXWiFi::directByteDelivery() const
{
	return true;
}

// Emulated time to stall the CPU (in slices) before declaring a FIFO
// underrun, and the slice size while waiting.
//
// This mirrors the real SM-X WiFi hardware: on an IN of the data port
// while the UART FIFO is empty, the device asserts WAIT, stalling the
// Z80 until a byte arrives or a timeout (25 ms on the real device)
// elapses, after which 0xFF is returned and the underrun bit is set.
// The driver (ESPUNAPI.asm) reads the response header status-guarded and
// then fires a blind INI/INIR for the announced payload size, which
// always outruns the UART stream; it detects the resulting underrun
// (status bit 4) after the loop and recovers via the 'r' retry command,
// which re-sends the whole response. So underruns are harmless by design
// and never corrupt the received data.
//
// The stall is done with cpu.wait() in small emulated-time slices: only
// the CPU is stopped, the rest of the machine (VDP, sound, timers, ...)
// keeps running, so other devices' emulation is not affected. The slice
// must be no larger than the fastest UART char time (11.6 us @ 859372
// baud) because cpu.wait() cannot be woken early: an arriving byte is
// only noticed at the next slice boundary.
//
// FIFO_TIMEOUT is 50 ms: more than the real device's 25 ms, because in
// the emulated setup the producer chain goes through a host USB-serial
// converter (adapter latency timer, driver buffering and packing),
// which adds gaps the real device (direct 3.3 V serial, no converter)
// does not have. 50 ms was found to cover that extra latency and keep
// the driver's retry path quiet during normal operation.
static constexpr auto FIFO_TIMEOUT = EmuDuration::msec(50);
static constexpr auto FIFO_POLL = EmuDuration::usec(12);

uint8_t SMXWiFi::readFIFO(EmuTime time)
{
	// Pop one byte if available (mutex-protected).
	auto tryPop = [this](uint8_t& v) {
		std::scoped_lock lock(fifoMutex);
		if (fifo.empty()) return false;
		v = fifo.front();
		fifo.pop_front();
		return true;
	};
	uint8_t v;
	if (tryPop(v)) return v;
	if (!quickRcvSetting.getBoolean()) {
		// Quick receive disabled: no WAIT stall on an empty FIFO; the
		// driver is told (status bit 3) that quick receive is
		// unsupported and polls the status register instead.
		return 0xFF;
	}

	// FIFO empty: give the producer (the emulated ESP32 thread or a real
	// ESP over the serial port) a short chance to provide data by
	// stalling only the CPU in emulated-time slices — the rest of the
	// machine keeps running.
	//
	// cpu.wait() only advances emulated time; the real time the stall
	// actually takes can be much less (a real ESP32 over the serial
	// port, or the emulated ESP32 waiting on host network data, needs
	// wall-clock time to deliver). Sleep the shortfall so the stall
	// keeps up with the real clock; if timer granularity overshoots,
	// the next iteration detects it and the emulation catches up again.
	auto deadline = time + FIFO_TIMEOUT;
	auto emuStart = time;
	auto realStart = std::chrono::steady_clock::now();
	while (time < deadline) {
		cpu.wait(time + FIFO_POLL);
		time = getCurrentTime();
		if (tryPop(v)) return v;
		auto target = realStart + std::chrono::duration<double>(
			(time - emuStart).toDouble());
		if (auto shortfall = target - std::chrono::steady_clock::now();
		    shortfall > std::chrono::microseconds(0)) {
			Timer::sleep(std::chrono::duration_cast<
			             std::chrono::microseconds>(shortfall).count());
		}
	}

	underrun = true;
	getCliComm().printInfo(
		"SMXWiFi: FIFO underrun: no data received within 50 ms");
	return 0xFF;
}

uint8_t SMXWiFi::peekFIFO() const
{
	std::scoped_lock lock(fifoMutex);
	return fifo.empty() ? 0xFF : fifo.front();
}

uint8_t SMXWiFi::readStatus()
{
	std::scoped_lock lock(fifoMutex);
	uint8_t status = peekStatusLocked();
	underrun = false;
	return status;
}

uint8_t SMXWiFi::peekStatusLocked() const
{
	uint8_t status = 0;
	if (!fifo.empty()) status |= 0x01;
	if (quickRcvSetting.getBoolean()) status |= 0x08; // quick receive
	if (underrun) status |= 0x10;
	return status;
}

uint8_t SMXWiFi::peekStatus() const
{
	std::scoped_lock lock(fifoMutex);
	return peekStatusLocked();
}

void SMXWiFi::writeCommand(uint8_t value, EmuTime time)
{
	updateEndpoint(time);
	if (value <= 9) {
		uartSpeed = value;
		if (isRealEndpoint()) {
			getPluggedRS232Dev().setBaudRate(baudRates[value]);
		} else {
			emulated->setBaudRate(baudRates[value]);
		}
	} else if (value == 20) {
		resetFIFO();
		emulated->resetFifo();
	}
}

void SMXWiFi::resetFIFO()
{
	std::scoped_lock lock(fifoMutex);
	fifo.clear();
	underrun = false;
}

// The UART talks to the real ESP32 only when the connector holds an
// RS232Raw whose host serial port is actually open; otherwise the
// emulated ESP32 is the active endpoint: nothing is plugged, a
// non-RS232Raw device is plugged, or the port could not be opened (no
// port selected, port in use, ...).
bool SMXWiFi::isRealEndpoint() const
{
	auto* raw = dynamic_cast<const RS232Raw*>(&getPluggedRS232Dev());
	return raw && raw->isOpen();
}

// Re-evaluates the active endpoint and performs the switch when it
// changed: stale bytes are flushed, the emulated ESP32 is reset (it
// always starts clean after a migration) and, when switching to the real
// endpoint, the current UART configuration is pushed to the serial device
// (the RS232Raw port may have opened with its own defaults).
void SMXWiFi::updateEndpoint(EmuTime time)
{
	bool real = isRealEndpoint();
	if (real == realEndpointActive) return;
	realEndpointActive = real;
	resetFIFO();
	emulated->resetState();
	if (real) {
		applyParamsToReal(time);
	}
}

// Pushes the current UART configuration to the plugged device: the baud
// rate (configurable via the command register) and the fixed format
// 8N1 with DTR/RTS deasserted.
void SMXWiFi::applyParamsToReal(EmuTime time) const
{
	auto& dev = getPluggedRS232Dev();
	dev.setBaudRate(baudRates[uartSpeed]);
	dev.setDataBits(DataBits::D8);
	dev.setStopBits(StopBits::S1);
	dev.setParityBit(false, Parity::EVEN);
	dev.setDTR(false, time);
	dev.setRTS(false, time);
}

template<typename Archive>
void SMXWiFi::serialize(Archive& ar, unsigned /*version*/)
{
	ar.template serializeBase<MSXDevice>(*this);
	ar.template serializeBase<RS232Connector>(*this);
	ar.serialize("uartSpeed", uartSpeed);
	if constexpr (Archive::IS_LOADER) {
		resetFIFO();
		emulated->resetState();
		updateEndpoint(EmuTime::zero());
	}
}
INSTANTIATE_SERIALIZE_METHODS(SMXWiFi);
REGISTER_MSXDEVICE(SMXWiFi, "SMXWiFi");

} // namespace openmsx