/*
 *   engine.c
 *
 *   This file is part of Emu48
 *
 *   Copyright (C) 1995 Sebastien Carlier
 *
 */
#include "pch.h"
#include "Emu48.h"
#include "Opcodes.h"
#include "io.h"
#include "debugger.h"

#define SAMPLE    16384						// speed adjust sample frequency

BOOL    bInterrupt = FALSE;
UINT    nState = SM_INVALID;
UINT    nNextState = SM_RUN;
BOOL    bEnableSlow = TRUE;					// slow down is enabled
BOOL    bRealSpeed = FALSE;
BOOL    bKeySlow = FALSE;					// slow down for key emulation
BOOL    bSoundSlow = FALSE;					// slow down for sound emulation
UINT    nOpcSlow = 0;						// no. of opcodes to slow down

CHIPSET Chipset;

TCHAR   szSerialWire[16];					// devicename for wire port
TCHAR   szSerialIr[16];						// devicename for IR port

DWORD   dwSXCycles = 82;					// SX cpu cycles in interval
DWORD   dwGXCycles = 123;					// GX cpu cycles in interval
DWORD   dwGPCycles = 123*3;			    	// g+ cpu cycles in interval    // CdB for HP: add apples display management
DWORD   dwG2Cycles = 123*2;					// gII cpu cycles in interval    // CdB for HP: add apples display management

DWORD   dwT2Cycles = 0xFFFFFFFF;			// CPU cycles in 16384 Hz time frame

// variables for debugger engine
HANDLE  hEventDebug;						// event handle to stop cpu thread

BOOL    bDbgAutoStateCtrl = TRUE;			// debugger suspend control by SwitchToState()
INT     nDbgState = DBG_OFF;				// state of debugger

BOOL    bDbgNOP3 = FALSE;					// halt on NOP3 (#420 opcode)
BOOL    bDbgRPL = FALSE;					// halt on RPL entry
BOOL    bDbgCode = FALSE;					// halt on DOCODE entry

BOOL    bDbgSkipInt = FALSE;				// execute interrupt handler

DWORD   dwDbgStopPC = -1;					// stop address for goto cursor
DWORD   dwDbgRplPC = -1;					// stop address for RPL breakpoint

DWORD   dwDbgRstkp;							// stack recursion level of step over end
DWORD   dwDbgRstk;							// possible return address

DWORD   *pdwInstrArray = NULL;				// last instruction array
WORD    wInstrSize = 256;					// size of last instruction array
WORD    wInstrWp;							// write pointer of instruction array
WORD    wInstrRp;							// read pointer of instruction array

VOID (*fnOutTrace)(VOID) = NULL;			// callback function for file trace

static INT   nDbgRplBreak = BN_ASM;			// flag for RPL breakpoint detection
static INT   nDbgOldState = DBG_OFF;		// old state of debugger for suspend/resume

static BOOL  bCpuSlow = FALSE;				// enable/disable real speed

static DWORD dwEDbgT2 = 0;					// debugger timer2 emulation
static DWORD dwEDbgCycles = 0;				// debugger cycle counter

static DWORD dwOldCyc;						// cpu cycles at last event
static DWORD dwSpeedRef;					// timer value at last event
static DWORD dwTickRef;						// sample timer ticks

#include "Ops.h"

// save last instruction in circular instruction buffer
static __inline VOID SaveInstrAddr(DWORD dwAddr)
{
	EnterCriticalSection(&csDbgLock);
	{
		if (pdwInstrArray)					// circular buffer allocated
		{
			pdwInstrArray[wInstrWp] = dwAddr;
			wInstrWp = (wInstrWp + 1) % wInstrSize;
			if (wInstrWp == wInstrRp)
				wInstrRp = (wInstrRp + 1) % wInstrSize;
		}
	}
	LeaveCriticalSection(&csDbgLock);
	return;
}

static __inline VOID Debugger(VOID)			// debugger part
{
	LARGE_INTEGER lDummyInt;				// sample timer ticks
	BOOL bStopEmulation;
	LPBYTE I = FASTPTR(Chipset.pc);			// get opcode stream

	UpdateDbgCycleCounter();				// update 64 bit cpu cycle counter

	SaveInstrAddr(Chipset.pc);				// save pc in last instruction buffer
	if (fnOutTrace != NULL)					// has a trace function
	{
		fnOutTrace();						// write file trace
	}

	nDbgRplBreak = BN_ASM;					// notify ASM breakpoint

	// check for code breakpoints
	bStopEmulation = CheckBreakpoint(Chipset.pc, 1, BP_EXEC);

	// check for memory breakpoints, opcode #14x or #15x
	if (I[0] == 0x1 && (I[1] == 0x4 || I[1] == 0x5))
	{
		DWORD dwData  = (I[2] & 0x1) ? Chipset.d1 : Chipset.d0;
		UINT  nType   = (I[2] & 0x2) ? BP_READ : BP_WRITE;

		DWORD dwRange;
		if (I[1] == 0x4)					// B,A
		{
			dwRange = (I[2] & 0x8) ? 2 : 5;
		}
		else								// number of nibbles, (P,WP,XS,X,S,M,W)
		{
			dwRange = (I[2] & 0x8) ? (I[3]+1) : (F_l[I[3]]);
		}
		#if defined DEBUG_DEBUGGER
		{
			TCHAR buffer[256];
			wsprintf(buffer,_T("Memory breakpoint %.5lx, %u\n",dwData,dwRange));
			OutputDebugString(buffer);
		}
		#endif
		bStopEmulation |= CheckBreakpoint(dwData, dwRange, nType);
	}

	// check for step cursor
	bStopEmulation |= (dwDbgStopPC == Chipset.pc);

	// NOP3, opcode #420 (GOC)
	if (bDbgNOP3 && I[0] == 0x4 && I[1] == 0x2 && I[2] == 0x0)
		bStopEmulation = TRUE;

	// stop on first instruction of DOCODE object
	if (bDbgCode && (Chipset.pc == 0x02DDE || Chipset.pc == 0x02E3C))
	{
		// return address
		DWORD dwAddr = Chipset.rstk[(Chipset.rstkp-1)&7];

		_ASSERT(I[0] == 0 && I[1] == 1);	// stopped at RTN opcode

		if (MapData(dwAddr) != M_ROM)		// address not in ROM
			dwDbgStopPC = dwAddr;			// then stop
	}

	// check for RPL breakpoint
	if (dwDbgRplPC == Chipset.pc)
	{
		dwDbgRplPC = -1;
		nDbgRplBreak = bStopEmulation ? BN_ASM_BT : BN_RPL;
		bStopEmulation = TRUE;
	}

	// RPL breakpoints, PC=(A), opcode #808C or PC=(C), opcode #808E
	if (I[0] == 0x8 && I[1] == 0x0 && I[2] == 0x8 && (I[3] == 0xC || I[3] == 0xE ))
	{
		// get next RPL entry
		DWORD dwAddr = Npack((I[3] == 0xC) ? Chipset.A : Chipset.C,5);

		if (bDbgRPL || CheckBreakpoint(dwAddr, 1, BP_RPL))
		{
			BYTE  byRplPtr[5];

			Npeek(byRplPtr,dwAddr,5);		// get PC address of next opcode
			dwDbgRplPC = Npack(byRplPtr,5);	// set RPL breakpoint
		}
	}

	// step over interrupt execution
	if (bDbgSkipInt && !bStopEmulation && !Chipset.inte)
		return;

	// check for step into
	bStopEmulation |= (nDbgState == DBG_STEPINTO);

	// check for step over
	bStopEmulation |= (nDbgState == DBG_STEPOVER) && dwDbgRstkp == Chipset.rstkp;

	// check for step out, something was popped from hardware stack
	if (nDbgState == DBG_STEPOUT && dwDbgRstkp == Chipset.rstkp)
	{
		_ASSERT(bStopEmulation == FALSE);
		if ((bStopEmulation = (Chipset.pc == dwDbgRstk)) == FALSE)
		{
			// it was C=RSTK, check for next object popped from hardware stack
			dwDbgRstkp = (Chipset.rstkp-1)&7;
			dwDbgRstk  = Chipset.rstk[dwDbgRstkp];
		}
	}

	if (bStopEmulation)						// stop condition
	{
		StopTimers();						// hold timer values when emulator is stopped
		if (Chipset.IORam[TIMER2_CTRL]&RUN)	// check if timer running
		{
			if (dwEDbgT2 == Chipset.t2)
			{
				// cpu cycles for one timer2 tick elapsed
				if ((DWORD) (Chipset.cycles & 0xFFFFFFFF) - dwEDbgCycles
					>= (SAMPLE / 8192) * dwT2Cycles)
				{
					--Chipset.t2;
					// adjust cycles reference
					dwEDbgCycles += (SAMPLE / 8192) * dwT2Cycles;
				}
			}
			else							// new timer2 value
			{
				// new cycle reference
				dwEDbgCycles = (DWORD) (Chipset.cycles & 0xFFFFFFFF);
			}

			// check rising edge of Bit 8 of timer2
			if ((dwEDbgT2 & 0x100) == 0 && (Chipset.t2 & 0x100) != 0)
				Chipset.t1 = (Chipset.t1 - 1) & 0xF;
		}
		dwEDbgT2 = Chipset.t2;				// timer2 check reference value

		// redraw debugger window and stop
		NotifyDebugger(nDbgRplBreak);
		WaitForSingleObject(hEventDebug,INFINITE);

		StartTimers();						// continue timers

		if (nDbgState >= DBG_OFF)			// if debugger is active
		{
			Chipset.Shutdn = FALSE;
			Chipset.bShutdnWake = FALSE;
		}

		// init slow down part
		dwOldCyc = (DWORD) (Chipset.cycles & 0xFFFFFFFF);
		QueryPerformanceCounter(&lDummyInt);
		dwSpeedRef = lDummyInt.LowPart;
	}
	return;
}

VOID SuspendDebugger(VOID)
{
	// auto control enabled, emulation halted by debugger
	if (bDbgAutoStateCtrl && nDbgState > DBG_OFF)
	{
		nDbgOldState = nDbgState;			// save old state
		nDbgState = DBG_SUSPEND;			// suspend state
		SetEvent(hEventDebug);				// exit debugger
	}
	return;
}

VOID ResumeDebugger(VOID)
{
	// auto control enabled, debugger is suspended
	if (bDbgAutoStateCtrl && nDbgState == DBG_SUSPEND)
	{
		// active RPL breakpoint
		if (nDbgRplBreak) dwDbgRplPC = Chipset.pc;
		nDbgState = nDbgOldState;			// set to old debugger state
		if (Chipset.Shutdn)					// inside shutdown
			SetEvent(hEventShutdn);			// leave it to call debugger
	}
	return;
}

static __inline VOID CheckDisp(BOOL bSync)
{
	if (disp == 0) return;					// no display update need

	// update display when drawing top line or display is off
	if (bSync && GetLineCounter() != 0x3F && (Chipset.IORam[0x00]&8))
		return;

	_ASSERT((disp & DISP_POINTER) == 0);	// display pointer already updated
	if (disp & DISP_MAIN)  UpdateMainDisplay();
	if (disp & DISP_MENUE) UpdateMenuDisplay();
	disp = 0;								// display updated
	return;
}

static __inline VOID AdjustSpeed(VOID)		// adjust emulation speed
{
	// emulation slow down
	if (   bEnableSlow
		&& (bCpuSlow || bKeySlow || bSoundSlow || nOpcSlow > 0))
	{
		DWORD dwCycles,dwTicks;

		EnterCriticalSection(&csSlowLock);
		{
			// cycles elapsed for next check
			if ((dwCycles = (DWORD) (Chipset.cycles & 0xFFFFFFFF)-dwOldCyc) >= dwT2Cycles)
			{
				LARGE_INTEGER lAct;
				do
				{
					VERIFY(QueryPerformanceCounter(&lAct));

					// get time difference
					dwTicks = lAct.LowPart - dwSpeedRef;
				}
				// ticks elapsed or negative number (workaround for QueryPerformanceCounter() in Win2k)
				while (dwTicks <= dwTickRef || (dwTicks & 0x80000000) != 0);

				dwOldCyc += dwT2Cycles;		// adjust cycles reference
				dwSpeedRef += dwTickRef;	// adjust reference time
			}

			if (nOpcSlow > 0) --nOpcSlow;	// decr. slow down opcode counter
		}
		LeaveCriticalSection(&csSlowLock);
	}
	return;
}

static __inline VOID SetT2Cycles(VOID)		// set device specific cpu cycles in interval
{
	switch (cCurrentRomType)
	{
	case 'S': // HP48SX
		dwT2Cycles = dwSXCycles;
		break;
	case '6': // HP38G with 64KB RAM
	case 'A': // HP38G
	case 'E': // HP39/40G
	case 'G': // HP48GX
	case 'X': // HP49G
	default:
		dwT2Cycles = dwGXCycles;
		break;
	case 'P': // HP39G+
	case 'Q': // HP49G+
		dwT2Cycles = dwGPCycles;
		break;
	case '2': // HP48GII
		dwT2Cycles = dwG2Cycles;
		break;
	}
	return;
}

VOID CheckSerial(VOID)
{
	// COM port closed and serial on
	if (CommIsOpen() == FALSE && (Chipset.IORam[IOC] & SON) != 0)
	{
		CommOpen(szSerialWire,szSerialIr); // open COM ports
	}

	// COM port opened and serial off
	if (CommIsOpen() == TRUE && (Chipset.IORam[IOC] & SON) == 0)
	{
		CommClose();					// close COM port
	}
	return;
}

VOID InitAdjustSpeed(VOID)
{
	// slow down function not initalized
	if (!bEnableSlow || (!bCpuSlow && !bKeySlow && !bSoundSlow && nOpcSlow == 0))
	{
		LARGE_INTEGER lTime;				// sample timer ticks

		EnterCriticalSection(&csSlowLock);
		{
			// save reference cycles
			dwOldCyc = (DWORD) (Chipset.cycles & 0xFFFFFFFF);
			QueryPerformanceCounter(&lTime);// get timer ticks
			dwSpeedRef = lTime.LowPart;		// save reference time
		}
		LeaveCriticalSection(&csSlowLock);
	}
	return;
}

VOID AdjKeySpeed(VOID)						// slow down key repeat
{
	WORD i;
	BOOL bKey;

	bKey = FALSE;							// search for a pressed key
	for (i = 0;i < ARRAYSIZEOF(Chipset.Keyboard_Row) && !bKey;++i)
		bKey = (Chipset.Keyboard_Row[i] != 0);

	if (bKey)								// key pressed
	{
		InitAdjustSpeed();					// init variables if necessary
	}
	bKeySlow = bKey;						// save new state
	return;
}

VOID SetSpeed(BOOL bAdjust)					// set emulation speed
{
	if (bAdjust)							// switch to real speed
	{
		InitAdjustSpeed();					// init variables if necessary
	}
	bCpuSlow = bAdjust;						// save emulation speed
	return;
}

VOID UpdateKdnBit(VOID)						// update KDN bit
{
	if (   Chipset.intk
		&& (Chipset.IORam[TIMER2_CTRL]&RUN) != 0
		&& (DWORD) (Chipset.cycles & 0xFFFFFFFF) - Chipset.dwKdnCycles > dwT2Cycles * 16)
		IOBit(SRQ2,KDN,Chipset.in != 0);
	return;
}

BOOL WaitForSleepState(VOID)				// wait for cpu SHUTDN then sleep state
{
	DWORD dwRefTime;

	SuspendDebugger();						// suspend debugger

	dwRefTime = timeGetTime();
	// wait for the SHUTDN command with 1.5 sec timeout
	while (timeGetTime() - dwRefTime < 1500L && !Chipset.Shutdn)
		Sleep(0);

	if (Chipset.Shutdn)						// not timeout, cpu is down
		SwitchToState(SM_SLEEP);			// go to sleep state
	else
		ResumeDebugger();					// timeout, resume to debugger

	return SM_SLEEP != nNextState;			// state not changed, emulator was busy
}

UINT SwitchToState(UINT nNewState)
{
	UINT nOldState = nState;

	if (nState == nNewState) return nOldState;
	switch (nState)
	{
	case SM_RUN: // Run
		switch (nNewState)
		{
		case SM_INVALID: // -> Invalid
			nNextState = SM_INVALID;
			if (Chipset.Shutdn)
				SetEvent(hEventShutdn);
			else
				bInterrupt = TRUE;
			SuspendDebugger();				// suspend debugger
			while (nState!=nNextState) Sleep(0);
			break;
		case SM_RETURN: // -> Return
			DisableDebugger();				// disable debugger
			nNextState = SM_INVALID;
			if (Chipset.Shutdn)
				SetEvent(hEventShutdn);
			else
				bInterrupt = TRUE;
			while (nState!=nNextState) Sleep(0);
			nNextState = SM_RETURN;
			SetEvent(hEventShutdn);
			WaitForSingleObject(hThread,INFINITE);
			break;
		case SM_SLEEP: // -> Sleep
			nNextState = SM_SLEEP;
			bInterrupt = TRUE;				// exit main loop
			SuspendDebugger();				// suspend debugger
			SetEvent(hEventShutdn);			// exit shutdown
			while (nState!=nNextState) Sleep(0);
			bInterrupt = FALSE;
			ResetEvent(hEventDebug);
			ResetEvent(hEventShutdn);
			break;
		}
		break;
	case SM_INVALID: // Invalid
		switch (nNewState)
		{
		case SM_RUN: // -> Run
			nNextState = SM_RUN;
			// don't enter opcode loop on interrupt request
			bInterrupt = Chipset.Shutdn || Chipset.SoftInt;
			ResumeDebugger();
			SetEvent(hEventShutdn);
			while (nState!=nNextState) Sleep(0);
			break;
		case SM_RETURN: // -> Return
			DisableDebugger();				// disable debugger
			nNextState = SM_RETURN;
			SetEvent(hEventShutdn);
			WaitForSingleObject(hThread,INFINITE);
			break;
		case SM_SLEEP: // -> Sleep
			nNextState = SM_SLEEP;
			SetEvent(hEventShutdn);
			while (nState!=nNextState) Sleep(0);
			break;
		}
		break;
	case SM_SLEEP: // Sleep
		switch (nNewState)
		{
		case SM_RUN: // -> Run
			nNextState = SM_RUN;
			// don't enter opcode loop on interrupt request
			bInterrupt = (nDbgState == DBG_OFF) && (Chipset.Shutdn || Chipset.SoftInt);
			ResumeDebugger();
			SetEvent(hEventShutdn);			// leave sleep state
			break;
		case SM_INVALID: // -> Invalid
			nNextState = SM_INVALID;
			SetEvent(hEventShutdn);
			while (nState!=nNextState) Sleep(0);
			break;
		case SM_RETURN: // -> Return
			DisableDebugger();				// disable debugger
			nNextState = SM_INVALID;
			SetEvent(hEventShutdn);
			while (nState!=nNextState) Sleep(0);
			nNextState = SM_RETURN;
			SetEvent(hEventShutdn);
			WaitForSingleObject(hThread,INFINITE);
			break;
		}
		break;
	}
	return nOldState;
}

UINT WorkerThread(LPVOID pParam)
{
	LARGE_INTEGER lDummyInt;				// sample timer ticks
	QueryPerformanceFrequency(&lDummyInt);	// init timer ticks
	lDummyInt.QuadPart /= SAMPLE;			// calculate sample ticks
	dwTickRef = lDummyInt.LowPart;			// sample timer ticks
	_ASSERT(dwTickRef);						// tick resolution error

loop:
	while (nNextState == SM_INVALID)		// go into invalid state
	{
		OnToolMacroStop();					// close open keyboard macro handler
		CommClose();						// close COM port
		nState = SM_INVALID;				// in invalid state
		WaitForSingleObject(hEventShutdn,INFINITE);
		if (nNextState == SM_RETURN)		// go into return state
		{
			nState = SM_RETURN;				// in return state
			return 0;						// kill thread
		}
		CheckSerial();						// test if UART on
	}
	while (nNextState == SM_RUN)
	{
		if (nState != SM_RUN)
		{
			nState = SM_RUN;
			// clear port2 status bits
			Chipset.cards_status &= ~(PORT2_PRESENT | PORT2_WRITE);
			if (pbyPort2 || Port2)			// card plugged in port2
			{
				Chipset.cards_status |= PORT2_PRESENT;

				if (bPort2Writeable)		// is card writeable
					Chipset.cards_status |= PORT2_WRITE;
			}
			// card detection off and timer running
			if ((Chipset.IORam[CARDCTL] & ECDT) == 0 && (Chipset.IORam[TIMER2_CTRL] & RUN) != 0)
			{
				BOOL bNINT2 = Chipset.IORam[SRQ1] == 0 && (Chipset.IORam[SRQ2] & LSRQ) == 0;
				BOOL bNINT  = (Chipset.IORam[CARDCTL] & SMP) == 0;

				// state of CDT2
				bNINT2 = bNINT2 && (Chipset.cards_status & (P2W|P2C)) != P2C;
				// state of CDT1
				bNINT  = bNINT  && (Chipset.cards_status & (P1W|P1C)) != P1C;

				IOBit(SRQ2,NINT2,bNINT2);
				IOBit(SRQ2,NINT,bNINT);
			}
			RomSwitch(Chipset.Bank_FF);		// select HP49G ROM bank and update memory mapping
			UpdateContrast(Chipset.contrast);
			UpdateDisplayPointers();
			UpdateMainDisplay();
			UpdateMenuDisplay();
			RefreshDisp0();    // CdB for HP: add apples display management
			UpdateAnnunciators(0x3F);
			// init speed reference
			dwOldCyc = (DWORD) (Chipset.cycles & 0xFFFFFFFF);
			QueryPerformanceCounter(&lDummyInt);
			dwSpeedRef = lDummyInt.LowPart;
			SetT2Cycles();					// set device specific cpu cycles in interval
			SetHP48Time();					// update HP48 time & date
			// start display counter/update engine
			StartDisplay((BYTE)(((Chipset.IORam[LINECOUNT+1]<<4)|Chipset.IORam[LINECOUNT])&0x3F));
			StartBatMeasure();				// start battery measurement
			StartTimers();
		}
		PCHANGED;
		while (!bInterrupt)
		{
			if (nDbgState > DBG_OFF)		// debugger active
			{
				Debugger();

				// if suspended skip next opcode execution
				if (nDbgState == DBG_SUSPEND)
				{
					if (Chipset.Shutdn) break;
					continue;
				}
			}

			EvalOpcode(FASTPTR(Chipset.pc)); // execute opcode

			// check for display update in BW mode
			if (!bGrayscale) CheckDisp(!Chipset.Shutdn);
			AdjustSpeed();					// adjust emulation speed
		}
		bInterrupt = FALSE;					// be sure to reenter opcode loop

		// enter SHUTDN handler only in RUN mode
		if (Chipset.Shutdn && !(nDbgState == DBG_STEPINTO || nDbgState == DBG_STEPOVER))
		{
			if (!Chipset.SoftInt)			// ignore SHUTDN on interrupt request
				WaitForSingleObject(hEventShutdn,INFINITE);
			else
				Chipset.bShutdnWake = TRUE;	// waked by interrupt

			if (Chipset.bShutdnWake)		// waked up by timer, keyboard or serial
			{
				Chipset.bShutdnWake = FALSE;
				Chipset.Shutdn = FALSE;
				// init speed reference
				dwOldCyc = (DWORD) (Chipset.cycles & 0xFFFFFFFF);
				QueryPerformanceCounter(&lDummyInt);
				dwSpeedRef = lDummyInt.LowPart;
				nOpcSlow = 0;				// no opcodes to slow down
			}
		}
		if (Chipset.SoftInt)
		{
			Chipset.SoftInt = FALSE;
			if (Chipset.inte)
			{
				Chipset.inte = FALSE;
				rstkpush(Chipset.pc);
				Chipset.pc = 0xf;
			}
		}
	}
	_ASSERT(nNextState != SM_RUN);

	StopDisplay();							// stop display counter/update
	StopBatMeasure();						// stop battery measurement
	StopTimers();

	while (nNextState == SM_SLEEP)			// go into sleep state
	{
		nState = SM_SLEEP;					// in sleep state
		WaitForSingleObject(hEventShutdn,INFINITE);
	}
	goto loop;
	UNREFERENCED_PARAMETER(pParam);
}
