//	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/binary.h>
#include <vd2/system/math.h>
#include <at/atcore/logging.h>
#include <at/atcore/scheduler.h>
#include "disk.h"
#include "fdc.h"

extern ATLogChannel g_ATLCDisk;
ATLogChannel g_ATLCFDC(true, false, "FDC", "Floppy drive controller");

ATFDCEmulator::ATFDCEmulator() {
	mpFnDrqChange = [](bool drq) {};
	mpFnIrqChange = [](bool drq) {};
}

ATFDCEmulator::~ATFDCEmulator() {
	Shutdown();
}

void ATFDCEmulator::Init(ATScheduler *sch) {
	mpScheduler = sch;
}

void ATFDCEmulator::Shutdown() {
	if (mpScheduler) {
		mpScheduler->UnsetEvent(mpStateEvent);
		mpScheduler = nullptr;
	}
}

void ATFDCEmulator::Reset() {
	AbortCommand();

	mState = kState_Idle;
	mRegCommand = 0xD8;		// force interrupt immediate
	mRegTrack = 0;
	mRegSector = 0;
	mRegStatus = 0;
	mRegData = 0;
	mbIrqPending = false;
	mbDataReadPending = false;

	mpFnDrqChange(false);

	mWeakBitLFSR = 1;

	mpScheduler->UnsetEvent(mpStateEvent);

	mpDiskInterface->SetShowMotorActive(true);
	mpDiskInterface->SetShowActivity(false, 0);
}

void ATFDCEmulator::SetAccurateTimingEnabled(bool enabled) {
	mbUseAccurateTiming = enabled;
}

void ATFDCEmulator::SetMotorRunning(bool running) {
	if (mbMotorRunning == running)
		return;

	UpdateRotationalPosition();
	mbMotorRunning = running;
}

void ATFDCEmulator::SetCurrentTrack(uint32 track) {
	if (mPhysTrack != track) {
		mPhysTrack = track;

		g_ATLCFDC("Physical track is now %u\n", track);
	}
}

void ATFDCEmulator::SetDiskImage(IATDiskImage *image) {
	mpDiskImage = image;
}

void ATFDCEmulator::SetDiskInterface(ATDiskInterface *diskIf) {
	mpDiskInterface = diskIf;
}

uint8 ATFDCEmulator::DebugReadByte(uint8 address) const {
	switch(address & 3) {
		case 0:
		default:
			return mRegStatus;

		case 1:
			return mRegTrack;

		case 2:
			return mRegSector;

		case 3:
			return mRegData;
	}
}

uint8 ATFDCEmulator::ReadByte(uint8 address) {
	uint8 v = DebugReadByte(address);

	switch(address & 3) {
		case 0:
			// reading status clears IRQ request
			if (mbIrqPending) {
				mbIrqPending = false;

				mpFnIrqChange(false);
			}

			break;

		case 3:
			if (mbDataReadPending) {
				mbDataReadPending = false;

				mpFnDrqChange(false);
			}

			mRegStatus &= ~0x02;
			break;
	}

	return v;
}

void ATFDCEmulator::WriteByte(uint8 address, uint8 value) {
	switch(address & 3) {
		case 0:
		default:
			if ((value & 0xF0) == 0xD0)
				AbortCommand();

			// deassert IRQ
			if (mbIrqPending) {
				mbIrqPending = false;
				mpFnIrqChange(false);
			}

			mRegCommand = value;
			SetTransition(kState_BeginCommand, 1);
			break;

		case 1:
			mRegTrack = value;
			break;

		case 2:
			mRegSector = value;
			break;

		case 3:
			mRegData = value;
			if (mbDataWritePending) {
				mbDataWritePending = false;

				mpFnDrqChange(false);

				mRegStatus &= 0xFD;
			}
			break;
	}
}

void ATFDCEmulator::OnScheduledEvent(uint32 id) {
	mpStateEvent = nullptr;

	RunStateMachine();
}

void ATFDCEmulator::AbortCommand() {
	mState = kState_Idle;
	mpScheduler->UnsetEvent(mpStateEvent);

	mbDataReadPending = false;
	mbDataWritePending = false;
	mpFnDrqChange(false);
}

void ATFDCEmulator::RunStateMachine() {
	switch(mState) {
		case kState_Idle:
			break;

		case kState_BeginCommand:
			g_ATLCFDC("Beginning command $%02X\n", mRegCommand);

			mRegStatus &= 0x80;
			mRegStatus |= 0x01;

			switch(mRegCommand & 0xF0) {
				case 0x00: SetTransition(kState_Restore, 1); break;
				case 0x10: SetTransition(kState_Seek, 1); break;
				case 0x20:
				case 0x30: SetTransition(kState_Step, 1); break;
				case 0x40:
				case 0x50: SetTransition(kState_StepIn, 1); break;
				case 0x60:
				case 0x70: SetTransition(kState_StepOut, 1); break;
				case 0x80:
				case 0x90: SetTransition(kState_ReadSector, 1); break;
				case 0xA0:
				case 0xB0: SetTransition(kState_WriteSector, 1); break;
				case 0xC0: SetTransition(kState_ReadAddress, 1); break;
				case 0xD0: SetTransition(kState_ForceInterrupt, 1); break;
				case 0xE0: SetTransition(kState_ReadTrack, 1); break;
				case 0xF0: SetTransition(kState_WriteTrack, 1); break;
			}
			break;

		case kState_EndTypeICommand:
			// update write protect
			if (mpDiskImage && !mpDiskInterface->IsDiskWritable())
				mRegStatus |= 0x40;

			SetTransition(kState_EndCommand, 1);
			break;

		case kState_EndCommand:
			g_ATLCFDC("Ending command $%02X\n", mRegCommand);

			mState = kState_Idle;
			mRegStatus &= 0xFC;		// clear BUSY and DRQ

			if (mbDataReadPending || mbDataWritePending) {
				mbDataReadPending = false;
				mbDataWritePending = false;

				mpFnDrqChange(false);
			}

			// Set INTRQ.
			//
			// Force Interrupt with bits 0-3 all clear is a special case -- it is
			// documented as not setting the interrupt.
			if (!mbIrqPending && mRegCommand != 0xD0) {
				mbIrqPending = true;
				mpFnIrqChange(true);
			}

			mpDiskInterface->SetShowActivity(false, 0);
			break;

		case kState_Restore:
			mOpCount = 256;

			SetTransition(kState_Restore_Step, 1);
			break;

		case kState_Restore_Step:
			if (--mOpCount > 0) {
				static constexpr uint32 kDelayTable[4] = {
					(uint32)(0.5 +  6.0 / 1000.0 * 500000.0),	// 6ms
					(uint32)(0.5 + 12.0 / 1000.0 * 500000.0),	// 12ms
					(uint32)(0.5 + 20.0 / 1000.0 * 500000.0),	// 20ms
					(uint32)(0.5 + 30.0 / 1000.0 * 500000.0),	// 30ms
				};

				SetTransition(kState_Restore_Step, kDelayTable[mRegCommand & 3]);
			} else {
				// set seek error status bit
				mRegStatus |= 0x10;

				SetTransition(kState_EndTypeICommand, 1);
			}
			break;

		case kState_Seek:
		case kState_Step:
		case kState_StepIn:
		case kState_StepOut:
		case kState_ReadTrack:
			SetTransition(kState_EndTypeICommand, 1);
			break;

		case kState_WriteTrack:
			// set status to write protect and fail out immediately
			mRegStatus = 0x41;
			SetTransition(kState_EndCommand, 1);
			break;

		case kState_ReadAddress: {
			mTransferIndex = 0;
			mTransferLength = 0;
			mbDataReadPending = false;

			UpdateRotationalPosition();

			uint32 delay = kCyclesPerRotation * 2;

			mActiveSectorStatus = 0x10;

			if (mpDiskImage && mbMotorRunning) {
				// find next sector in rotational order on the entire track
				const float posf = (float)mRotPos / (float)kCyclesPerRotation;
				float bestDistance = 2.0f;
				float bestRotPos = 0;
				uint32 bestVSec = 0;
				uint32 bestVSecCount = 0;
				uint32 bestPhysSec = 0;
				uint32 bestPSecOffset = 0;
				int bestWeakDataOffset = -1;
				uint32 bestSectorSize = 0;

				for(uint32 sector = 1; sector <= 18; ++sector) {
					const uint32 vsec = mPhysTrack * 18 + sector;

					ATDiskVirtualSectorInfo vsi {};

					if (vsec <= mpDiskImage->GetVirtualSectorCount())
						mpDiskImage->GetVirtualSectorInfo(vsec - 1, vsi);

					if (vsi.mNumPhysSectors) {
						for(uint32 i=0; i<vsi.mNumPhysSectors; ++i) {
							ATDiskPhysicalSectorInfo psi {};

							mpDiskImage->GetPhysicalSectorInfo(vsi.mStartPhysSector + i, psi);

							if ((psi.mFDCStatus & 0x18) == 0x10)
								continue;

							float distance = psi.mRotPos - posf;
							distance -= floorf(distance);

							if (distance < bestDistance) {
								bestDistance = distance;
								bestPhysSec = vsi.mStartPhysSector + i;
								bestRotPos = psi.mRotPos;

								// We need uninverted status since we're in the FDC (inversion is
								// handled in the 810 emulation code).
								mActiveSectorStatus = ~psi.mFDCStatus;
								bestWeakDataOffset = psi.mWeakDataOffset;
								bestSectorSize = psi.mSize;
								bestVSec = vsec;
								bestVSecCount = vsi.mNumPhysSectors;
								bestPSecOffset = i;
							}
						}
					}
				}

				if (bestVSec) {
					// check for a long sector not actually stored in the image
					if ((mActiveSectorStatus & 0x02) && bestSectorSize < 256)
						bestSectorSize = 256;

					// Synthesize the six bytes returned for the Read Address command.
					mTransferBuffer[0] = (uint8)((bestVSec - 1) / 18);
					mTransferBuffer[1] = 0;
					mTransferBuffer[2] = (uint8)((bestVSec - 1) % 18 + 1);
					mTransferBuffer[3] = (bestSectorSize >= 1024 ? 3 : bestSectorSize >= 512 ? 2 : bestSectorSize >= 256 ? 1 : 0);

					const uint8 crcbuf[5] = {
						0xFE,		// IDAM
						mTransferBuffer[0],
						mTransferBuffer[1],
						mTransferBuffer[2],
						mTransferBuffer[3]
					};

					uint16 crc = 0xFFFF;
					for(const uint8 c : crcbuf) {
						crc ^= (uint16_t)c << 8;
						for(int j=0; j<8; ++j) {
							uint16_t feedback = (crc & 0x8000) ? 0x1021 : 0;
							crc += crc;
							crc ^= feedback;
						}
					}

					VDWriteUnalignedBEU16(&mTransferBuffer[4], crc);

					mTransferLength = 6;

					if (mbUseAccurateTiming) {
						// Compute rotational delay. It takes about 26 raw bytes in the standard 810
						// format from start of address to start of first byte, so including the
						// first byte, that's 26*8*4 = 832 cycles of latency until first DRQ.
						delay = VDRoundToInt(bestDistance * (float)kCyclesPerRotation + 832);
					} else {
						// Warp disk to start of sector data field.
						mRotPos = VDRoundToInt(bestRotPos * (float)kCyclesPerRotation) + 832;

						if (mRotPos >= kCyclesPerRotation)
							mRotPos -= kCyclesPerRotation;

						// Use short delay.
						delay = 300;
					}

					mpDiskInterface->SetShowActivity(true, bestVSec);

					if (g_ATLCDisk.IsEnabled() && bestVSec) {
						g_ATLCDisk("Reading address vsec=%3d (%d/%d) (trk=%d), psec=%3d, rot=%.2f >> %.2f >> %.2f%s.\n"
								, bestVSec
								, bestPSecOffset + 1
								, bestVSecCount
								, mPhysTrack
								, (uint32)bestPhysSec
								, (float)mRotPos / (float)kCyclesPerRotation
								, bestRotPos
								, (float)mRotPos / (float)kCyclesPerRotation
								,  bestWeakDataOffset >= 0 ? " (w/weak bits)"
									: (mActiveSectorStatus & 0x02) ? " (w/long sector)"		// must use DRQ as lost data differs between drives
									: (mActiveSectorStatus & 0x08) ? " (w/CRC error)"
									: (mActiveSectorStatus & 0x10) ? " (w/missing sector)"
									: (mActiveSectorStatus & 0x20) ? " (w/deleted sector)"
									: ""
								);
					}

					// Don't flag a CRC without missing record since the data
					// frame is not validated on a Read Address.
					if ((mActiveSectorStatus & 0x18) == 0x08)
						mActiveSectorStatus &= ~0x08;
				}
			}

			SetTransition(kState_ReadSector_TransferByte, delay);
			break;
		}

		case kState_ReadSector: {
			mTransferIndex = 0;
			mTransferLength = 0;
			mbDataReadPending = false;

			UpdateRotationalPosition();

			uint32 delay = kCyclesPerRotation * 2;

			mActiveSectorStatus = 0x10;

			if (mpDiskImage && mRegSector > 0 && mRegSector <= 18 && mbMotorRunning) {
				uint32 vsec = mPhysTrack * 18 + mRegSector;

				ATDiskVirtualSectorInfo vsi {};

				if (vsec <= mpDiskImage->GetVirtualSectorCount())
					mpDiskImage->GetVirtualSectorInfo(vsec - 1, vsi);

				if (vsi.mNumPhysSectors) {
					// find next sector in rotational order
					const float posf = (float)mRotPos / (float)kCyclesPerRotation;
					float bestDistance = 2.0f;
					float bestRotPos = 0;
					uint32 bestPhysSec = 0;
					int bestWeakDataOffset = -1;

					for(uint32 i=0; i<vsi.mNumPhysSectors; ++i) {
						ATDiskPhysicalSectorInfo psi {};

						mpDiskImage->GetPhysicalSectorInfo(vsi.mStartPhysSector + i, psi);

						float distance = psi.mRotPos - posf;
						distance -= floorf(distance);

						if (distance < bestDistance) {
							bestDistance = distance;
							bestPhysSec = vsi.mStartPhysSector + i;
							bestRotPos = psi.mRotPos;

							// We need uninverted status since we're in the FDC (inversion is
							// handled in the 810 emulation code).
							mActiveSectorStatus = ~psi.mFDCStatus;
							bestWeakDataOffset = psi.mWeakDataOffset;
							mTransferLength = std::min<uint32>(psi.mSize, vdcountof(mTransferBuffer));
						}
					}

					// check for a long sector not actually stored in the image
					if ((mActiveSectorStatus & 0x02) && mTransferLength < 256)
						mTransferLength = 256;

					memset(mTransferBuffer, 0xFF, mTransferLength);
					mpDiskImage->ReadPhysicalSector(bestPhysSec, mTransferBuffer, mTransferLength);

					// apply weak bits
					if (bestWeakDataOffset >= 0) {
						for(uint32 i = (uint32)bestWeakDataOffset; i < mTransferLength; ++i) {
							mTransferBuffer[i] ^= (uint8)mWeakBitLFSR;

							mWeakBitLFSR = (mWeakBitLFSR << 8) + (0xff & ((mWeakBitLFSR >> (28 - 8)) ^ (mWeakBitLFSR >> (31 - 8))));
						}
					}

					// We have to invert the data, because all Atari drives write data inverted.
					for(uint32 i=0; i<mTransferLength; ++i)
						mTransferBuffer[i] = ~mTransferBuffer[i];

					if (mbUseAccurateTiming) {
						// Compute rotational delay. It takes about 26 raw bytes in the standard 810
						// format from start of address to start of first byte, so including the
						// first byte, that's 26*8*4 = 832 cycles of latency until first DRQ.
						delay = VDRoundToInt(bestDistance * (float)kCyclesPerRotation + 832);
					} else {
						// Warp disk to start of sector data field.
						mRotPos = VDRoundToInt(bestRotPos * (float)kCyclesPerRotation) + 832;

						if (mRotPos >= kCyclesPerRotation)
							mRotPos -= kCyclesPerRotation;

						// Use short delay.
						delay = 1000;
					}

					mpDiskInterface->SetShowActivity(true, vsec);

					if (g_ATLCDisk.IsEnabled()) {
						g_ATLCDisk("Reading vsec=%3d (%d/%d) (trk=%d), psec=%3d, rot=%.2f >> %.2f >> %.2f%s.\n"
								, vsec
								, (uint32)bestPhysSec - vsi.mStartPhysSector + 1
								, vsi.mNumPhysSectors
								, (vsec - 1) / 18
								, (uint32)bestPhysSec
								, (float)mRotPos / (float)kCyclesPerRotation
								, bestRotPos
								, (float)mRotPos / (float)kCyclesPerRotation
								,  bestWeakDataOffset >= 0 ? " (w/weak bits)"
									: (mActiveSectorStatus & 0x02) ? " (w/long sector)"		// must use DRQ as lost data differs between drives
									: (mActiveSectorStatus & 0x08) ? " (w/CRC error)"
									: (mActiveSectorStatus & 0x10) ? " (w/missing sector)"
									: (mActiveSectorStatus & 0x20) ? " (w/deleted sector)"
									: ""
								);
					}
				}
			}

			SetTransition(kState_ReadSector_TransferByte, delay);
			break;
		}

		case kState_ReadSector_TransferByte:
			// check for lost data
			if (mbDataReadPending)
				mRegStatus |= 0x04;

			// set DRQ
			if (!mbDataReadPending) {
				mbDataReadPending = true;

				mpFnDrqChange(true);
			}

			mRegStatus |= 0x02;

			mRegData = mTransferBuffer[mTransferIndex];
			if (++mTransferIndex >= mTransferLength)
				SetTransition(kState_ReadSector_TransferComplete, 64);
			else
				SetTransition(kState_ReadSector_TransferByte, 32);
			break;

		case kState_ReadSector_TransferComplete:
			// Update status based on sector read:
			//
			//	bit 7 (not ready)			drop - recomputed
			//	bit 6 (record type)			drop - recomputed
			//	bit 5 (record type)			replicate to bit 5/6
			//	bit 4 (record not found)	keep
			//	bit 3 (CRC error)			keep
			//	bit 2 (lost data)			drop - driven by sequencer
			//	bit 1 (DRQ)					drop - driven by sequencer
			//	bit 0 (busy)				drop - driven by sequencer
			mRegStatus = (mRegStatus & ~0x78) + (mActiveSectorStatus & 0x38);
			mRegStatus += (mRegStatus & 0x20) * 2;

			SetTransition(kState_EndCommand, 1);
			break;

		case kState_WriteSector: {
			mTransferIndex = 0;
			mTransferLength = 0;

			UpdateRotationalPosition();

			if (!mpDiskInterface->IsDiskWritable()) {
				mRegStatus |= 0x40;
				SetTransition(kState_EndCommand, 1);
				break;
			}

			uint32 delay = kCyclesPerRotation * 2;

			mActiveSectorStatus = 0x10;

			if (mpDiskImage && mRegSector > 0 && mRegSector <= 18 && mbMotorRunning) {
				uint32 vsec = mPhysTrack * 18 + mRegSector;

				ATDiskVirtualSectorInfo vsi {};

				if (vsec <= mpDiskImage->GetVirtualSectorCount())
					mpDiskImage->GetVirtualSectorInfo(vsec - 1, vsi);

				if (vsi.mNumPhysSectors) {
					// find next sector in rotational order
					const float posf = (float)mRotPos / (float)kCyclesPerRotation;
					float bestDistance = 2.0f;
					float bestRotPos = 0;
					uint32 bestPhysSec = 0;
					int bestWeakDataOffset = -1;

					for(uint32 i=0; i<vsi.mNumPhysSectors; ++i) {
						ATDiskPhysicalSectorInfo psi {};

						mpDiskImage->GetPhysicalSectorInfo(vsi.mStartPhysSector + i, psi);

						float distance = psi.mRotPos - posf;
						distance -= floorf(distance);

						if (distance < bestDistance) {
							bestDistance = distance;
							bestPhysSec = vsi.mStartPhysSector + i;
							bestRotPos = psi.mRotPos;

							// We need uninverted status since we're in the FDC (inversion is
							// handled in the 810 emulation code).
							mActiveSectorStatus = ~psi.mFDCStatus;
							bestWeakDataOffset = psi.mWeakDataOffset;
							mTransferLength = std::min<uint32>(psi.mSize, vdcountof(mTransferBuffer));
						}
					}

					mActivePhysSector = bestPhysSec;

					// check for a long sector not actually stored in the image
					if ((mActiveSectorStatus & 0x02) && mTransferLength < 256)
						mTransferLength = 256;

					if (mbUseAccurateTiming) {
						// Compute rotational delay. It takes about 26 raw bytes in the standard 810
						// format from start of address to start of first byte, so including the
						// first byte, that's 26*8*4 = 832 cycles of latency until first DRQ.
						delay = VDRoundToInt(bestDistance * (float)kCyclesPerRotation + 832);
					} else {
						// Warp disk to start of sector data field.
						mRotPos = VDRoundToInt(bestRotPos * (float)kCyclesPerRotation) + 832;

						if (mRotPos >= kCyclesPerRotation)
							mRotPos -= kCyclesPerRotation;

						// Use short delay.
						delay = 1000;
					}

					mpDiskInterface->SetShowActivity(true, vsec);

					if (g_ATLCDisk.IsEnabled()) {
						g_ATLCDisk("Writing vsec=%3d (%d/%d) (trk=%d), psec=%3d, rot=%.2f >> %.2f >> %.2f%s.\n"
								, vsec
								, (uint32)bestPhysSec - vsi.mStartPhysSector + 1
								, vsi.mNumPhysSectors
								, (vsec - 1) / 18
								, (uint32)bestPhysSec
								, (float)mRotPos / (float)kCyclesPerRotation
								, bestRotPos
								, (float)mRotPos / (float)kCyclesPerRotation
								,  bestWeakDataOffset >= 0 ? " (w/weak bits)"
									: (mActiveSectorStatus & 0x02) ? " (w/long sector)"		// must use DRQ as lost data differs between drives
									: (mActiveSectorStatus & 0x08) ? " (w/CRC error)"
									: (mActiveSectorStatus & 0x10) ? " (w/missing sector)"
									: (mActiveSectorStatus & 0x20) ? " (w/deleted sector)"
									: ""
								);
					}
				}
			}

			if (mActiveSectorStatus & 0x10)
				SetTransition(kState_WriteSector_TransferComplete, delay);
			else
				SetTransition(kState_WriteSector_InitialDrq, delay);
			break;
		}

		case kState_WriteSector_InitialDrq:
			// Set DRQ and wait 9 bytes
			mbDataWritePending = true;
			mRegStatus |= 0x02;
			mpFnDrqChange(true);
			SetTransition(kState_WriteSector_CheckInitialDrq, 9 * kCyclesPerByte);
			break;

		case kState_WriteSector_CheckInitialDrq:
			if (mbDataWritePending) {
				// No initial data -- signal lost data and abort the command immediately
				mRegStatus |= 0x04;
				SetTransition(kState_WriteSector_TransferComplete, 1);
			} else {
				// Write DAM; DRQ does not get set again until after this occurs
				SetTransition(kState_WriteSector_TransferByte, 2 * kCyclesPerByte);
			}
			break;

		case kState_WriteSector_TransferByte:
			// Check for lost data and transfer to disk buffer; WD docs say that $00 is
			// written if data lost. Note that we need to invert the data as the disk image
			// interface expects computer-type noninverted data.
			if (mbDataWritePending) {
				mRegStatus |= 0x04;
				mTransferBuffer[mTransferIndex] = 0xFF;
			} else
				mTransferBuffer[mTransferIndex] = ~mRegData;

			// check if we have more bytes to write
			++mTransferIndex;
			if (mTransferIndex < mTransferLength) {
				// Yes, we do -- set DRQ, then wait a byte's time while this byte is
				// being written
				if (!mbDataWritePending) {
					mbDataWritePending = true;
					mRegStatus |= 0x02;

					mpFnDrqChange(true);
				}

				SetTransition(kState_WriteSector_TransferByte, kCyclesPerByte);
			} else {
				// No, we don't. Write the sector to persistent storage now, and delay four
				// bytes for the write timing for the remaining data (byte just transferred,
				// CRC 1 and 2, and $FF).
				if (mpDiskImage && mActivePhysSector < mpDiskImage->GetPhysicalSectorCount() && mpDiskInterface->IsDiskWritable()) {
					try {
						mpDiskInterface->OnDiskModified();
						mpDiskImage->WritePhysicalSector(mActivePhysSector, mTransferBuffer, mTransferLength);
					} catch(...) {
						// mark write fault
						mRegStatus |= 0x20;
					}
				} else {
					// mark write fault
					mRegStatus |= 0x20;
				}

				SetTransition(kState_WriteSector_TransferComplete, kCyclesPerByte * 4);
			}
			break;

		case kState_WriteSector_TransferComplete:
			// Update status based on sector write:
			//
			//	bit 7 (not ready)			drop - recomputed
			//	bit 6 (write protect)		drop - recomputed
			//	bit 5 (write fault)			drop - recomputed
			//	bit 4 (record not found)	keep
			//	bit 3 (CRC error)			keep
			//	bit 2 (lost data)			drop - driven by sequencer
			//	bit 1 (DRQ)					drop - driven by sequencer
			//	bit 0 (busy)				drop - driven by sequencer
			mRegStatus = (mRegStatus & ~0x18) + (mActiveSectorStatus & 0x18);

			SetTransition(kState_EndCommand, 1);
			break;

		case kState_ForceInterrupt:
			SetTransition(kState_ForceInterrupt_2, 1000);
			break;

		case kState_ForceInterrupt_2:
			SetTransition(kState_EndTypeICommand, 1);
			break;
	}
}

void ATFDCEmulator::SetTransition(State nextState, uint32 ticks) {
	mState = nextState;
	mpScheduler->SetEvent(ticks, this, 1, mpStateEvent);
}

void ATFDCEmulator::UpdateRotationalPosition() {
	if (mbMotorRunning) {
		const uint64 t = mpScheduler->GetTick64();
		mRotPos = (mRotPos + (t - mRotTimeBase)) % kCyclesPerRotation;
		mRotTimeBase = t;
	}
}
