//----------------------------------------------------------------------------
//
// File:        support.cpp
// Date:        15-Jan-2003
// Programmer:  Marc Rousseau
//
// Description:
//
// Copyright (c) 2003-2004 Marc Rousseau, All Rights Reserved.
//
// 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 2 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.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
//
// Revision History:
//
//   15-Jan-2003    Renamed from original fileio.cpp
//
//----------------------------------------------------------------------------

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include <map>
#if defined( __GNUC__ )
	#include <dirent.h>
	#include <fnmatch.h>
	#include <unistd.h>
#endif
#if defined( OS_WINDOWS )
	#include <windows.h>
#endif
#include "common.hpp"
#include "logger.hpp"
#include "cartridge.hpp"
#include "support.hpp"

DBG_REGISTER( __FILE__ );

constexpr char HOME_DIR[ ] = ".ti99sim";

#if defined( OS_LINUX ) || defined( OS_MACOSX )
	#define MKDIR( x,y ) mkdir( x, y )
	const char FILE_SEPERATOR = '/';
#elif defined( OS_OS2 ) || defined( OS_WINDOWS )
	#include <direct.h>
	#define MKDIR( x,y ) mkdir( x )
	const char FILE_SEPERATOR = '\\';
#endif

const char *GetCommonPath( )
{
	static const char *common = nullptr;

	if( common == nullptr )
	{
		#if defined( TI_DATA_DIR )

			common = TI_DATA_DIR;

		#elif defined( OS_LINUX ) || defined( OS_MACOSX )

			static const char *self[] =
			{
				"/proc/self/exe",		// path for Linux
				"/proc/curproc/file",	// path for some BSD variants
				"/proc/curproc/exe"		// path for NetBSD
			};

			static std::vector<char> buffer( 32 );

			common = "/opt/ti99sim";

			for( auto &path : self )
			{
				int length = 0;

				while(( length = readlink( path, buffer.data( ), buffer.size( ))) != -1 )
				{
					if( length < (int) buffer.size( ))
					{
						buffer[ length ] = '\0';
						if( char *end = strrchr( buffer.data( ), FILE_SEPERATOR ))
						{
							*end = '\0';
							if(( end = strrchr( buffer.data( ), FILE_SEPERATOR )) != nullptr )
							{
								if( strcmp( end + 1, "bin" ) == 0 )
								{
									*end = '\0';
								}
							}
						}

						common = buffer.data( );
						break;
					}

					buffer.resize( buffer.size( ) * 2 );
				}
			}

		#elif defined( OS_WINDOWS )

			static std::array<char> buffer[ MAX_PATH ];

			GetModuleFileName( nullptr, buffer, buffer.size( ));
			if( char *end = strrchr( buffer, FILE_SEPERATOR ))
			{
				*end = '\0';
			}

			common = buffer;

		#else

			common = "";

		#endif
	}

	return common;
}

const char *GetHomePath( )
{
	static const char *home = nullptr;

	if( home == nullptr )
	{
		const char *ptr = getenv( "HOME" );

		#if defined( OS_WINDOWS )

			std::array<char> fileName[ MAX_PATH ];

			if( ptr == nullptr )
			{
				HANDLE hProcessToken = NULL;

				OpenProcessToken( GetCurrentProcess( ), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken );

				DWORD nCount = fileName.size( );

				GetUserProfileDirectoryA( hProcessToken, fileName.data( ), &nCount );

				ptr = fileName;
			}

		#endif

		static std::vector<char> buffer(( ptr ? strlen( ptr ) : 1 ) + 1 + sizeof( HOME_DIR ));

		sprintf( buffer.data( ), "%s%c%s", ptr ? ptr : ".", FILE_SEPERATOR, HOME_DIR );

		home = buffer.data( );
	}

	return home;
}

std::string GetHomePath( const char *path )
{
	std::string homePath = GetHomePath( );

	if( path != nullptr )
	{
		homePath = homePath + FILE_SEPERATOR + path;
	}

	return homePath;
}

std::string CreateHomePath( const char *path )
{
	const char *home = GetHomePath( );

	MKDIR( home, 0775 );

	std::string homePath = home;

	if( path != nullptr )
	{
		homePath = homePath + FILE_SEPERATOR + path;

		MKDIR( homePath.c_str( ), 0775 );
	}

	return homePath;
}

bool IsAbsolutePath( const std::string &filename )
{
	if( filename[ 0 ] == FILE_SEPERATOR )
	{
		return true;
	}

#if defined( OS_WINDOWS )

	if( filename[ 1 ] == ':' )
	{
		return true;
	}

#endif

	return false;
}

bool IsWriteable( const std::string &filename )
{
	FUNCTION_ENTRY( nullptr, "IsWriteable", true );

	bool retVal = false;

	struct stat info;
	if( stat( filename.c_str( ), &info ) == 0 )
	{

#if defined( OS_WINDOWS )

		if( info.st_mode & S_IWRITE )
		{
			retVal = true;
		}

#else

		if( getuid( ) == info.st_uid )
		{
			if( info.st_mode & S_IWUSR )
			{
				retVal = true;
			}
		}
		else if( getgid( ) == info.st_gid )
		{
			if( info.st_mode & S_IWGRP )
			{
				retVal = true;
			}
		}
		else
		{
			if( info.st_mode & S_IWOTH )
			{
				retVal = true;
			}
		}

#endif

	}
	else
	{
		// TBD: Check write permissions to the directory
		retVal = true;
	}

	return retVal;
}

static bool TryPath( const std::string &path, const std::string &filename )
{
	FUNCTION_ENTRY( nullptr, "TryPath", true );

	std::string buffer;
	buffer.reserve( path.size( ) + filename.size( ) + 2 );

	buffer = path + FILE_SEPERATOR + filename;

	FILE *file = fopen( buffer.c_str( ), "rb" );
	if( file != nullptr )
	{
		fclose( file );
		return true;
	}

	return false;
}

std::list<std::string> GetFiles( const std::string &directory, const std::string &pattern, bool recurse )
{
	std::list<std::string> list;

	std::string base( directory.empty( ) ? "." : directory );

	if( *base.rbegin( ) == FILE_SEPERATOR )
	{
		base.resize( base.size( ) - 1 );
	}

#if defined( OS_LINUX ) || defined( OS_MACOSX )

	if( DIR *dir = opendir( base.c_str( )))
	{
		dirent *dp = nullptr;

		while(( dp = readdir( dir )) != nullptr )
		{
			if((( dp->d_type & DT_DIR ) != 0 ) && ( recurse == true ))
			{
				if( strcmp( dp->d_name, "." ) && strcmp( dp->d_name, ".." ))
				{
					std::string subdir = base + FILE_SEPERATOR + dp->d_name;

					list.splice( list.end( ), GetFiles( subdir.c_str( ), pattern, recurse ));
				}
			}
			if((( dp->d_type & DT_DIR ) == 0 ) && ( fnmatch( pattern.c_str( ), dp->d_name, FNM_CASEFOLD ) == 0 ))
			{
				list.push_back( base + FILE_SEPERATOR + dp->d_name );
			}
		}

		closedir( dir );
	}

#elif defined( OS_WINDOWS )

	HANDLE hFind;
	WIN32_FIND_DATA FindFileData;

	if( recurse == true )
	{
		std::string search = base + FILE_SEPERATOR + "*";

		if(( hFind = FindFirstFile( search.c_str( ), &FindFileData )) != INVALID_HANDLE_VALUE )
		{
			do
			{
				if(( FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) != 0 )
				{
					if( strcmp( FindFileData.cFileName, "." ) && strcmp( FindFileData.cFileName, ".." ))
					{
						std::string subdir = base + FILE_SEPERATOR + FindFileData.cFileName;

						list.splice( list.end( ), GetFiles( subdir.c_str( ), pattern, recurse ));
					}
				}
			}
			while( FindNextFile( hFind, &FindFileData ));

			FindClose( hFind );
		}
	}

	std::string search = base + FILE_SEPERATOR + pattern;

	if(( hFind = FindFirstFile( search.c_str( ), &FindFileData )) != INVALID_HANDLE_VALUE )
	{
		do
		{
			if(( FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) == 0 )
			{
				list.push_back( base + FILE_SEPERATOR + FindFileData.cFileName );
			}
		}
		while( FindNextFile( hFind, &FindFileData ));

		FindClose( hFind );
	}

#endif

	list.sort( );

	return list;
}

std::string LocateFile( const std::string &directory, const std::string &filename )
{
	FUNCTION_ENTRY( nullptr, "LocateFile", true );

	if( filename.empty( ))
	{
		return filename;
	}

	// If we were given an absolute path, see if it's valid or not
	if( IsAbsolutePath( filename ))
	{
		if( FILE *file = fopen( filename.c_str( ), "r" ))
		{
			fclose( file );
			return filename;
		}
		return "";
	}

	// Try: CWD
	if( TryPath( ".", filename ) == true )
	{
		return filename;
	}

	std::string fullname = directory;
	if( fullname.empty( ) == false )
	{
		fullname += FILE_SEPERATOR;
	}
	fullname += filename;

	std::string fullpath;

	// Try: ~/.ti99sim/path, /opt/ti99sim/path
	if(( TryPath( fullpath = GetHomePath( ), fullname )   == true ) ||
	   ( TryPath( fullpath = GetCommonPath( ), fullname ) == true ))
	{
		return fullpath + FILE_SEPERATOR + fullname;
	}

	return "";
}

std::list<std::string> LocateFiles( const std::string &directory, const std::string &pattern )
{
	static const char *paths[ ] =
	{
		GetCommonPath( ),	// Try: <install-dir>/path
		GetHomePath( ),		// Try: ~/.ti99sim/path
		"."					// Try: CWD
	};

	std::map<std::string,std::string> files;

	for( auto path : paths )
	{
		std::string testDir = path;
		if( strlen( path ) > 1 )
		{
			testDir = testDir + FILE_SEPERATOR + directory;
		}

#if defined( OS_LINUX ) || defined( OS_MACOSX )

		DIR *dir = opendir( testDir.c_str( ));
		if( dir != nullptr )
		{
			dirent *dp = nullptr;

			while(( dp = readdir( dir )) != nullptr )
			{
				if((( dp->d_type & DT_DIR ) == 0 ) && ( fnmatch( pattern.c_str( ), dp->d_name, FNM_CASEFOLD ) == 0 ))
				{
					files[ dp->d_name ] = testDir;
				}
			}

			closedir( dir );
		}

#elif defined( OS_WINDOWS )

		HANDLE hFind;
		WIN32_FIND_DATA FindFileData;

		testDir = testDir + FILE_SEPERATOR + pattern;

		if(( hFind = FindFirstFile( testDir.c_str( ), &FindFileData )) != INVALID_HANDLE_VALUE )
		{
			do
			{
				if(( FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) == 0 )
				{
					files[ FindFileData.cFileName ] = testDir;
				}
			}
			while( FindNextFile( hFind, &FindFileData ));
			FindClose( hFind );
		}

#endif
	}

	std::list<std::string> list;

	for( auto iter : files )
	{
		list.push_back( iter.second + FILE_SEPERATOR + iter.first );
	}

	list.sort( );

	return list;
}

std::string LocateCartridge( const std::string &directory, const std::string &sha1 )
{
	auto roms = LocateFiles( directory, "*.ctg" );

	for( auto &name : roms )
	{
		cRefPtr<cCartridge> ctg = new cCartridge( name );
		if( ctg->sha1( ) == sha1 )
		{
			return name;
		}
	}

	return "";
}

std::string LocateCartridge( const std::string &directory, const std::string &name, const std::list<std::string> &signatures )
{
	// Try to locate the cartridge by filename (legacy support)
	std::string romFile = LocateFile( directory, name );

	if( romFile.empty( ))
	{
		// Try to find the cartridge using the supplied signatures
		for( auto &sha1 : signatures )
		{
			romFile = LocateCartridge( directory, sha1 );
			if( !romFile.empty( ))
			{
				return romFile;
			}
		}
	}

	return romFile;
}

static std::string digest2string( const UINT8 digest[ ], size_t size )
{
	constexpr char hexDigits[ ] = "0123456789abcdef";

	std::string sum( size * 2, '0' );

	for( size_t i = 0; i < size; i++ )
	{
		sum[ 2 * i + 0 ] = hexDigits[ digest[ i ] >> 4 ];
		sum[ 2 * i + 1 ] = hexDigits[ digest[ i ] & 0x0F ];
	}

	return sum;
}

bool Is6K( const UINT8 *data, int size )
{
	if( size != 0x2000 )
	{
		return false;
	}

	for( int i = 0; i < 0x0800; i++ )
	{
		if( data[ 0x1800 + i ] != ( data[ 0x0800 + i ] | data[ 0x1000 + i ] ))
		{
			return false;
		}
	}

	return true;
}

std::string sha1( const void *data, size_t length )
{
	SHA1Context sha1;

	sha1.Update( data, length );

	return sha1.Digest( );
}

#if defined( OS_LINUX )

	#include <openssl/sha.h>

	void *CreateSHA1Context( )
	{
		SHA_CTX *context = new SHA_CTX;

		SHA1_Init( context );

		return context;
	}

	bool UpdateSHA1Context( void *context, const void *data, size_t length )
	{
		return SHA1_Update( static_cast<SHA_CTX*>( context ), data, length );
	}

	std::string GetDigest( void *context )
	{
		UINT8 digest[ SHA_DIGEST_LENGTH ] = { 0 };

		SHA1_Final( digest, static_cast<SHA_CTX*>( context ));

		return digest2string( digest, SHA_DIGEST_LENGTH );
	}

	void ReleaseSHA1Context( void *context )
	{
		delete static_cast<SHA_CTX*>( context );
	}

#endif

#if defined( OS_WINDOWS )

	#include <wincrypt.h>

	#define SHA_DIGEST_LENGTH 20

	struct SHA_CTX
	{
		HCRYPTPROV	hProv{ 0 };
		HCRYPTHASH	hHash{ 0 };
	};

	void *CreateSHA1Context( )
	{
		SHA_CTX *context = new SHA_CTX;

		if( CryptAcquireContext( &context->hProv, nullptr, nullptr, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT ))
		{
			if( CryptCreateHash( context->hProv, CALG_SHA1, 0, 0, &context->hHash ))
			{
				return context;
			}
			CryptReleaseContext( context->hProv, 0 );
		}

		delete context;

		return nullptr;
	}

	bool UpdateSHA1Context( void *context, const void *data, size_t length )
	{
		if( context )
		{
			return CryptHashData( static_cast<SHA_CTX*>( context )->hHash, static_cast<const BYTE*>( data ), length, 0 ) ? true : false;
		}

		return false;
	}

	std::string GetDigest( void *context )
	{
		UINT8 digest[ SHA_DIGEST_LENGTH ] = { 0 };

		if( context )
		{
			DWORD cbHash = SHA_DIGEST_LENGTH;

			if( !CryptGetHashParam( static_cast<SHA_CTX*>( context )->hHash, HP_HASHVAL, digest, &cbHash, 0 ))
			{
				memset( digest, 0, sizeof( digest ));
			}
		}

		return digest2string( digest, SHA_DIGEST_LENGTH );
	}

	void ReleaseSHA1Context( void *context )
	{
		if( context )
		{
			CryptDestroyHash( static_cast<SHA_CTX*>( context )->hHash );
			CryptReleaseContext( static_cast<SHA_CTX*>( context )->hProv, 0 );

			delete static_cast<SHA_CTX*>( context );
		}
	}

#endif
