/*
 *   display.c
 *
 *   This file is part of Emu48
 *
 *   Copyright (C) 2004 Christoph Gießelink
 *
 */
#include "pch.h"
#include "resource.h"
#include "Emu48.h"
#include "io.h"
#include "kml.h"

#define DISPLAY_FREQ	19					// display update 1/frequency (1/64) in ms

#define NOCOLORS	2

#define B 0x00000000						// black
#define W 0x00FFFFFF						// white
#define I 0xFFFFFFFF						// ignore

#define LCD_ROW		(36*4)					// max. pixel per line

// main display lines, handle zero lines exception
#define LINES(n)	(((n) == 0) ? 64 : ((n)+1))

UINT    nFullScreen = 0;
UINT    nVertical = 0;
UINT    nBackgroundX = 0;
UINT    nBackgroundY = 0;
UINT    nBackgroundW = 0;
UINT    nBackgroundH = 0;
UINT    nLcdX = 0;
UINT    nLcdY = 0;
UINT    nLcdZoom = 1;
UINT    nLcdDiv = 1;
LPBYTE	pbyLcd;
HDC     hLcdDC = NULL;
HDC     hMainDC = NULL;

static HBITMAP hLcdBitmap;
static HBITMAP hMainBitmap;

static INT nLcdWidth,nLcdHeight,nRot180;

static BYTE Buf[36];

static LARGE_INTEGER lLcdRef;				// reference time for VBL counter

static HANDLE hCThreadLcd = NULL;
static HANDLE hEventLcd;

static CONST DWORD Pattern[] =
{
	0x00000000, 0x00000001, 0x00000100, 0x00000101,
	0x00010000, 0x00010001, 0x00010100, 0x00010101,
	0x01000000, 0x01000001, 0x01000100, 0x01000101,
	0x01010000, 0x01010001, 0x01010100, 0x01010101
};

static DWORD dwKMLColor[64] =				// color table loaded by KML script
{
	W,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,
	B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,
	I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,
	I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I
};

static struct
{
	BITMAPINFOHEADER Lcd_bmih;
	RGBQUAD bmiColors[NOCOLORS];
} bmiLcd =
{
	{0x28,0/*x*/,0/*y*/,1,8,BI_RGB,0,0,0,NOCOLORS,0}
};

static VOID (*UpdateDisplayMem)(VOID) = NULL;

static VOID UpdateDisplayH(VOID);
static VOID UpdateDisplayV(VOID);

VOID UpdateContrast(BYTE byContrast)
{
	// when display is off use contrast 0
	if ((Chipset.IORam[BITOFFSET] & DON) == 0) byContrast = 0;

	bmiLcd.bmiColors[1] = *(RGBQUAD*)&dwKMLColor[byContrast];    // pixel on  color
	bmiLcd.bmiColors[0] = *(RGBQUAD*)&dwKMLColor[byContrast+32]; // pixel off color

	// if background color is undefined, use color 0 for compatibility
	if (I == *(DWORD*)&bmiLcd.bmiColors[0]) bmiLcd.bmiColors[0] = *(RGBQUAD*)&dwKMLColor[0];

	// update palette information
	EnterCriticalSection(&csGDILock);		// asynchronous display update!
	{
		_ASSERT(hLcdDC);
		DeleteObject(SelectObject(hLcdDC,CreateDIBSection(hWindowDC,
			(BITMAPINFO*)&bmiLcd,DIB_RGB_COLORS,&pbyLcd,NULL,0)));
		ZeroMemory(pbyLcd,bmiLcd.Lcd_bmih.biWidth * nLcdHeight);
	}
	LeaveCriticalSection(&csGDILock);
	return;
}

VOID SetLcdColor(UINT nId, UINT nRed, UINT nGreen, UINT nBlue)
{
	dwKMLColor[nId&0x3F] = ((nRed&0xFF)<<16)|((nGreen&0xFF)<<8)|(nBlue&0xFF);
	return;
}

VOID CreateLcdBitmap(VOID)
{
	INT nLcdBmpWidth;

	// create LCD bitmap
	if (nVertical > 0)						// landscape mode
	{
		// set display update routine
		UpdateDisplayMem = UpdateDisplayV;

		nLcdBmpWidth = 64;
		nLcdWidth  = 64;
		nLcdHeight = LCD_ROW;

		nRot180 = (nVertical > 1) ? -1 : 1;	// rotating 180 degree flag
	}
	else									// portrait mode
	{
		// set display update routine
		UpdateDisplayMem = UpdateDisplayH;

		nLcdBmpWidth = LCD_ROW;
		nLcdWidth  = 131;
		nLcdHeight = 64;
	}

	bmiLcd.Lcd_bmih.biWidth = nLcdBmpWidth;
	bmiLcd.Lcd_bmih.biHeight = nLcdHeight;
	if (nVertical == 0) bmiLcd.Lcd_bmih.biHeight = -bmiLcd.Lcd_bmih.biHeight;
	_ASSERT(hLcdDC == NULL);
	hLcdDC = CreateCompatibleDC(hWindowDC);
	_ASSERT(hLcdDC != NULL);
	hLcdBitmap = CreateDIBSection(hWindowDC,(BITMAPINFO*)&bmiLcd,DIB_RGB_COLORS,&pbyLcd,NULL,0);
	_ASSERT(hLcdBitmap != NULL);
	hLcdBitmap = SelectObject(hLcdDC,hLcdBitmap);
	_ASSERT(hPalette != NULL);
	SelectPalette(hLcdDC,hPalette,FALSE);	// set palette for LCD DC
	RealizePalette(hLcdDC);					// realize palette
	ASSERT(ARRAYSIZEOF(Pattern) == 16);		// check Nibble -> DIB mask pattern
	UpdateContrast(Chipset.contrast);
	return;
}

VOID DestroyLcdBitmap(VOID)
{
	// set contrast palette to startup colors
	WORD i = 0;   dwKMLColor[i++] = W;
	while(i < 32) dwKMLColor[i++] = B;
	while(i < 64) dwKMLColor[i++] = I;

	if (hLcdDC != NULL)
	{
		// destroy LCD bitmap
		DeleteObject(SelectObject(hLcdDC,hLcdBitmap));
		DeleteDC(hLcdDC);
		hLcdDC = NULL;
		hLcdBitmap = NULL;
	}
	return;
}

BOOL CreateMainBitmap(LPCTSTR szFilename)
{
	HPALETTE hAssertPalette;

	_ASSERT(hWindowDC != NULL);
	hMainDC = CreateCompatibleDC(hWindowDC);
	_ASSERT(hMainDC != NULL);
	if (hMainDC == NULL) return FALSE;		// quit if failed
	hMainBitmap = LoadBitmapFile(szFilename);
	if (hMainBitmap == NULL)
	{
		DeleteDC(hMainDC);
		hMainDC = NULL;
		return FALSE;
	}
	hMainBitmap = SelectObject(hMainDC,hMainBitmap);
	_ASSERT(hPalette != NULL);
	hAssertPalette = SelectPalette(hMainDC,hPalette,FALSE);
	_ASSERT(hAssertPalette != NULL);
	RealizePalette(hMainDC);
	return TRUE;
}

VOID DestroyMainBitmap(VOID)
{
	if (hMainDC != NULL)
	{
		// destroy Main bitmap
		DeleteObject(SelectObject(hMainDC,hMainBitmap));
		DeleteDC(hMainDC);
		hMainDC = NULL;
		hMainBitmap = NULL;
	}
	return;
}

//****************
//*
//* LCD functions
//*
//****************

static __inline VOID WritePixelH(BYTE **d,DWORD p)
{
	#define GRAYMASK	(((((NOCOLORS)-1)>>1)<<24) \
						|((((NOCOLORS)-1)>>1)<<16) \
						|((((NOCOLORS)-1)>>1)<<8)  \
						|((((NOCOLORS)-1)>>1)))

	*(((DWORD*)(*d))++) = ((*((DWORD*)(*d)) & GRAYMASK) << 1) | (p);
	return;
	#undef GRAYMASK
}

static VOID UpdateDisplayH(VOID)
{
	UINT  x, y, nLines;
	BYTE  *p;
	DWORD d;

	// main display area
	nLines = LINES(Chipset.lcounter);		// main display lines

	p = pbyLcd;								// bitmap offset
	d = 0;									// pixel offset counter
	for (y = 0; y < nLines; ++y)
	{
		// read line with actual start1 address!!
		Npeek(Buf,d+Chipset.start1,36);
		for (x = 0; x < 36; ++x)
		{
			WritePixelH(&p,Pattern[Buf[x]]);
		}
		d += (34 + Chipset.loffset + (Chipset.boffset / 4) * 2) & 0xFFFFFFFE;
	}

	EnterCriticalSection(&csGDILock);		// solving NT GDI problems
	{
		// main area
		StretchBlt(hWindowDC,
				   (nLcdX - nBackgroundX) * nAppZoom,
				   (nLcdY - nBackgroundY) * nAppZoom,
				   MulDiv(131,nLcdZoom,nLcdDiv) * nAppZoom,
				   MulDiv(nLines,nLcdZoom,nLcdDiv) * nAppZoom,
				   hLcdDC,
				   Chipset.boffset, 0,
				   131, nLines,
				   SRCCOPY);
	}
	LeaveCriticalSection(&csGDILock);

	// menu display area
	if (nLines < 64)						// menu area enabled
	{
		// check bitmap offset
		_ASSERT(p == pbyLcd + (nLines*LCD_ROW));
		d = 0;								// pixel offset counter
		for (y = nLines; y < 64; ++y)
		{
			// 34 nibbles are viewed
			Npeek(Buf,d+Chipset.start2,34);
			for (x = 0; x < 34; ++x)
			{
				WritePixelH(&p,Pattern[Buf[x]]);
			}
			// adjust pointer to 36 DIBPIXEL drawing calls
			p += (36-34) * sizeof(DWORD);
			d += 34;
		}

		EnterCriticalSection(&csGDILock);	// solving NT GDI problems
		{
			// menu area
			StretchBlt(hWindowDC,
					   (nLcdX - nBackgroundX) * nAppZoom,
					   (nLcdY - nBackgroundY + MulDiv(nLines,nLcdZoom,nLcdDiv)) * nAppZoom,
					   MulDiv(131,nLcdZoom,nLcdDiv) * nAppZoom,
					   MulDiv((64-nLines),nLcdZoom,nLcdDiv) * nAppZoom,
					   hLcdDC,
					   0, nLines,
					   131, 64-nLines,
					   SRCCOPY);
		}
		LeaveCriticalSection(&csGDILock);
	}
	return;
}

static __inline VOID WritePixelV(BYTE **d,DWORD p)
{
	#define GRAYMASK	(((NOCOLORS)-1)>>1)

	**d = ((**d & GRAYMASK) << 1) | (BYTE) (p >> 0);
	*d += 64;
	**d = ((**d & GRAYMASK) << 1) | (BYTE) (p >> 8);
	*d += 64;
	**d = ((**d & GRAYMASK) << 1) | (BYTE) (p >> 16);
	*d += 64;
	**d = ((**d & GRAYMASK) << 1) | (BYTE) (p >> 24);
	*d += 64;
	return;
	#undef GRAYMASK
}

static VOID UpdateDisplayV(VOID)
{
	UINT  x, y, nLines;
	BYTE  *p;
	DWORD d;

	// main display area
	nLines = LINES(Chipset.lcounter);		// main display lines

	d = 0;									// pixel offset counter
	for (y = 0; y < nLines; ++y)
	{
		p = pbyLcd + y;
		// read line with actual start1 address!!
		Npeek(Buf,d+Chipset.start1,36);
		for (x = 0; x < 36; ++x)
		{
			WritePixelV(&p,Pattern[Buf[x]]);
		}
		d += (34 + Chipset.loffset + (Chipset.boffset / 4) * 2) & 0xFFFFFFFE;
	}

	EnterCriticalSection(&csGDILock);		// solving NT GDI problems
	{
		// main area
		StretchBlt(hWindowDC,
				   (nLcdX - nBackgroundX) * nAppZoom,
				   (nLcdY - nBackgroundY) * nAppZoom,
				   nRot180 * MulDiv(nLines,nLcdZoom,nLcdDiv) * nAppZoom,
				   nRot180 * MulDiv(131,nLcdZoom,nLcdDiv) * nAppZoom,
				   hLcdDC,
				   0, (LCD_ROW-131)-Chipset.boffset,
				   nLines, 131,
				   SRCCOPY);
	}
	LeaveCriticalSection(&csGDILock);

	// menu display area
	if (nLines < 64)						// menu area enabled
	{
		// calculate bitmap offset
		d = 0;								// pixel offset counter
		for (y = nLines; y < 64; ++y)
		{
			p = pbyLcd + y;
			// 34 nibbles are viewed
			Npeek(Buf,d+Chipset.start2,34);
			for (x = 0; x < 34; ++x)
			{
				WritePixelV(&p,Pattern[Buf[x]]);
			}
			d += 34;
		}

		EnterCriticalSection(&csGDILock);	// solving NT GDI problems
		{
			// menu area
			StretchBlt(hWindowDC,
					   (nLcdX - nBackgroundX + nRot180 * MulDiv(nLines,nLcdZoom,nLcdDiv)) * nAppZoom,
					   (nLcdY - nBackgroundY) * nAppZoom,
					   nRot180 * MulDiv((64-nLines),nLcdZoom,nLcdDiv) * nAppZoom,
					   nRot180 * MulDiv(131,nLcdZoom,nLcdDiv) * nAppZoom,
					   hLcdDC,
					   nLines, LCD_ROW-131,
					   64-nLines, 131,
					   SRCCOPY);
		}
		LeaveCriticalSection(&csGDILock);
	}
	return;
}

VOID UpdateAnnunciators(VOID)
{
	BYTE c;

	c = (BYTE)(Chipset.IORam[ANNCTRL] | (Chipset.IORam[ANNCTRL+1]<<4));
	// switch annunciators off if timer stopped
	if ((c & AON) == 0 || (Chipset.IORam[TIMER2_CTRL] & RUN) == 0)
		c = 0;

	DrawAnnunciator(1,c&LA1);
	DrawAnnunciator(2,c&LA2);
	DrawAnnunciator(3,c&LA3);
	DrawAnnunciator(4,c&LA4);
	DrawAnnunciator(5,c&LA5);
	DrawAnnunciator(6,c&LA6);
	return;
}

static DWORD WINAPI LcdThread(LPVOID pParam)
{
	while (WaitForSingleObject(hEventLcd,DISPLAY_FREQ) == WAIT_TIMEOUT)
	{
		UpdateDisplayMem();					// update display
		QueryPerformanceCounter(&lLcdRef);	// actual time
	}
	return 0;
	UNREFERENCED_PARAMETER(pParam);
}

// LCD line counter calculation
BYTE GetLineCounter(VOID)
{
	LARGE_INTEGER lLC;
	BYTE          byTime;

	if (hCThreadLcd == NULL)				// display off
		return ((Chipset.IORam[LINECOUNT+1] & (LC5|LC4)) << 4) | Chipset.IORam[LINECOUNT];

	QueryPerformanceCounter(&lLC);			// get elapsed time since display update

	// elapsed ticks so far
	byTime = (BYTE) (((lLC.QuadPart - lLcdRef.QuadPart) << 12) / lFreq.QuadPart);

	if (byTime > 0x3F) byTime = 0x3F;		// all counts made

	return 0x3F - byTime;					// update display between VBL counter 0x3F-0x3E
}

VOID StartDisplay(BYTE byInitial)
{
	if (hCThreadLcd)						// LCD update thread running
		return;								// -> quit

	if (Chipset.IORam[BITOFFSET]&DON)		// display on?
	{
		// event to cancel Lcd refresh loop
		hEventLcd = CreateEvent(NULL,FALSE,FALSE,NULL);

		QueryPerformanceCounter(&lLcdRef);	// actual time of top line

		// adjust startup counter to get the right VBL value
		_ASSERT(byInitial <= 0x3F);			// line counter value 0 - 63
		lLcdRef.QuadPart -= ((LONGLONG) (0x3F - byInitial) * lFreq.QuadPart) >> 12;

		// start Lcd update thread
		VERIFY(hCThreadLcd = CreateThread(NULL,0,&LcdThread,NULL,0,NULL));
	}
	return;
}

VOID StopDisplay(VOID)
{
	// dummy region, WM_PAINT redraw always complete display area
	RECT Rect = { (nLcdX - nBackgroundX) * nAppZoom,
				  (nLcdY - nBackgroundY) * nAppZoom,
				  (nLcdX - nBackgroundX) * nAppZoom + 1,
				  (nLcdY - nBackgroundY) * nAppZoom + 1
				};

	BYTE a[2];
	ReadIO(a,LINECOUNT,2,TRUE);				// update VBL at display off time

	if (hCThreadLcd == NULL)				// thread stopped
		return;								// -> quit

	SetEvent(hEventLcd);					// leave Lcd update thread
	WaitForSingleObject(hCThreadLcd,INFINITE);
	CloseHandle(hCThreadLcd);
	hCThreadLcd = NULL;						// set flag display update stopped
	CloseHandle(hEventLcd);					// close Lcd event

	ZeroMemory(pbyLcd, bmiLcd.Lcd_bmih.biWidth * nLcdHeight);
	InvalidateRect(hWnd,&Rect,FALSE);
	return;
}

VOID ResizeWindow(VOID)
{
	RECT rc;

	if (hWnd == NULL) return;				// return if window closed

	GetWindowRect(hWnd, &rc);

	// change client position
	rc.top = ((nFullScreen & TOPBAR_OFF) != 0) ? 0 : lrcTop;
	SetWindowPos(hWnd, NULL,
		rc.left,
		rc.top,
		nBackgroundW * nAppZoom,
		nBackgroundH * nAppZoom,
		SWP_NOZORDER);

	GetClientRect(hWnd, &rc);
	if (rc.bottom < (LONG) (nBackgroundH * nAppZoom))
	{
		SetWindowPos(hWnd, NULL, 0, 0,
			nBackgroundW * nAppZoom,
			(2 * nBackgroundH - rc.bottom) * nAppZoom,
			SWP_NOMOVE | SWP_NOZORDER);
	}
	InvalidateRect(hWnd,NULL,TRUE);
	return;
}

