/*
** cmdlib.cpp
** Misc utilities (mostly file handling stuff)
**
**---------------------------------------------------------------------------
** Copyright 1999-2016 Randy Heit
** Copyright 2019 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/


#include "cmdlib.h"
#include "fs_findfile.h"
#include "filesystem.h"
#include "files.h"
#include "md5.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>

#ifndef _WIN32
#include <pwd.h>
#include <unistd.h>
#endif

/*
progdir will hold the path up to the game directory, including the slash

  f:\quake\
  /raid/quake/

gamedir will hold progdir + the game directory (id1, id2, etc)

  */

FString progdir;

//==========================================================================
//
// IsSeperator
//
// Returns true if the character is a path seperator.
//
//==========================================================================

static inline bool IsSeperator (int c, bool forcebackslash = false)
{
	if (c == '/')
		return true;
#ifdef _WIN32
	if (c == '\\')
		return true;
#else
	if (forcebackslash && c == '\\')
		return true;
#endif
	return false;
}

//==========================================================================
//
// FixPathSeperator
//
// Convert backslashes to forward slashes.
//
//==========================================================================

void FixPathSeperator (char *path)
{
	while (*path)
	{
		if (*path == '\\')
			*path = '/';
		path++;
	}
}

//==========================================================================
//
// copystring
//
// Replacement for strdup that uses new instead of malloc.
//
//==========================================================================

char *copystring (const char *s)
{
	char *b;
	if (s)
	{
		size_t len = strlen (s) + 1;
		b = new char[len];
		memcpy (b, s, len);
	}
	else
	{
		b = new char[1];
		b[0] = '\0';
	}
	return b;
}

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

						MISC FUNCTIONS

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


//==========================================================================
//
// FileExists
//
// Returns true if the given path exists and is a readable file.
//
//==========================================================================

bool FileExists (const char *filename)
{
	bool isdir;
	bool res = DirEntryExists(filename, &isdir);
	return res && !isdir;
}

//==========================================================================
//
// FileReadable
//
// Returns true if the file can be read.
//
//==========================================================================

bool FileReadable(const char *filename)
{
#ifndef _WIN32
	return access (filename, R_OK) == 0;
#else
	auto wstr = WideString(filename);
	return _waccess (wstr.c_str(), 4) == 0;
#endif
}

//==========================================================================
//
// DirExists
//
// Returns true if the given path exists and is a directory.
//
//==========================================================================

bool DirExists(const char *filename)
{
	bool isdir;
	bool res = DirEntryExists(filename, &isdir);
	return res && isdir;
}

//==========================================================================
//
// DirEntryExists
//
// Returns true if the given path exists, be it a directory or a file.
//
//==========================================================================

bool DirEntryExists(const char *pathname, bool *isdir)
{
	if (isdir) *isdir = false;
	if (pathname == NULL || *pathname == 0)
		return false;

#ifndef _WIN32
	struct stat info;
	bool res = stat(pathname, &info) == 0;
#else
	// Windows must use the wide version of stat to preserve non-standard paths.
	auto wstr = WideString(pathname);
	struct _stat64 info;
	bool res = _wstat64(wstr.c_str(), &info) == 0;
#endif
	if (isdir) *isdir = !!(info.st_mode & S_IFDIR);
	return res;
}

//==========================================================================
//
// DirEntryExists
//
// Returns true if the given path exists, be it a directory or a file.
//
//==========================================================================

bool GetFileInfo(const char* pathname, size_t *size, time_t *time)
{
	if (pathname == NULL || *pathname == 0)
		return false;

#ifndef _WIN32
	struct stat info;
	bool res = stat(pathname, &info) == 0;
#else
	// Windows must use the wide version of stat to preserve non-standard paths.
	auto wstr = WideString(pathname);
	struct _stat64 info;
	bool res = _wstat64(wstr.c_str(), &info) == 0;
#endif
	if (!res || (info.st_mode & S_IFDIR)) return false;
	if (size) *size = (size_t)info.st_size;
	if (time) *time = info.st_mtime;
	return res;
}

//==========================================================================
//
// DefaultExtension		-- FString version
//
// Appends the extension to a pathname if it does not already have one.
//
//==========================================================================

void DefaultExtension (FString &path, const char *extension, bool forcebackslash)
{
	const char *src = &path[int(path.Len())-1];

	while (src != &path[0] && !IsSeperator(*src, forcebackslash))
	{
		if (*src == '.')
			return;                 // it has an extension
		src--;
	}

	path += extension;
}


//==========================================================================
//
// ExtractFilePath
//
// Returns the directory part of a pathname.
//
// FIXME: should include the slash, otherwise
// backing to an empty path will be wrong when appending a slash
//
//==========================================================================

FString ExtractFilePath (const char *path, bool forcebackslash)
{
	const char *src;

	src = path + strlen(path) - 1;

//
// back up until a \ or the start
//
	while (src != path && !IsSeperator(*(src-1), forcebackslash))
		src--;

	return FString(path, src - path);
}

//==========================================================================
//
// ExtractFileBase
//
// Returns the file part of a pathname, optionally including the extension.
//
//==========================================================================

FString ExtractFileBase (const char *path, bool include_extension, bool forcebackslash)
{
	const char *src, *dot;

	src = path + strlen(path) - 1;

	if (src >= path)
	{
		// back up until a / or the start
		while (src != path && !IsSeperator(*(src-1), forcebackslash))
			src--;

		// Check for files with drive specification but no path
#if defined(_WIN32)
		if (src == path && src[0] != 0)
		{
			if (src[1] == ':')
				src += 2;
		}
#endif

		if (!include_extension && (dot = strrchr(src, '.')))
		{
			return FString(src, dot - src);
		}
		else
		{
			return FString(src);
		}
	}
	return FString();
}

//==========================================================================
//
// SplitPath
//
// splits a path into directory, base name and extension
//
//==========================================================================

 void SplitPath(const char* path, FString& directory, FString& base, FString& ext, bool forcebackslash)
{
	directory = ExtractFilePath(path, forcebackslash);
	base = ExtractFileBase(path, forcebackslash);
	auto dot = base.LastIndexOf('.');
	if (dot > -1)
	{
		ext = base.Mid(dot + 1);
		base.Truncate(dot);
	}
	else
		ext = "";
}


//==========================================================================
//
// StripExtension
//
// Returns the path with the extension removed
//
//==========================================================================

FString StripExtension(const char* path)
{
	const char* src;
	if (*path == 0) return "";

	src = path + strlen(path) - 1;

	//
	// back up until a . and abort on a '/'
	//
	while (src != path && !IsSeperator(*(src - 1)))
	{
		if (*src == '.') return FString(path, src - path);
		src--;
	}

	return path;

}

//==========================================================================
//
// IsNum
//
// [RH] Returns true if the specified string is a valid decimal number
//
//==========================================================================

bool IsNum (const char *str)
{
	while (*str)
	{
		if (((*str < '0') || (*str > '9')) && (*str != '-'))
		{
			return false;
		}
		str++;
	}
	return true;
}

//==========================================================================
//
// CheckWildcards
//
// [RH] Checks if text matches the wildcard pattern using ? or *
//
//==========================================================================

bool CheckWildcards (const char *pattern, const char *text)
{
	if (pattern == NULL || text == NULL)
		return true;

	while (*pattern)
	{
		if (*pattern == '*')
		{
			char stop = tolower (*++pattern);
			while (*text && tolower(*text) != stop)
			{
				text++;
			}
			if (*text && tolower(*text) == stop)
			{
				if (CheckWildcards (pattern, text++))
				{
					return true;
				}
				pattern--;
			}
		}
		else if (*pattern == '?' || tolower(*pattern) == tolower(*text))
		{
			pattern++;
			text++;
		}
		else
		{
			return false;
		}
	}
	return (*pattern | *text) == 0;
}

//==========================================================================
//
// FormatGUID
//
// [RH] Print a GUID to a text buffer using the standard format.
//
//==========================================================================

void FormatGUID (char *buffer, size_t buffsize, const GUID &guid)
{
	snprintf (buffer, buffsize, "{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}",
		(uint32_t)guid.Data1, guid.Data2, guid.Data3,
		guid.Data4[0], guid.Data4[1],
		guid.Data4[2], guid.Data4[3],
		guid.Data4[4], guid.Data4[5],
		guid.Data4[6], guid.Data4[7]);
}

//==========================================================================
//
// myasctime
//
// [RH] Returns the current local time as ASCII, even if it's too early
//
//==========================================================================

const char *myasctime ()
{
	static char readabletime[50];
	time_t clock;
	struct tm *lt;

	time (&clock);
	lt = localtime (&clock);
	if (lt != NULL)
	{
		strftime(readabletime, 50, "%F %T", lt);
		return readabletime;
	}
	else
	{
		return "Unknown\n";
	}
}

//==========================================================================
//
// CreatePath
//
// Creates a directory including all levels necessary
//
//==========================================================================
#ifdef _WIN32
void DoCreatePath(const char *fn)
{
	char drive[_MAX_DRIVE];
	char dir[_MAX_DIR];
	_splitpath_s(fn, drive, sizeof drive, dir, sizeof dir, nullptr, 0, nullptr, 0);

	if ('\0' == *dir)
	{
		// Root/current/parent directory always exists
		return;
	}

	char path[_MAX_PATH];
	_makepath_s(path, sizeof path, drive, dir, nullptr, nullptr);

	if ('\0' == *path)
	{
		// No need to process empty relative path
		return;
	}

	// Remove trailing path separator(s)
	for (size_t i = strlen(path); 0 != i; --i)
	{
		char& lastchar = path[i - 1];

		if ('/' == lastchar || '\\' == lastchar)
		{
			lastchar = '\0';
		}
		else
		{
			break;
		}
	}

	// Create all directories for given path
	if ('\0' != *path)
	{
		DoCreatePath(path);
#ifdef _WIN32
		auto wpath = WideString(path);
		_wmkdir(wpath.c_str());
#else
		_mkdir(path);
#endif
	}
}

void CreatePath(const char *fn)
{
	char c = fn[strlen(fn)-1];

	if (c != '\\' && c != '/') 
	{
		FString name(fn);
		name += '/';
		DoCreatePath(name.GetChars());
	}
	else
	{
		DoCreatePath(fn);
	}
}
#else
void CreatePath(const char *fn)
{
	char *copy, *p;

	if (fn[0] == '/' && fn[1] == '\0')
	{
		return;
	}
	p = copy = strdup(fn);
	do
	{
		p = strchr(p + 1, '/');
		if (p != NULL)
		{
			*p = '\0';
		}
		if (!DirEntryExists(copy) && mkdir(copy, 0755) == -1)
		{
			// failed
			free(copy);
			return;
		}
		if (p != NULL)
		{
			*p = '/';
		}
	} while (p);
	free(copy);
}
#endif

void RemoveFile(const char* file)
{
#ifndef _WIN32
	remove(file);
#else
	auto wpath = WideString(file);
	_wremove(wpath.c_str());
#endif
}

int RemoveDir(const char* file)
{
#ifndef _WIN32
	return rmdir(file);
#else
	auto wpath = WideString(file);
	return _wrmdir(wpath.c_str());
#endif
}

//==========================================================================
//
// strbin	-- In-place version
//
// [RH] Replaces the escape sequences in a string with actual escaped characters.
// This operation is done in-place. The result is the new length of the string.
//
//==========================================================================

int strbin (char *str)
{
	char *start = str;
	char *p = str, c;
	int i;

	while ( (c = *p++) ) {
		if (c != '\\') {
			*str++ = c;
		} else if (*p != 0) {
			switch (*p) {
				case 'a':
					*str++ = '\a';
					break;
				case 'b':
					*str++ = '\b';
					break;
				case 'c':
					*str++ = '\034';	// TEXTCOLOR_ESCAPE
					break;
				case 'f':
					*str++ = '\f';
					break;
				case 'n':
					*str++ = '\n';
					break;
				case 't':
					*str++ = '\t';
					break;
				case 'r':
					*str++ = '\r';
					break;
				case 'v':
					*str++ = '\v';
					break;
				case '?':
					*str++ = '\?';
					break;
				case '\n':
					break;
				case 'x':
				case 'X':
					c = 0;
					for (i = 0; i < 2; i++)
					{
						p++;
						if (*p >= '0' && *p <= '9')
							c = (c << 4) + *p-'0';
						else if (*p >= 'a' && *p <= 'f')
							c = (c << 4) + 10 + *p-'a';
						else if (*p >= 'A' && *p <= 'F')
							c = (c << 4) + 10 + *p-'A';
						else
						{
							p--;
							break;
						}
					}
					*str++ = c;
					break;
				case '0':
				case '1':
				case '2':
				case '3':
				case '4':
				case '5':
				case '6':
				case '7':
					c = *p - '0';
					for (i = 0; i < 2; i++)
					{
						p++;
						if (*p >= '0' && *p <= '7')
							c = (c << 3) + *p - '0';
						else
						{
							p--;
							break;
						}
					}
					*str++ = c;
					break;
				default:
					*str++ = *p;
					break;
			}
			p++;
		}
	}
	*str = 0;
	return int(str - start);
}

//==========================================================================
//
// strbin1	-- String-creating version
//
// [RH] Replaces the escape sequences in a string with actual escaped characters.
// The result is a new string.
//
//==========================================================================

FString strbin1 (const char *start)
{
	FString result;
	const char *p = start;
	char c;
	int i;

	while ( (c = *p++) ) {
		if (c != '\\') {
			result << c;
		} else if (*p) {
			switch (*p) {
				case 'a':
					result << '\a';
					break;
				case 'b':
					result << '\b';
					break;
				case 'c':
					result << '\034';	// TEXTCOLOR_ESCAPE
					break;
				case 'f':
					result << '\f';
					break;
				case 'n':
					result << '\n';
					break;
				case 't':
					result << '\t';
					break;
				case 'r':
					result << '\r';
					break;
				case 'v':
					result << '\v';
					break;
				case '?':
					result << '\?';
					break;
				case '\n':
					break;
				case 'x':
				case 'X':
					c = 0;
					for (i = 0; i < 2; i++)
					{
						p++;
						if (*p >= '0' && *p <= '9')
							c = (c << 4) + *p-'0';
						else if (*p >= 'a' && *p <= 'f')
							c = (c << 4) + 10 + *p-'a';
						else if (*p >= 'A' && *p <= 'F')
							c = (c << 4) + 10 + *p-'A';
						else
						{
							p--;
							break;
						}
					}
					result << c;
					break;
				case '0':
				case '1':
				case '2':
				case '3':
				case '4':
				case '5':
				case '6':
				case '7':
					c = *p - '0';
					for (i = 0; i < 2; i++)
					{
						p++;
						if (*p >= '0' && *p <= '7')
							c = (c << 3) + *p - '0';
						else
						{
							p--;
							break;
						}
					}
					result << c;
					break;
				default:
					result << *p;
					break;
			}
			p++;
		}
	}
	return result;
}

//==========================================================================
//
// ExpandEnvVars
//
// Expands environment variable references in a string. Intended primarily
// for use with IWAD search paths in config files.
//
//==========================================================================

FString ExpandEnvVars(const char *searchpathstring)
{
	static const char envvarnamechars[] =
		"01234567890"
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		"_"
		"abcdefghijklmnopqrstuvwxyz";

	if (searchpathstring == NULL)
		return FString("");

	const char *dollar = strchr(searchpathstring, '$');
	if (dollar == NULL)
	{
		return FString(searchpathstring);
	}

	const char *nextchars = searchpathstring;
	FString out = FString(searchpathstring, dollar - searchpathstring);
	while ( (dollar != NULL) && (*nextchars != 0) )
	{
		size_t length = strspn(dollar + 1, envvarnamechars);
		if (length != 0)
		{
			FString varname = FString(dollar + 1, length);
			if (varname.CompareNoCase("progdir") == 0)
			{
				out += progdir;
			}
			else
			{
				char *varvalue = getenv(varname.GetChars());
				if ( (varvalue != NULL) && (strlen(varvalue) != 0) )
				{
					out += varvalue;
				}
			}
		}
		else
		{
			out += '$';
		}
		nextchars = dollar + length + 1;
		dollar = strchr(nextchars, '$');
		if (dollar != NULL)
		{
			out += FString(nextchars, dollar - nextchars);
		}
	}
	if (*nextchars != 0)
	{
		out += nextchars;
	}
	return out;
}

//==========================================================================
//
// NicePath
//
// Handles paths with leading ~ characters on Unix as well as environment
// variable substitution. On Windows, this is identical to ExpandEnvVars.
//
//==========================================================================

FString NicePath(const char *path)
{
#ifdef _WIN32
	if (*path == '\0')
	{
		return FString(".");
	}
	return ExpandEnvVars(path);
#else
	if (path == NULL || *path == '\0')
	{
		return FString("");
	}
	if (*path != '~')
	{
		return ExpandEnvVars(path);
	}

	passwd *pwstruct;
	const char *slash;

	if (path[1] == '/' || path[1] == '\0')
	{ // Get my home directory
		pwstruct = getpwuid(getuid());
		slash = path + 1;
	}
	else
	{ // Get somebody else's home directory
		slash = strchr(path, '/');
		if (slash == NULL)
		{
			slash = path + strlen(path);
		}
		FString who(path, slash - path);
		pwstruct = getpwnam(who.GetChars());
	}
	if (pwstruct == NULL)
	{
		return ExpandEnvVars(path);
	}
	FString where(pwstruct->pw_dir);
	if (*slash != '\0')
	{
		where += ExpandEnvVars(slash);
	}
	return where;
#endif
}


//==========================================================================
//
//
//
//==========================================================================

bool IsAbsPath(const char *name)
{
    if (IsSeperator(name[0])) return true;
#ifdef _WIN32
    /* [A-Za-z]: (for Windows) */
    if (isalpha((uint8_t)name[0]) && name[1] == ':')    return true;
#endif /* _WIN32 */
    return 0;
}

//==========================================================================
//
//
//
//==========================================================================

void NormalizeFileName(FString& str)
{
	FixPathSeperator(str);
	auto splits = str.Split("/");
	for (unsigned i = 1; i < splits.Size(); i++)
	{
		if (splits[i].Compare(".") == 0)
		{
			splits.Delete(i);
			i--;
		}

		if (splits[i].Compare("..") == 0 && splits[i - 1].Compare("..") != 0)
		{
			splits.Delete(i);
			splits.Delete(i - 1);
			i -= 2;
			if (i < 1) i = 1;
		}
	}
	str = splits[0];
	for (unsigned i = 1; i < splits.Size(); i++)
	{
		str << "/" << splits[i];
	}
}

//==========================================================================
//
//
//
//==========================================================================

FString M_ZLibError(int zerr)
{
	if (zerr >= 0)
	{
		return "OK";
	}
	else if (zerr < -6)
	{
		FString out;
		out.Format("%d", zerr);
		return out;
	}
	else
	{
		static const char* errs[6] =
		{
			"Errno",
			"Stream Error",
			"Data Error",
			"Memory Error",
			"Buffer Error",
			"Version Error"
		};
		return errs[-zerr - 1];
	}
}

void md5Update(FileReader& file, MD5Context& md5, unsigned len)
{
	uint8_t readbuf[8192];
	unsigned t;

	while (len > 0)
	{
		t = std::min<unsigned>(len, sizeof(readbuf));
		len -= t;
		t = (unsigned)file.Read(readbuf, t);
		md5.Update(readbuf, t);
	}
}


//==========================================================================
//
// uppercoppy
//
// [RH] Copy up to 8 chars, upper-casing them in the process
//==========================================================================

void uppercopy(char* to, const char* from)
{
	int i;

	for (i = 0; i < 8 && from[i]; i++)
		to[i] = toupper(from[i]);
	for (; i < 8; i++)
		to[i] = 0;
}

//==========================================================================
//
// GetStringFromLump
//
// Loads a zero terminated string from a lump in the file system
//==========================================================================

FString GetStringFromLump(int lump, bool zerotruncate)
{
	auto fd = fileSystem.ReadFile(lump);
	FString ScriptBuffer(fd.string(), fd.size());
	if (zerotruncate) ScriptBuffer.Truncate(strlen(ScriptBuffer.GetChars()));	// this is necessary to properly truncate the generated string to not contain 0 bytes.
	return ScriptBuffer;
}
