/**************************************************************

   custom_video_ati.cpp - ATI legacy library
   ---------------------------------------------------------

   Switchres   Modeline generation engine for emulation

   License     GPL-2.0+
   Copyright   2010-2021 Chris Kennedy, Antonio Giner,
                         Alexandre Wodarczyk, Gil Delescluse

 **************************************************************/

#include <windows.h>
#include <stdio.h>
#include "custom_video_ati.h"
#include "log.h"


//============================================================
//  ati_timing::ati_timing
//============================================================

ati_timing::ati_timing(char *device_name, custom_video_settings *vs)
{
	m_vs = *vs;
	strcpy (m_device_name, device_name);
	strcpy (m_device_key, m_vs.device_reg_key);
}

//============================================================
//  ati_timing::ati_timing
//============================================================

bool ati_timing::init()
{
	log_verbose("ATI legacy init\n");

	// Get Windows version
	win_version = os_version();

	if (win_version > 5 && !is_elevated())
	{
		log_error("ATI legacy error: the program needs administrator rights.\n");
		return false;
	}

	return true;
}

//============================================================
//  ati_timing::get_timing
//============================================================

bool ati_timing::get_timing(modeline *mode)
{
	HKEY hKey;
	char lp_name[1024];
	char lp_data[68];
	DWORD length;
	bool found = false;
	int refresh_label = mode->refresh_label? mode->refresh_label : mode->refresh * win_interlace_factor(mode);
	int vfreq_incr = 0;

	if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, m_device_key, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
	{
		sprintf(lp_name, "DALDTMCRTBCD%dx%dx0x%d", mode->width, mode->height, refresh_label);
		length = sizeof(lp_data);

		if (RegQueryValueExA(hKey, lp_name, NULL, NULL, (LPBYTE)lp_data, &length) == ERROR_SUCCESS && length == sizeof(lp_data))
			found = true;
		else if (win_version > 5 && mode->interlace)
		{
			vfreq_incr = 1;
			sprintf(lp_name, "DALDTMCRTBCD%dx%dx0x%d", mode->width, mode->height, refresh_label + vfreq_incr);
			if (RegQueryValueExA(hKey, lp_name, NULL, NULL, (LPBYTE)lp_data, &length) == ERROR_SUCCESS && length == sizeof(lp_data))
				found = true;
		}
		if (found)
		{
			mode->pclock  = get_DWORD_BCD(36, lp_data) * 10000;
			mode->hactive = get_DWORD_BCD(8, lp_data);
			mode->hbegin  = get_DWORD_BCD(12, lp_data);
			mode->hend    = get_DWORD_BCD(16, lp_data) + mode->hbegin;
			mode->htotal  = get_DWORD_BCD(4, lp_data);
			mode->vactive = get_DWORD_BCD(24, lp_data);
			mode->vbegin  = get_DWORD_BCD(28, lp_data);
			mode->vend    = get_DWORD_BCD(32, lp_data) + mode->vbegin;
			mode->vtotal  = get_DWORD_BCD(20, lp_data);
			mode->interlace = (get_DWORD(0, lp_data) & CRTC_INTERLACED)?1:0;
			mode->hsync     = (get_DWORD(0, lp_data) & CRTC_H_SYNC_POLARITY)?0:1;
			mode->vsync     = (get_DWORD(0, lp_data) & CRTC_V_SYNC_POLARITY)?0:1;
			mode->hfreq = mode->pclock / mode->htotal;
			mode->vfreq = mode->hfreq / mode->vtotal * (mode->interlace?2:1);
			mode->refresh_label = refresh_label;
			mode->type |= CUSTOM_VIDEO_TIMING_ATI_LEGACY;

			int checksum = 65535 - get_DWORD(0, lp_data) - mode->htotal - mode->hactive - mode->hend
						- mode->vtotal - mode->vactive - mode->vend - mode->pclock/10000;
			if (checksum != get_DWORD(64, lp_data))
				log_verbose("bad checksum! ");
		}
		RegCloseKey(hKey);
		return (found);
	}
	log_verbose("Failed opening registry entry for mode. ");
	return false;
}

//============================================================
//  ati_timing::set_timing
//============================================================

bool ati_timing::set_timing(modeline *mode)
{
	HKEY hKey;
	char lp_name[1024];
	char lp_data[68];
	long checksum;
	bool found = false;
	int refresh_label = mode->refresh_label? mode->refresh_label : mode->refresh * win_interlace_factor(mode);
	int vfreq_incr = 0;

	memset(lp_data, 0, sizeof(lp_data));
	set_DWORD_BCD(lp_data, (int)mode->pclock/10000, 36);
	set_DWORD_BCD(lp_data, mode->hactive, 8);
	set_DWORD_BCD(lp_data, mode->hbegin, 12);
	set_DWORD_BCD(lp_data, mode->hend - mode->hbegin, 16);
	set_DWORD_BCD(lp_data, mode->htotal, 4);
	set_DWORD_BCD(lp_data, mode->vactive, 24);
	set_DWORD_BCD(lp_data, mode->vbegin, 28);
	set_DWORD_BCD(lp_data, mode->vend - mode->vbegin, 32);
	set_DWORD_BCD(lp_data, mode->vtotal, 20);
	set_DWORD(lp_data, (mode->interlace?CRTC_INTERLACED:0) | (mode->hsync?0:CRTC_H_SYNC_POLARITY) | (mode->vsync?0:CRTC_V_SYNC_POLARITY), 0);

	checksum = 65535 - get_DWORD(0, lp_data) - mode->htotal - mode->hactive - mode->hend
			- mode->vtotal - mode->vactive - mode->vend - mode->pclock/10000;
	set_DWORD(lp_data, checksum, 64);

	if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, m_device_key, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
	{
		sprintf (lp_name, "DALDTMCRTBCD%dx%dx0x%d", mode->width, mode->height, refresh_label);

		if (RegQueryValueExA(hKey, lp_name, NULL, NULL, NULL, NULL) == ERROR_SUCCESS)
			found = true;
		else if (win_version > 5 && mode->interlace)
		{
			vfreq_incr = 1;
			sprintf(lp_name, "DALDTMCRTBCD%dx%dx0x%d", mode->width, mode->height, refresh_label + vfreq_incr);
			if (RegQueryValueExA(hKey, lp_name, NULL, NULL, NULL, NULL) == ERROR_SUCCESS)
				found = true;
		}

		if (!(found && RegSetValueExA(hKey, lp_name, 0, REG_BINARY, (LPBYTE)lp_data, 68) == ERROR_SUCCESS))
			log_info("Failed saving registry entry %s\n", lp_name);

		RegCloseKey(hKey);
		return (found);
	}

	log_info("Failed updating registry entry for mode.\n");
	return 0;
}

//============================================================
//  ati_timing::update_mode
//============================================================

bool ati_timing::update_mode(modeline *mode)
{
	if (!set_timing(mode))
		return false;

	mode->type |= CUSTOM_VIDEO_TIMING_ATI_LEGACY;

	// ATI needs a call to EnumDisplaySettings to refresh timings
	refresh_timings();

	return true;
}

//============================================================
//  ati_refresh_timings
//============================================================

void ati_timing::refresh_timings(void)
{
	int iModeNum = 0;
	DEVMODEA lpDevMode;

	memset(&lpDevMode, 0, sizeof(DEVMODEA));
	lpDevMode.dmSize = sizeof(DEVMODEA);

	while (EnumDisplaySettingsExA(m_device_name, iModeNum, &lpDevMode, 0) != 0)
		iModeNum++;
}

//============================================================
//  adl_timing::process_modelist
//============================================================

bool ati_timing::process_modelist(std::vector<modeline *> modelist)
{
	bool error = false;

	for (auto &mode : modelist)
	{
		if (!set_timing(mode))
		{
			mode->type |= MODE_ERROR;
			error = true;
		}
		else
		{
			mode->type &= ~MODE_ERROR;
			mode->type |= CUSTOM_VIDEO_TIMING_ATI_LEGACY;
		}
	}

	refresh_timings();
	return !error;
}

//============================================================
// get_DWORD
//============================================================

int ati_timing::get_DWORD(int i, char *lp_data)
{
	char out[32] = "";
	UINT32 x;

	sprintf(out, "%02X%02X%02X%02X", lp_data[i]&0xFF, lp_data[i+1]&0xFF, lp_data[i+2]&0xFF, lp_data[i+3]&0xFF);
	sscanf(out, "%08X", &x);
	return x;
}

//============================================================
// get_DWORD_BCD
//============================================================

int ati_timing::get_DWORD_BCD(int i, char *lp_data)
{
	char out[32] = "";
	UINT32 x;

	sprintf(out, "%02X%02X%02X%02X", lp_data[i]&0xFF, lp_data[i+1]&0xFF, lp_data[i+2]&0xFF, lp_data[i+3]&0xFF);
	sscanf(out, "%d", &x);
	return x;
}

//============================================================
// set_DWORD
//============================================================

void ati_timing::set_DWORD(char *data_string, UINT32 data_dword, int offset)
{
	char *p_dword = (char*)&data_dword;

	data_string[offset]   = p_dword[3]&0xFF;
	data_string[offset+1] = p_dword[2]&0xFF;
	data_string[offset+2] = p_dword[1]&0xFF;
	data_string[offset+3] = p_dword[0]&0xFF;
}

//============================================================
// set_DWORD_BCD
//============================================================

void ati_timing::set_DWORD_BCD(char *data_string, UINT32 data_dword, int offset)
{
	if (data_dword < 100000000)
	{
		int low_word, high_word;
		int a, b, c, d;
		char out[32] = "";

		low_word = data_dword % 10000;
		high_word = data_dword / 10000;

		sprintf(out, "%d %d %d %d", high_word / 100, high_word % 100 , low_word / 100, low_word % 100);
		sscanf(out, "%02X %02X %02X %02X", &a, &b, &c, &d);

		data_string[offset]   = a;
		data_string[offset+1] = b;
		data_string[offset+2] = c;
		data_string[offset+3] = d;
	}
}

//============================================================
// os_version
//============================================================

int ati_timing::os_version(void)
{
	OSVERSIONINFOA lpVersionInfo;

	memset(&lpVersionInfo, 0, sizeof(OSVERSIONINFOA));
	lpVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
	GetVersionExA (&lpVersionInfo);

	return lpVersionInfo.dwMajorVersion;
}

//============================================================
//  is_elevated
//============================================================

bool ati_timing::is_elevated()
{
	HANDLE htoken;
	bool result = false;

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &htoken))
		return false;

	TOKEN_ELEVATION te = {0};
	DWORD dw_return_length;

	if (GetTokenInformation(htoken, TokenElevation, &te, sizeof(te), &dw_return_length))
	{
		if (te.TokenIsElevated)
		{
			result = true;
		}
	}

	CloseHandle(htoken);
	return (result);
}

//============================================================
// win_interlace_factor
//============================================================

int ati_timing::win_interlace_factor(modeline *mode)
{
	if (win_version > 5 && mode->interlace)
		return 2;

	return 1;
}