//
// Hard drive support
//
// by James Hammons
// (C) 2019 Underground Software
//
// This is done by emulating the Apple 2 High-Speed SCSI card.
//
// How it works:
//
// First 1K is the driver ROM, repeated four times.  After that, there are 31 1K
// chunks that are addressed in the $CC00-$CFFF address range; $C800-$CBFF is a
// 1K RAM space (8K static RAM, bank switched).
//

#include "harddrive.h"
#include "apple2.h"
#include "dis65c02.h"
#include "fileio.h"
#include "firmware/a2hs-scsi.h"
#include "log.h"
#include "mmu.h"
#include "settings.h"
#include "v65c02.h"		// For dumpDis...


static uint8_t romBank = 0;
static uint8_t ramBank = 0;
static uint8_t deviceID = 7;
static bool dmaSwitch = false;
static uint8_t staticRAM[0x2000] = { 0 };
static uint8_t reg[16];

// Stuff that will have to GTFO of here
static uint8_t * hdData = NULL;

enum {
	DVM_DATA_OUT = 0, DVM_DATA_IN = 1, DVM_COMMAND = 2, DVM_STATUS = 3,
	DVM_MESSAGE_OUT = 6, DVM_MESSAGE_IN = 7, DVM_BUS_FREE = 8,
	DVM_ARBITRATE = 16, DVM_SELECT = 32
};

static bool DATA_BUS = false;
static bool DMA_MODE = false;
static bool BSY = false;
static bool ATN = false;
static bool SEL = false;
static bool ACK = false;
static bool RST = false;
static bool MSG = false;
static bool C_D = false;
static bool I_O = false;
static bool REQ = false;
static bool DEV_BSY = false;
static bool DRQ = false;
static bool DACK = false;
static uint8_t devMode = DVM_BUS_FREE;
static uint8_t cmdLength;
static uint8_t cmd[256];
static uint32_t bytesToSend;
static uint8_t * buf;
static uint32_t bufPtr;
static uint8_t response;


static inline void SetNextState(uint8_t state)
{
	devMode = state;
	MSG = (state & 0x04 ? true : false);
	C_D = (state & 0x02 ? true : false);
	I_O = (state & 0x01 ? true : false);
	cmdLength = 0;
}


static void RunDevice(void)
{
	// Let's see where it's really going...
/*	if (mainCPU.pc == 0xCE7E)
		dumpDis = true;//*/

	// These are SCSI messages sent in response to certain commands
	static uint8_t readCapacity[8] = { 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00 };
	static uint8_t inquireData[30] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'S', 'E', 'A', 'G', 'A', 'T', 'E', ' ', '3', '1', '3', '3', '7', ' ' };
	static uint8_t badSense[20] = { 0x70, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

	if (RST)
	{
		devMode = DVM_BUS_FREE;
		DEV_BSY = false;
		return;
	}

	switch (devMode)
	{
	case DVM_BUS_FREE:
		// We never initiate, so this we don't worry about whether or not the
		// bus is free.
	case DVM_ARBITRATE:
		// Likewise, we don't arbitrate either.
		break;

	case DVM_SELECT:
		// If we're in Selection phase, see if our ID is on the bus, and, if so,
		// go on to the next phase (since the Target drives the phase dance).
		if ((reg[0] & 0x40) && DATA_BUS)
		{
			DEV_BSY = true;

			// Preset response code to "Good"
			response = 0x00;

			if (ATN)
				SetNextState(DVM_MESSAGE_OUT);
			else
				// If no ATN is asserted, go to COMMAND?  Dunno, the firmware
				// doesn't ever go there; it *always* starts with MESSAGE OUT.
				SetNextState(DVM_COMMAND);
		}

		break;

	case DVM_DATA_OUT:
//WriteLog("   >>> DATA OUT PHASE (bts=%u)\n", bytesToSend);
		if (!ACK)
			REQ = true;

		if (DMA_MODE)
		{
			if (!DACK)
			{
				DRQ = true;
			}
			else if (DRQ && DACK)
			{
				if (buf)
					buf[bufPtr] = reg[0];

				DRQ = DACK = false;
				bytesToSend--;
				bufPtr++;

				if (bytesToSend == 0)
				{
					REQ = false;
					SetNextState(DVM_STATUS);
					buf = NULL;
				}
			}
		}

		break;

	case DVM_DATA_IN:
//WriteLog("   >>> DATA IN PHASE (bts=%u)\n", bytesToSend);
		if (!ACK)
			REQ = true;

		if (DMA_MODE)
		{
			if (!DACK)
			{
				// If there's no buffer set up, send zeroes...
				reg[6] = (buf == NULL ? 0 : buf[bufPtr]);
				DRQ = true;
			}
			else if (DRQ && DACK)
			{
				DRQ = DACK = false;
				bytesToSend--;
				bufPtr++;

				if (bytesToSend == 0)
				{
					REQ = false;
					SetNextState(DVM_STATUS);
					buf = NULL;
				}
			}
		}

		break;

	case DVM_COMMAND:
	{
		if (!ACK)
			REQ = true;
		else if (REQ && ACK)
		{
			cmd[cmdLength++] = reg[0];
			REQ = false;
		}

		uint8_t cmdType = (cmd[0] & 0xE0) >> 5;

		if ((cmdType == 0) && (cmdLength == 6))
		{
			// Handle "Test Unit Ready" command
			if (cmd[0] == 0)
			{
				WriteLog("HD: Received command TEST UNIT READY\n");
				SetNextState(DVM_STATUS);
			}
			// Handle "Request Sense" command
			else if (cmd[0] == 0x03)
			{
				WriteLog("HD: Received command REQUEST SENSE [%02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5]);
				SetNextState(DVM_DATA_IN);
				bytesToSend = cmd[4];

				// Return error for LUNs other than 0
				if ((cmd[1] & 0xE0) != 0)
				{
					buf = badSense;
					bufPtr = 0;
				}
			}
			// Handle "Read" (6) command
			else if (cmd[0] == 0x08)
			{
				WriteLog("HD: Received command READ(6) [%02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5]);
				SetNextState(DVM_DATA_IN);
				bytesToSend = cmd[4] * 512; // amount is set in blocks
				uint32_t lba = ((cmd[1] & 0x1F) << 16) | (cmd[2] << 8) | cmd[3];
				buf = (hdData != NULL ? &hdData[lba * 512] : NULL);
				bufPtr = 0;
			}
			// Handle "Inquire" command
			else if (cmd[0] == 0x12)
			{
				WriteLog("HD: Received command INQUIRE [%02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5]);
				SetNextState(DVM_DATA_IN);
				bytesToSend = cmd[4];
				buf = inquireData;
				bufPtr = 0;

				// Reject all but LUN 0
				if ((cmd[1] & 0xE0) != 0)
					response = 0x02; // "Check Condition" code
			}
			// Handle "Mode Select" command
			else if (cmd[0] == 0x15)
			{
				WriteLog("HD: Received command MODE SELECT [%02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5]);
				SetNextState(DVM_DATA_OUT);
				bytesToSend = cmd[4];
			}
			// Handle "Mode Sense" command
			else if (cmd[0] == 0x1A)
			{
				WriteLog("HD: Received command MODE SENSE [%02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5]);
				SetNextState(DVM_DATA_IN);
				bytesToSend = cmd[4];
			}
			else
			{
				WriteLog("HD: Received unhandled 6 command [%02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5]);
				// Return failure...
				SetNextState(DVM_STATUS);
				response = 0x02; // Check condition code
			}
		}
		else if (((cmdType == 1) || (cmdType == 2)) && (cmdLength == 10))
		{
			// Handle "Read Capacity" command
			if (cmd[0] == 0x25)
			{
				WriteLog("HD: Received command READ CAPACITY [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9]);
				SetNextState(DVM_DATA_IN);
				bytesToSend = 8;//it's always 8...//cmd[4];
				// N.B.: We need to hook this up to the actual emulated HD size...
				buf = readCapacity;
				bufPtr = 0;
			}
			// Handle "Read" (10) command
			else if (cmd[0] == 0x28)
			{
				WriteLog("HD: Received command READ(10) [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9]);
				// Drive next phase
				SetNextState(DVM_DATA_IN);
				bytesToSend = ((cmd[7] << 8) | cmd[8]) * 512; // amount is set in blocks
				uint32_t lba = (cmd[2] << 24) | (cmd[3] << 16) | (cmd[4] << 8) | cmd[5];
				buf = (hdData != NULL ? &hdData[lba * 512] : NULL);
				bufPtr = 0;
			}
			// Handle "Write" (10) command
			else if (cmd[0] == 0x2A)
			{
				WriteLog("HD: Received command WRITE(10) [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9]);
				// Drive next phase
				SetNextState(DVM_DATA_OUT);
				bytesToSend = ((cmd[7] << 8) | cmd[8]) * 512; // amount is set in blocks
				uint32_t lba = (cmd[2] << 24) | (cmd[3] << 16) | (cmd[4] << 8) | cmd[5];
				buf = (hdData != NULL ? &hdData[lba * 512] : NULL);
				bufPtr = 0;
			}
			else
			{
				WriteLog("HD: Received unhandled 10 command [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9]);
				// Return failure...
				SetNextState(DVM_STATUS);
				response = 0x02; // "Check Condition" code
			}
		}
		else if ((cmdType == 5) && (cmdLength == 12))
		{
			WriteLog("HD: Received unhandled 12 command [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9], cmd[10], cmd[11]);
			// Return failure...
			SetNextState(DVM_STATUS);
			response = 0x02; // "Check Condition" code
		}

		break;
	}

	case DVM_STATUS:
		if (!ACK)
		{
			// Return A-OK for everything for now...
			reg[0] = 0; // N.B.: This is necessary for some reason...
			REQ = true;
		}
		else if (REQ && ACK)
		{
			REQ = false;
			SetNextState(DVM_MESSAGE_IN);
		}

		break;

	case DVM_MESSAGE_OUT:
		if (!ACK)
			REQ = true;
		if (REQ && ACK)
		{
//			WriteLog("HD: Write to target value $%02X\n", reg[0]);
			REQ = false;
			SetNextState(DVM_COMMAND);
		}

		break;

	case DVM_MESSAGE_IN:
		if (!ACK)
		{
			// Return appropriate response
			reg[0] = response;
			REQ = true;
		}
		else if (REQ && ACK)
		{
			REQ = false;
			DEV_BSY = false;
			SetNextState(DVM_BUS_FREE);
		}

		break;
	}
}


static uint8_t SlotIOR(uint16_t address)
{
	// This should prolly go somewhere else...
	RunDevice();

	uint8_t response = reg[address & 0x0F];

	switch (address & 0x0F)
	{
		case 0x00:
			// (RO) Current SCSI Data register
			break;
		case 0x01:
			// Initiator Command register.  Bits, from hi to lo:
			// ASS. /RST, AIP, LA, ASS. /ACK, A./BSY, A./SEL, A./ATN, DATA BUS

			// Simulate ARBITRATE signal
			if (reg[2] & 0x01)
				response |= 0x40;

			break;
		case 0x02:
			// Mode register (chip control)
			break;
		case 0x03:
			// Target Command register (SCSI bus info xfer phase)
			break;
		case 0x04:
			// (RO) Current SCSI Bus Status register:  Bits from hi to lo:
			// /RST, /BSY, /REQ, /MSG, /C/D, /I/O, /SEL, /DBP
/*if (((mainCPU.pc != 0xCD7C) && (mainCPU.pc != 0xCD5F)) || (romBank != 16))
	WriteLog("  [%02X %02X %02X %02X %02X %02X %02X %02X] [$C81F=$%02X $C80D=$%02X $C80A=$%02X $C887=$%02X $C806=$%02X $C88F=$%02X $C8EC=$%02X $4F=$%02X]\n", reg[0], reg[1], reg[2], reg[3], reg[4], reg[5], reg[6], reg[7], staticRAM[0x1F], staticRAM[0x0D], staticRAM[0x0A], staticRAM[0x87], staticRAM[0x06], staticRAM[0x8F], staticRAM[0xEC], ram[0x4F]);//*/

			response = (RST ? 0x80 : 0) | (BSY | DEV_BSY ? 0x40 : 0) | (REQ ? 0x20 : 0) | (MSG ? 0x10 : 0) | (C_D ? 0x08 : 0) | (I_O ? 0x04 : 0) | (SEL ? 0x02 : 0);
			break;
		case 0x05:
		{
			// (RO) Bus and Status register
			response = (ACK ? 0x01 : 0) | (ATN ? 0x02 : 0) | (DRQ ? 0x40 : 0);
			uint8_t tgtMode = (MSG ? 0x04 : 0) | (C_D ? 0x02 : 0) | (I_O ? 0x01 : 0);

			if ((reg[3] & 0x07) == tgtMode)
				response |= 0x08;

			break;
		}
		case 0x06:
			// (RO) Input Data register (read from from SCSI bus)
			if (DRQ)
				DACK = true;

			break;
		case 0x07:
			// (RO) Reset Parity/Interrupt
			// Resets PARITY ERR (bit 6), IRQ (bit 5), BUSY ERROR (bit 3) in
			// register 5 (Bus & Status)
			break;
		case 0x0C:
			response = 0x10 | (dmaSwitch ? 0x40 : 0);
			break;
		case 0x0E:
			response = romBank | (deviceID << 5);
			break;
	}

#if 0
	char SCSIName[16][256] = {
		"(RO) Current SCSI Data",
		"Initiator Command",
		"Mode",
		"Target Command",
		"(RO) Current SCSI Bus Status",
		"(RO) Bus and Status",
		"(RO) Input Data",
		"(RO) Reset Parity/Interrupt",
		"DMA Address LO",
		"DMA Address HI",
		"DMA Count LO",
		"DMA Count HI",
		"$C",
		"$D",
		"Bank/SCSI ID",
		"$F"
	};

	if (((mainCPU.pc != 0xCD7C) && (mainCPU.pc != 0xCD5F)) || (romBank != 16))
		WriteLog("HD Slot I/O read %s ($%02X <- $%X, PC=%04X:%u)\n", SCSIName[address & 0x0F], response, address & 0x0F, mainCPU.pc, romBank);
#endif

	return response;
}


static void SlotIOW(uint16_t address, uint8_t byte)
{
	switch (address & 0x0F)
	{
		case 0x00:
			// (WO) Output Data register (data sent over SCSI bus)
			if (DRQ)
				DACK = true;

			break;
		case 0x01:
			// Initiator Command register.  Bits, from hi to lo:
			// ASS. /RST, AIP, LA, ASS. /ACK, A./BSY, A./SEL, A./ATN, DATA BUS
			DATA_BUS = (byte & 0x01 ? true : false);
			ATN = (byte & 0x02 ? true : false);
			SEL = (byte & 0x04 ? true : false);
			BSY = (byte & 0x08 ? true : false);
			ACK = (byte & 0x10 ? true : false);
			RST = (byte & 0x80 ? true : false);

			if (!(SEL || BSY || DEV_BSY))
				devMode = DVM_BUS_FREE;

			if (SEL && (devMode == DVM_ARBITRATE))
				devMode = DVM_SELECT;

			break;
		case 0x02:
			// Mode register (chip control)
			if ((byte & 0x01) && (devMode == DVM_BUS_FREE))
				devMode = DVM_ARBITRATE;

			// Dma ReQuest is reset here (as well as by hitting a pin)
			DMA_MODE = (byte & 0x02 ? true : false);

			if (!DMA_MODE)
				DRQ = DACK = false;

			break;
		case 0x03:
			// Target Command register (SCSI bus info xfer phase)
			break;
		case 0x04:
			// (WO) Select Enable register
			break;
		case 0x05:
			// (WO) Start DMA Send (initiates DMA send)
			DRQ = true;
			break;
		case 0x06:
			// (WO) Start DMA Target Receive (initiate DMA receive--tgt mode)
			DRQ = true;
			break;
		case 0x07:
			// (WO) Start DMA Initiator Receive (initiate DMA receive--ini mode)
			DRQ = true;
			break;
		case 0x08:
			// Lo byte of DMA address?
			break;
		case 0x09:
			// Hi byte of DMA address?
			break;
		case 0x0A:
			// 2's complement of lo byte of transfer amount?
			break;
		case 0x0B:
			// 2's complement of hi byte of transfer amount?
			break;
		case 0x0C:
			// Control/status register?
			break;
		case 0x0D:
			// ???
			break;
		case 0x0E:
			// Bottom 5 bits of $E set the ROM bank
			romBank = byte & 0x1F;
			break;
		case 0x0F:
			// Bottom 3 bits of $F set the RAM bank
			ramBank = byte & 0x07;
			break;
	}

	reg[address & 0x0F] = byte;

#if 0
	char SCSIName[16][256] = {
		"(WO) Output Data",
		"Initiator Command",
		"Mode",
		"Target Command",
		"(WO) Select Enable",
		"(WO) Start DMA Send",
		"(WO) Start DMA Target Receive",
		"(WO) Start DMA Initiator Receive",
		"DMA Address LO",
		"DMA Address HI",
		"DMA Count LO",
		"DMA Count HI",
		"$C",
		"$D",
		"Bank/SCSI ID",
		"$F"
	};
	char SCSIPhase[11][256] = { "DATA OUT", "DATA IN", "COMMAND", "STATUS", "ERR4", "ERR5", "MESSAGE OUT", "MESSAGE IN", "BUS FREE", "ARBITRATE", "SELECT" };


	WriteLog("HD Slot I/O write %s ($%02X -> $%X, PC=%04X:%u) [%s]\n", SCSIName[address & 0x0F], byte, address & 0x0F, mainCPU.pc, romBank, SCSIPhase[devMode]);

	if ((address & 0x0F) == 0x0E)
	{
		if (mainCPU.pc == 0xC78B)
		{
			uint16_t sp = mainCPU.sp;
			uint16_t pc = ram[0x100 + sp + 1] | (ram[0x100 + sp + 2] << 8);
			WriteLog("   *** Returning to bank %u, $%04X\n", romBank, pc + 1);
		}
		else if (mainCPU.pc == 0xC768)
		{
			WriteLog("   *** Calling to bank %u:%u\n", mainCPU.a, (mainCPU.y & 0xE0) >> 5);
		}

		WriteLog("  [%02X %02X %02X %02X %02X %02X %02X %02X] [$C81F=$%02X $C80D=$%02X $C80A=$%02X $C887=$%02X $C806=$%02X $C88F=$%02X $C8EC=$%02X $4F=$%02X]\n", reg[0], reg[1], reg[2], reg[3], reg[4], reg[5], reg[6], reg[7], staticRAM[0x1F], staticRAM[0x0D], staticRAM[0x0A], staticRAM[0x87], staticRAM[0x06], staticRAM[0x8F], staticRAM[0xEC], ram[0x4F]);
	}
#endif

	// This should prolly go somewhere else...
	RunDevice();
}


static uint8_t SlotROM(uint16_t address)
{
	return a2hsScsiROM[address];
}


static uint8_t SlotIOExtraR(uint16_t address)
{
	if (address < 0x400)
		return staticRAM[(ramBank * 0x400) + address];
	else
		return a2hsScsiROM[(romBank * 0x400) + address - 0x400];
}


static void SlotIOExtraW(uint16_t address, uint8_t byte)
{
	if (address < 0x400)
		staticRAM[(ramBank * 0x400) + address] = byte;
	else
//	{
		WriteLog("HD: Unhandled HD 1K ROM write ($%02X) @ $C%03X...\n", byte, address + 0x800);

/*		if ((mainCPU.pc == 0xCDDD) && (romBank == 11))
			dumpDis = true;//*/
//	}
}


void InstallHardDrive(uint8_t slot)
{
	SlotData hd = { SlotIOR, SlotIOW, SlotROM, 0, SlotIOExtraR, SlotIOExtraW };
	InstallSlotHandler(slot, &hd);
	char fnBuf[MAX_PATH + 1];

	// If this fails to read the file, the pointer is set to NULL
	uint32_t size = 0, skip = (uint32_t)-1;
	sprintf(fnBuf, "%s%s", settings.disksPath, settings.hd[0]);

	// Check to see which type of HD image we have...
	char * ext = strrchr(settings.hd[0], '.');

	if (ext != NULL)
	{
		if (strcasecmp(ext, ".2mg") == 0)
			skip = 0x40;
		else if (strcasecmp(ext, ".hdv") == 0)
			skip = 0;
	}

	if (skip == (uint32_t)-1)
	{
		hdData = NULL;
		WriteLog("HD: Unknown HD image file: %s\n", settings.hd[0]);
		return;
	}

	hdData = ReadFile(fnBuf, &size, skip);

	if (hdData)
		WriteLog("HD: Read Hard Drive image file '%s', %u bytes ($%X)\n", settings.hd[0], size, size);
	else
		WriteLog("HD: Could not read Hard Drive image file!\n");
}
