/*
AppleWin : An Apple //e emulator for Windows

Copyright (C) 1994-1996, Michael O'Brien
Copyright (C) 1999-2001, Oliver Schmidt
Copyright (C) 2002-2005, Tom Charlesworth
Copyright (C) 2006-2015, Tom Charlesworth, Michael Pohoreski

AppleWin 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.

AppleWin 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 AppleWin; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* Description: Hard drive emulation
 *
 * Author: Copyright (c) 2005, Robert Hoem
 */

#include "StdAfx.h"

#include "Harddisk.h"
#include "Core.h"
#include "Interface.h"
#include "CPU.h"
#include "DiskImage.h"	// ImageError_e, Disk_Status_e
#include "Memory.h"
#include "Registry.h"
#include "SaveState.h"
#include "YamlHelper.h"

#include "Debugger/Debug.h"
#include "../resource/resource.h"

/*
Memory map (for slot 7):

    C0F0	(r)   EXECUTE AND RETURN STATUS
	C0F1	(r)   STATUS (or ERROR): b7=busy, b0=error
	C0F2	(r/w) COMMAND
	C0F3	(r/w) UNIT NUMBER
	C0F4	(r/w) LOW BYTE OF MEMORY BUFFER
	C0F5	(r/w) HIGH BYTE OF MEMORY BUFFER
	C0F6	(r/w) LOW BYTE OF BLOCK NUMBER
	C0F7	(r/w) HIGH BYTE OF BLOCK NUMBER
	C0F8    (r)   NEXT BYTE (legacy read-only port - still supported)

Firmware notes:
. ROR ABS16,X and ROL ABS16,X - only used for $C081+s*$10 STATUS register:
    6502:  double read (old data), write (old data), write (new data). The writes are harmless as writes to STATUS are ignored.
    65C02: double read (old data), write (new data). The write is harmless as writes to STATUS are ignored.
. STA ABS16,X does a false-read. This is harmless for writable I/O registers, since the false-read has no side effect.

*/

/*
Hard drive emulation in AppleWin.

Concept
    To emulate a 32mb hard drive connected to an Apple IIe via AppleWin.
    Designed to work with Autoboot Rom and Prodos.

Overview
  1. Hard drive image file
      The hard drive image file (.HDV) will be formatted into blocks of 512
      bytes, in a linear fashion. The internal formatting and meaning of each
      block to be decided by the Apple's operating system (ProDOS). To create
      an empty .HDV file, just create a 0 byte file.

  2. Emulation code
      There are 4 commands ProDOS will send to a block device.
      Listed below are each command and how it's handled:

      1. STATUS
          In the emulation's case, returns only a DEVICE OK (0), DEVICE I/O ERROR ($27) or DEVICE NOT CONNECTED ($28)
          DEVICE NOT CONNECTED only returned if no HDV file is selected.

      2. READ
          Loads requested block into a 512 byte buffer by attempting to seek to
            location in HDV file.
          If seek fails, returns a DEVICE I/O ERROR.  Resets m_buf_ptr used by legacy HD_NEXTBYTE
          Copies requested block from a 512 byte buffer to the Apple's memory.
          Sets STATUS.busy=1 until the DMA operation completes.
          Returns a DEVICE OK if read was successful, or a DEVICE I/O ERROR otherwise.

      3. WRITE
          Copies requested block from the Apple's memory to a 512 byte buffer
            then attempts to seek to requested block.
          If the seek fails (usually because the seek is beyond the EOF for the
            HDV file), the emulation will attempt to "grow" the HDV file to accommodate.
            Once the file can accommodate, or if the seek did not fail, the buffer is
            written to the HDV file.  NOTE: A2PC will grow *AND* shrink the HDV file.
		  Sets STATUS.busy=1 until the DMA operation completes.
          I didn't see the point in shrinking the file as this behaviour would require
            patching prodos (to detect DELETE FILE calls).

      4. FORMAT
          Ignored.  This would be used for low level formatting of the device
            (as in the case of a tape or SCSI drive, perhaps).

  3. Bugs
      The only thing I've noticed is that Copy II+ 7.1 seems to crash or stall
      occasionally when trying to calculate how many free blocks are available
      when running a catalog.  This might be due to the great number of blocks
      available.  Also, DDD pro will not optimise the disk correctly (it's
      doing a disk defragment of some sort, and when it requests a block outside
      the range of the image file, it starts getting I/O errors), so don't
      bother.  Any program that preforms a read before write to an "unwritten"
      block (a block that should be located beyond the EOF of the .HDV, which is
      valid for writing but not for reading until written to) will fail with I/O
      errors (although these are few and far between).

      I'm sure there are programs out there that may try to use the I/O ports in
      ways they weren't designed (like telling Ultima 5 that you have a Phasor
      sound card in slot 7 is a generally bad idea) will cause problems.
*/



HarddiskInterfaceCard::HarddiskInterfaceCard(UINT slot) :
	Card(CT_GenericHDD, slot)
{
	if (m_slot != SLOT5 && m_slot != SLOT7)	// fixme
		ThrowErrorInvalidSlot();

	m_unitNum = HARDDISK_1 << 7;	// b7=unit

	// The HDD interface has a single Command register for both drives:
	// . ProDOS will write to Command before switching drives
	m_command = 0;

	// Interface busy doing DMA for r/w when current cycle is earlier than this cycle
	m_notBusyCycle = 0;

	m_saveDiskImage = true;	// Save the DiskImage name to Registry
}

HarddiskInterfaceCard::~HarddiskInterfaceCard(void)
{
	CleanupDriveInternal(HARDDISK_1);
	CleanupDriveInternal(HARDDISK_2);
}

void HarddiskInterfaceCard::Reset(const bool powerCycle)
{
	m_hardDiskDrive[HARDDISK_1].m_error = 0;
	m_hardDiskDrive[HARDDISK_2].m_error = 0;
}

//===========================================================================

void HarddiskInterfaceCard::InitializeIO(LPBYTE pCxRomPeripheral)
{
	const DWORD HARDDISK_FW_SIZE = APPLE_SLOT_SIZE;

	BYTE* pData = GetFrame().GetResource(IDR_HDDRVR_FW, "FIRMWARE", HARDDISK_FW_SIZE);
	if (pData == NULL)
		return;

	memcpy(pCxRomPeripheral + m_slot * APPLE_SLOT_SIZE, pData, HARDDISK_FW_SIZE);

	RegisterIoHandler(m_slot, IORead, IOWrite, NULL, NULL, this, NULL);
}

//===========================================================================

void HarddiskInterfaceCard::CleanupDriveInternal(const int iDrive)
{
	if (m_hardDiskDrive[iDrive].m_imagehandle)
	{
		ImageClose(m_hardDiskDrive[iDrive].m_imagehandle);
		m_hardDiskDrive[iDrive].m_imagehandle = NULL;
	}

	m_hardDiskDrive[iDrive].m_imageloaded = false;

	m_hardDiskDrive[iDrive].m_imagename.clear();
	m_hardDiskDrive[iDrive].m_fullname.clear();
	m_hardDiskDrive[iDrive].m_strFilenameInZip.clear();
}

void HarddiskInterfaceCard::CleanupDrive(const int iDrive)
{
	CleanupDriveInternal(iDrive);

	SaveLastDiskImage(iDrive);
}

//===========================================================================

void HarddiskInterfaceCard::NotifyInvalidImage(const std::string & szImageFilename)
{
	// TC: TO DO - see Disk2InterfaceCard::NotifyInvalidImage()

	std::string strText = StrFormat("Unable to open the file %s.",
									szImageFilename.c_str());

	GetFrame().FrameMessageBox(strText.c_str(),
							   g_pAppTitle.c_str(),
							   MB_ICONEXCLAMATION | MB_SETFOREGROUND);
}

//===========================================================================

void HarddiskInterfaceCard::LoadLastDiskImage(const int drive)
{
	_ASSERT(drive == HARDDISK_1 || drive == HARDDISK_2);

	const std::string regKey = (drive == HARDDISK_1)
		? REGVALUE_LAST_HARDDISK_1
		: REGVALUE_LAST_HARDDISK_2;

	char pathname[MAX_PATH];

	std::string regSection = RegGetConfigSlotSection(m_slot);
	if (RegLoadString(regSection.c_str(), regKey.c_str(), TRUE, pathname, MAX_PATH, TEXT("")) && (pathname[0] != 0))
	{
		m_saveDiskImage = false;
		bool res = Insert(drive, pathname);
		m_saveDiskImage = true;

		if (!res)
		{
			NotifyInvalidImage(pathname);
			CleanupDrive(drive);
		}
	}
}

//===========================================================================

void HarddiskInterfaceCard::SaveLastDiskImage(const int drive)
{
	_ASSERT(drive == HARDDISK_1 || drive == HARDDISK_2);

	if (!m_saveDiskImage)
		return;

	std::string regSection = RegGetConfigSlotSection(m_slot);
	RegSaveValue(regSection.c_str(), REGVALUE_CARD_TYPE, TRUE, CT_GenericHDD);

	const std::string regKey = (drive == HARDDISK_1)
		? REGVALUE_LAST_HARDDISK_1
		: REGVALUE_LAST_HARDDISK_2;

	const std::string& pathName = HarddiskGetFullPathName(drive);

	RegSaveString(regSection.c_str(), regKey.c_str(), TRUE, pathName);

	//

	// For now, only update 'HDV Starting Directory' for slot7 & drive1
	// . otherwise you'll get inconsistent results if you set drive1, then drive2 (and the images were in different folders)
	if (m_slot != SLOT7 || drive != HARDDISK_1)
		return;

	const size_t slash = pathName.find_last_of(PATH_SEPARATOR);
	if (slash != std::string::npos)
	{
		const std::string dirName = pathName.substr(0, slash + 1);
		RegSaveString(REG_PREFS, REGVALUE_PREF_HDV_START_DIR, 1, dirName);
	}
}

//===========================================================================

const std::string& HarddiskInterfaceCard::GetFullName(const int iDrive)
{
	return m_hardDiskDrive[iDrive].m_fullname;
}

const std::string& HarddiskInterfaceCard::HarddiskGetFullPathName(const int iDrive)
{
	return ImageGetPathname(m_hardDiskDrive[iDrive].m_imagehandle);
}

const std::string& HarddiskInterfaceCard::DiskGetBaseName(const int iDrive)
{
	return m_hardDiskDrive[iDrive].m_imagename;
}

void HarddiskInterfaceCard::GetFilenameAndPathForSaveState(std::string& filename, std::string& path)
{
	filename = "";
	path = "";

	for (UINT i=HARDDISK_1; i<=HARDDISK_2; i++)
	{
		if (!m_hardDiskDrive[i].m_imageloaded)
			continue;

		filename = DiskGetBaseName(i);
		std::string pathname = HarddiskGetFullPathName(i);

		int idx = pathname.find_last_of(PATH_SEPARATOR);
		if (idx >= 0 && idx+1 < (int)pathname.length())	// path exists?
		{
			path = pathname.substr(0, idx+1);
			return;
		}

		_ASSERT(0);
		break;
	}
}

//===========================================================================

void HarddiskInterfaceCard::Destroy(void)
{
	m_saveDiskImage = false;
	CleanupDrive(HARDDISK_1);

	m_saveDiskImage = false;
	CleanupDrive(HARDDISK_2);

	m_saveDiskImage = true;
}

//===========================================================================

// Pre: pathname likely to include path (but can also just be filename)
bool HarddiskInterfaceCard::Insert(const int iDrive, const std::string& pathname)
{
	if (pathname.empty())
		return false;

	if (m_hardDiskDrive[iDrive].m_imageloaded)
		Unplug(iDrive);

	// Check if image is being used by the other HDD, and unplug it in order to be swapped
	{
		const std::string & pszOtherPathname = HarddiskGetFullPathName(!iDrive);

		char szCurrentPathname[MAX_PATH]; 
		DWORD uNameLen = GetFullPathName(pathname.c_str(), MAX_PATH, szCurrentPathname, NULL);
		if (uNameLen == 0 || uNameLen >= MAX_PATH)
			strcpy_s(szCurrentPathname, MAX_PATH, pathname.c_str());

		if (!strcmp(pszOtherPathname.c_str(), szCurrentPathname))
		{
			Unplug(!iDrive);
			GetFrame().FrameRefreshStatus(DRAW_LEDS | DRAW_DISK_STATUS);
		}
	}

	const bool bCreateIfNecessary = false;		// NB. Don't allow creation of HDV files
	const bool bExpectFloppy = false;
	const bool bIsHarddisk = true;
	ImageError_e Error = ImageOpen(pathname,
		&m_hardDiskDrive[iDrive].m_imagehandle,
		&m_hardDiskDrive[iDrive].m_bWriteProtected,
		bCreateIfNecessary,
		m_hardDiskDrive[iDrive].m_strFilenameInZip,	// TODO: Use this
		bExpectFloppy);

	m_hardDiskDrive[iDrive].m_imageloaded = (Error == eIMAGE_ERROR_NONE);

	m_hardDiskDrive[iDrive].m_status_next = DISK_STATUS_OFF;
	m_hardDiskDrive[iDrive].m_status_prev = DISK_STATUS_OFF;

	if (Error == eIMAGE_ERROR_NONE)
	{
		GetImageTitle(pathname.c_str(), m_hardDiskDrive[iDrive].m_imagename, m_hardDiskDrive[iDrive].m_fullname);
		Snapshot_UpdatePath();
	}

	SaveLastDiskImage(iDrive);

	return m_hardDiskDrive[iDrive].m_imageloaded;
}

//-----------------------------------------------------------------------------

bool HarddiskInterfaceCard::SelectImage(const int drive, LPCSTR pszFilename)
{
	TCHAR directory[MAX_PATH];
	TCHAR filename[MAX_PATH];

	StringCbCopy(filename, MAX_PATH, pszFilename);

	RegLoadString(TEXT(REG_PREFS), TEXT(REGVALUE_PREF_HDV_START_DIR), 1, directory, MAX_PATH, TEXT(""));
	std::string title = StrFormat("Select HDV Image For HDD %d", drive + 1);

	OPENFILENAME ofn;
	memset(&ofn, 0, sizeof(OPENFILENAME));
	ofn.lStructSize     = sizeof(OPENFILENAME);
	ofn.hwndOwner       = GetFrame().g_hFrameWindow;
	ofn.hInstance       = GetFrame().g_hInstance;
	ofn.lpstrFilter     = TEXT("Hard Disk Images (*.hdv,*.po,*.2mg,*.2img,*.gz,*.zip)\0*.hdv;*.po;*.2mg;*.2img;*.gz;*.zip\0")
						  TEXT("All Files\0*.*\0");
	ofn.lpstrFile       = filename;
	ofn.nMaxFile        = MAX_PATH;
	ofn.lpstrInitialDir = directory;
	ofn.Flags           = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;	// Don't allow creation & hide the read-only checkbox
	ofn.lpstrTitle      = title.c_str();

	bool bRes = false;

	if (GetOpenFileName(&ofn))
	{
		std::string openFilename = filename;
		if ((!ofn.nFileExtension) || !filename[ofn.nFileExtension])
			openFilename += TEXT(".hdv");
		
		if (Insert(drive, openFilename))
		{
			bRes = true;
		}
		else
		{
			NotifyInvalidImage(openFilename);
		}
	}

	return bRes;
}

bool HarddiskInterfaceCard::Select(const int iDrive)
{
	return SelectImage(iDrive, TEXT(""));
}

//===========================================================================

void HarddiskInterfaceCard::Unplug(const int iDrive)
{
	if (m_hardDiskDrive[iDrive].m_imageloaded)
	{
		CleanupDrive(iDrive);
		Snapshot_UpdatePath();
	}
}

bool HarddiskInterfaceCard::IsDriveUnplugged(const int iDrive)
{
	return m_hardDiskDrive[iDrive].m_imageloaded == false;
}

//===========================================================================

#define DEVICE_OK				0x00
#define DEVICE_IO_ERROR			0x27
#define DEVICE_NOT_CONNECTED	0x28	// No device detected/connected

BYTE __stdcall HarddiskInterfaceCard::IORead(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG nExecutedCycles)
{
	const UINT slot = ((addr & 0xff) >> 4) - 8;
	HarddiskInterfaceCard* pCard = (HarddiskInterfaceCard*)MemGetSlotParameters(slot);
	HardDiskDrive* pHDD = &(pCard->m_hardDiskDrive[pCard->m_unitNum >> 7]);	// bit7 = drive select

	CpuCalcCycles(nExecutedCycles);
	const UINT CYCLES_FOR_DMA_RW_BLOCK = HD_BLOCK_SIZE;
	const UINT PAGE_SIZE = 256;

	BYTE r = DEVICE_OK;
	pHDD->m_status_next = DISK_STATUS_READ;

	switch (addr & 0xF)
	{
		case 0x0:
			if (pHDD->m_imageloaded)
			{
				// based on loaded data block request, load block into memory
				// returns status
				switch (pCard->m_command)
				{
					default:
					case 0x00: //status
						if (ImageGetImageSize(pHDD->m_imagehandle) == 0)
						{
							pHDD->m_error = 1;
							r = DEVICE_IO_ERROR;
						}
						break;
					case 0x01: //read
						if ((pHDD->m_diskblock * HD_BLOCK_SIZE) < ImageGetImageSize(pHDD->m_imagehandle))
						{
							bool breakpointHit = false;

							bool bRes = ImageReadBlock(pHDD->m_imagehandle, pHDD->m_diskblock, pHDD->m_buf);
							if (bRes)
							{
								pHDD->m_buf_ptr = 0;

								// Apple II's MMU could be setup so that read & write memory is different,
								// so can't use 'mem' (like we can for HDD block writes)
								WORD dstAddr = pHDD->m_memblock;
								UINT remaining = HD_BLOCK_SIZE;
								BYTE* pSrc = pHDD->m_buf;

								while (remaining)
								{
									memdirty[dstAddr >> 8] = 0xFF;
									LPBYTE page = memwrite[dstAddr >> 8];
									if (!page)	// I/O space or ROM
									{
										if (g_nAppMode == MODE_STEPPING)
											DebuggerBreakOnDmaToOrFromIoMemory(dstAddr, true);	//  GH#1007
										//else // Show MessageBox?

										bRes = false;
										break;
									}

									// handle both page-aligned & non-page aligned destinations
									UINT size = PAGE_SIZE - (dstAddr & 0xff);
									if (size > remaining) size = remaining;	// clip the last memcpy for the unaligned case

									if (g_nAppMode == MODE_STEPPING)
										breakpointHit = DebuggerCheckMemBreakpoints(dstAddr, size, true);	// GH#1103

									memcpy(page + (dstAddr & 0xff), pSrc, size);
									pSrc += size;
									dstAddr = (dstAddr + size) & (MEMORY_LENGTH-1);	// wraps at 64KiB boundary

									remaining -= size;
								}
							}

							if (bRes)
							{
								pHDD->m_error = 0;
								r = 0;

								if (!breakpointHit)
									pCard->m_notBusyCycle = g_nCumulativeCycles + (UINT64)CYCLES_FOR_DMA_RW_BLOCK;
							}
							else
							{
								pHDD->m_error = 1;
								r = DEVICE_IO_ERROR;
							}
						}
						else
						{
							pHDD->m_error = 1;
							r = DEVICE_IO_ERROR;
						}
						break;
					case 0x02: //write
						{
							pHDD->m_status_next = DISK_STATUS_WRITE;	// or DISK_STATUS_PROT if we ever enable write-protect on HDD
							bool bRes = true;
							const bool bAppendBlocks = (pHDD->m_diskblock * HD_BLOCK_SIZE) >= ImageGetImageSize(pHDD->m_imagehandle);
							bool breakpointHit = false;

							if (bAppendBlocks)
							{
								memset(pHDD->m_buf, 0, HD_BLOCK_SIZE);

								// Inefficient (especially for gzip/zip files!)
								UINT uBlock = ImageGetImageSize(pHDD->m_imagehandle) / HD_BLOCK_SIZE;
								while (uBlock < pHDD->m_diskblock)
								{
									bRes = ImageWriteBlock(pHDD->m_imagehandle, uBlock++, pHDD->m_buf);
									_ASSERT(bRes);
									if (!bRes)
										break;
								}
							}

							// Trap and error on any accesses that overlap with I/O memory (GH#1007)
							if ((pHDD->m_memblock < APPLE_IO_BEGIN && ((pHDD->m_memblock + HD_BLOCK_SIZE - 1) >= APPLE_IO_BEGIN))	// 1) Starts before I/O, but ends in I/O memory
								|| ((pHDD->m_memblock >> 12) == (APPLE_IO_BEGIN >> 12)))											// 2) Starts in I/O memory
							{
								WORD dstAddr = ((pHDD->m_memblock >> 12) == (APPLE_IO_BEGIN >> 12)) ? pHDD->m_memblock : APPLE_IO_BEGIN;

								if (g_nAppMode == MODE_STEPPING)
									DebuggerBreakOnDmaToOrFromIoMemory(dstAddr, false);
								//else // Show MessageBox?

								bRes = false;
							}
							else
							{
								// NB. Do the writes in units of PAGE_SIZE so that DMA breakpoints are consistent with reads
								WORD srcAddr = pHDD->m_memblock;
								UINT remaining = HD_BLOCK_SIZE;
								BYTE* pDst = pHDD->m_buf;

								while (remaining)
								{
									UINT size = PAGE_SIZE - (srcAddr & 0xff);
									if (size > remaining) size = remaining;	// clip the last memcpy for the unaligned case

									if (g_nAppMode == MODE_STEPPING)
										breakpointHit = DebuggerCheckMemBreakpoints(srcAddr, size, false);

									memcpy(pDst, mem + srcAddr, size);
									pDst += size;
									srcAddr = (srcAddr + size) & (MEMORY_LENGTH - 1);	// wraps at 64KiB boundary

									remaining -= size;
								}
							}

							if (bRes)
								bRes = ImageWriteBlock(pHDD->m_imagehandle, pHDD->m_diskblock, pHDD->m_buf);

							if (bRes)
							{
								pHDD->m_error = 0;
								r = 0;

								if (!breakpointHit)
									pCard->m_notBusyCycle = g_nCumulativeCycles + (UINT64)CYCLES_FOR_DMA_RW_BLOCK;
							}
							else
							{
								pHDD->m_error = 1;
								r = DEVICE_IO_ERROR;
							}
						}
						break;
					case 0x03: //format
						pHDD->m_status_next = DISK_STATUS_WRITE;	// or DISK_STATUS_PROT if we ever enable write-protect on HDD
						break;
				}
			}
			else
			{
				pHDD->m_status_next = DISK_STATUS_OFF;
				pHDD->m_error = 1;
				r = DEVICE_NOT_CONNECTED;	// GH#452
			}
		break;
	case 0x1: // m_error
		if (pHDD->m_error & 0x7f)
			pHDD->m_error = 1;		// Firmware requires that b0=1 for an error
		else
			pHDD->m_error = 0;

		if (g_nCumulativeCycles <= pCard->m_notBusyCycle)
			pHDD->m_error |= 0x80;	// Firmware requires that b7=1 for busy (eg. busy doing r/w DMA operation)
		else
			pHDD->m_status_next = DISK_STATUS_OFF; // TODO: FIXME: ??? YELLOW ??? WARNING

		r = pHDD->m_error;
		break;
	case 0x2:
		r = pCard->m_command;
		break;
	case 0x3:
		r = pCard->m_unitNum;
		break;
	case 0x4:
		r = (BYTE)(pHDD->m_memblock & 0x00FF);
		break;
	case 0x5:
		r = (BYTE)(pHDD->m_memblock & 0xFF00 >> 8);
		break;
	case 0x6:
		r = (BYTE)(pHDD->m_diskblock & 0x00FF);
		break;
	case 0x7:
		r = (BYTE)(pHDD->m_diskblock & 0xFF00 >> 8);
		break;
	case 0x8:	// Legacy: continue to support this I/O port for old HDD firmware
		r = pHDD->m_buf[pHDD->m_buf_ptr];
		if (pHDD->m_buf_ptr < sizeof(pHDD->m_buf)-1)
			pHDD->m_buf_ptr++;
		break;
	default:
		pHDD->m_status_next = DISK_STATUS_OFF;
		r = IO_Null(pc, addr, bWrite, d, nExecutedCycles);
	}

	pCard->UpdateLightStatus(pHDD);
	return r;
}

//-----------------------------------------------------------------------------

BYTE __stdcall HarddiskInterfaceCard::IOWrite(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG nExecutedCycles)
{
	const UINT slot = ((addr & 0xff) >> 4) - 8;
	HarddiskInterfaceCard* pCard = (HarddiskInterfaceCard*)MemGetSlotParameters(slot);
	HardDiskDrive* pHDD = &(pCard->m_hardDiskDrive[pCard->m_unitNum >> 7]);	// bit7 = drive select

	BYTE r = DEVICE_OK;

	switch (addr & 0xF)
	{
	case 0x0:	// r/o: status
	case 0x1:	// r/o: execute
	case 0x8:	// r/o: legacy next-data port
		// Writing to these 3 read-only registers is a no-op.
		// NB. Don't change m_status_next, as UpdateLightStatus() has a huge performance cost!
		// Firmware has a busy-wait loop doing "rol hd_status,x"
		// - this RMW opcode does an IORead() then an IOWrite(), and the loop iterates ~100 times!
		break;
	case 0x2:
		pCard->m_command = d;
		break;
	case 0x3:
		// b7    = drive#
		// b6..4 = slot#
		// b3..0 = ?
		pCard->m_unitNum = d;
		break;
	case 0x4:
		pHDD->m_memblock = (pHDD->m_memblock & 0xFF00) | d;
		break;
	case 0x5:
		pHDD->m_memblock = (pHDD->m_memblock & 0x00FF) | (d << 8);
		break;
	case 0x6:
		pHDD->m_diskblock = (pHDD->m_diskblock & 0xFF00) | d;
		break;
	case 0x7:
		pHDD->m_diskblock = (pHDD->m_diskblock & 0x00FF) | (d << 8);
		break;
	default:
		pHDD->m_status_next = DISK_STATUS_OFF;
		r = IO_Null(pc, addr, bWrite, d, nExecutedCycles);
	}

	pCard->UpdateLightStatus(pHDD);
	return r;
}

//===========================================================================

void HarddiskInterfaceCard::UpdateLightStatus(HardDiskDrive* pHDD)
{
	if (pHDD->m_status_prev != pHDD->m_status_next) // Update LEDs if state changes
	{
		pHDD->m_status_prev = pHDD->m_status_next;
		GetFrame().FrameRefreshStatus(DRAW_LEDS | DRAW_DISK_STATUS);
	}
}

void HarddiskInterfaceCard::GetLightStatus(Disk_Status_e *pDisk1Status)
{
	HardDiskDrive* pHDD = &m_hardDiskDrive[m_unitNum >> 7];	// bit7 = drive select
	*pDisk1Status = pHDD->m_status_prev;
}

//===========================================================================

bool HarddiskInterfaceCard::ImageSwap(void)
{
	std::swap(m_hardDiskDrive[HARDDISK_1], m_hardDiskDrive[HARDDISK_2]);

	SaveLastDiskImage(HARDDISK_1);
	SaveLastDiskImage(HARDDISK_2);

	GetFrame().FrameRefreshStatus(DRAW_LEDS);

	return true;
}

//===========================================================================

// Unit version history:
// 2: Updated $C7nn firmware to fix GH#319
// 3: Updated $Csnn firmware to fix GH#996 (now slot-independent code)
//    Added: Not Busy Cycle
static const UINT kUNIT_VERSION = 3;

#define SS_YAML_VALUE_CARD_HDD "Generic HDD"

#define SS_YAML_KEY_CURRENT_UNIT "Current Unit"
#define SS_YAML_KEY_COMMAND "Command"

#define SS_YAML_KEY_HDDUNIT "Unit"
#define SS_YAML_KEY_FILENAME "Filename"
#define SS_YAML_KEY_ERROR "Error"
#define SS_YAML_KEY_MEMBLOCK "MemBlock"
#define SS_YAML_KEY_DISKBLOCK "DiskBlock"
#define SS_YAML_KEY_IMAGELOADED "ImageLoaded"
#define SS_YAML_KEY_STATUS_NEXT "Status Next"
#define SS_YAML_KEY_STATUS_PREV "Status Prev"
#define SS_YAML_KEY_BUF_PTR "Buffer Offset"
#define SS_YAML_KEY_BUF "Buffer"
#define SS_YAML_KEY_NOT_BUSY_CYCLE "Not Busy Cycle"

const std::string& HarddiskInterfaceCard::GetSnapshotCardName(void)
{
	static const std::string name(SS_YAML_VALUE_CARD_HDD);
	return name;
}

void HarddiskInterfaceCard::SaveSnapshotHDDUnit(YamlSaveHelper& yamlSaveHelper, UINT unit)
{
	YamlSaveHelper::Label label(yamlSaveHelper, "%s%d:\n", SS_YAML_KEY_HDDUNIT, unit);
	yamlSaveHelper.SaveString(SS_YAML_KEY_FILENAME, m_hardDiskDrive[unit].m_fullname);
	yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_ERROR, m_hardDiskDrive[unit].m_error);
	yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_MEMBLOCK, m_hardDiskDrive[unit].m_memblock);
	yamlSaveHelper.SaveHexUint32(SS_YAML_KEY_DISKBLOCK, m_hardDiskDrive[unit].m_diskblock);
	yamlSaveHelper.SaveBool(SS_YAML_KEY_IMAGELOADED, m_hardDiskDrive[unit].m_imageloaded);
	yamlSaveHelper.SaveUint(SS_YAML_KEY_STATUS_NEXT, m_hardDiskDrive[unit].m_status_next);
	yamlSaveHelper.SaveUint(SS_YAML_KEY_STATUS_PREV, m_hardDiskDrive[unit].m_status_prev);
	yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_BUF_PTR, m_hardDiskDrive[unit].m_buf_ptr);

	// New label
	{
		YamlSaveHelper::Label buffer(yamlSaveHelper, "%s:\n", SS_YAML_KEY_BUF);
		yamlSaveHelper.SaveMemory(m_hardDiskDrive[unit].m_buf, HD_BLOCK_SIZE);
	}
}

void HarddiskInterfaceCard::SaveSnapshot(YamlSaveHelper& yamlSaveHelper)
{
	YamlSaveHelper::Slot slot(yamlSaveHelper, GetSnapshotCardName(), m_slot, kUNIT_VERSION);

	YamlSaveHelper::Label state(yamlSaveHelper, "%s:\n", SS_YAML_KEY_STATE);
	yamlSaveHelper.Save("%s: %d # b7=unit\n", SS_YAML_KEY_CURRENT_UNIT, m_unitNum);
	yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_COMMAND, m_command);
	yamlSaveHelper.SaveHexUint64(SS_YAML_KEY_NOT_BUSY_CYCLE, m_notBusyCycle);

	SaveSnapshotHDDUnit(yamlSaveHelper, HARDDISK_1);
	SaveSnapshotHDDUnit(yamlSaveHelper, HARDDISK_2);
}

bool HarddiskInterfaceCard::LoadSnapshotHDDUnit(YamlLoadHelper& yamlLoadHelper, UINT unit)
{
	std::string hddUnitName = std::string(SS_YAML_KEY_HDDUNIT) + (unit == HARDDISK_1 ? std::string("0") : std::string("1"));
	if (!yamlLoadHelper.GetSubMap(hddUnitName))
		throw std::runtime_error("Card: Expected key: " + hddUnitName);

	m_hardDiskDrive[unit].m_fullname.clear();
	m_hardDiskDrive[unit].m_imagename.clear();
	m_hardDiskDrive[unit].m_imageloaded = false;	// Default to false (until image is successfully loaded below)
	m_hardDiskDrive[unit].m_status_next = DISK_STATUS_OFF;
	m_hardDiskDrive[unit].m_status_prev = DISK_STATUS_OFF;

	std::string filename = yamlLoadHelper.LoadString(SS_YAML_KEY_FILENAME);
	m_hardDiskDrive[unit].m_error = yamlLoadHelper.LoadUint(SS_YAML_KEY_ERROR);
	m_hardDiskDrive[unit].m_memblock = yamlLoadHelper.LoadUint(SS_YAML_KEY_MEMBLOCK);
	m_hardDiskDrive[unit].m_diskblock = yamlLoadHelper.LoadUint(SS_YAML_KEY_DISKBLOCK);
	yamlLoadHelper.LoadBool(SS_YAML_KEY_IMAGELOADED);	// Consume
	Disk_Status_e diskStatusNext = (Disk_Status_e) yamlLoadHelper.LoadUint(SS_YAML_KEY_STATUS_NEXT);
	Disk_Status_e diskStatusPrev = (Disk_Status_e) yamlLoadHelper.LoadUint(SS_YAML_KEY_STATUS_PREV);
	m_hardDiskDrive[unit].m_buf_ptr = yamlLoadHelper.LoadUint(SS_YAML_KEY_BUF_PTR);
	if (m_hardDiskDrive[unit].m_buf_ptr >= sizeof(m_hardDiskDrive[unit].m_buf))	// pre-v3 save-states would leave m_buf_ptr==0x200 after reading a block
		m_hardDiskDrive[unit].m_buf_ptr = sizeof(m_hardDiskDrive[unit].m_buf) - 1;

	if (!yamlLoadHelper.GetSubMap(SS_YAML_KEY_BUF))
		throw std::runtime_error(hddUnitName + ": Missing: " + SS_YAML_KEY_BUF);
	yamlLoadHelper.LoadMemory(m_hardDiskDrive[unit].m_buf, HD_BLOCK_SIZE);

	yamlLoadHelper.PopMap();
	yamlLoadHelper.PopMap();

	//

	bool bResSelectImage = false;

	if (!filename.empty())
	{
		DWORD dwAttributes = GetFileAttributes(filename.c_str());
		if (dwAttributes == INVALID_FILE_ATTRIBUTES)
		{
			// Get user to browse for file
			bResSelectImage = SelectImage(unit, filename.c_str());

			dwAttributes = GetFileAttributes(filename.c_str());
		}

		bool bImageError = (dwAttributes == INVALID_FILE_ATTRIBUTES);
		if (!bImageError)
		{
			if (!Insert(unit, filename.c_str()))
				bImageError = true;

			// HD_Insert() sets up:
			// . m_imagename
			// . m_fullname
			// . m_imageloaded
			// . hd_status_next = DISK_STATUS_OFF
			// . hd_status_prev = DISK_STATUS_OFF

			m_hardDiskDrive[unit].m_status_next = diskStatusNext;
			m_hardDiskDrive[unit].m_status_prev = diskStatusPrev;
		}
	}

	return bResSelectImage;
}

bool HarddiskInterfaceCard::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT version)
{
	if (version < 1 || version > kUNIT_VERSION)
		ThrowErrorInvalidVersion(version);

	if (version <= 2 && (regs.pc >> 8) == (0xC0|m_slot))
		throw std::runtime_error("HDD card: 6502 is running old HDD firmware");

	m_unitNum = yamlLoadHelper.LoadUint(SS_YAML_KEY_CURRENT_UNIT);	// b7=unit
	m_command = yamlLoadHelper.LoadUint(SS_YAML_KEY_COMMAND);

	if (version >= 3)
		m_notBusyCycle = yamlLoadHelper.LoadUint64(SS_YAML_KEY_NOT_BUSY_CYCLE);

	// Unplug all HDDs first in case HDD-2 is to be plugged in as HDD-1
	for (UINT i=0; i<NUM_HARDDISKS; i++)
	{
		Unplug(i);
		m_hardDiskDrive[i].clear();
	}

	bool bResSelectImage1 = LoadSnapshotHDDUnit(yamlLoadHelper, HARDDISK_1);
	bool bResSelectImage2 = LoadSnapshotHDDUnit(yamlLoadHelper, HARDDISK_2);

	if (!bResSelectImage1 && !bResSelectImage2)
		RegSaveString(TEXT(REG_PREFS), TEXT(REGVALUE_PREF_HDV_START_DIR), 1, Snapshot_GetPath());

	GetFrame().FrameRefreshStatus(DRAW_LEDS | DRAW_DISK_STATUS);

	return true;
}
