#include "MSXCPU.hh"

#include "CPUCore.hh"
#include "MSXCPUInterface.hh"
#include "R800.hh"
#include "Z80.hh"

#include "Debugger.hh"
#include "IntegerSetting.hh"
#include "MSXMotherBoard.hh"
#include "Scheduler.hh"
#include "TclObject.hh"
#include "serialize.hh"

#include "outer.hh"
#include "ranges.hh"
#include "unreachable.hh"
#include "xrange.hh"

#include <algorithm>
#include <cassert>
#include <memory>

namespace openmsx {

MSXCPU::MSXCPU(MSXMotherBoard& motherboard_)
	: motherboard(motherboard_)
	, diHaltCallback(
		motherboard.getCommandController(), "di_halt_callback",
		"Tcl proc called when the CPU executed a DI/HALT sequence",
		"default_di_halt_callback",
		Setting::Save::YES) // user must be able to override
	, z80(std::make_unique<CPUCore<Z80TYPE>>(
		motherboard, "z80", diHaltCallback, EmuTime::zero()))
	, r800(motherboard.isTurboR()
		? std::make_unique<CPUCore<R800TYPE>>(
			motherboard, "r800", diHaltCallback, EmuTime::zero())
		: nullptr)
	, timeInfo(motherboard.getMachineInfoCommand())
	, z80FreqInfo(motherboard.getMachineInfoCommand(), "z80_freq", *z80)
	, r800FreqInfo(r800
		? std::make_unique<CPUFreqInfoTopic>(
			motherboard.getMachineInfoCommand(), "r800_freq", *r800)
		: nullptr)
	, debuggable(motherboard_)
{
	motherboard.getDebugger().setCPU(this);
	motherboard.getScheduler().setCPU(this);

	z80->freqLocked.attach(*this);
	z80->freqValue.attach(*this);
	if (r800) {
		r800->freqLocked.attach(*this);
		r800->freqValue.attach(*this);
	}
	invalidateMemCacheSlot();
}

MSXCPU::~MSXCPU()
{
	z80->freqLocked.detach(*this);
	z80->freqValue.detach(*this);
	if (r800) {
		r800->freqLocked.detach(*this);
		r800->freqValue.detach(*this);
	}
	motherboard.getScheduler().setCPU(nullptr);
	motherboard.getDebugger() .setCPU(nullptr);
}

void MSXCPU::setInterface(MSXCPUInterface* interface_)
{
	interface = interface_;
	          z80 ->setInterface(interface);
	if (r800) r800->setInterface(interface);
}

void MSXCPU::doReset(EmuTime time)
{
	          z80 ->doReset(time);
	if (r800) r800->doReset(time);

	invalidateAllSlotsRWCache(0x0000, 0x10000);
}

void MSXCPU::setActiveCPU(Type cpu)
{
	if (cpu == Type::R800) assert(r800);

	bool tmp = cpu == Type::Z80;
	if (tmp != z80Active) {
		exitCPULoopSync();
		newZ80Active = tmp;
	}
}

void MSXCPU::setDRAMmode(bool dram)
{
	assert(r800);
	r800->setDRAMmode(dram);
}

void MSXCPU::execute(bool fastForward)
{
	if (z80Active != newZ80Active) {
		EmuTime time = getCurrentTime();
		z80Active = newZ80Active;
		z80Active ? z80 ->warp(time)
		          : r800->warp(time);
		// copy Z80/R800 cache lines
		auto zCache = z80 ->getCacheLines();
		auto rCache = r800->getCacheLines();
		auto from = z80Active ? rCache : zCache;
		auto to   = z80Active ? zCache : rCache;
		copy_to_range(from.read,  to.read);
		copy_to_range(from.write, to.write);
	}
	z80Active ? z80 ->execute(fastForward)
	          : r800->execute(fastForward);
}

void MSXCPU::exitCPULoopSync()
{
	z80Active ? z80 ->exitCPULoopSync()
	          : r800->exitCPULoopSync();
}
void MSXCPU::exitCPULoopAsync()
{
	z80Active ? z80 ->exitCPULoopAsync()
	          : r800->exitCPULoopAsync();
}

EmuTime MSXCPU::getCurrentTime() const
{
	return z80Active ? z80 ->getCurrentTime()
	                 : r800->getCurrentTime();
}

void MSXCPU::setNextSyncPoint(EmuTime time)
{
	z80Active ? z80 ->setNextSyncPoint(time)
	          : r800->setNextSyncPoint(time);
}

void MSXCPU::invalidateMemCacheSlot()
{
	if (interface) {
		for (auto i : xrange(4)) {
			slots[i] = 4 * interface->getPrimarySlot(i)
			             + interface->getSecondarySlot(i);
		}
	} else {
		std::ranges::fill(slots, 0);
	}

	// nullptr: means not a valid entry and not yet attempted to fill this entry
	for (auto i : xrange(16)) {
		std::ranges::fill(slotReadLines[i], nullptr);
		std::ranges::fill(slotWriteLines[i], nullptr);
	}
}

void MSXCPU::updateVisiblePage(uint8_t page, uint8_t primarySlot, uint8_t secondarySlot)
{
	assert(primarySlot < 4);
	assert(secondarySlot < 4);

	auto from = slots[page];
	auto to = narrow<uint8_t>(4 * primarySlot + secondarySlot);
	slots[page] = to;

	auto [cpuReadLines, cpuWriteLines] = z80Active ? z80->getCacheLines() : r800->getCacheLines();

	unsigned first = page * (0x4000 / CacheLine::SIZE);
	unsigned num = 0x4000 / CacheLine::SIZE;
	std::copy_n(&cpuReadLines      [first], num, &slotReadLines [from][first]);
	std::copy_n(&slotReadLines [to][first], num, &cpuReadLines        [first]);
	std::copy_n(&cpuWriteLines     [first], num, &slotWriteLines[from][first]);
	std::copy_n(&slotWriteLines[to][first], num, &cpuWriteLines       [first]);

	if (r800) r800->updateVisiblePage(page, primarySlot, secondarySlot);
}

void MSXCPU::invalidateAllSlotsRWCache(uint16_t start, unsigned size)
{
	if (interface) interface->tick(CacheLineCounters::InvalidateAllSlots);
	auto [cpuReadLines, cpuWriteLines] = z80Active ? z80->getCacheLines() : r800->getCacheLines();

	unsigned first = start / CacheLine::SIZE;
	unsigned num = (size + CacheLine::SIZE - 1) / CacheLine::SIZE;
	std::ranges::fill(subspan(cpuReadLines,  first, num), nullptr); // nullptr: means not a valid entry and not
	std::ranges::fill(subspan(cpuWriteLines, first, num), nullptr); //   yet attempted to fill this entry

	for (auto i : xrange(16)) {
		std::ranges::fill(subspan(slotReadLines [i], first, num), nullptr);
		std::ranges::fill(subspan(slotWriteLines[i], first, num), nullptr);
	}
}

template<bool READ, bool WRITE, bool SUB_START>
void MSXCPU::setRWCache(unsigned start, unsigned size, const uint8_t* rData, uint8_t* wData, int ps, int ss,
                        std::span<const uint8_t, 256> disallowRead,
                        std::span<const uint8_t, 256> disallowWrite)
{
	if constexpr (!SUB_START) {
		assert(rData == nullptr);
		assert(wData == nullptr);
	}

	// aligned on cache lines
	assert((start & CacheLine::LOW) == 0);
	assert((size  & CacheLine::LOW) == 0);

	unsigned slot = 4 * ps + ss;
	unsigned page = start >> 14;
	assert(((start + size - 1) >> 14) == page); // all in same page
	if constexpr (SUB_START && READ)  rData -= start;
	if constexpr (SUB_START && WRITE) wData -= start;

	// select between 'active' or 'shadow' cache lines
	auto [readLines, writeLines] = [&] {
		if (slot == slots[page]) {
			return z80Active ? z80->getCacheLines() : r800->getCacheLines();
		} else {
			return CacheLines{.read  = slotReadLines [slot],
			                  .write = slotWriteLines[slot]};
		}
	}();

	unsigned first = start / CacheLine::SIZE;
	unsigned num = size / CacheLine::SIZE;

	static auto* const NON_CACHEABLE = std::bit_cast<uint8_t*>(uintptr_t(1));
	for (auto i : xrange(num)) {
		if constexpr (READ)  readLines [first + i] = disallowRead [first + i] ? NON_CACHEABLE : rData;
		if constexpr (WRITE) writeLines[first + i] = disallowWrite[first + i] ? NON_CACHEABLE : wData;
	}
}

static constexpr void extendForAlignment(unsigned& start, unsigned& size)
{
	constexpr unsigned MASK = ~CacheLine::LOW; // not CacheLine::HIGH because 0x0000ff00 != 0xffffff00

	auto end = start + size;
	start &= MASK;                            // round down to cacheline
	end = (end + CacheLine::SIZE - 1) & MASK; // round up to cacheline
	size = end - start;
}

void MSXCPU::invalidateRWCache(unsigned start, unsigned size, int ps, int ss,
                               std::span<const uint8_t, 256> disallowRead,
                               std::span<const uint8_t, 256> disallowWrite)
{
	// unaligned [start, start+size) is OK for invalidate, then simply invalidate a little more.
	extendForAlignment(start, size);
	setRWCache<true, true, false>(start, size, nullptr, nullptr, ps, ss, disallowRead, disallowWrite);
}
void MSXCPU::invalidateRCache(unsigned start, unsigned size, int ps, int ss,
                              std::span<const uint8_t, 256> disallowRead,
                              std::span<const uint8_t, 256> disallowWrite)
{
	extendForAlignment(start, size);
	setRWCache<true, false, false>(start, size, nullptr, nullptr, ps, ss, disallowRead, disallowWrite);
}
void MSXCPU::invalidateWCache(unsigned start, unsigned size, int ps, int ss,
                              std::span<const uint8_t, 256> disallowRead,
                              std::span<const uint8_t, 256> disallowWrite)
{
	extendForAlignment(start, size);
	setRWCache<false, true, false>(start, size, nullptr, nullptr, ps, ss, disallowRead, disallowWrite);
}

void MSXCPU::fillRWCache(unsigned start, unsigned size, const uint8_t* rData, uint8_t* wData, int ps, int ss,
                         std::span<const uint8_t, 256> disallowRead,
                         std::span<const uint8_t, 256> disallowWrite)
{
	setRWCache<true, true, true>(start, size, rData, wData, ps, ss, disallowRead, disallowWrite);
}
void MSXCPU::fillRCache(unsigned start, unsigned size, const uint8_t* rData, int ps, int ss,
                        std::span<const uint8_t, 256> disallowRead,
                        std::span<const uint8_t, 256> disallowWrite)
{
	setRWCache<true, false, true>(start, size, rData, nullptr, ps, ss, disallowRead, disallowWrite);
}
void MSXCPU::fillWCache(unsigned start, unsigned size, uint8_t* wData, int ps, int ss,
                        std::span<const uint8_t, 256> disallowRead,
                        std::span<const uint8_t, 256> disallowWrite)
{
	setRWCache<false, true, true>(start, size, nullptr, wData, ps, ss, disallowRead, disallowWrite);
}

void MSXCPU::raiseIRQ()
{
	          z80 ->raiseIRQ();
	if (r800) r800->raiseIRQ();
}
void MSXCPU::lowerIRQ()
{
	          z80 ->lowerIRQ();
	if (r800) r800->lowerIRQ();
}
void MSXCPU::raiseNMI()
{
	          z80 ->raiseNMI();
	if (r800) r800->raiseNMI();
}
void MSXCPU::lowerNMI()
{
	          z80 ->lowerNMI();
	if (r800) r800->lowerNMI();
}

bool MSXCPU::isM1Cycle(unsigned address) const
{
	return z80Active ? z80 ->isM1Cycle(address)
	                 : r800->isM1Cycle(address);
}

void MSXCPU::setZ80Freq(unsigned freq)
{
	z80->setFreq(freq);
}

void MSXCPU::wait(EmuTime time)
{
	z80Active ? z80 ->wait(time)
	          : r800->wait(time);
}

EmuTime MSXCPU::waitCyclesZ80(EmuTime time, unsigned cycles)
{
	return z80Active ? z80 ->waitCycles(time, cycles)
	                 : time;
}

EmuTime MSXCPU::waitCyclesR800(EmuTime time, unsigned cycles)
{
	return z80Active ? time
	                 : r800->waitCycles(time, cycles);
}

CPURegs& MSXCPU::getRegisters()
{
	if (z80Active) {
		return *z80;
	} else {
		return *r800;
	}
}

void MSXCPU::update(const Setting& setting) noexcept
{
	          z80 ->update(setting);
	if (r800) r800->update(setting);
	exitCPULoopSync();
}

void MSXCPU::setPaused(bool paused)
{
	if (z80Active) {
		z80 ->setExtHALT(paused);
		z80 ->exitCPULoopSync();
	} else {
		r800->setExtHALT(paused);
		r800->exitCPULoopSync();
	}
}


// class TimeInfoTopic

MSXCPU::TimeInfoTopic::TimeInfoTopic(InfoCommand& machineInfoCommand)
	: InfoTopic(machineInfoCommand, "time")
{
}

void MSXCPU::TimeInfoTopic::execute(
	std::span<const TclObject> /*tokens*/, TclObject& result) const
{
	const auto& cpu = OUTER(MSXCPU, timeInfo);
	result = cpu.getCurrentTime().toDouble();
}

std::string MSXCPU::TimeInfoTopic::help(std::span<const TclObject> /*tokens*/) const
{
	return "Prints the time in seconds that the MSX is powered on\n";
}


// class CPUFreqInfoTopic

MSXCPU::CPUFreqInfoTopic::CPUFreqInfoTopic(
		InfoCommand& machineInfoCommand,
		const std::string& name_, CPUClock& clock_)
	: InfoTopic(machineInfoCommand, name_)
	, clock(clock_)
{
}

void MSXCPU::CPUFreqInfoTopic::execute(
	std::span<const TclObject> /*tokens*/, TclObject& result) const
{
	result = clock.getFreq();
}

std::string MSXCPU::CPUFreqInfoTopic::help(std::span<const TclObject> /*tokens*/) const
{
	return "Returns the actual frequency of this CPU.\n"
	       "This frequency can vary because:\n"
	       " - the user has overridden the freq via the '{z80,r800}_freq' setting\n"
	       " - (only on some MSX machines) the MSX software can switch the Z80 between 2 frequencies\n"
	       "See also the '{z80,r800}_freq_locked' setting.\n";
}


// class Debuggable

static constexpr static_string_view CPU_REGS_DESC =
	"Registers of the active CPU (Z80 or R800).\n"
	"Each byte in this debuggable represents one 8 bit register:\n"
	"  0 ->  A      1 ->  F      2 -> B       3 -> C\n"
	"  4 ->  D      5 ->  E      6 -> H       7 -> L\n"
	"  8 ->  A'     9 ->  F'    10 -> B'     11 -> C'\n"
	" 12 ->  D'    13 ->  E'    14 -> H'     15 -> L'\n"
	" 16 -> IXH    17 -> IXL    18 -> IYH    19 -> IYL\n"
	" 20 -> PCH    21 -> PCL    22 -> SPH    23 -> SPL\n"
	" 24 ->  I     25 ->  R     26 -> IM     27 -> IFF1/2\n"
	"The last position (27) contains the IFF1 and IFF2 flags in respectively\n"
	"bit 0 and 1. Bit 2 contains 'IFF1 AND last-instruction-was-not-EI', so\n"
	"this effectively indicates that the CPU could accept an interrupt at\n"
	"the start of the current instruction.\n";

MSXCPU::Debuggable::Debuggable(MSXMotherBoard& motherboard_)
	: SimpleDebuggable(motherboard_, "CPU regs", CPU_REGS_DESC, 28)
{
}

uint8_t MSXCPU::Debuggable::read(unsigned address)
{
	auto& cpu = OUTER(MSXCPU, debuggable);
	const CPURegs& regs = cpu.getRegisters();
	switch (address) {
	case  0: return regs.getA();
	case  1: return regs.getF();
	case  2: return regs.getB();
	case  3: return regs.getC();
	case  4: return regs.getD();
	case  5: return regs.getE();
	case  6: return regs.getH();
	case  7: return regs.getL();
	case  8: return regs.getA2();
	case  9: return regs.getF2();
	case 10: return regs.getB2();
	case 11: return regs.getC2();
	case 12: return regs.getD2();
	case 13: return regs.getE2();
	case 14: return regs.getH2();
	case 15: return regs.getL2();
	case 16: return regs.getIXh();
	case 17: return regs.getIXl();
	case 18: return regs.getIYh();
	case 19: return regs.getIYl();
	case 20: return regs.getPCh();
	case 21: return regs.getPCl();
	case 22: return regs.getSPh();
	case 23: return regs.getSPl();
	case 24: return regs.getI();
	case 25: return regs.getR();
	case 26: return regs.getIM();
	case 27: return uint8_t(1 *  regs.getIFF1() +
	                        2 *  regs.getIFF2() +
	                        4 * (regs.getIFF1() && !regs.prevWasEI()));
	default: UNREACHABLE;
	}
}

void MSXCPU::Debuggable::write(unsigned address, uint8_t value)
{
	auto& cpu = OUTER(MSXCPU, debuggable);
	CPURegs& regs = cpu.getRegisters();
	switch (address) {
	case  0: regs.setA(value); break;
	case  1: regs.setF(value); break;
	case  2: regs.setB(value); break;
	case  3: regs.setC(value); break;
	case  4: regs.setD(value); break;
	case  5: regs.setE(value); break;
	case  6: regs.setH(value); break;
	case  7: regs.setL(value); break;
	case  8: regs.setA2(value); break;
	case  9: regs.setF2(value); break;
	case 10: regs.setB2(value); break;
	case 11: regs.setC2(value); break;
	case 12: regs.setD2(value); break;
	case 13: regs.setE2(value); break;
	case 14: regs.setH2(value); break;
	case 15: regs.setL2(value); break;
	case 16: regs.setIXh(value); break;
	case 17: regs.setIXl(value); break;
	case 18: regs.setIYh(value); break;
	case 19: regs.setIYl(value); break;
	case 20: regs.setPCh(value); break;
	case 21: regs.setPCl(value); break;
	case 22: regs.setSPh(value); break;
	case 23: regs.setSPl(value); break;
	case 24: regs.setI(value); break;
	case 25: regs.setR(value); break;
	case 26:
		if (value < 3) regs.setIM(value);
		break;
	case 27:
		regs.setIFF1((value & 0x01) != 0);
		regs.setIFF2((value & 0x02) != 0);
		// can't change afterEI
		break;
	default:
		UNREACHABLE;
	}
}

// version 1: initial version
// version 2: activeCPU,newCPU -> z80Active,newZ80Active
// version 3: removed resetTime
template<typename Archive>
void MSXCPU::serialize(Archive& ar, unsigned version)
{
	if (ar.versionAtLeast(version, 2)) {
		          ar.serialize("z80",  *z80);
		if (r800) ar.serialize("r800", *r800);
		ar.serialize("z80Active",    z80Active,
		             "newZ80Active", newZ80Active);
	} else {
		// backwards-compatibility
		assert(Archive::IS_LOADER);

		          ar.serializeWithID("z80",  *z80);
		if (r800) ar.serializeWithID("r800", *r800);
		CPUBase* activeCPU = nullptr;
		CPUBase* newCPU = nullptr;
		ar.serializePointerID("activeCPU", activeCPU);
		ar.serializePointerID("newCPU",    newCPU);
		z80Active = activeCPU == z80.get();
		if (newCPU) {
			newZ80Active = newCPU == z80.get();
		} else {
			newZ80Active = z80Active;
		}
	}

	if constexpr (Archive::IS_LOADER) {
		invalidateMemCacheSlot();
		invalidateAllSlotsRWCache(0x0000, 0x10000);
	}
}
INSTANTIATE_SERIALIZE_METHODS(MSXCPU);

} // namespace openmsx
