/*
 *  Copyright (C) 2002-2021  The DOSBox Team
 *
 *  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.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "dosbox.h"
#include "cross.h"
#include "support.h"
#include "dos_inc.h"
#include <string>
#include <limits.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <regex>

#if defined(MACOSX)
std::string MacOSXEXEPath;
std::string MacOSXResPath;
#endif

#ifdef WIN32
#ifndef _WIN32_IE
#define _WIN32_IE 0x0400
#endif
#include <shlobj.h>
typedef wchar_t host_cnv_char_t;
#else
typedef char host_cnv_char_t;
#endif
extern std::string prefix_local;
extern bool gbk, notrycp, hidenonrep, ignorespecial;
extern char *CodePageHostToGuest(const host_cnv_char_t *s);
bool isKanji1_gbk(uint8_t chr), CodePageHostToGuestUTF16(char *d/*CROSS_LEN*/,const uint16_t *s/*CROSS_LEN*/);

#if defined HAVE_SYS_TYPES_H && defined HAVE_PWD_H
#include <sys/types.h>
#include <pwd.h>
#endif

#if defined __MINGW32__
#define _mkdir(x) mkdir(x)
#endif

int resolveopt = 1;
void autoExpandEnvironmentVariables(std::string & text, bool dosvar) {
    static std::regex env(dosvar?"\\%([^%]+)%":"\\$\\{([^}]+)\\}");
    std::smatch match;
    while (std::regex_search(text, match, env)) {
        const char * s = getenv(match[1].str().c_str());
        const std::string var(s == NULL ? "" : s);
        text.replace(static_cast<size_t>(match.position(0)), static_cast<size_t>(match.length(0)), var);
    }
}

// Resolve environment variables (%VAR% [DOS/Windows] or ${VAR} [Linux/macOS]), and tilde (~) in Linux/macOS
void ResolvePath(std::string& in) {
    if (!resolveopt) return;
#if defined(WIN32)
    if (resolveopt==3) return;
    if (resolveopt==2) {autoExpandEnvironmentVariables(in, true);return;}
    char path[300],temp[300],*tempd=temp;
    strcpy(tempd, in.c_str());
    if (strchr(tempd, '%')&&ExpandEnvironmentStrings(tempd,path,300))
        tempd=path;
    in=std::string(tempd);
#elif defined(OS2)
    if (resolveopt==3) return;
    autoExpandEnvironmentVariables(in, true);
#else
    struct stat test;
    if (stat(in.c_str(),&test))
        Cross::ResolveHomedir(in);
    if (resolveopt==3) return;
    autoExpandEnvironmentVariables(in, resolveopt==2);
#endif
}

#if defined(WIN32) && !defined(HX_DOS)
static std::string W32_ConfDir(bool create) {
    char result[MAX_PATH] = { 0 };
#if !defined(_WIN32_WINDOWS)
    BOOL r = SHGetSpecialFolderPathA(NULL, result, CSIDL_LOCAL_APPDATA, create ? 1 : 0);
    if(!r || result[0] == 0)
        r = SHGetSpecialFolderPathA(NULL, result, CSIDL_APPDATA, create ? 1 : 0);
#else
    BOOL r = GetModuleFileNameA(NULL, result, MAX_PATH);
    while(r && result[r] != '\\') result[r--] = '\0';
#endif

    if(!r || result[0] == 0) {
        const char* windir = getenv("windir");
        if(!windir) windir = "c:\\windows";
        strncpy(result, windir, MAX_PATH - 1);
        result[MAX_PATH - 1] = '\0';
        const char* appdata = "\\Application Data";
        size_t len = strlen(result);
        if(len + strlen(appdata) < MAX_PATH)
            strcat(result, appdata);
    }

    if(create) _mkdir(result);
    return std::string(result);
}
#endif

std::string Cross::GetPlatformResDir() {
    std::string in;

#if defined(MACOSX)
    in = MacOSXResPath;
    if(in.empty()) {
        in = "/usr/local/share/dosbox-x";
        struct stat info;
        if((stat(in.c_str(), &info) != 0) || (!(info.st_mode & S_IFDIR)))
            in = "/usr/share/dosbox-x";
        if((stat(in.c_str(), &info) != 0) || (!(info.st_mode & S_IFDIR)))
            in = RESDIR;
    }

#elif defined(RISCOS)
    in = "/<DosBox-X$Dir>/resources";

#elif defined(LINUX)
    const char* xdg_data_home = getenv("XDG_DATA_HOME");
    const std::string data_home =
        (xdg_data_home && xdg_data_home[0] == '/') ? xdg_data_home : "~/.local/share";

    in = data_home + "/dosbox-x";
    ResolveHomedir(in);

    struct stat info;
    if((stat(in.c_str(), &info) != 0) || (!(info.st_mode & S_IFDIR)))
        in = "/usr/local/share/dosbox-x";
    if((stat(in.c_str(), &info) != 0) || (!(info.st_mode & S_IFDIR)))
        in = "/usr/share/dosbox-x";
    if((stat(in.c_str(), &info) != 0) || (!(info.st_mode & S_IFDIR)))
        in = RESDIR;

#elif defined(WIN32)
    in = "C:\\DOSBox-X";
# if defined(RESDIR)
    struct stat info;
    if((stat(in.c_str(), &info) != 0) || (!(info.st_mode & S_IFDIR)))
        in = RESDIR;
# endif

#elif defined(RESDIR)
    in = RESDIR;
#endif

    if(!in.empty())
        in += CROSS_FILESPLIT;

    return in;
}


std::string Cross::GetPlatformConfigDir()
{
    std::string dir;

#if defined(WIN32) && !defined(HX_DOS)
    dir = W32_ConfDir(false);
    dir += "\\DOSBox-X";

#elif defined(MACOSX)
    dir = "~/Library/Preferences";
    ResolveHomedir(dir);

#elif defined(HAIKU)
    dir = "~/config/settings/dosbox-x";
    ResolveHomedir(dir);

#elif defined(RISCOS)
    dir = "/<Choices$Write>/DosBox-X";

#elif !defined(HX_DOS)
    const char* xdg_conf_home = getenv("XDG_CONFIG_HOME");
    const std::string conf_home = (xdg_conf_home && xdg_conf_home[0] == '/')
        ? xdg_conf_home
        : "~/.config";

    dir = conf_home + "/dosbox-x";
    ResolveHomedir(dir);
#endif

    if(!dir.empty())
        dir += CROSS_FILESPLIT;

    return dir;
}


std::string Cross::GetPlatformConfigName()
{
    std::string name;

#ifdef WIN32
#define DEFAULT_CONFIG_FILE "dosbox-x-" VERSION ".conf"
#elif defined(MACOSX)
#define DEFAULT_CONFIG_FILE "DOSBox-X " VERSION " Preferences"
#elif defined(OS2) && defined(C_SDL2)
#define DEFAULT_CONFIG_FILE "dosbox-x-" PACKAGE_VERSION ".conf"
#else /* linux, freebsd */
#define DEFAULT_CONFIG_FILE "dosbox-x-" VERSION ".conf"
#endif

    name = DEFAULT_CONFIG_FILE;
    return name;
}

std::string Cross::CreatePlatformConfigDir()
{
    std::string path;

#if defined(WIN32) && !defined(HX_DOS)
    path = W32_ConfDir(true);
    path += "\\DOSBox-X";
    _mkdir(path.c_str());

#elif defined(MACOSX)
    path = "~/Library/Preferences";
    ResolveHomedir(path);
    // Don't create it. Assume it exists

#elif defined(HAIKU)
    path = "~/config/settings/dosbox-x";
    ResolveHomedir(path);
    mkdir(path.c_str(), 0700);

#elif defined(RISCOS)
    path = "/<Choices$Write>/DosBox-X";
    mkdir(path.c_str(), 0700);

#elif !defined(HX_DOS)
    const char* xdg_conf_home = getenv("XDG_CONFIG_HOME");
    const std::string conf_home =
        (xdg_conf_home && xdg_conf_home[0] == '/') ? xdg_conf_home : "~/.config";
    path = conf_home + "/dosbox-x";
    ResolveHomedir(path);
    mkdir(path.c_str(), 0700);
#endif

    path += CROSS_FILESPLIT;
    return path;
}


void Cross::ResolveHomedir(std::string & temp_line) {
	if(!temp_line.size() || temp_line[0] != '~') return; //No ~

	if(temp_line.size() == 1 || temp_line[1] == CROSS_FILESPLIT) { //The ~ and ~/ variant
		char * home = getenv("HOME");
		if(home) temp_line.replace(0,1,std::string(home));
#if defined HAVE_SYS_TYPES_H && defined HAVE_PWD_H
	} else { // The ~username variant
		std::string::size_type namelen = temp_line.find(CROSS_FILESPLIT);
		if(namelen == std::string::npos) namelen = temp_line.size();
		std::string username = temp_line.substr(1,namelen - 1);
		struct passwd* pass = getpwnam(username.c_str());
		if(pass) temp_line.replace(0,namelen,pass->pw_dir); //namelen -1 +1(for the ~)
#endif // USERNAME lookup code
	}
}

void Cross::CreateDir(std::string const& in) {
#ifdef WIN32
	_mkdir(in.c_str());
#else
	mkdir(in.c_str(),0700);
#endif
}

bool Cross::IsPathAbsolute(std::string const& in) {
	// Absolute paths
#if defined (WIN32) || defined(OS2)
	// drive letter
	if (in.size() > 2 && in[1] == ':' ) return true;
	// UNC path
	else if (in.size() > 2 && in[0]=='\\' && in[1]=='\\') return true;
#else
	if (in.size() > 1 && in[0] == '/' ) return true;
#endif
	return false;
}

#if defined (WIN32)
extern bool isDBCSCP();

static bool isDBCSlead(const wchar_t fchar) {
	if ((fchar & 0x00FF) == fchar) return false;
	uint16_t uname[4];
	char text[3];
	uname[0]=fchar;
	uname[1]=0;
	text[0]=0;
	text[1]=0;
	text[2]=0;
	if (CodePageHostToGuestUTF16(text,uname)) return isKanji1_gbk(text[0] & 0xFF);
	else return false;
}

/* does the filename fit the 8.3 format? */
static bool is_filename_8by3w(const wchar_t* fname) {
    notrycp = true;
    if (CodePageHostToGuest(fname)==NULL) {notrycp = false;return false;}
    notrycp = false;
    int i;

    /* Is the first part 8 chars or less? */
    i=0;
    while (*fname != 0 && *fname != L'.') {
		if (*fname<=32||*fname==127||*fname==L'"'||*fname==L'+'||*fname==L'='||*fname==L','||*fname==L';'||*fname==L':'||*fname==L'<'||*fname==L'>'||*fname==L'['||*fname==L']'||*fname==L'|'||*fname==L'?'||*fname==L'*') return false;
		if ((IS_PC98_ARCH && (*fname & 0xFF00u) != 0u && (*fname & 0xFCu) != 0x08u) || (isDBCSCP() && isDBCSlead(*fname))) i++;
		fname++; i++; 
	}
    if (i > 8) return false;

    if (*fname == L'.') {
		if (i==0 && *(fname+1) != 0 && !(*(fname+1) == L'.' && *(fname+2) == 0)) return false;
		fname++;
	}

    /* Is the second part 3 chars or less? A second '.' also makes it a LFN */
    i=0;
    while (*fname != 0 && *fname != L'.') {
		if (*fname<=32||*fname==127||*fname==L'"'||*fname==L'+'||*fname==L'='||*fname==L','||*fname==L';'||*fname==L':'||*fname==L'<'||*fname==L'>'||*fname==L'['||*fname==L']'||*fname==L'|'||*fname==L'?'||*fname==L'*') return false;
		if ((IS_PC98_ARCH && (*fname & 0xFF00u) != 0u && (*fname & 0xFCu) != 0x08u) || (isDBCSCP() && isDBCSlead(*fname))) i++;
		fname++; i++;
	}
    if (i > 3) return false;

    /* if there is anything beyond this point, it's an LFN */
    if (*fname != 0) return false;

    return true;
}

dir_information* open_directoryw(const wchar_t* dirname) {
	if (dirname == NULL) return NULL;

	size_t len = wcslen(dirname);
	if (len == 0) return NULL;

	static dir_information dir;

	wcsncpy(dir.wbase_path(),dirname,MAX_PATH);

	if (dirname[len-1] == '\\') wcscat(dir.wbase_path(),L"*.*");
	else                        wcscat(dir.wbase_path(),L"\\*.*");

    dir.wide = true;
	dir.handle = INVALID_HANDLE_VALUE;

	return (_waccess(dirname,0) ? NULL : &dir);
}

dir_information* open_directory(const char* dirname) {
	if (dirname == NULL) return NULL;

	size_t len = strlen(dirname);
	if (len == 0) return NULL;

	static dir_information dir;

	safe_strncpy(dir.base_path,dirname,MAX_PATH);

	if (dirname[len-1] == '\\') strcat(dir.base_path,"*.*");
	else                        strcat(dir.base_path,"\\*.*");

    dir.wide = false;
	dir.handle = INVALID_HANDLE_VALUE;

	return (_access(dirname,0) ? NULL : &dir);
}

bool read_directory_firstw(dir_information* dirp, wchar_t* entry_name, wchar_t* entry_sname, bool& is_directory) {
    if (!dirp->wide) return false;

    // TODO: offer a config.h option to opt out of Windows widechar functions
	do {
		dirp->handle = FindFirstFileW(dirp->wbase_path(), &dirp->search_data.w);
		if (INVALID_HANDLE_VALUE == dirp->handle) return false;
	} while (hidenonrep&&CodePageHostToGuest(dirp->search_data.w.cFileName)==NULL);

	// TODO: offer a config.h option to opt out of Windows widechar functions
	wcsncpy(entry_name,dirp->search_data.w.cFileName,(MAX_PATH<CROSS_LEN)?MAX_PATH:CROSS_LEN);
	if (dirp->search_data.w.cAlternateFileName[0] != 0 && is_filename_8by3w(dirp->search_data.w.cFileName))
		wcsncpy(entry_sname,dirp->search_data.w.cFileName,13);
	else
		wcsncpy(entry_sname,dirp->search_data.w.cAlternateFileName,13);

	if (dirp->search_data.w.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) is_directory = true;
	else is_directory = false;

	return true;
}

bool read_directory_nextw(dir_information* dirp, wchar_t* entry_name, wchar_t* entry_sname, bool& is_directory) {
    if (!dirp->wide) return false;

    // TODO: offer a config.h option to opt out of Windows widechar functions
	int result;
	do {
		result = FindNextFileW(dirp->handle, &dirp->search_data.w);
		if (result==0) return false;
	} while (hidenonrep&&CodePageHostToGuest(dirp->search_data.w.cFileName)==NULL);

    // TODO: offer a config.h option to opt out of Windows widechar functions
	wcsncpy(entry_name,dirp->search_data.w.cFileName,(MAX_PATH<CROSS_LEN)?MAX_PATH:CROSS_LEN);
	if (dirp->search_data.w.cAlternateFileName[0] != 0 && is_filename_8by3w(dirp->search_data.w.cFileName))
		wcsncpy(entry_sname,dirp->search_data.w.cFileName,13);
	else
		wcsncpy(entry_sname,dirp->search_data.w.cAlternateFileName,13);

	if (dirp->search_data.w.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) is_directory = true;
	else is_directory = false;

	return true;
}

bool read_directory_first(dir_information* dirp, char* entry_name, char* entry_sname, bool& is_directory) {
    if (!dirp) return false;
    if (dirp->wide) return false;

    // TODO: offer a config.h option to opt out of Windows widechar functions
	dirp->handle = FindFirstFileA(dirp->base_path, &dirp->search_data.a);
	if (INVALID_HANDLE_VALUE == dirp->handle) {
		return false;
	}

    // TODO: offer a config.h option to opt out of Windows widechar functions
	safe_strncpy(entry_name,dirp->search_data.a.cFileName,(MAX_PATH<CROSS_LEN)?MAX_PATH:CROSS_LEN);
    safe_strncpy(entry_sname,dirp->search_data.a.cAlternateFileName,13);
 
	if (dirp->search_data.a.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) is_directory = true;
	else is_directory = false;

	return true;
}

bool read_directory_next(dir_information* dirp, char* entry_name, char* entry_sname, bool& is_directory) {
    if (!dirp) return false;
    if (dirp->wide) return false;

    // TODO: offer a config.h option to opt out of Windows widechar functions
	int result = FindNextFileA(dirp->handle, &dirp->search_data.a);
	if (result==0) return false;

    // TODO: offer a config.h option to opt out of Windows widechar functions
	safe_strncpy(entry_name,dirp->search_data.a.cFileName,(MAX_PATH<CROSS_LEN)?MAX_PATH:CROSS_LEN);
	safe_strncpy(entry_sname,dirp->search_data.a.cAlternateFileName,13);

	if (dirp->search_data.a.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) is_directory = true;
	else is_directory = false;

	return true;
}

void close_directory(dir_information* dirp) {
	if (dirp && dirp->handle != INVALID_HANDLE_VALUE) {
		FindClose(dirp->handle);
		dirp->handle = INVALID_HANDLE_VALUE;
	}
}

#else

dir_information* open_directory(const char* dirname) {
	static dir_information dir;
	dir.dir=opendir(dirname);
	safe_strncpy(dir.base_path,dirname,CROSS_LEN);
	return dir.dir?&dir:NULL;
}

bool read_directory_first(dir_information* dirp, char* entry_name, char* entry_sname, bool& is_directory) {	
	if (!dirp) return false;
	struct dirent* dentry;
	std::string::size_type const prefix_lengh = prefix_local.size();
	do {
		dentry = readdir(dirp->dir);
		if (dentry==NULL) return false;
	} while ((hidenonrep && CodePageHostToGuest(dentry->d_name)==NULL) || (ignorespecial && (strlen(dentry->d_name) > prefix_lengh+5) && strncmp(dentry->d_name,prefix_local.c_str(),prefix_lengh) == 0));

//	safe_strncpy(entry_name,dentry->d_name,(FILENAME_MAX<MAX_PATH)?FILENAME_MAX:MAX_PATH);	// [include stdio.h], maybe pathconf()
	safe_strncpy(entry_name,dentry->d_name,CROSS_LEN);
	entry_sname[0]=0;

#ifdef DIRENT_HAS_D_TYPE
	if(dentry->d_type == DT_DIR) {
		is_directory = true;
		return true;
	} else if(dentry->d_type == DT_REG) {
		is_directory = false;
		return true;
	}
#endif

	// probably use d_type here instead of a full stat()
	static char buffer[2*CROSS_LEN] = { 0 };
	buffer[0] = 0;
	strcpy(buffer,dirp->base_path);
	strcat(buffer,entry_name);
	struct stat status;
	if (stat(buffer,&status)==0) is_directory = (S_ISDIR(status.st_mode)>0);
	else is_directory = false;

	return true;
}

bool read_directory_next(dir_information* dirp, char* entry_name, char* entry_sname, bool& is_directory) {
	if (!dirp) return false;
	struct dirent* dentry;
	std::string::size_type const prefix_lengh = prefix_local.size();
	do {
		dentry = readdir(dirp->dir);
		if (dentry==NULL) return false;
	} while ((hidenonrep && CodePageHostToGuest(dentry->d_name)==NULL) || (ignorespecial && (strlen(dentry->d_name) > prefix_lengh+5) && strncmp(dentry->d_name,prefix_local.c_str(),prefix_lengh) == 0));

//	safe_strncpy(entry_name,dentry->d_name,(FILENAME_MAX<MAX_PATH)?FILENAME_MAX:MAX_PATH);	// [include stdio.h], maybe pathconf()
	safe_strncpy(entry_name,dentry->d_name,CROSS_LEN);
	entry_sname[0]=0;

#ifdef DIRENT_HAS_D_TYPE
	if(dentry->d_type == DT_DIR) {
		is_directory = true;
		return true;
	} else if(dentry->d_type == DT_REG) {
		is_directory = false;
		return true;
	}
#endif

	// probably use d_type here instead of a full stat()
	static char buffer[2*CROSS_LEN] = { 0 };
	buffer[0] = 0;
	strcpy(buffer,dirp->base_path);
	strcat(buffer,entry_name);
	struct stat status;

	if (stat(buffer,&status)==0) is_directory = (S_ISDIR(status.st_mode)>0);
	else is_directory = false;

	return true;
}

void close_directory(dir_information* dirp) {
	if (dirp) closedir(dirp->dir);
}

#endif

FILE *fopen_wrap(const char *path, const char *mode) {
#if !defined(WIN32) && !defined(OS2) && !defined(MACOSX) && defined(HAVE_REALPATH)
	char work[CROSS_LEN] = {0};
	strncpy(work,path,CROSS_LEN-1);
	char* last = strrchr(work,'/');
	
	if (last) {
		if (last != work) {
			*last = 0;
			//If this compare fails, then we are dealing with files in / 
			//Which is outside the scope, but test anyway. 
			//However as realpath only works for existing files. The testing is 
			//in that case not done against new files.
		}
		char* check = realpath(work,NULL);
		if (check) {
			if ( ( strlen(check) == 5 && strcmp(check,"/proc") == 0) || strncmp(check,"/proc/",6) == 0) {
//				LOG_MSG("lst hit %s blocking!",path);
				free(check);
				return NULL;
			}
			free(check);
		}
	}

#if 0
//Lightweight version, but then existing files can still be read, which is not ideal	
	if (strpbrk(mode,"aw+") != NULL) {
		LOG_MSG("pbrk ok");
		char* check = realpath(path,NULL);
		//Will be null if file doesn't exist.... ENOENT
		//TODO What about unlink /proc/self/mem and then create it ?
		//Should be safe for what we want..
		if (check) {
			if (strncmp(check,"/proc/",6) == 0) {
				free(check);
				return NULL;
			}
			free(check);
		}
	}
*/
#endif //0 
#endif

	return fopen(path,mode);
}

const char* get_time(void)
{
    static char buf[16];
    time_t curtime;
    struct tm* loctime;

    curtime = time(NULL);
    loctime = localtime(&curtime);
    strftime(buf, sizeof(buf), "%H:%M:%S", loctime);
    return buf;
}
