/************************************************************************\
 * File Version Information
 * $Header: /Altair32v2/cass.c 39    11/08/06 9:21p Racini $
 ************************************************************************/
/************************************************************************\
  MITS Altair Emulator
  Cassette Device module
 
  Copyright (c) 2001-2006 Richard A. Cini
  Tape counter mechanics Copyright (c) 2001 Jim Battle/Sol-20 Project
 
 
  NOTE: Timer routines assume a 1200-baud cassette interface.

  This module emulates the 88-ACR audio cassette interface connected
  to an audio cassette deck. The ACR consisted of two boards -- an
  analog interface and a modified SIO interface. This allowed the cassette
  deck to look like a character device to the loader. The cassette tape
  format is the MITS Absolute Tape Format, which is the same format
  used for the paper tapes. This means that the audio cassette is a 
  magnetic representation of a paper tape.

  Here's the layout of the ACR configuration:

  Port 	Dir		Function
  ----	---		--------
   06h	R/W		ACR status register
   07h	R/W		ACR data register

  Notable hittest coordinates:
	The hotspot on each cassette key is 38x38. There is a 10px space between
		the keys. This results in the top-left corner of each key being 48px
		from the one next to it. 

Change Log: 	
  2003/10/09  RAC -- Initial coding of new implementation.
  2003/10/21  RAC -- New read code confirmed working with toggle boot
  						loader. Working on write code.
  2003/10/27  RAC -- Renamed module to reflect final usage as cassette
  						drive. Write code works and BASIC can CLOAD/CSAVE.
  2003/11/14  RAC -- Added routines for rolling tape counter bitmaps.
  2003/11/19  RAC -- Completed integration of tape counter. Works.
  2003/11/30  RAC -- Replaced dialog background with new cassette tape gfx.
  2003/12/04  RAC -- Initial coding of hit-testing complete. Works.
  2004/01/22  RAC -- InitDialog retrieves saved window coordinates from INI.
  2004/03/09  RAC -- Diff'ed more changes from FJS (Fred J. Scipione)
  2004/07/30  RAC -- RELEASE MARKER -- v3.00.0135
  2006/05/12  RAC -- RELEASE MARKER -- v3.10.0200
  2006/11/15  RAC -- RELEASE MARKER -- v3.20.0400
\************************************************************************/
#include <windows.h>	// required for all Windows applications
#include <windowsx.h>
#include <commdlg.h>
#include <commctrl.h>
#include <stdio.h>		// C-lib
#include <string.h>
#include "altair32.h"	// includes "resource.h"
#include "comcthlp.h"	// common control macro file


/**  Defines  *************************************************/
#define CASS_WIDTH  371
#define CASS_HEIGHT 364
#define CASS_BaudRate  1200		// 1200 baud
#define CASS_tmrfrq		200		// 200ms timer tick frequency

// Switch positions - values are significant
// 2-state switches cycle mod 2, 3-state mod 3
#define	SW_UP	 0
#define	SW_DOWN	 1
#define	SW_NEUT	 2

// Switch type defs - values are significant
#define	SWTYPE_1  0	// two-position spring loaded switch [(on)-off-(on)]
#define	SWTYPE_2  2	// two-position toggle switch [on-off]
#define	SWTYPE_3  3	// three-position toggle switch [on-off-on]

// Switch colors
#define SW_RED  0
#define SW_BLK  1

// Port status
#define RxStatC_BIT	0x01	// 1=no char, 0=DAV Cassette
#define TxStatC_BIT	0x80	// 1=busy, 0=ready Cassette


/**  Forward Prototypes  **************************************/
static  BOOL AttachPTPFile( HWND, int );
static  BOOL DetachPTPFile( HWND );
static  BOOL DoRewind( HWND );			// does actual movement
static  BOOL DoFastForward( HWND );		// does actual movement
static  void KS_DoRecord( short, short );
static  void KS_DoPlay( short, short  );
static  void KS_DoRewind( short, short );
static  void KS_DoFastForward( short, short );
static  void KS_DoStop(  short, short  );
static  void KS_DoPause(  short, short  );
LRESULT CALLBACK WndProcPTP2( HWND, UINT, WPARAM, LPARAM );
static	short KSHitTest( LONG );
static  void DrawPanelBitmap( HDC, HBITMAP, int, int );
static  void DrawSwitches( HDC );
static  void DrawRecordLED( HDC );
static	void DrawCounter( HDC, HBITMAP, int, int );
static  void DoKeySwitch( short, short );
static	int  CASS_Read ( void );
static	void CASS_Write ( byte );
static	int  CASS_Stat ( void );


/**  Module Globals - Private  ********************************/
static char		szBuffer[64 + _MAX_FNAME + _MAX_EXT];	// general message bufr
static char		szOpenFileTitle[_MAX_FNAME+_MAX_EXT];
static char 	szFileDir[_MAX_PATH + _MAX_FNAME + _MAX_EXT];
static char* 	szFileAccessMode[] = {"rb","a+b"};	//read_bin, append_bin
static char* 	szFileAccessTxt[] = {" (Read)"," (Write)"};
static FILE		*pFileIO = NULL;
static fpos_t 	pos = 0;			// stream pointer variable
static HBITMAP 	g_hDigits = NULL;	// handle to digit image map (loaded in _Init)
static HBITMAP  g_hPnlBmp = NULL;	// handle to dialog background (loaded in _Init)
static HBITMAP	g_hLED_on = NULL;	// handle to LED_ON bitmap (loaded in _Init)
static HBITMAP	g_hLED_off = NULL;	// handle to LED_OFF bitmap (loaded in _Init)
static HBITMAP  g_hKSr_UP = NULL;	// red key up (NEW)
static HBITMAP  g_hKSr_DN = NULL;	// red key down (NEW)
static HBITMAP  g_hKSb_UP = NULL;	// black key up (NEW)
static HBITMAP  g_hKSb_DN = NULL;	// black key down (NEW)
static HDC		g_hWndDC = NULL;	// handle to dialog DC
static int		g_curtime = 0;		// tape time in 0.1sec (3bytes/count@300 baud; 12@1200)
static int		giPTPStat = 0;
static int		iAdvanceMode = 0; 	//0=stop, 1=rew, 2=ff
static int		iPTPStatus = ( TxStatC_BIT | RxStatC_BIT );	// status variable
static int		ks_record = 0;			// 0=normal, 1=record
static RECT 	g_rcTime;			// location of timer digits
static short  	Keyswitchdown = -1;

struct keys {
	short x;				// h_pos of center +/-19
	short y;				// v_pos of center +/-19
	short pos;				// switch position
	short type;			// switch type
	short color;			// color (red/black)
	void (*action)(short, short);		// switch action handler // FJS
} KeyList[] = {	//coords work for hittest. don't work for drawing
	{  68, 383, SW_UP, SWTYPE_2, SW_RED, KS_DoRecord },	//rec
	{ 113, 383, SW_UP, SWTYPE_2, SW_BLK, KS_DoPlay },	//play
	{ 158, 383, SW_UP, SWTYPE_2, SW_BLK, KS_DoRewind },	//rew
	{ 203, 383, SW_UP, SWTYPE_2, SW_BLK, KS_DoFastForward }, //ff
	{ 248, 383, SW_UP, SWTYPE_1, SW_BLK, KS_DoStop },	//stop
//	{ 293, 383, SW_UP, SWTYPE_2, SW_BLK, KS_DoPause },	//pause (not supported)
};

#define	N_KEYS (sizeof(KeyList)/sizeof(KeyList[0]))

/**  Module Globals - Public  *********************************/
HWND hwndCASS = NULL;			// PTP window handle
// int  iTargetOS = IDR_BASIC; 	// tape BASIC or CP/M


/**  Private Routines  ****************************************/
/*--  AttachPTPFile  ------------------------------------------
	Assigns a file to the paper tape device.
	
-------------------------------------------------------------*/
static BOOL AttachPTPFile(HWND hwnd, int iTapeMode)
{
	char	szFileName[_MAX_PATH];	// fully-qualified filename buffer
	char	szFileTitle[_MAX_FNAME + _MAX_EXT];	// filename only
	static  char szFilter[] = "Paper Tape Files (.TAP;.PTP)\0*.tap;*.ptp\0" \
                              "All Files (*.*)\0*.*\0\0";
	DWORD   fstat = 0xFFFFFFFF;		//unsigned long
	fpos_t	temp_pos = 0;
	OPENFILENAME ofn;			// OpenFilename structure for file access


	szFileName[0] = 0;
	szFileTitle[0] = 0;
	strcpy (szFileDir, winstate.szFilesDir);

	// setup OFN structure
	ofn.lStructSize       = sizeof(OPENFILENAME);
	ofn.hwndOwner         = hwnd;
	ofn.hInstance         = NULL;
	ofn.lpstrFilter       = szFilter;		// filter string
	ofn.lpstrCustomFilter = NULL;			// used to preserve user-defined filters
	ofn.nMaxCustFilter    = 0;				// size of custom filter buffer
	ofn.nFilterIndex      = 0;				// current filter index
	ofn.lpstrFile         = szFileName;	// contains full path and filename on return
	ofn.nMaxFile          = _MAX_PATH;		// sizeof lpstrFile
	ofn.lpstrFileTitle    = szFileTitle;	// filename and extension only
	ofn.nMaxFileTitle     = _MAX_FNAME + _MAX_EXT;	// sizeof lpstrFileTitle
 	ofn.lpstrInitialDir   = szFileDir;		// initial directory
	ofn.lpstrTitle        = "Open Paper Tape File";	// title bar string or NULL
	ofn.Flags             = OFN_HIDEREADONLY | OFN_CREATEPROMPT;
	ofn.nFileOffset       = 0;				// offset to filename in lpstrFile
	ofn.nFileExtension    = 0;				// offset in TCHARs to ext. in lpstrFile
	ofn.lpstrDefExt       = "tap";			// default extension
	ofn.lCustData         = 0L;			// data passed to hook procedure
	ofn.lpfnHook          = NULL;			// pointer to hook procedure
	ofn.lpTemplateName    = NULL;			// dialog box template

	// GOFN returns zero on error
	if (!GetOpenFileName(&ofn))
		return FALSE;

	fstat = GetFileAttributes(szFileName);
	if (fstat == 0xFFFFFFFF){		// FNF
		// create the tape file
		if((pFileIO = fopen(szFileName, "wb")) == NULL) {
			HOST_ThrowErrMsg("AttachPTPFile: Unable to create image file!");
			return FALSE;
		}
		fclose (pFileIO);
		pFileIO = NULL;
	}

	// reopen file for r/w
	if((pFileIO = fopen(szFileName, szFileAccessMode[iTapeMode])) == NULL) {
		HOST_ThrowErrMsg("AttachPTPFile: Unable to open image file!");
		return FALSE;
	}

	// Confirm attachment and finish up
	strcpy(szOpenFileTitle, szFileTitle);
	strcat(szOpenFileTitle, szFileAccessTxt[iTapeMode]);
	wsprintf(szBuffer, "AttachPTPFile: Image file %s attached successfully!", szFileTitle);
	Status_SetText(hwndStatusBar, STATBAR_READY, 0, szBuffer);
	Status_SetText(hwndStatusBar, STATBAR_CASS, 0, "Cass On");
	DoCaption(hwnd, "Cassette Deck", szOpenFileTitle);
	fgetpos(pFileIO, &temp_pos);
	//SetDlgItemInt(hwndCASS, IDC_EDIT_PTR, (int)temp_pos, FALSE);
	DrawCounter(g_hWndDC, g_hDigits, g_curtime, 1);
	return TRUE;
}


/*--  DetachPTPFile  ------------------------------------------
	Deassigns a file from the paper tape device. Called from
	WM_COMMAND in dialog WndProc.
	
-------------------------------------------------------------*/
static BOOL DetachPTPFile(HWND hwnd)
{

	if (pFileIO == NULL)	// make sure pointer is valid before closing.
		return TRUE;

	// Query user to confirm detachment
	wsprintf(szBuffer, "Do you really want to detach this image file from the emulator?");
	if (MessageBox(hwnd, szBuffer, winstate.szAppName, MB_YESNO | MB_SYSTEMMODAL |
		MB_DEFBUTTON2 | MB_ICONWARNING) == IDNO)
		return FALSE;

	fclose(pFileIO);
	pFileIO = NULL;
	pos = g_curtime = 0;
	DoCaption(hwnd, "Cassette Deck", "");
	wsprintf(szBuffer, "DetachPTPFile: Image file detached successfully!");
	Status_SetText(hwndStatusBar, STATBAR_READY, 0, szBuffer);
	Status_SetText(hwndStatusBar, STATBAR_CASS, 0, "Cass Off");
	SetDlgItemInt(hwndCASS, IDC_EDIT_PTR, (int)pos, FALSE);
	DrawCounter(g_hWndDC, g_hDigits, g_curtime, 1);
	return TRUE;
}


/*--  KS_DoRecord-----------------------------------------------
	Do record button function.

	PARAMETERS:
		i:		switch index 0-5
		pos:	switch position UP or DOWN

-------------------------------------------------------------*/
static void KS_DoRecord(short i, short pos)
{

	SendMessage(hwndCASS,WM_COMMAND,IDC_BTN_PTPREC,0);
}


/*--  KS_DoPlay  -----------------------------------------------
	Do play button function.

	PARAMETERS:
		i:		switch index 0-5
		pos:	switch position UP or DOWN
	
-------------------------------------------------------------*/
static void KS_DoPlay(short i, short pos)
{

	SendMessage(hwndCASS,WM_COMMAND,IDC_BTN_PTPPLAY,0);
}


/*--  KS_DoRewind  ---------------------------------------------
	Do rewind button function.

	PARAMETERS:
		i:		switch index 0-5
		pos:	switch position UP or DOWN

	This is the HIT_TEST interface to Rewind. Need separate
	routine since the actual worker functions are called under
	the control of WM_TIMER.
-------------------------------------------------------------*/
static void KS_DoRewind(short i, short pos)
{

	SendMessage(hwndCASS,WM_COMMAND,IDC_BTN_PTPREW,0);
}


/*--  DoRewind  -----------------------------------------------
	Rewinds the tape. Called from WM_COMMAND in dialog WndProc.

	Note on baud rate. Baud is a unit of modulation rate. One baud
	corresponds to a rate of one unit interval per second, where
	the modulation rate is expressed as the reciprocal of the
	duration in seconds of the shortest unit interval. So, a
	20ms signaling speed is 50 baud. Any synchronization signals
	are included in the calculation of the rate in bauds but not
	in the computation of bit rate (bauds times log2n where n is
	the number of discrete states). Common, diluted meaning is
	1 baud = 1 bit per second. A baud the number of	times per
	second that the carrier signal changes state. So, a 300
	baud modem transmits at 1200 bits/sec (moving 4 bits/baud).	
	
-------------------------------------------------------------*/
static BOOL DoRewind(HWND hwnd)
{
	fpos_t tmp_pos = 0;
	int old_stat = 0;
	

	if (pFileIO == NULL)
		return TRUE;

	// signal error condition while manipulating stream pointer
	old_stat = iPTPStatus;
	iPTPStatus |= RxStatC_BIT;

	// Get current position
	fgetpos(pFileIO, &tmp_pos);
	
	/* Begin tape movement. A 1200 baud (using the modern definition
	 * of "bits-per-second") cassette deck moves 150 8-bit cps. With
	 * a 200ms timer, we do a count of 30 for each time period.
	 */
	tmp_pos -= (fpos_t)(CASS_BaudRate/(8*(1000/CASS_tmrfrq)));

	// make sure we don't set a position <0
	if (tmp_pos < 0){
		iAdvanceMode = 0;
		tmp_pos = 0;
	}

	// set new pointer in stream and update dialog
	fsetpos(pFileIO, &tmp_pos);
	g_curtime = (int)tmp_pos;
	SetDlgItemInt(hwndCASS, IDC_EDIT_PTR, (int)tmp_pos, FALSE);
	DrawCounter(g_hWndDC, g_hDigits, g_curtime, 1);

	// restore status
	iPTPStatus = old_stat;
	return TRUE;
}


/*--  KS_DoFastForward  ----------------------------------------
	Do fast forward button function.

	PARAMETERS:
		i:		switch index 0-5
		pos:	switch position UP or DOWN

	This is the HIT_TEST interface to Fast Forward. Need
	separate routine since the actual worker functions are
	called under the control of WM_TIMER.
-------------------------------------------------------------*/
static void KS_DoFastForward(short i, short pos)
{

	SendMessage(hwndCASS,WM_COMMAND,IDC_BTN_PTPFF,0);
}


/*--  DoFastForward  ------------------------------------------
	Fast forwards the tape. Called from WM_COMMAND in dialog
	WndProc.
	
-------------------------------------------------------------*/
static BOOL DoFastForward(HWND hwnd)
{

	fpos_t tmp_pos = 0;
	int old_stat = 0;
	

	if (pFileIO == NULL)
		return TRUE;

	// signal error condition while manipulating stream pointer
	old_stat = iPTPStatus;
	iPTPStatus |= RxStatC_BIT;

	// Get current position
	fgetpos(pFileIO, &tmp_pos);
	
	/* Begin tape movement. A 1200 baud (using the modern definition
	 * of "bits-per-second") cassette deck moves 150 8-bit cps. With
	 * a 200ms timer, we do a count of 30 for each time period.
	 */
	tmp_pos += (fpos_t)(CASS_BaudRate/(8*(1000/CASS_tmrfrq)));
	fsetpos(pFileIO, &tmp_pos);	// set the position

	// make sure we don't set a position >EOF. Don't know if this will work.
	if (feof(pFileIO)){
		iAdvanceMode = 0;
		fseek(pFileIO, 0, SEEK_END);	// seek to end
		fgetpos(pFileIO, &tmp_pos);	// re-get position at end of file
	}

	g_curtime = (int)tmp_pos;
	SetDlgItemInt(hwndCASS, IDC_EDIT_PTR, (int)tmp_pos, FALSE);
	DrawCounter(g_hWndDC, g_hDigits, g_curtime, 1);

	// restore status
	iPTPStatus = old_stat;
	return TRUE;
}


/*--  KS_DoStop  -----------------------------------------------
	Do stop button function.

	PARAMETERS:
		i:		switch index 0-5
		pos:	switch position UP or DOWN

-------------------------------------------------------------*/
static void KS_DoStop(short i, short pos)
{

	SendMessage(hwndCASS,WM_COMMAND,IDC_BTN_PTPSTOP,0);
}


#if 0	// not used yet
/*--  KS_DoPause  ----------------------------------------------
	Do pause button function. IGNORE PAUSE

	PARAMETERS:
		i:		switch index 0-5
		pos:	switch position UP or DOWN

-------------------------------------------------------------*/
static void KS_DoPause(short i, short pos)
{
	// do nothing...for now.
}
#endif


/*--  DrawPanelBitmap  ----------------------------------------
	Draws a bitmap in a window. hdc is the hDC to the cassette
	window. hBitmap is the handle to the panel bitmap.
	
-------------------------------------------------------------*/
static void DrawPanelBitmap(HDC hdc, HBITMAP hBitmap, int xStart, int yStart)
{

	BITMAP bm;
	HDC    hdcMem;
	POINT  ptSize, ptOrg;
	

	hdcMem = CreateCompatibleDC(hdc);
	SelectObject(hdcMem, hBitmap);
	SetMapMode(hdcMem, GetMapMode(hdc));
	GetObject(hBitmap, sizeof(BITMAP), (LPVOID) &bm);
	ptSize.x = bm.bmWidth;
	ptSize.y = bm.bmHeight;
	DPtoLP (hdc, &ptSize, 1);
	ptOrg.x = 0;
	ptOrg.y = 0;
	DPtoLP(hdcMem, &ptOrg, 1);
	BitBlt(hdc, xStart, yStart, ptSize.x, ptSize.y, hdcMem, ptOrg.x, ptOrg.y, SRCCOPY );
	DeleteDC(hdcMem);
}


#if 0	// not used yet
/*--  DrawSwitches  -------------------------------------------
	Draws the switches.
	
-------------------------------------------------------------*/
static void DrawSwitches(HDC hdc)
{

	int x;


	for (x=0; x < N_KEYS; x++){
		switch ( KeyList[x].pos ){
			case SW_UP:
				if (KeyList[x].color == SW_RED){
					DrawPanelBitmap(hdc, g_hKSr_UP, KeyList[x].x, KeyList[x].y);
					break;
				}
				else if (KeyList[x].color == SW_BLK){
					DrawPanelBitmap(hdc, g_hKSb_UP, KeyList[x].x, KeyList[x].y);
					break;
				}
				else
					break;

			case SW_DOWN:
				if (KeyList[x].color == SW_RED){
					DrawPanelBitmap(hdc, g_hKSr_DN, KeyList[x].x, KeyList[x].y);
					break;
				}
				else if (KeyList[x].color == SW_BLK){
					DrawPanelBitmap(hdc, g_hKSb_DN, KeyList[x].x, KeyList[x].y);
					break;
				}
				else
					break;
		}
	}
}
#endif


/*--  DrawRecordLED  ------------------------------------------
	Draws the record LED.
	
-------------------------------------------------------------*/
static void DrawRecordLED(HDC hdc)
{

	if (ks_record == 0)
		DrawPanelBitmap(hdc, g_hLED_off, 281, 288);
	else if (ks_record == 1)
		DrawPanelBitmap(hdc, g_hLED_on, 281, 288);
	else
		return;
}


/*--  DrawCounter  --------------------------------------------
	Updates the timer digits on the cassette drive. Counter
	reflects 10ths of a second. Count is three bytes per 10th
	at 300 baud; 12 bytes/count at 1200 baud.	
-------------------------------------------------------------*/
static void DrawCounter(HDC hdc, HBITMAP hBitmap, int time, int force)
{

	/* hdc is the destinationDC (DC of the client area). hBitmap is
	 * a handle to a bitmap (the digit bitmap) which is converted to
	 * the sourceDC (hDigitDC) using SelectObject.
	 */

	if (hwndCASS == NULL)	// If window is closed, do nothing.
		return;

	if (force || (g_curtime != time)) {
		HDC hDigitDC;
		int i;
		int phase = time % 10;
		int secs = time / 10;
		RECT rc = g_rcTime;

		ASSERT(hdc != NULL);	// make sure we have a valid handle
		ASSERT(hBitmap != NULL);	// make sure we have a valid handle
		
		hDigitDC = CreateCompatibleDC(hdc);	// deleted below
		SelectObject(hDigitDC, hBitmap);
		SetMapMode(hDigitDC, GetMapMode(hdc));	// don't know if needed

		// draw frame around timer digits
		InflateRect(&rc, 4, 4);
		DrawEdge(hdc, &rc, EDGE_SUNKEN, BF_RECT);

		for(i=0; i<4; i++) {
			static const int powers[] = { 1000, 100, 10, 1 };
			int pow = powers[i];
			int digit = (secs / pow) % 10;
			int thisphase = ((i == 4) || ((secs+1)%pow == 0)) ? phase : 0;
			if (thisphase == 0) {
				// digit is centered perfectly
				BitBlt( hdc,			// dest DC
					g_rcTime.left+9*i,	// dest left
					g_rcTime.top,		// dest upper
					9, 20,				// width, height
					hDigitDC,			// src DC
					9*digit, 0,			// src upper left (x,y)
					SRCCOPY );			// raster op
			} else {
				// digit is rolling -- need parts of two digits
				BitBlt( hdc,			// dest DC
					g_rcTime.left+9*i,	// dest left
					g_rcTime.top,		// dest upper
					9, 20-2*thisphase,	// width, height
					hDigitDC,			// src DC
					9*digit, 2*thisphase,	// src upper left (x,y)
					SRCCOPY );			// raster op
				BitBlt( hdc,			// dest DC
					g_rcTime.left+9*i,	// dest left
					g_rcTime.bottom-2*thisphase,	// dest upper
					9, 2*thisphase,		// width, height
					hDigitDC,			// src DC
					9*((digit+1)%10), 0,	// src upper left (x,y)
					SRCCOPY );			// raster op
			}	//else
		}	// for
		g_curtime = time;
		DeleteDC(hDigitDC);
    }	// if
}


/*--  KSHitTest  ---------------------------------------------
	Called by WM_LBUTTONDOWN to map the mouse coordinates
	to a switch number later used by DoKeySwitch to actually
	perform the switch action.

	Params:
		(lParam):	mouse coordinates (y,x)

-------------------------------------------------------------*/
static short KSHitTest(LONG lParam)
{
	register short i;
	register x = LOWORD( lParam );
	register y = HIWORD( lParam );

	for ( i=0; i< N_KEYS; i++ ) {
		if ( ABS( x-KeyList[i].x ) < 38 && ABS(y-KeyList[i].y) < 38 ) {
			switch ( KeyList[i].type ) {
				
				case SWTYPE_1:
					if ( y < KeyList[i].y )
						KeyList[i].pos = SW_UP;
					else
						KeyList[i].pos = SW_DOWN;
					Keyswitchdown = i;
					break;
				
				case SWTYPE_3:
					KeyList[i].pos = ++KeyList[i].pos % 3;
					break;
				
				case SWTYPE_2:
					KeyList[i].pos = ++KeyList[i].pos % 2;
					break;
			}

			DoKeySwitch( i, KeyList[i].pos );
			return ( i );
		}
	}
	return (-1);
}


/*--  DoKeySwitch  --------------------------------------------
	BitBlt proper switch image and call related switch handler.

	PARAMETERS:
		i:		switch index 0-5
		pos:	switch position UP or DOWN

	WM_LBUTTONDOWN calls HitTest. HitTest maps switch coordinates
	to an index and then calls DoSwitch. DoSwitch BitBlit's the
	proper switch image and then calls the associated switch
	handler. WM_LBUTTONUP will also call DoSwitch, but only for
	spring-to-neutral switches (like the FP).		
-------------------------------------------------------------*/
static void DoKeySwitch( short i, short pos )
{

	/* Notes:
		If REC, need to blt REC and PLAY buttons and light
			REC_LED.
		Any other key, except PAUSE, will release any other pressed
			key. If REC/PLAY keys were down, this will also turn
			off the REC_LED.
		Pressing REW/FF while in PLAY is not allowed (not prototypical)
	*/

	// need to add BitBlt'er code here	
	// LED coordinates (center) is at 296,274. Other coordinates come
	// from the KeyList[] array.

	KeyList[i].action( i, pos );		// call handler
}




/**  Public Routines  *****************************************/
/**************************************************************\
*   I/O Instruction Handlers  *********************************|
***************************************************************|
*
*	I/O instruction handlers are called from the CPU module
*  	when an IN or OUT instruction is issued.
*
*  	Each function is passed an 'io' flag, where 0 means a read
*  	from the port, and 1 means a write to the port.  On input,
*	the actual input is passed as the return value. On output,
*	'data' is written to the device.
\**************************************************************/
/*--  acr06h  -------------------------------------------------
	This is the status register handler for the cassette port.
	io=0=read, 1=write.

-------------------------------------------------------------*/
int acr06h(int io, int data)
{

	switch (io) {

		case 0:						// READ
			return CASS_Stat();
			
		case 1:						// WRITE
			return 0;				// write to status register unsupported
			
		default:					// DEFAULT
			return CASS_Stat();
	}
}


/*--  acr07h  -------------------------------------------------
	This is the data register handler for the cassette port.
	io=0=read, 1=write.

-------------------------------------------------------------*/
int acr07h(int io, int data)
{


	switch (io) {
		case 0:						// READ
			return CASS_Read();
			
		case 1:						// WRITE
			CASS_Write((byte) data);
			return 0;
			
		default:					// DEFAULT
			return 0xff;			// read from inactive port returns 0xff
	}

}


/*--  CASS_OpenWin --------------------------------------------
	Called by main UI code to open up a	modeless dialog for
	the PTP device.
	
-------------------------------------------------------------*/
void CASS_OpenWin( void )
{

	// CreateDialog returns NULL on failure
	if (!IsWindow(hwndCASS)) {
		hwndCASS = CreateDialog(winstate.hInst,
								"CassDev",
								winstate.hWnd,
								(DLGPROC) WndProcPTP2);
		ShowWindow (hwndCASS, SW_SHOW);
	}
}


/*--  CASS_Read  ----------------------------------------------
	Called by the I/O handler in pio.c. Reads a character from
	the file stream and sets status variable to be returned by
	_Stat. Will return character read or 0xff on error (on 8080,
	reading	from unconnected port address returns 0xff).

	NOTE: Tape files are opened R/W (i.e., "change"). When
	switching between reading and writing to the same tape file,
	the code needs to execute one of the following stream
	functions in order to flush the buffer: fseek, rewind,
	fflush, or fsetpos.
-------------------------------------------------------------*/
static int CASS_Read( void )
{

	static int eof_flag;			// preserve between calls
	static byte bufchar = 0xff;	// character read from stream
	byte buftemp = 0xff;
	fpos_t	temp_pos = 0;


	if (pFileIO == NULL)
		return 0xff;

	if (!eof_flag){							// not EOF yet
		fread(&bufchar, 1, 1, pFileIO);	// get char. this one we return to caller
		fgetpos(pFileIO, &temp_pos);		// to remember where to go

		fread(&buftemp, 1, 1, pFileIO);	// get next char; throw away
		if (feof(pFileIO)){					// if error after 2d read
			Status_SetText(hwndStatusBar, STATBAR_READY, 0, "EOF error on read of file stream.");
			iPTPStatus |= RxStatC_BIT;		// no data available (1=no data)
			eof_flag = 1;
		}
		else {
			iPTPStatus &= ~RxStatC_BIT;			// signal data available
			eof_flag = 0;
		}
		fsetpos(pFileIO, &temp_pos);		// go back to where we were
		g_curtime = (int)temp_pos;
		DrawCounter(g_hWndDC, g_hDigits, g_curtime, 1);
		SetDlgItemInt(hwndCASS, IDC_EDIT_PTR, (int) temp_pos, FALSE);
		return bufchar;
	}
	else									// already EOF
		return bufchar;					// 0 after EOF
}


/*--  CASS_Write  ---------------------------------------------
	Called by the I/O handler in sio.c. Writes character to the
	file stream and sets status variable to be returned by
	_Stat.

	Need to implement tape counter.
-------------------------------------------------------------*/
static void CASS_Write( byte data )
{

	fpos_t	temp_pos = 0;

	
	if (pFileIO == NULL)
		return;

	// Begin writing to file stream
	if (fputc(data, pFileIO) != EOF){	// write character to stream
		iPTPStatus &= ~TxStatC_BIT;		// TxStat=OK
		fgetpos(pFileIO, &temp_pos);
		g_curtime = (int)temp_pos;
		DrawCounter(g_hWndDC, g_hDigits, g_curtime, 1);
		SetDlgItemInt(hwndCASS, IDC_EDIT_PTR, (int)temp_pos, FALSE);
		return;
	}

	iPTPStatus |= TxStatC_BIT;			// assume TxStat=busy
	Status_SetText(hwndStatusBar, STATBAR_READY, 0, "Write error on cassette device.");
	return;
}


/*--  CASS_Stat  ----------------------------------------------
	Called by code in sio.c. Need special handling due to
	differences in returned values for paper tape BASIC and
	CP/M. BASIC Status returns three values:_WDB (busy), _RDE
	(no input but can write) or ~_RDE (input and can write).
	CP/M returns three values:_WDB (busy), _RDE2 (no input but
	can write) or RDE3 (input and can write) for use with CP/M.
-------------------------------------------------------------*/
static int CASS_Stat( void )
{

	return giPTPStat;
}


/*--  CASS_Init  ----------------------------------------------
	Called by emulator startup code. Maybe we can move to
	_INITDIALOG?
-------------------------------------------------------------*/
void CASS_Init(void)
{

	// location of timer digits
	g_rcTime.top    = 270;	//45
	g_rcTime.left   = 45;	//250
	g_rcTime.bottom = g_rcTime.top  + 20;	// 20 pixels high
	g_rcTime.right  = g_rcTime.left + 9*4;	// 4 digits 9 pixels wide

}


/*--  CASS_Destroy  -------------------------------------------
	Closes any open image files and destroys persistent objects.
	If calling _Destroy from the DlgProc, call with flag=0 so
	that any open file is available while the window is closed.
	If calling from MainWndProc during shutdown, call with
	flag=1 to ensure that any open file is closed. 
-------------------------------------------------------------*/
void CASS_Destroy(int flag)
{
	// Probably should make arrays of DCs and hBitmaps
	
	if (flag)
		CASS_Shutdown();			// shutdown file on emulator exit

	// delete hDCs
	if (g_hWndDC != NULL) {			// global handle to dialog DC
		DeleteDC(g_hWndDC);
		g_hWndDC = NULL;
	}

	// delete hBitmaps
	if (g_hDigits != NULL) {		// global handle to digits bitmap
		DeleteObject(g_hDigits);
		g_hDigits = NULL;
	}
	if (g_hPnlBmp != NULL) {		// global handle to panel bitmap
		DeleteObject(g_hPnlBmp);
		g_hPnlBmp = NULL;
	}
	if (g_hLED_on != NULL) {		// global handle to led_on bitmap
		DeleteObject(g_hLED_on);
		g_hLED_on = NULL;
	}
	if (g_hLED_off != NULL) {		// global handle to led_off bitmap
		DeleteObject(g_hLED_off);
		g_hLED_off = NULL;
	}
	if (g_hKSr_UP != NULL) {		// global handle to red switch up
		DeleteObject(g_hKSr_UP);
		g_hKSr_UP = NULL;
	}
	if (g_hKSr_DN != NULL) {		// global handle to red switch down
		DeleteObject(g_hKSr_DN);
		g_hKSr_DN = NULL;
	}
	if (g_hKSb_UP != NULL) {		// global handle to black switch up
		DeleteObject(g_hKSb_UP);
		g_hKSb_UP = NULL;
	}
	if (g_hKSb_DN != NULL) {		// global handle to black switch down
		DeleteObject(g_hKSb_DN);
		g_hKSb_DN = NULL;
	}
}


/*--  CASS_Shutdown -------------------------------------------
	Called by emulator power switch handler and by _Destroy.
	Ensures that any opened tape image files are closed properly
	before reinitializing the emulator or upon exit (when _Destroy
	is called with flag=1).	
-------------------------------------------------------------*/
void CASS_Shutdown(void)
{

	if (pFileIO != NULL){
		// File must be open, so close it.
		fclose(pFileIO);
		pFileIO = NULL;
		pos = g_curtime = 0;
		Status_SetText(hwndStatusBar, STATBAR_READY, 0, "Successfully closed cassette file.");
		Status_SetText(hwndStatusBar, STATBAR_CASS, 0, "Cass Off");
		iPTPStatus = ( TxStatC_BIT | RxStatC_BIT );
	}

	// If window was open previously, destroy it.
	// NULLCHG
	if (hwndCASS != NULL){
		DestroyWindow(hwndCASS);
		hwndCASS = NULL;
	}

}


/**************************************************************\
*   Windows Callback Routines  ********************************|
\**************************************************************/
LRESULT CALLBACK WndProcPTP2(HWND hDlg, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
	char *ProfBuf;
	char buf2[80];
	static HDC HDCdlg;
	static int x_size, y_size;
	RECT rc;	// RECT for moving window
	register x,y;


    switch (iMsg) {
		case WM_LBUTTONDOWN:
			x=LOWORD( lParam );
			y=HIWORD( lParam );

			//Reset button HIT_TEST. If HIT, send BOT message
			if (IN_RECT(x,y,99,99+10,270,270+15)) {
				SendMessage(hDlg,WM_COMMAND,IDC_BTN_PTPBOT,0);
				break;
			}

			KSHitTest( lParam );
			break;

		case WM_LBUTTONUP:
			if ( Keyswitchdown >= 0 ) {
				DoKeySwitch( Keyswitchdown, KeyList[Keyswitchdown].pos = SW_UP );
				Keyswitchdown = -1;
			}
			break;

		case WM_INITDIALOG:
			if (pFileIO == NULL){	// pickup current PTP status
				DoCaption(hDlg, "Cassette Deck", "");	//start at untitled
	    		EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPSTOP), FALSE);
				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPBOT), FALSE);
    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREW), FALSE);
    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPFF), FALSE);
				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPPLAY), TRUE);
				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREC), TRUE);
				EnableWindow(GetDlgItem(hDlg, IDR_CPM), FALSE);
	    		EnableWindow(GetDlgItem(hDlg, IDR_BASIC), FALSE);
			}
			else {
				DoCaption(hDlg, "Cassette Deck", szOpenFileTitle);
	    		EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPSTOP), TRUE);
				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPBOT), TRUE);
	    		EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREW), TRUE);
    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPFF), TRUE);
				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPPLAY), FALSE);
				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREC), FALSE);
				EnableWindow(GetDlgItem(hDlg, IDR_CPM), TRUE);
	    		EnableWindow(GetDlgItem(hDlg, IDR_BASIC), TRUE);
			}

			SendMessage(GetDlgItem(hDlg,IDC_BTN_PTPSTOP),
				BM_SETIMAGE,
				IMAGE_ICON,
				(long)LoadIcon(winstate.hInst, "STOP"));

			SendMessage(GetDlgItem(hDlg,IDC_BTN_PTPBOT),
				BM_SETIMAGE,
				IMAGE_ICON,
				(long)LoadIcon(winstate.hInst, "BEGIN1")); // GCC CHANGE

			SendMessage(GetDlgItem(hDlg,IDC_BTN_PTPREW),
				BM_SETIMAGE,
				IMAGE_ICON,
				(long)LoadIcon(winstate.hInst, "REWIND"));

			SendMessage(GetDlgItem(hDlg,IDC_BTN_PTPPLAY),
				BM_SETIMAGE,
				IMAGE_ICON,
				(long)LoadIcon(winstate.hInst, "PLAY"));
			
			SendMessage(GetDlgItem(hDlg,IDC_BTN_PTPREC),
				BM_SETIMAGE,
				IMAGE_ICON,
				(long)LoadIcon(winstate.hInst, "RECORD"));
			
			SendMessage(GetDlgItem(hDlg,IDC_BTN_PTPFF),
				BM_SETIMAGE,
				IMAGE_ICON,
				(long)LoadIcon(winstate.hInst, "FASTFWD"));

			// Load various resources
			g_hLED_on = LoadBitmap(winstate.hInst, "rled_on");
			g_hLED_off = LoadBitmap(winstate.hInst, "rled_off");
			g_hKSr_UP = LoadBitmap(winstate.hInst, "kred_up");
			g_hKSr_DN = LoadBitmap(winstate.hInst, "kred_dn");
			g_hKSb_UP = LoadBitmap(winstate.hInst, "kblk_up");
			g_hKSb_DN = LoadBitmap(winstate.hInst, "kblk_dn");
			g_hPnlBmp = LoadBitmap(winstate.hInst,"CASSPANEL");
			g_hDigits = LoadBitmap(winstate.hInst, MAKEINTRESOURCE(IDB_DIGITS));
			g_hWndDC = HDCdlg = GetDC(hDlg);

			// Move window into position
			GetWindowRect (hDlg, &rc);	// device window
			x_size = (int)(rc.right - rc.left);	// base size x
			y_size = (int)(rc.bottom - rc.top);	// base size y
			ProfBuf = SYSCFG_ReadConfigInfo("windows", "cassette");
		    if (sscanf(ProfBuf, "%d,%d", &rc.left, &rc.top) == 2) {
				MoveWindow(hDlg, rc.left, rc.top, x_size, y_size , TRUE);
		    }

			// do final configuring of dialog
			SetDlgItemInt(hDlg, IDC_EDIT_PTR, 0, FALSE);
			SetFocus(GetDlgItem (hDlg, IDC_BTN_PTPPLAY));

			// set up the rew/ff timer. 1000ms=1sec
			//hwnd, timerID, interval, lpCallback
			SetTimer(hDlg, 1, CASS_tmrfrq, NULL);	// 200ms
			return FALSE;				// don't let Windows set default focus

		case WM_CTLCOLOREDIT:
		case WM_PAINT:
			DrawPanelBitmap(HDCdlg, g_hPnlBmp, 0, 45);
			DrawCounter(HDCdlg, g_hDigits, g_curtime, 1);	// forced update
			DrawRecordLED(HDCdlg);
			break;

		case WM_TIMER:
			// use for rew/ff
			if (iAdvanceMode == 1){
				DoRewind(hDlg);
			}
			else if (iAdvanceMode == 2){
				DoFastForward(hDlg);
			}
			break;

		case WM_COMMAND:
	    	switch (LOWORD (wParam)) {
				case IDC_BTN_PTPSTOP:	// detach a tape image
					// new function...click once to stop RW/FF; second click
					// to eject tape.
					if (iAdvanceMode > 0){	// first time
						iAdvanceMode = 0;	// stop advancing and exit
						break;
					}
					
					if (DetachPTPFile(hDlg)){
	    				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPSTOP), FALSE);
		    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPBOT), FALSE);
		    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREW), FALSE);
		    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPFF), FALSE);
						EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPPLAY), TRUE);
						SetFocus(GetDlgItem (hDlg, IDC_BTN_PTPPLAY));
						EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREC), TRUE);
						EnableWindow(GetDlgItem(hDlg, IDR_CPM), FALSE);
			    		EnableWindow(GetDlgItem(hDlg, IDR_BASIC), FALSE);
					}
	    			break;
	    			
				case IDC_BTN_PTPBOT:	// rewind tape to start
					if (pFileIO != NULL){
						iAdvanceMode = 0;	// stop advance mode
						ks_record = 0;		// normal mode
						rewind(pFileIO);
						fgetpos (pFileIO, &pos);
						g_curtime = (int)pos;
						DrawCounter(g_hWndDC, g_hDigits, g_curtime, 1);
						SetDlgItemInt(hwndCASS, IDC_EDIT_PTR, (int) pos, FALSE);
					}
					break;

				case IDC_BTN_PTPREW:
					iAdvanceMode = 1;
					ks_record = 0;		// normal mode
					break;

				case IDC_BTN_PTPFF:
					iAdvanceMode = 2;
					ks_record = 0;		// normal mode
					break;

				case IDC_BTN_PTPPLAY:
					iAdvanceMode = 0;	// no advance mode
					ks_record = 0;		// normal mode
					if (AttachPTPFile(hDlg, 0)){	// if success, enable DET and disable ATT
						DoCaption(hDlg, "Cassette Deck", szOpenFileTitle);
	    				EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPSTOP), TRUE);
						SetFocus(GetDlgItem (hDlg, IDC_BTN_PTPSTOP));
		    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPBOT), TRUE);
			    		EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREW), TRUE);
		    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPFF), TRUE);
						EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPPLAY), FALSE);
						EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREC), FALSE);
						EnableWindow(GetDlgItem(hDlg, IDR_CPM), TRUE);
			    		EnableWindow(GetDlgItem(hDlg, IDR_BASIC), TRUE);
						KeyList[1].pos = SW_DOWN;
					}
					else {
						KeyList[1].pos = SW_UP;
					}
					break;

				case IDC_BTN_PTPREC:
					iAdvanceMode = 0;	// no advance mode
					if (AttachPTPFile(hDlg, 1)){	// if success, enable DET and disable ATT
						ks_record = 1;		// record mode
						DoCaption(hDlg, "Cassette Deck", szOpenFileTitle);
						EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPSTOP), TRUE);
						SetFocus(GetDlgItem (hDlg, IDC_BTN_PTPSTOP));
		    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPBOT), TRUE);
			    		EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREW), TRUE);
		    			EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPFF), TRUE);
			    		EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPPLAY), FALSE);
						EnableWindow(GetDlgItem(hDlg, IDC_BTN_PTPREC), FALSE);
						EnableWindow(GetDlgItem(hDlg, IDR_CPM), TRUE);
			    		EnableWindow(GetDlgItem(hDlg, IDR_BASIC), TRUE);
						KeyList[0].pos = SW_DOWN;	//rec
						KeyList[1].pos = SW_DOWN;	//play
					}
					else {
						KeyList[0].pos = SW_UP;
						KeyList[1].pos = SW_UP;
					}
					break;

				case IDCANCEL:
					iAdvanceMode = 0;		// don't rew/ff when closed
					ks_record = 0;			// normal mode
					KillTimer(hDlg, 1);	// don't need timer
					
					GetWindowRect(hDlg, &rc);
				    sprintf(buf2, "%d,%d", rc.left, rc.top);
				    SYSCFG_WriteConfigInfo("windows", "cassette", buf2);

					CASS_Destroy(0);		// destroys DCs and handles but keeps file open
					DestroyWindow(hDlg);
					hwndCASS = NULL;
					SetForegroundWindow(winstate.hWnd);	// main window
					break;
			}	// end of switch (wParam)
 			break;
	}	// message case
	return FALSE;
}

/*  end of file: cass.c  */
