//	Altirra - Atari 800/800XL/5200 emulator
//	Copyright (C) 2009-2016 Avery Lee
//
//	This program is free software; you can redistribute it and/or modify
//	it under the terms of the GNU General Public License as published by
//	the Free Software Foundation; either version 2 of the License, or
//	(at your option) any later version.
//
//	This program is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//	GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License
//	along with this program; if not, write to the Free Software
//	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

#include <stdafx.h>
#include <vd2/system/hash.h>
#include <vd2/system/int128.h>
#include <vd2/system/math.h>
#include <at/atcore/audiosource.h>
#include <at/atcore/logging.h>
#include <at/atcore/propertyset.h>
#include <at/atcore/deviceserial.h>
#include "audiosyncmixer.h"
#include "diskdrivefull.h"
#include "memorymanager.h"
#include "firmwaremanager.h"
#include "debuggerlog.h"

ATLogChannel g_ATLCDiskEmu(true, false, "DISKEMU", "Disk drive emulation");

void ATCreateDeviceDiskDrive810(const ATPropertySet& pset, IATDevice **dev) {
	vdrefptr<ATDeviceDiskDriveFull> p(new ATDeviceDiskDriveFull(false, false));
	p->SetSettings(pset);

	*dev = p.release();
}

void ATCreateDeviceDiskDrive810Archiver(const ATPropertySet& pset, IATDevice **dev) {
	vdrefptr<ATDeviceDiskDriveFull> p(new ATDeviceDiskDriveFull(false, true));
	p->SetSettings(pset);

	*dev = p.release();
}

void ATCreateDeviceDiskDriveHappy810(const ATPropertySet& pset, IATDevice **dev) {
	vdrefptr<ATDeviceDiskDriveFull> p(new ATDeviceDiskDriveFull(true, false));
	p->SetSettings(pset);

	*dev = p.release();
}

extern const ATDeviceDefinition g_ATDeviceDefDiskDrive810 = { "diskdrive810", nullptr, L"810 disk drive (full emulation)", ATCreateDeviceDiskDrive810 };
extern const ATDeviceDefinition g_ATDeviceDefDiskDrive810Archiver = { "diskdrive810archiver", nullptr, L"810 Archiver disk drive (full emulation)", ATCreateDeviceDiskDrive810Archiver };
extern const ATDeviceDefinition g_ATDeviceDefDiskDriveHappy810 = { "diskdrivehappy810", nullptr, L"Happy 810 disk drive (full emulation)", ATCreateDeviceDiskDriveHappy810 };

ATDeviceDiskDriveFull::ATDeviceDiskDriveFull(bool happy810, bool archiver)
	: mbHappy810(happy810)
	, mb810Archiver(archiver)
{
	std::fill(std::begin(mStepBreakpointMap), std::end(mStepBreakpointMap), true);
}

ATDeviceDiskDriveFull::~ATDeviceDiskDriveFull() {
}

void *ATDeviceDiskDriveFull::AsInterface(uint32 iid) {
	switch(iid) {
		case IATDeviceScheduling::kTypeID: return static_cast<IATDeviceScheduling *>(this);
		case IATDeviceFirmware::kTypeID: return static_cast<IATDeviceFirmware *>(this);
		case IATDeviceDiskDrive::kTypeID: return static_cast<IATDeviceDiskDrive *>(this);
		case IATDeviceSIO::kTypeID: return static_cast<IATDeviceSIO *>(this);
		case IATDeviceAudioOutput::kTypeID: return static_cast<IATDeviceAudioOutput *>(this);
		case IATDeviceDebugTarget::kTypeID: return static_cast<IATDeviceDebugTarget *>(this);
		case IATDebugTargetBreakpoints::kTypeID: return static_cast<IATDebugTargetBreakpoints *>(this);
		case IATDebugTargetHistory::kTypeID: return static_cast<IATDebugTargetHistory *>(this);
		case IATDebugTargetExecutionControl::kTypeID: return static_cast<IATDebugTargetExecutionControl *>(this);
	}

	return nullptr;
}

void ATDeviceDiskDriveFull::GetDeviceInfo(ATDeviceInfo& info) {
	info.mpDef = mb810Archiver ? &g_ATDeviceDefDiskDrive810Archiver : mbHappy810 ? &g_ATDeviceDefDiskDriveHappy810 : &g_ATDeviceDefDiskDrive810;
}

void ATDeviceDiskDriveFull::Init() {
	// The 810's memory map:
	//
	//	000-07F  FDC
	//	080-0FF  6810 memory
	//	100-17F  RIOT memory (mirror)
	//	180-1FF  RIOT memory
	//	200-27F  FDC (mirror)
	//	280-2FF  6810 memory
	//	300-37F  RIOT registers (mirror)
	//	300-3FF  RIOT registers
	//	400-47F  FDC (mirror)
	//	480-4FF  6810 memory (mirror)
	//	500-5FF  RIOT memory (mirror)
	//	600-67F  FDC (mirror)
	//	680-6FF  6810 memory (mirror)
	//	700-7FF  RIOT registers (mirror)
	//	800-FFF  ROM

	uintptr *readmap = mCoProc.GetReadMap();
	uintptr *writemap = mCoProc.GetWriteMap();

	// set up FDC/6810 handlers
	mReadNodeFDCRAM.mpRead = [](uint32 addr, void *thisptr0) -> uint8 {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		if (addr >= 0x80)
			return thisptr->mRAM[addr];
		else
			return ~thisptr->mFDC.ReadByte((uint8)addr);
	};

	mReadNodeFDCRAM.mpDebugRead = [](uint32 addr, void *thisptr0) -> uint8 {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		if (addr >= 0x80)
			return thisptr->mRAM[addr];
		else
			return ~thisptr->mFDC.DebugReadByte((uint8)addr);
	};

	mReadNodeFDCRAM.mpThis = this;

	mWriteNodeFDCRAM.mpWrite = [](uint32 addr, uint8 val, void *thisptr0) {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		if (addr >= 0x80)
			thisptr->mRAM[addr] = val;
		else
			thisptr->mFDC.WriteByte((uint8)addr, ~val);
	};

	mWriteNodeFDCRAM.mpThis = this;

	// set up RIOT memory handlers
	mReadNodeRIOTRAM.mpRead = [](uint32 addr, void *thisptr0) -> uint8 {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		return thisptr->mRAM[addr & 0x7F];
	};

	mReadNodeRIOTRAM.mpDebugRead = mReadNodeRIOTRAM.mpRead;
	mReadNodeRIOTRAM.mpThis = this;

	mWriteNodeRIOTRAM.mpWrite = [](uint32 addr, uint8 val, void *thisptr0) {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		thisptr->mRAM[addr & 0x7F] = val;
	};

	mWriteNodeRIOTRAM.mpThis = this;

	// set up RIOT register handlers
	mReadNodeRIOTRegisters.mpRead = [](uint32 addr, void *thisptr0) -> uint8 {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		return thisptr->mRIOT.ReadByte((uint8)addr);
	};

	mReadNodeRIOTRegisters.mpDebugRead = [](uint32 addr, void *thisptr0) -> uint8 {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		return thisptr->mRIOT.DebugReadByte((uint8)addr);
	};

	mReadNodeRIOTRegisters.mpThis = this;

	mWriteNodeRIOTRegisters.mpWrite = [](uint32 addr, uint8 val, void *thisptr0) {
		auto *thisptr = (ATDeviceDiskDriveFull *)thisptr0;

		thisptr->OnRIOTRegisterWrite(addr, val);
	};

	mWriteNodeRIOTRegisters.mpThis = this;

	// set up lower half of memory map
	writemap[0] = (uintptr)&mWriteNodeFDCRAM + 1;
	writemap[2] = (uintptr)&mWriteNodeFDCRAM + 1;
	writemap[4] = (uintptr)&mWriteNodeFDCRAM + 1;
	writemap[6] = (uintptr)&mWriteNodeFDCRAM + 1;
	writemap[1] = (uintptr)&mWriteNodeRIOTRAM + 1;
	writemap[5] = (uintptr)&mWriteNodeRIOTRAM + 1;
	writemap[3] = (uintptr)&mWriteNodeRIOTRegisters + 1;
	writemap[7] = (uintptr)&mWriteNodeRIOTRegisters + 1;

	readmap[0] = (uintptr)&mReadNodeFDCRAM + 1;
	readmap[2] = (uintptr)&mReadNodeFDCRAM + 1;
	readmap[4] = (uintptr)&mReadNodeFDCRAM + 1;
	readmap[6] = (uintptr)&mReadNodeFDCRAM + 1;
	readmap[1] = (uintptr)&mReadNodeRIOTRAM + 1;
	readmap[5] = (uintptr)&mReadNodeRIOTRAM + 1;
	readmap[3] = (uintptr)&mReadNodeRIOTRegisters + 1;
	readmap[7] = (uintptr)&mReadNodeRIOTRegisters + 1;

	// Replicate read and write maps 16 times (A12 ignored, A13-A15 nonexistent)
	for(int i=16; i<256; i+=16) {
		std::copy(readmap + 0, readmap + 8, readmap + i);
		std::copy(writemap + 0, writemap + 8, writemap + i);
	}

	if (mbHappy810) {
		for(uint32 mirror = 0; mirror < 256; mirror += 32) {
			// setup RAM entries ($0800-13FF)
			for(uint32 i=0; i<12; ++i) {
				readmap[i+8+mirror] = (uintptr)(mRAM + 0x100) - (0x800 + (mirror << 8));
				writemap[i+8+mirror] = (uintptr)(mRAM + 0x100) - (0x800 + (mirror << 8));
			}

			// setup ROM entries ($1400-1FFF), ignore first 2K of ROM (not visible to CPU)
			for(uint32 i=0; i<12; ++i) {
				readmap[i+20+mirror] = (uintptr)mROM + 0x400 - (0x1400 + (mirror << 8));
				writemap[i+20+mirror] = (uintptr)mDummyWrite - ((i + 20 + mirror) << 8);
			}
		}
	} else {
		// Set ROM entries (they must all be different).
		for(int i=0; i<256; i+=16) {
			std::fill(readmap + i + 8, readmap + i + 16, (uintptr)mROM - (0x800 + (i << 8)));

			for(int j=0; j<8; ++j)
				writemap[i+j+8] = (uintptr)mDummyWrite - ((i+j) << 8);
		}
	}

	mRIOT.SetOnIrqChanged([this](bool state) { mFDC.OnIndexPulse(state); });
	mRIOT.Init(&mDriveScheduler);
	mRIOT.Reset();

	// Clear port B bit 1 (/READY) and bit 7 (/DATAOUT)
	mRIOT.SetInputB(0x00, 0x82);

	// Set port A bits 0 and 2 to select D1:., and clear DRQ/IRQ from FDC
	mRIOT.SetInputA(0x05, 0xC5);	// D1:
//	mRIOT.SetInputA(0x04, 0xC5);	// D2:

	mFDC.Init(&mDriveScheduler);
	mFDC.SetDiskInterface(mpDiskInterface);
	mFDC.SetOnDrqChange([this](bool drq) { mRIOT.SetInputA(drq ? 0x80 : 0x00, 0x80); });
	mFDC.SetOnIrqChange([this](bool irq) { mRIOT.SetInputA(irq ? 0x40 : 0x00, 0x40); });

	OnDiskChanged();
	OnWriteModeChanged();
	OnTimingModeChanged();
	OnAudioModeChanged();

	UpdateRotationStatus();
	UpdateROMBank();
}

void ATDeviceDiskDriveFull::Shutdown() {
	if (mRotationSoundId) {
		mpAudioSyncMixer->StopSound(mRotationSoundId);
		mRotationSoundId = 0;
	}

	mDriveScheduler.UnsetEvent(mpEventDriveReceiveBit);

	if (mpSlowScheduler) {
		mpSlowScheduler->UnsetEvent(mpRunEvent);
		mpSlowScheduler = nullptr;
	}

	if (mpScheduler) {
		mpScheduler->UnsetEvent(mpTransmitEvent);
		mpScheduler = nullptr;
	}

	mpFwMgr = nullptr;

	if (mpSIOMgr) {
		mpSIOMgr->RemoveRawDevice(this);
		mpSIOMgr = nullptr;
	}

	if (mpDiskInterface) {
		mpDiskInterface->RemoveClient(this);
		mpDiskInterface = nullptr;
	}

	mpDiskDriveManager = nullptr;
}

void ATDeviceDiskDriveFull::WarmReset() {
	mLastSync = ATSCHEDULER_GETTIME(mpScheduler);
	mLastSyncDriveTime = ATSCHEDULER_GETTIME(&mDriveScheduler);

	// If the computer resets, its transmission is interrupted.
	mDriveScheduler.UnsetEvent(mpEventDriveReceiveBit);
}

void ATDeviceDiskDriveFull::ColdReset() {
	memset(mRAM, 0, sizeof mRAM);

	mCoProc.ColdReset();
	mRIOT.Reset();
	mFDC.Reset();

	// clear DRQ/IRQ from FDC -> RIOT port A
	mRIOT.SetInputA(0x00, 0xC0);

	mDriveScheduler.UnsetEvent(mpEventDriveTransmitBit);

	mpScheduler->UnsetEvent(mpTransmitEvent);
	mTransmitQueue.clear();
	
	// start the disk drive on a track other than 0/20/39, just to make things interesting
	mCurrentTrack = 10;
	mFDC.SetCurrentTrack(mCurrentTrack);
	mFDC.SetMotorRunning(true);

	WarmReset();
}

void ATDeviceDiskDriveFull::InitScheduling(ATScheduler *sch, ATScheduler *slowsch) {
	mpScheduler = sch;
	mpSlowScheduler = slowsch;

	mpSlowScheduler->SetEvent(1, this, 1, mpRunEvent);
}

void ATDeviceDiskDriveFull::InitFirmware(ATFirmwareManager *fwman) {
	mpFwMgr = fwman;

	ReloadFirmware();
}

bool ATDeviceDiskDriveFull::ReloadFirmware() {
	const uint64 id = mpFwMgr->GetFirmwareOfType(mb810Archiver ? kATFirmwareType_810Archiver : mbHappy810 ? kATFirmwareType_Happy810 : kATFirmwareType_810, true);
	
	const vduint128 oldHash = VDHash128(mROM, sizeof mROM);

	uint8 firmware[sizeof(mROM)] = {};

	uint32 len = 0;
	mpFwMgr->LoadFirmware(id, firmware, 0, mb810Archiver || mbHappy810 ? 0x1000 : 0x800, nullptr, &len);

	// If we're in Happy 810 mode, check whether we only have the visible 3K of the
	// ROM or a full 4K image. Both are supported.
	if (mbHappy810 && len == 0xC00) {
		memmove(firmware + 0x400, firmware, 0xC00);
		memset(firmware, 0, 0x400);
	}

	memcpy(mROM, firmware, sizeof mROM);

	const vduint128 newHash = VDHash128(mROM, sizeof mROM);

	return oldHash != newHash;
}

const wchar_t *ATDeviceDiskDriveFull::GetWritableFirmwareDesc(uint32 idx) const {
	return nullptr;
}

bool ATDeviceDiskDriveFull::IsWritableFirmwareDirty(uint32 idx) const {
	return false;
}

void ATDeviceDiskDriveFull::SaveWritableFirmware(uint32 idx, IVDStream& stream) {
}

void ATDeviceDiskDriveFull::InitDiskDrive(IATDiskDriveManager *ddm) {
	mpDiskDriveManager = ddm;
	mpDiskInterface = ddm->GetDiskInterface(0);
	mpDiskInterface->AddClient(this);
}

void ATDeviceDiskDriveFull::InitSIO(IATDeviceSIOManager *mgr) {
	mpSIOMgr = mgr;
	mpSIOMgr->AddRawDevice(this);
}

auto ATDeviceDiskDriveFull::OnSerialBeginCommand(const ATDeviceSIOCommand& cmd)
	-> CmdResponse
{
	return kCmdResponse_NotHandled;
}

void ATDeviceDiskDriveFull::OnSerialAbortCommand() {
}

void ATDeviceDiskDriveFull::OnSerialReceiveComplete(uint32 id, const void *data, uint32 len, bool checksumOK) {
}

void ATDeviceDiskDriveFull::OnSerialFence(uint32 id) {
}

auto ATDeviceDiskDriveFull::OnSerialAccelCommand(const ATDeviceSIORequest& request)
	-> CmdResponse
{
	return kCmdResponse_NotHandled;
}

void ATDeviceDiskDriveFull::InitAudioOutput(IATAudioOutput *output, ATAudioSyncMixer *syncmixer) {
	mpAudioSyncMixer = syncmixer;
}

IATDebugTarget *ATDeviceDiskDriveFull::GetDebugTarget(uint32 index) {
	if (index == 0)
		return this;

	return nullptr;
}

const char *ATDeviceDiskDriveFull::GetName() {
	return "Disk Drive CPU";
}

ATDebugDisasmMode ATDeviceDiskDriveFull::GetDisasmMode() {
	return kATDebugDisasmMode_65C816;
}

void ATDeviceDiskDriveFull::GetExecState(ATCPUExecState& state) {
	mCoProc.GetExecState(state);
}

void ATDeviceDiskDriveFull::SetExecState(const ATCPUExecState& state) {
	mCoProc.SetExecState(state);
}

sint32 ATDeviceDiskDriveFull::GetTimeSkew() {
	// The 810's CPU runs at 500KHz, while the computer runs at 1.79MHz. We use
	// a ratio of 229/64.

	const uint32 t = ATSCHEDULER_GETTIME(mpScheduler);
	const uint32 cycles = (t - mLastSync) + ((mCoProc.GetCyclesLeft() * 229 + mSubCycleAccum + 63) >> 6);

	return -(sint32)cycles;
}

uint8 ATDeviceDiskDriveFull::ReadByte(uint32 address) {
	if (address >= 0x10000)
		return 0;

	const uintptr pageBase = mCoProc.GetReadMap()[address >> 8];

	if (pageBase & 1) {
		const auto *node = (const ATCoProcReadMemNode *)(pageBase - 1);

		return node->mpRead(address, node->mpThis);
	}

	return *(const uint8 *)(pageBase + address);
}

void ATDeviceDiskDriveFull::ReadMemory(uint32 address, void *dst, uint32 n) {
	const uintptr *readMap = mCoProc.GetReadMap();

	while(n) {
		if (address >= 0x10000) {
			memset(dst, 0, n);
			break;
		}

		uint32 tc = 256 - (address & 0xff);
		if (tc > n)
			tc = n;

		const uintptr pageBase = readMap[address >> 8];

		if (pageBase & 1) {
			const auto *node = (const ATCoProcReadMemNode *)(pageBase - 1);

			for(uint32 i=0; i<tc; ++i)
				((uint8 *)dst)[i] = node->mpRead(address++, node->mpThis);
		} else {
			memcpy(dst, (const uint8 *)(pageBase + address), tc);

			address += tc;
		}

		n -= tc;
		dst = (char *)dst + tc;
	}
}

uint8 ATDeviceDiskDriveFull::DebugReadByte(uint32 address) {
	if (address >= 0x10000)
		return 0;

	const uintptr pageBase = mCoProc.GetReadMap()[address >> 8];

	if (pageBase & 1) {
		const auto *node = (ATCoProcReadMemNode *)(pageBase - 1);

		return node->mpRead(address, node->mpThis);
	}

	return *(const uint8 *)(pageBase + address);
}

void ATDeviceDiskDriveFull::DebugReadMemory(uint32 address, void *dst, uint32 n) {
	const uintptr *readMap = mCoProc.GetReadMap();

	while(n) {
		if (address >= 0x10000) {
			memset(dst, 0, n);
			break;
		}

		uint32 tc = 256 - (address & 0xff);
		if (tc > n)
			tc = n;

		const uintptr pageBase = readMap[address >> 8];

		if (pageBase & 1) {
			const auto *node = (const ATCoProcReadMemNode *)(pageBase - 1);

			for(uint32 i=0; i<tc; ++i)
				((uint8 *)dst)[i] = node->mpDebugRead(address++, node->mpThis);
		} else {
			memcpy(dst, (const uint8 *)(pageBase + address), tc);

			address += tc;
		}

		n -= tc;
		dst = (char *)dst + tc;
	}

}

void ATDeviceDiskDriveFull::WriteByte(uint32 address, uint8 value) {
	if (address >= 0x10000)
		return;

	const uintptr pageBase = mCoProc.GetWriteMap()[address >> 8];

	if (pageBase & 1) {
		auto& writeNode = *(ATCoProcWriteMemNode *)(pageBase - 1);

		writeNode.mpWrite(address, value, writeNode.mpThis);
	} else {
		*(uint8 *)(pageBase + address) = value;
	}
}

void ATDeviceDiskDriveFull::WriteMemory(uint32 address, const void *src, uint32 n) {
	const uintptr *writeMap = mCoProc.GetWriteMap();

	while(n) {
		if (address >= 0x10000)
			break;

		const uintptr pageBase = writeMap[address >> 8];

		if (pageBase & 1) {
			auto& writeNode = *(ATCoProcWriteMemNode *)(pageBase - 1);

			writeNode.mpWrite(address, *(const uint8 *)src, writeNode.mpThis);
			++address;
			src = (const uint8 *)src + 1;
			--n;
		} else {
			uint32 tc = 256 - (address & 0xff);
			if (tc > n)
				tc = n;

			memcpy((uint8 *)(pageBase + address), src, tc);

			n -= tc;
			address += tc;
			src = (const char *)src + tc;
		}
	}
}

bool ATDeviceDiskDriveFull::GetHistoryEnabled() const {
	return !mHistory.empty();
}

void ATDeviceDiskDriveFull::SetHistoryEnabled(bool enable) {
	if (enable) {
		if (mHistory.empty()) {
			mHistory.resize(131072, ATCPUHistoryEntry());
			mCoProc.SetHistoryBuffer(mHistory.data());
		}
	} else {
		if (!mHistory.empty()) {
			decltype(mHistory) tmp;
			tmp.swap(mHistory);
			mHistory.clear();
			mCoProc.SetHistoryBuffer(nullptr);
		}
	}
}

std::pair<uint32, uint32> ATDeviceDiskDriveFull::GetHistoryRange() const {
	const uint32 hcnt = mCoProc.GetHistoryCounter();

	return std::pair<uint32, uint32>(hcnt - 131072, hcnt);
}

uint32 ATDeviceDiskDriveFull::ExtractHistory(const ATCPUHistoryEntry **hparray, uint32 start, uint32 n) const {
	if (!n || mHistory.empty())
		return 0;

	const ATCPUHistoryEntry *hstart = mHistory.data();
	const ATCPUHistoryEntry *hend = hstart + 131072;
	const ATCPUHistoryEntry *hsrc = hstart + (start & 131071);

	for(uint32 i=0; i<n; ++i) {
		*hparray++ = hsrc;

		if (++hsrc == hend)
			hsrc = hstart;
	}

	return n;
}

uint32 ATDeviceDiskDriveFull::ConvertRawTimestamp(uint32 rawTimestamp) const {
	// mLastSync is the machine cycle at which all sub-cycles have been pushed into the
	// coprocessor, and the coprocessor's time base is the sub-cycle corresponding to
	// the end of that machine cycle.
	return mLastSync - (((mCoProc.GetTimeBase() - rawTimestamp) * 229 + mSubCycleAccum + 63) >> 6);
}

void ATDeviceDiskDriveFull::SetBreakpointHandler(IATCPUBreakpointHandler *handler) {
	mpBreakpointHandler = handler;

	if (mBreakpointCount)
		mCoProc.SetBreakpointMap(mBreakpointMap, handler);
}

void ATDeviceDiskDriveFull::ClearBreakpoint(uint16 pc) {
	if (mBreakpointMap[pc]) {
		mBreakpointMap[pc] = false;

		if (!--mBreakpointCount && !mpStepHandler)
			mCoProc.SetBreakpointMap(nullptr, nullptr);
	}
}

void ATDeviceDiskDriveFull::SetBreakpoint(uint16 pc) {
	if (!mBreakpointMap[pc]) {
		mBreakpointMap[pc] = true;

		if (!mBreakpointCount++ && !mpStepHandler)
			mCoProc.SetBreakpointMap(mBreakpointMap, mpBreakpointHandler);
	}
}

void ATDeviceDiskDriveFull::Break() {
	CancelStep();
}

bool ATDeviceDiskDriveFull::StepInto(const vdfunction<void(bool)>& fn) {
	CancelStep();

	mpStepHandler = fn;
	mbStepOut = false;
	mStepStartSubCycle = mCoProc.GetTime();
	mCoProc.SetBreakpointMap(mStepBreakpointMap, this);
	Sync();
	return true;
}

bool ATDeviceDiskDriveFull::StepOver(const vdfunction<void(bool)>& fn) {
	CancelStep();

	mpStepHandler = fn;
	mbStepOut = true;
	mStepStartSubCycle = mCoProc.GetTime();
	mStepOutS = mCoProc.GetS();
	mCoProc.SetBreakpointMap(mStepBreakpointMap, this);
	Sync();
	return true;
}

bool ATDeviceDiskDriveFull::StepOut(const vdfunction<void(bool)>& fn) {
	CancelStep();

	mpStepHandler = fn;
	mbStepOut = true;
	mStepStartSubCycle = mCoProc.GetTime();
	mStepOutS = mCoProc.GetS() + 1;
	mCoProc.SetBreakpointMap(mStepBreakpointMap, this);
	Sync();
	return true;
}

void ATDeviceDiskDriveFull::StepUpdate() {
	Sync();
}

void ATDeviceDiskDriveFull::RunUntilSynced() {
	CancelStep();
	Sync();
}

bool ATDeviceDiskDriveFull::CheckBreakpoint(uint32 pc) {
	bool bpHit = false;

	if (mBreakpointCount && mBreakpointMap[(uint16)pc] && mpBreakpointHandler->CheckBreakpoint(pc))
		bpHit = true;

	if (mBreakpointCount)
		mCoProc.SetBreakpointMap(mBreakpointMap, mpBreakpointHandler);
	else {
		if (mCoProc.GetTime() == mStepStartSubCycle)
			return false;

		if (mbStepOut) {
			// Keep stepping if wrapped(s < s0).
			if ((mCoProc.GetS() - mStepOutS) & 0x80)
				return false;
		}

		mCoProc.SetBreakpointMap(nullptr, nullptr);
	}

	auto p = std::move(mpStepHandler);
	mpStepHandler = nullptr;

	p(!bpHit);

	return true;
}

void ATDeviceDiskDriveFull::OnScheduledEvent(uint32 id) {
	if (id == kEventId_Run) {
		mpRunEvent = mpSlowScheduler->AddEvent(1, this, 1);

		mDriveScheduler.UpdateTick64();
		Sync();
	} else if (id == kEventId_Transmit) {
		mpTransmitEvent = nullptr;

		if (!mTransmitQueue.empty()) {
			auto&& qxmt = mTransmitQueue.front();
			mTransmitQueue.pop_front();

			mpSIOMgr->SendRawByte(qxmt.mByte, qxmt.mCyclesPerBit, false, qxmt.mbFramingError);

			QueueNextTransmit();
		}
	} else if (id == kEventId_DriveReceiveBit) {
		const uint8 newState = (mReceiveShiftRegister & 1) ? 0x00 : 0x80;

		mReceiveShiftRegister >>= 1;
		mpEventDriveReceiveBit = nullptr;

		if (mReceiveShiftRegister) {
			mReceiveTimingAccum += mReceiveTimingStep;
			mpEventDriveReceiveBit = mDriveScheduler.AddEvent(mReceiveTimingAccum >> 10, this, kEventId_DriveReceiveBit);
			mReceiveTimingAccum &= 0x3FF;
		}

		mRIOT.SetInputB(newState, 0x80);
	} else if (id == kEventId_DriveTransmitBit) {
		mpEventDriveTransmitBit = 0;

		const uint8 bit = mRIOT.ReadOutputB() & 1;

		mTransmitShiftRegister >>= 1;
		mTransmitShiftRegister += bit << 8;

		if (mTransmitShiftRegister & 0x10000) {
			// This is the stop bit. Send POKEY the byte even if it's bad; force a framing error if
			// the stop bit is wrong.
			const uint32 driveTime = ATSCHEDULER_GETTIME(&mDriveScheduler);
			const uint32 driveTimeDeltaU = driveTime - mLastSyncDriveTime;
			sint32 driveTimeDelta = driveTimeDeltaU < UINT32_C(0x80000000) ? (sint32)driveTimeDeltaU : -(sint32)(UINT32_C(0) - driveTimeDeltaU);
			sint32 computerTimeDelta = (driveTimeDelta * 229 + 32) >> 6;

			const uint32 kTransmitLatency = 200;

			QueuedTransmit qxmt {};
			qxmt.mTime = mLastSync + kTransmitLatency + (uint32)computerTimeDelta;
			qxmt.mCyclesPerBit = mTransmitCyclesPerBit;
			qxmt.mByte = (uint8)mTransmitShiftRegister;
			qxmt.mbFramingError = !bit;

			mTransmitQueue.push_back(qxmt);

			if (!mpTransmitEvent)
				QueueNextTransmit();
		} else {
			if (mTransmitShiftRegister & 0x2000000) {
				// This is the start bit. Check that the start bit is still the correct polarity;
				// if not, just ignore the byte.
				if (bit)
					return;
			}

			mTransmitTimingAccum += mTransmitTimingStep;
			mpEventDriveTransmitBit = mDriveScheduler.AddEvent(mTransmitTimingAccum >> 10, this, kEventId_DriveTransmitBit);
			mTransmitTimingAccum &= 0x3FF;
		}
	}
}

void ATDeviceDiskDriveFull::OnCommandStateChanged(bool asserted) {
	Sync();

	mRIOT.SetInputB(asserted ? 0xFF : 0x00, 0x40);
}

void ATDeviceDiskDriveFull::OnMotorStateChanged(bool asserted) {
}

void ATDeviceDiskDriveFull::OnReceiveByte(uint8 c, bool command, uint32 cyclesPerBit) {
	Sync();

	mReceiveShiftRegister = c + c + 0x200;

	// The conversion fraction we need here is 64/229, but that denominator is awkward.
	// Approximate it with 286/1024.
	mReceiveTimingAccum = 0x200;
	mReceiveTimingStep = cyclesPerBit * 286;

	mDriveScheduler.SetEvent(1, this, kEventId_DriveReceiveBit, mpEventDriveReceiveBit);
}

void ATDeviceDiskDriveFull::OnSendReady() {
}

void ATDeviceDiskDriveFull::OnDiskChanged() {
	IATDiskImage *image = mpDiskInterface->GetDiskImage();

	mFDC.SetDiskImage(image);

	UpdateWriteProtectStatus();
}

void ATDeviceDiskDriveFull::OnWriteModeChanged() {
	UpdateWriteProtectStatus();
}

void ATDeviceDiskDriveFull::OnTimingModeChanged() {
	mFDC.SetAccurateTimingEnabled(mpDiskInterface->IsAccurateSectorTimingEnabled());
}

void ATDeviceDiskDriveFull::OnAudioModeChanged() {
	mbSoundsEnabled = mpDiskInterface->AreDriveSoundsEnabled();

	UpdateRotationStatus();
}

void ATDeviceDiskDriveFull::CancelStep() {
	if (mpStepHandler) {
		if (mBreakpointCount)
			mCoProc.SetBreakpointMap(mBreakpointMap, mpBreakpointHandler);
		else
			mCoProc.SetBreakpointMap(nullptr, nullptr);

		auto p = std::move(mpStepHandler);
		mpStepHandler = nullptr;

		p(false);
	}
}


void ATDeviceDiskDriveFull::Sync() {
	AccumSubCycles();

	for(;;) {
		if (!mCoProc.GetCyclesLeft()) {
			if (mSubCycleAccum < 229)
				break;

			mSubCycleAccum -= 229;

			ATSCHEDULER_ADVANCE(&mDriveScheduler);

			mCoProc.AddCycles(1);
		}

		mCoProc.Run();

		if (mCoProc.GetCyclesLeft())
			break;
	}
}

void ATDeviceDiskDriveFull::AccumSubCycles() {
	const uint32 t = ATSCHEDULER_GETTIME(mpScheduler);
	const uint32 cycles = t - mLastSync;

	mLastSync = t;

	mSubCycleAccum += cycles * 64;

	mLastSyncDriveTime = ATSCHEDULER_GETTIME(&mDriveScheduler) + (mSubCycleAccum / 229);
}

void ATDeviceDiskDriveFull::BeginTransmit() {
	mDriveScheduler.UnsetEvent(mpEventDriveTransmitBit);

	mTransmitResetCounter = mpSIOMgr->GetRecvResetCounter();

	const uint32 cyclesPerBit = mpSIOMgr->GetCyclesPerBitRecv();

	if (cyclesPerBit < 4 || cyclesPerBit > 10000)
		return;

	mTransmitCyclesPerBit = cyclesPerBit;

	// Convert from computer cycles to device cycles (22.10fx).
	mTransmitTimingStep = cyclesPerBit * 286;

	// Begin first sample half a bit in.
	mTransmitTimingAccum = 0x100 + (mTransmitTimingStep >> 1);

	mpEventDriveTransmitBit = mDriveScheduler.AddEvent(mTransmitTimingAccum >> 10, this, kEventId_DriveTransmitBit);
	mTransmitTimingAccum &= 0x3FF;

	mTransmitShiftRegister = 0x4000000;
}

void ATDeviceDiskDriveFull::QueueNextTransmit() {
	while(!mTransmitQueue.empty()) {
		auto&& qxmt = mTransmitQueue.front();

		const uint32 t = ATSCHEDULER_GETTIME(mpScheduler);
		const uint32 delay = qxmt.mTime - t;

		if ((delay - 1) < 10000) {
			mpScheduler->SetEvent(delay, this, kEventId_Transmit, mpTransmitEvent);
			break;
		}

		mTransmitQueue.pop_front();
	}
}

void ATDeviceDiskDriveFull::OnRIOTRegisterWrite(uint32 addr, uint8 val) {
	// check for a write to DRA or DDRA
	if ((addr & 6) == 0) {
		// compare outputs before and after write
		const uint8 outprev = mRIOT.ReadOutputA();
		mRIOT.WriteByte((uint8)addr, val);
		const uint8 outnext = mRIOT.ReadOutputA();

		// check for a spindle motor state change
		const uint8 delta = outprev ^ outnext;
		if (delta & 2) {
			const bool running = (outnext & 2) != 0;
			mFDC.SetMotorRunning(running);

			UpdateRotationStatus();
		}

		// check for a ROM bank change (Archiver)
		if (mb810Archiver && (delta & 8))
			UpdateROMBank();
		return;
	}

	// check for a write to DRB or DDRB
	if ((addr & 6) == 2) {
		// compare outputs before and after write
		const uint8 outprev = mRIOT.ReadOutputB();
		mRIOT.WriteByte((uint8)addr, val);
		const uint8 outnext = mRIOT.ReadOutputB();

		// check for negative transition on PB0, indicating possible start bit
		if (outprev & ~outnext & 1) {
			// start new transmission if either we're not transmitting or the serial input port on POKEY has been reset
			if (mTransmitResetCounter != mpSIOMgr->GetRecvResetCounter() || !mpEventDriveTransmitBit)
				BeginTransmit();
		}

		// check for stepping transition
		if ((outprev ^ outnext) & 0x3C) {
			// OK, now compare the phases. The 810 has track 0 at a phase pattern of
			// 0x24 (%1001); seeks toward track 0 rotate to lower phases (right shift).
			// If we don't have exactly two phases energized, ignore the change. If
			// the change inverts all phases, ignore it.

			static const sint8 kOffsetTable[]={
				-1, -1, -1,  1,
				-1, -1,  2, -1,
				-1,  0, -1, -1,
				 3, -1, -1, -1

			};

			const sint8 newOffset = kOffsetTable[(outnext >> 2) & 15];

			g_ATLCDiskEmu("Stepper phases now: %X\n", outnext & 0x3C);

			if (newOffset >= 0) {
				switch(((uint32)newOffset - mCurrentTrack) & 3) {
					case 1:		// step in (increasing track number)
						if (mCurrentTrack < 45) {
							++mCurrentTrack;
							mFDC.SetCurrentTrack(mCurrentTrack);
						}

						PlayStepSound();
						break;

					case 3:		// step out (decreasing track number)
						if (mCurrentTrack > 0) {
							--mCurrentTrack;
							mFDC.SetCurrentTrack(mCurrentTrack);

							PlayStepSound();
						}
						break;

					case 0:
					case 2:
					default:
						// no step or indeterminate -- ignore
						break;
				}
			}
		}
	} else {
		mRIOT.WriteByte((uint8)addr, val);
	}
}

void ATDeviceDiskDriveFull::PlayStepSound() {
	if (!mbSoundsEnabled)
		return;

	const uint32 t = ATSCHEDULER_GETTIME(&mDriveScheduler);
	
	if (t - mLastStepSoundTime > 50000)
		mLastStepPhase = 0;

	mpAudioSyncMixer->AddSound(kATAudioMix_Drive, 0, kATAudioSampleId_DiskStep1, 0.3f + 0.7f * cosf((float)mLastStepPhase++ * nsVDMath::kfPi * 0.5f));

	mLastStepSoundTime = t;
}

void ATDeviceDiskDriveFull::UpdateRotationStatus() {
	if (mRIOT.ReadOutputA() & 2) {
		mpDiskInterface->SetShowMotorActive(true);

		if (!mRotationSoundId) {
			mRotationSoundId = mpAudioSyncMixer->AddLoopingSound(kATAudioMix_Drive, 0, kATAudioSampleId_DiskRotation, 1.0f);
		}
	} else {
		mpDiskInterface->SetShowMotorActive(false);

		if (mRotationSoundId) {
			mpAudioSyncMixer->StopSound(mRotationSoundId);
			mRotationSoundId = 0;
		}
	}
}

void ATDeviceDiskDriveFull::UpdateROMBank() {
	if (mb810Archiver) {
		// Thanks to ijor for providing the ROM banking info for the Archiver.
		uintptr romref = ((mRIOT.ReadOutputA() & 8) ? (uintptr)(mROM + 0x800) : (uintptr)mROM) - 0x800;

		// Fixup ROM entries in read map (they must be different for each mirror).
		uintptr *rmdst = mCoProc.GetReadMap() + 8;

		for(int i=0; i<16; ++i) {
			std::fill(rmdst, rmdst + 8, romref);

			romref -= 0x1000;
			rmdst += 16;
		}

	}
}

void ATDeviceDiskDriveFull::UpdateWriteProtectStatus() {
	const bool wpsense = mpDiskInterface->GetDiskImage() && !mpDiskInterface->IsDiskWritable();

	mRIOT.SetInputA(wpsense ? 0x10 : 0x00, 0x10);
}
