/*
identification.c - unique id generation
Copyright (C) 2017 mittorn

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
*/

#include "common.h"
#include <fcntl.h>
#if !XASH_WIN32
#include <dirent.h>
#endif
static char id_md5[33];
static char id_customid[MAX_STRING];

/*
==========================================================

simple 64-bit one-hash-func bloom filter
should be enough to determine if device exist in identifier

==========================================================
*/
typedef uint64_t bloomfilter_t;

static bloomfilter_t id;

#define bf64_mask ((1U<<6)-1)

static bloomfilter_t BloomFilter_Process( const char *buffer, int size )
{
	dword crc32;
	bloomfilter_t value = 0;

	if( size <= 0 || size > 512 )
		return 0;

	CRC32_Init( &crc32 );
	CRC32_ProcessBuffer( &crc32, buffer, size );

	while( crc32 )
	{
		value |= ((uint64_t)1) << ( crc32 & bf64_mask );
		crc32 = crc32 >> 6;
	}

	return value;
}

static bloomfilter_t BloomFilter_ProcessStr( const char *buffer )
{
	return BloomFilter_Process( buffer, Q_strlen( buffer ) );
}

static uint BloomFilter_Weight( bloomfilter_t value )
{
	int weight = 0;

	while( value )
	{
		if( value & 1 )
			weight++;
		value = value >> 1;
#if _MSC_VER == 1200
		value &= 0x7FFFFFFFFFFFFFFF;
#endif
	}

	return weight;
}

static qboolean BloomFilter_ContainsString( bloomfilter_t filter, const char *str )
{
	bloomfilter_t value = BloomFilter_ProcessStr( str );

	return (filter & value) == value;
}

/*
=============================================

IDENTIFICATION

=============================================
*/
#define MAXBITS_GEN 30
#define MAXBITS_CHECK MAXBITS_GEN + 6

static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path );

static void ID_BloomFilter_f( void )
{
	bloomfilter_t value = 0;
	int i;

	for( i = 1; i < Cmd_Argc(); i++ )
		value |= BloomFilter_ProcessStr( Cmd_Argv( i ) );

	Msg( "%d %016llX\n", BloomFilter_Weight( value ), value );

	// test
	// for( i = 1; i < Cmd_Argc(); i++ )
	//	Msg( "%s: %d\n", Cmd_Argv( i ), BloomFilter_ContainsString( value, Cmd_Argv( i ) ) );
}

static qboolean ID_VerifyHEX( const char *hex )
{
	uint chars = 0;
	char prev = 0;
	qboolean monotonic = true; // detect 11:22...
	int weight = 0;

	while( *hex++ )
	{
		char ch = Q_tolower( *hex );

		if( ( ch >= 'a' && ch <= 'f') || ( ch >= '0' && ch <= '9' ) )
		{
			if( prev && ( ch - prev < -1 || ch - prev > 1 ) )
				monotonic = false;

			if( ch >= 'a' )
				chars |= 1 << (ch - 'a' + 10);
			else
				chars |= 1 << (ch - '0');

			prev = ch;
		}
	}

	if( monotonic )
		return false;

	while( chars )
	{
		if( chars & 1 )
			weight++;

		chars = chars >> 1;

		if( weight > 2 )
			return true;
	}

	return false;
}

static void ID_VerifyHEX_f( void )
{
	if( ID_VerifyHEX( Cmd_Argv( 1 ) ) )
		Msg( "Good\n" );
	else
		Msg( "Bad\n" );
}

#if XASH_LINUX
static qboolean ID_ProcessCPUInfo( bloomfilter_t *value )
{
	int cpuinfofd = open( "/proc/cpuinfo", O_RDONLY );
	char buffer[1024], *pbuf, *pbuf2;
	int ret;

	if( cpuinfofd < 0 )
		return false;

	if( (ret = read( cpuinfofd, buffer, 1023 ) ) < 0 )
		return false;

	close( cpuinfofd );

	buffer[ret] = 0;

	if( !ret )
		return false;

	pbuf = Q_strstr( buffer, "Serial" );
	if( !pbuf )
		return false;
	pbuf += 6;

	if( ( pbuf2 = Q_strchr( pbuf, '\n' ) ) )
		*pbuf2 = 0;
	else
		pbuf2 = pbuf + Q_strlen( pbuf );

	if( !ID_VerifyHEX( pbuf ) )
		return false;

	*value |= BloomFilter_Process( pbuf, pbuf2 - pbuf );
	return true;
}

static qboolean ID_ValidateNetDevice( const char *dev )
{
	const char *prefix = "/sys/class/net";
	byte *pfile;
	int assignType;

	// These devices are fake, their mac address is generated each boot, while assign_type is 0
	if( !Q_strnicmp( dev, "ccmni", sizeof( "ccmni" ) ) ||
		!Q_strnicmp( dev, "ifb", sizeof( "ifb" ) ) )
		return false;

	pfile = FS_LoadDirectFile( va( "%s/%s/addr_assign_type", prefix, dev ), NULL );

	// if NULL, it may be old kernel
	if( pfile )
	{
		assignType = Q_atoi( (char*)pfile );

		Mem_Free( pfile );

		// check is MAC address is constant
		if( assignType != 0 )
			return false;
	}

	return true;
}

static int ID_ProcessNetDevices( bloomfilter_t *value )
{
	const char *prefix = "/sys/class/net";
	DIR *dir;
	struct dirent *entry;
	int count = 0;

	if( !( dir = opendir( prefix ) ) )
		return 0;

	while( ( entry = readdir( dir ) ) && BloomFilter_Weight( *value ) < MAXBITS_GEN )
	{
		if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) )
			continue;

		if( !ID_ValidateNetDevice( entry->d_name ) )
			continue;

		count += ID_ProcessFile( value, va( "%s/%s/address", prefix, entry->d_name ) );
	}
	closedir( dir );
	return count;
}

static int ID_CheckNetDevices( bloomfilter_t value )
{
	const char *prefix = "/sys/class/net";

	DIR *dir;
	struct dirent *entry;
	int count = 0;
	bloomfilter_t filter = 0;

	if( !( dir = opendir( prefix ) ) )
		return 0;

	while( ( entry = readdir( dir ) ) )
	{
		if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) )
			continue;

		if( !ID_ValidateNetDevice( entry->d_name ) )
			continue;

		if( ID_ProcessFile( &filter, va( "%s/%s/address", prefix, entry->d_name ) ) )
			count += ( value & filter ) == filter, filter = 0;
	}

	closedir( dir );
	return count;
}

static void ID_TestCPUInfo_f( void )
{
	bloomfilter_t value = 0;

	if( ID_ProcessCPUInfo( &value ) )
		Msg( "Got %016llX\n", value );
	else
		Msg( "Could not get serial\n" );
}

#endif

static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path )
{
	int fd = open( path, O_RDONLY );
	char buffer[256];
	int ret;

	if( fd < 0 )
		return false;

	if( (ret = read( fd, buffer, 255 ) ) < 0 )
		return false;

	close( fd );

	if( !ret )
		return false;

	buffer[ret] = 0;

	if( !ID_VerifyHEX( buffer ) )
		return false;

	*value |= BloomFilter_Process( buffer, ret );
	return true;
}

#if !XASH_WIN32
static int ID_ProcessFiles( bloomfilter_t *value, const char *prefix, const char *postfix )
{
	DIR *dir;
	struct dirent *entry;
	int count = 0;

	if( !( dir = opendir( prefix ) ) )
	    return 0;

	while( ( entry = readdir( dir ) ) && BloomFilter_Weight( *value ) < MAXBITS_GEN )
	{
		if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) )
			continue;

		count += ID_ProcessFile( value, va( "%s/%s/%s", prefix, entry->d_name, postfix ) );
	}
	closedir( dir );
	return count;
}

static int ID_CheckFiles( bloomfilter_t value, const char *prefix, const char *postfix )
{
	DIR *dir;
	struct dirent *entry;
	int count = 0;
	bloomfilter_t filter = 0;

	if( !( dir = opendir( prefix ) ) )
	    return 0;

	while( ( entry = readdir( dir ) ) )
	{
		if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) )
			continue;

		if( ID_ProcessFile( &filter, va( "%s/%s/%s", prefix, entry->d_name, postfix ) ) )
			count += ( value & filter ) == filter, filter = 0;
	}

	closedir( dir );
	return count;
}
#else
static int ID_GetKeyData( HKEY hRootKey, char *subKey, char *value, LPBYTE data, DWORD cbData )
{
	HKEY hKey;

	if( RegOpenKeyEx( hRootKey, subKey, 0, KEY_QUERY_VALUE, &hKey ) != ERROR_SUCCESS )
		return 0;

	if( RegQueryValueEx( hKey, value, NULL, NULL, data, &cbData ) != ERROR_SUCCESS )
	{
		RegCloseKey( hKey );
		return 0;
	}

	RegCloseKey( hKey );
	return 1;
}
static int ID_SetKeyData( HKEY hRootKey, char *subKey, DWORD dwType, char *value, LPBYTE data, DWORD cbData)
{
	HKEY hKey;
	if( RegCreateKey( hRootKey, subKey, &hKey ) != ERROR_SUCCESS )
		return 0;

	if( RegSetValueEx( hKey, value, 0, dwType, data, cbData ) != ERROR_SUCCESS )
	{
		RegCloseKey( hKey );
		return 0;
	}

	RegCloseKey( hKey );
	return 1;
}

#define BUFSIZE 4096

static int ID_RunWMIC(char *buffer, const char *cmdline)
{
	HANDLE g_IN_Rd = NULL;
	HANDLE g_IN_Wr = NULL;
	HANDLE g_OUT_Rd = NULL;
	HANDLE g_OUT_Wr = NULL;
	DWORD dwRead;
	BOOL bSuccess = FALSE;
	SECURITY_ATTRIBUTES saAttr;

	STARTUPINFO si = {0};

	PROCESS_INFORMATION pi = {0};
	saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
	saAttr.bInheritHandle = TRUE;
	saAttr.lpSecurityDescriptor = NULL;

	CreatePipe( &g_IN_Rd, &g_IN_Wr, &saAttr, 0 );
	CreatePipe( &g_OUT_Rd, &g_OUT_Wr, &saAttr, 0 );
	SetHandleInformation( g_IN_Wr, HANDLE_FLAG_INHERIT, 0 );

	si.cb = sizeof(STARTUPINFO);
	si.dwFlags = STARTF_USESTDHANDLES;
	si.hStdInput = g_IN_Rd;
	si.hStdOutput = g_OUT_Wr;
	si.hStdError = g_OUT_Wr;
	si.wShowWindow = SW_HIDE;
	si.dwFlags |= STARTF_USESTDHANDLES;

	CreateProcess( NULL, (char*)cmdline, NULL, NULL, true, CREATE_NO_WINDOW , NULL, NULL, &si, &pi );

	CloseHandle( g_OUT_Wr );
	CloseHandle( g_IN_Wr );

	WaitForSingleObject( pi.hProcess, 500 );

	bSuccess = ReadFile( g_OUT_Rd, buffer, BUFSIZE, &dwRead, NULL );
	buffer[BUFSIZE-1] = 0;
	CloseHandle( g_IN_Rd );
	CloseHandle( g_OUT_Rd );

	return bSuccess;
}

static int ID_ProcessWMIC( bloomfilter_t *value, const char *cmdline )
{
	char buffer[BUFSIZE], token[BUFSIZE], *pbuf;
	int count = 0;

	if( !ID_RunWMIC( buffer, cmdline ) )
		return 0;
	pbuf = COM_ParseFile( buffer, token, sizeof( token )); // Header
	while( pbuf = COM_ParseFile( pbuf, token, sizeof( token ) ) )
	{
		if( !ID_VerifyHEX( token ) )
			continue;

		*value |= BloomFilter_ProcessStr( token );
		count ++;
	}

	return count;
}

static int ID_CheckWMIC( bloomfilter_t value, const char *cmdline )
{
	char buffer[BUFSIZE], token[BUFSIZE], *pbuf;
	int count = 0;

	if( !ID_RunWMIC( buffer, cmdline ) )
		return 0;
	pbuf = COM_ParseFile( buffer, token, sizeof( token )); // Header
	while( pbuf = COM_ParseFile( pbuf, token, sizeof( token ) ) )
	{
		bloomfilter_t filter;

		if( !ID_VerifyHEX( token ) )
			continue;

		filter = BloomFilter_ProcessStr( token );
		count += ( filter & value ) == filter;
	}

	return count;
}
#endif


#if XASH_IOS
char *IOS_GetUDID( void );
#endif

static bloomfilter_t ID_GenerateRawId( void )
{
	bloomfilter_t value = 0;
	int count = 0;

#if XASH_LINUX
#if XASH_ANDROID && !XASH_DEDICATED
	{
		const char *androidid = Android_GetAndroidID();
		if( androidid && ID_VerifyHEX( androidid ) )
		{
			value |= BloomFilter_ProcessStr( androidid );
			count ++;
		}
	}
#endif
	count += ID_ProcessCPUInfo( &value );
	count += ID_ProcessFiles( &value, "/sys/block", "device/cid" );
	count += ID_ProcessNetDevices( &value );
#endif
#if XASH_WIN32
	count += ID_ProcessWMIC( &value, "wmic path win32_physicalmedia get SerialNumber " );
	count += ID_ProcessWMIC( &value, "wmic bios get serialnumber " );
#endif
#if XASH_IOS
	{
		value |= BloomFilter_ProcessStr(IOS_GetUDID());
		count ++;
	}
#endif
	return value;
}

static uint ID_CheckRawId( bloomfilter_t filter )
{
	bloomfilter_t value = 0;
	int count = 0;

#if XASH_LINUX
#if XASH_ANDROID && !XASH_DEDICATED
	{
		const char *androidid = Android_GetAndroidID();
		if( androidid && ID_VerifyHEX( androidid ) )
		{
			value = BloomFilter_ProcessStr( androidid );
			count += (filter & value) == value;
			value = 0;
		}
	}
#endif
	count += ID_CheckNetDevices( filter );
	count += ID_CheckFiles( filter, "/sys/block", "device/cid" );
	if( ID_ProcessCPUInfo( &value ) )
		count += (filter & value) == value;
#endif

#if XASH_WIN32
	count += ID_CheckWMIC( filter, "wmic path win32_physicalmedia get SerialNumber" );
	count += ID_CheckWMIC( filter, "wmic bios get serialnumber" );
#endif

#if XASH_IOS
	{
		value = BloomFilter_ProcessStr(IOS_GetUDID());
		count += (filter & value) == value;
		value = 0;
	}
#endif
#if 0
	Msg( "ID_CheckRawId: %d\n", count );
#endif
	return count;
}

#define SYSTEM_XOR_MASK 0x10331c2dce4c91db
#define GAME_XOR_MASK 0x7ffc48fbac1711f1

static void ID_Check( void )
{
	uint weight = BloomFilter_Weight( id );
	uint mincount = weight >> 2;

	if( mincount < 1 )
		mincount = 1;

	if( weight > MAXBITS_CHECK )
	{
		id = 0;
#if 0
		Msg( "ID_Check(): fail %d\n", weight );
#endif
		return;
	}

	if( ID_CheckRawId( id ) < mincount )
		id = 0;
#if 0
	Msg( "ID_Check(): success %d\n", weight );
#endif
}

const char *ID_GetMD5( void )
{
	if( id_customid[0] )
		return id_customid;
	return id_md5;
}

/*
===============
ID_SetCustomClientID

===============
*/
void GAME_EXPORT ID_SetCustomClientID( const char *id )
{
	if( !id )
		return;

	Q_strncpy( id_customid, id, sizeof( id_customid  ) );
}

void ID_Init( void )
{
	MD5Context_t hash = {0};
	byte md5[16];
	int i;

	Cmd_AddRestrictedCommand( "bloomfilter", ID_BloomFilter_f, "print bloomfilter raw value of arguments set");
	Cmd_AddRestrictedCommand( "verifyhex", ID_VerifyHEX_f, "check if id source seems to be fake" );
#if XASH_LINUX
	Cmd_AddRestrictedCommand( "testcpuinfo", ID_TestCPUInfo_f, "try read cpu serial" );
#endif

#if XASH_ANDROID && !XASH_DEDICATED
	sscanf( Android_LoadID(), "%016llX", &id );
	if( id )
	{
		id ^= SYSTEM_XOR_MASK;
		ID_Check();
	}

#elif XASH_WIN32
	{
		CHAR szBuf[MAX_PATH];
		ID_GetKeyData( HKEY_CURRENT_USER, "Software\\Xash3D\\", "xash_id", szBuf, MAX_PATH );

		sscanf(szBuf, "%016llX", &id);
		id ^= SYSTEM_XOR_MASK;
		ID_Check();
	}
#else
	{
		const char *home = getenv( "HOME" );
		if( COM_CheckString( home ) )
		{
			FILE *cfg = fopen( va( "%s/.config/.xash_id", home ), "r" );
			if( !cfg )
				cfg = fopen( va( "%s/.local/.xash_id", home ), "r" );
			if( !cfg )
				cfg = fopen( va( "%s/.xash_id", home ), "r" );
			if( cfg )
			{
				if( fscanf( cfg, "%016llX", &id ) > 0 )
				{
					id ^= SYSTEM_XOR_MASK;
					ID_Check();
				}
				fclose( cfg );
			}
		}
	}
#endif
	if( !id )
	{
		const char *buf = (const char*) FS_LoadFile( ".xash_id", NULL, false );
		if( buf )
		{
			sscanf( buf, "%016llX", &id );
			id ^= GAME_XOR_MASK;
			ID_Check();
		}
	}
	if( !id )
		id = ID_GenerateRawId();

	MD5Init( &hash );
	MD5Update( &hash, (byte *)&id, sizeof( id ) );
	MD5Final( (byte*)md5, &hash );

	for( i = 0; i < 16; i++ )
		Q_snprintf( &id_md5[i*2], sizeof( id_md5 ) - i * 2, "%02hhx", md5[i] );

#if XASH_ANDROID && !XASH_DEDICATED
	Android_SaveID( va("%016llX", id^SYSTEM_XOR_MASK ) );
#elif XASH_WIN32
	{
		CHAR Buf[MAX_PATH];
		sprintf( Buf, "%016llX", id^SYSTEM_XOR_MASK );
		ID_SetKeyData( HKEY_CURRENT_USER, "Software\\Xash3D\\", REG_SZ, "xash_id", Buf, Q_strlen(Buf) );
	}
#else
	{
		const char *home = getenv( "HOME" );
		if( COM_CheckString( home ) )
		{
			FILE *cfg = fopen( va( "%s/.config/.xash_id", home ), "w" );
			if( !cfg )
				cfg = fopen( va( "%s/.local/.xash_id", home ), "w" );
			if( !cfg )
				cfg = fopen( va( "%s/.xash_id", home ), "w" );
			if( cfg )
			{
				fprintf( cfg, "%016llX", id^SYSTEM_XOR_MASK );
				fclose( cfg );
			}
		}
	}
#endif
	FS_WriteFile( ".xash_id", va("%016llX", id^GAME_XOR_MASK), 16 );
#if 0
	Msg("MD5 id: %s\nRAW id:%016llX\n", id_md5, id );
#endif
}
