/* win_utils.c - utility functions for Windows and CygWin */
#if defined(_WIN32) || defined(__CYGWIN__)
#include <windows.h>
#include "win_utils.h"

/**
 * Set process priority and affinity to use any CPU but the first one,
 * this improves benchmark results on a multi-core systems.
 */
void set_benchmark_cpu_affinity(void)
{
	DWORD_PTR dwProcessMask, dwSysMask, dwDesired;

	SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);

	if ( GetProcessAffinityMask(GetCurrentProcess(), &dwProcessMask, &dwSysMask) ) {
		dwDesired = dwSysMask & (dwProcessMask & ~1); /* remove the first processor */
		dwDesired = (dwDesired ? dwDesired : dwSysMask & ~1);
		if (dwDesired != 0) {
			SetProcessAffinityMask(GetCurrentProcess(), dwDesired);
		}
	}
}

#ifdef _WIN32
/* Windows-only (non-CygWin) functions */

#include <share.h> /* for _SH_DENYWR */
#include <fcntl.h> /* for _O_RDONLY, _O_BINARY */
#include <io.h> /* for isatty */
#include <assert.h>
#include <errno.h>
#include <locale.h>

#include "file.h"
#include "parse_cmdline.h"
#include "rhash_main.h"

/**
 * Convert a c-string to wide character string using given codepage
 *
 * @param str the string to convert
 * @param codepage the codepage to use
 * @return converted string on success, NULL on fail
 */
static wchar_t* cstr_to_wchar(const char* str, int codepage)
{
	wchar_t* buf;
	int size = MultiByteToWideChar(codepage, MB_ERR_INVALID_CHARS, str, -1, NULL, 0);
	if (size == 0) return NULL; /* conversion failed */

	buf = (wchar_t*)rsh_malloc(size * sizeof(wchar_t));
	MultiByteToWideChar(codepage, 0, str, -1, buf, size);
	return buf;
}

/**
 * Convert c-string to wide string using primary or secondary codepage.
 *
 * @param str the C-string to convert
 * @param try_no 0 for primary codepage, 1 for a secondary one
 * @return converted wide string on success, NULL on error
 */
wchar_t* c2w(const char* str, int try_no)
{
	int is_utf = (try_no == (opt.flags & OPT_UTF8 ? 0 : 1));
	int codepage = (is_utf ? CP_UTF8 : (opt.flags & OPT_OEM) ? CP_OEMCP : CP_ACP);
	return cstr_to_wchar(str, codepage);
}

/**
 * Convert C-string path to a wide-string path, prepending a long path prefix
 * if it is needed to access the file.
 *
 * @param str the C-string to convert
 * @param try_no 0 for primary codepage, 1 for a secondary one
 * @return converted wide string on success, NULL on error
 */
wchar_t* c2w_long_path(const char* str, int try_no)
{
	wchar_t* wstr = c2w(str, try_no);
	wchar_t* long_path;
	if (!wstr) return NULL;
	long_path = get_long_path_if_needed(wstr);
	if (!long_path) return wstr;
	free(wstr);
	return long_path;
}

/**
 * Convert a UTF8-encoded string to wide string.
 *
 * @param str the UTF8-encoded string to convert
 * @return wide string on success, NULL on error
 */
static wchar_t* utf8_to_wchar(const char* utf8_str)
{
	return cstr_to_wchar(utf8_str, CP_UTF8);
}

/**
 * Convert a wide character string to c-string using given codepage.
 * Optionally set a flag if conversion failed.
 *
 * @param wstr the wide string to convert
 * @param codepage the codepage to use
 * @param failed pointer to the flag, to on failed conversion, can be NULL
 * @return converted string on success, NULL on fail
 */
char* wchar_to_cstr(const wchar_t* wstr, int codepage, int* failed)
{
	int size;
	char *buf;
	BOOL bUsedDefChar, *lpUsedDefaultChar;
	if (codepage == -1) {
		codepage = (opt.flags & OPT_UTF8 ? CP_UTF8 : (opt.flags & OPT_OEM) ? CP_OEMCP : CP_ACP);
	}
	/* note: lpUsedDefaultChar must be NULL for CP_UTF8, otrherwise WideCharToMultiByte() will fail */
	lpUsedDefaultChar = (failed && codepage != CP_UTF8 ? &bUsedDefChar : NULL);

	size = WideCharToMultiByte(codepage, 0, wstr, -1, 0, 0, 0, 0);
	if (size == 0) {
		if (failed) *failed = 1;
		return NULL; /* conversion failed */
	}
	buf = (char*)rsh_malloc(size);
	WideCharToMultiByte(codepage, 0, wstr, -1, buf, size, 0, lpUsedDefaultChar);
	if (failed) *failed = (lpUsedDefaultChar && *lpUsedDefaultChar);
	return buf;
}

/**
 * Convert wide string to multi-byte c-string using codepage specified
 * by command line options.
 *
 * @param wstr the wide string to convert
 * @return c-string on success, NULL on fail
 */
char* w2c(const wchar_t* wstr)
{
	return wchar_to_cstr(wstr, -1, NULL);
}

/**
 * Convert given C-string from encoding specified by
 * command line options to utf8.
 *
 * @param str the string to convert
 * @return converted string on success, NULL on fail
 */
char* win_to_utf8(const char* str)
{
	char* res;
	wchar_t* buf;

	assert((opt.flags & OPT_ENCODING) != 0);
	if (opt.flags & OPT_UTF8) return rsh_strdup(str);

	if ((buf = c2w(str, 0)) == NULL) return NULL;
	res = wchar_to_cstr(buf, CP_UTF8, NULL);
	free(buf);
	return res;
}

#define UNC_PREFIX_SIZE 4

/**
 * Allocate a wide string containing long file path with UNC prefix,
 * if it is needed to access the path, otherwise return NULL.
 *
 * @param wpath original file path, can be a relative one
 * @return allocated long file path if it is needed to access
 *         the path, NULL otherwise
 */
wchar_t* get_long_path_if_needed(const wchar_t* wpath)
{
	if (wcslen(wpath) > 200
		&& (wpath[0] != L'\\' ||  wpath[1] != L'\\'
		|| wpath[2] != L'?' ||  wpath[3] != L'\\'))
	{
		wchar_t* result;
		DWORD size = GetFullPathNameW(wpath, 0, NULL, NULL);
		if (!size) return NULL;
		result = (wchar_t*)rsh_malloc((size + UNC_PREFIX_SIZE) * sizeof(wchar_t));
		wcscpy(result, L"\\\\?\\");
		size = GetFullPathNameW(wpath, size, result + UNC_PREFIX_SIZE, NULL);
		if (size > 0) return result;
		free(result);
	}
	return NULL;
}

/**
 * Open file path given in the current encoding, using desired shared access.
 *
 * @param path file path
 * @param mode string specifying file opening mode
 * @param exclusive non-zero to prohibit write access to the file
 * @return file descriptor on success, NULL on error
 */
FILE* win_fopen_ex(const char* path, const char* mode, int exclusive)
{
	FILE* fd = 0;
	int i;
	wchar_t* wmode = utf8_to_wchar(mode);
	assert(wmode != NULL);

	/* try two code pages */
	for (i = 0; i < 2; i++) {
		wchar_t* wpath = c2w_long_path(path, i);
		if (wpath == NULL) continue;
		fd = _wfsopen(wpath, wmode, (exclusive ? _SH_DENYWR : _SH_DENYNO));
		free(wpath);
		if (fd || errno != ENOENT) break;
	}
	free(wmode);
	return fd;
}

/**
 * Check if given file can be opened with exclusive write access.
 *
 * @param path path to the file
 * @return 1 if file can be opened, 0 otherwise
 */
int can_open_exclusive(const char* path)
{
	int i, res = 0;
	for (i = 0; i < 2 && res == 0; i++) {
		int fd;
		wchar_t* wpath = c2w_long_path(path, i);
		if (wpath == NULL) continue;
		fd = _wsopen(wpath, _O_RDONLY | _O_BINARY, _SH_DENYWR, 0);
		if (fd >= 0) {
			res = 1;
			_close(fd);
		}
		free(wpath);
	}
	return res;
}

/* the range of error codes for access errors */
#define MIN_EACCES_RANGE ERROR_WRITE_PROTECT
#define MAX_EACCES_RANGE ERROR_SHARING_BUFFER_EXCEEDED

/**
 * Convert the GetLastError() value to the assignable to errno.
 *
 * @return errno-compatible error code
 */
static int convert_last_error_to_errno(void)
{
	DWORD error_code = GetLastError();
	switch (error_code)
	{
	case NO_ERROR:
		return 0;
	case ERROR_FILE_NOT_FOUND:
	case ERROR_PATH_NOT_FOUND:
	case ERROR_INVALID_DRIVE:
	case ERROR_BAD_NETPATH:
	case ERROR_BAD_PATHNAME:
	case ERROR_FILENAME_EXCED_RANGE:
		return ENOENT;
	case ERROR_TOO_MANY_OPEN_FILES:
		return EMFILE;
	case ERROR_ACCESS_DENIED:
	case ERROR_SHARING_VIOLATION:
		return EACCES;
	case ERROR_NETWORK_ACCESS_DENIED:
	case ERROR_FAIL_I24:
	case ERROR_SEEK_ON_DEVICE:
		return EACCES;
	case ERROR_LOCK_VIOLATION:
	case ERROR_DRIVE_LOCKED:
	case ERROR_NOT_LOCKED:
	case ERROR_LOCK_FAILED:
		return EACCES;
	case ERROR_INVALID_HANDLE:
		return EBADF;
	case ERROR_NOT_ENOUGH_MEMORY:
	case ERROR_INVALID_BLOCK:
	case ERROR_NOT_ENOUGH_QUOTA:
		return ENOMEM;
	case ERROR_INVALID_ACCESS:
	case ERROR_INVALID_DATA:
	case  ERROR_INVALID_PARAMETER:
		return EINVAL;
	case ERROR_BROKEN_PIPE:
	case ERROR_NO_DATA:
		return EPIPE;
	case ERROR_DISK_FULL:
		return ENOSPC;
	case ERROR_ALREADY_EXISTS:
		return EEXIST;
	case ERROR_NESTING_NOT_ALLOWED:
		return EAGAIN;
	}

	/* try to detect error by range */
	if (MIN_EACCES_RANGE <= error_code && error_code <= MAX_EACCES_RANGE) {
		return EACCES;
	} else {
		return EINVAL;
	}
}

/**
 * Assign errno to the error value converted from the GetLastError().
 */
void set_errno_from_last_file_error(void)
{
	errno = convert_last_error_to_errno();
}

/**
 * Concatenate directory path with filename, unicode version.
 *
 * @param dir_path directory path
 * @param dir_len length of directory path in characters
 * @param filename the file name to append to the directory
 * @return concatenated path
 */
wchar_t* make_pathw(const wchar_t* dir_path, size_t dir_len, wchar_t* filename)
{
	wchar_t* res;
	size_t len;

	if (dir_path == 0) dir_len = 0;
	else {
		/* remove leading path separators from filename */
		while (IS_PATH_SEPARATOR_W(*filename)) filename++;

		if (dir_len == (size_t)-1) dir_len = wcslen(dir_path);
	}
	len = wcslen(filename);

	res = (wchar_t*)rsh_malloc((dir_len + len + 2) * sizeof(wchar_t));
	if (dir_len > 0) {
		memcpy(res, dir_path, dir_len * sizeof(wchar_t));
		if (res[dir_len - 1] != L'\\') {
			/* append path separator to the directory */
			res[dir_len++] = L'\\';
		}
	}

	/* append filename */
	memcpy(res + dir_len, filename, (len + 1) * sizeof(wchar_t));
	return res;
}

/* functions to setup/restore console */

/**
 * Restore console on program exit.
 */
static void restore_cursor(void)
{
	CONSOLE_CURSOR_INFO cci;
	HANDLE hOut = GetStdHandle(STD_ERROR_HANDLE);
	if (hOut != INVALID_HANDLE_VALUE && rhash_data.saved_cursor_size) {
		/* restore cursor size and visibility */
		cci.dwSize = rhash_data.saved_cursor_size;
		cci.bVisible = 1;
		SetConsoleCursorInfo(hOut, &cci);
	}
}

/**
 * Prepare console on program initialization: change console font codepage
 * according to program options and hide cursor.
 */
void setup_console(void)
{
	/* the default encoding is UTF-8 */
	if ((opt.flags & OPT_ENCODING) == 0)
		opt.flags |= OPT_UTF8;

	if ((opt.flags & OPT_UTF8) != 0)
	{
		/* note: we are using numbers 1 = _fileno(stdout), 2 = _fileno(stderr) */
		/* cause _fileno() is undefined,  when compiling as strict ansi C. */
		if (isatty(1))
		{
			_setmode(1, _O_U8TEXT);
			rhash_data.output_flags |= 1;
		}
		if (isatty(2))
		{
			_setmode(2, _O_U8TEXT);
			rhash_data.output_flags |= 2;
		}
#ifdef USE_GETTEXT
		bind_textdomain_codeset(TEXT_DOMAIN, "utf-8");
#endif /* USE_GETTEXT */
	}
	else
	{
		setlocale(LC_CTYPE, opt.flags & OPT_OEM ? ".OCP" : ".ACP");
	}
}

void hide_cursor(void)
{
	CONSOLE_CURSOR_INFO cci;
	HANDLE hOut = GetStdHandle(STD_ERROR_HANDLE);
	if (hOut != INVALID_HANDLE_VALUE && GetConsoleCursorInfo(hOut, &cci))
	{
		/* store current cursor size and visibility flag */
		rhash_data.saved_cursor_size = (cci.bVisible ? cci.dwSize : 0);

		/* now hide cursor */
		cci.bVisible = 0;
		SetConsoleCursorInfo(hOut, &cci); /* hide cursor */
		rsh_install_exit_handler(restore_cursor);
	}
}

/**
 * Detect the program directory.
 */
void init_program_dir(void)
{
	wchar_t* program_path = NULL;
	DWORD buf_size;
	DWORD len;
	for (buf_size = 2048;; buf_size += 2048)
	{
		program_path = (wchar_t*)rsh_malloc(sizeof(wchar_t) * buf_size);
		len = GetModuleFileNameW(NULL, program_path, buf_size);
		if (len && len < buf_size) break;
		free(program_path);
		if (!len || buf_size >= 32768) return;
	}
	/* remove trailng file name with the last separator */
	for (; len > 0 && !IS_PATH_SEPARATOR_W(program_path[len]); len--);
	for (; len > 0 && IS_PATH_SEPARATOR_W(program_path[len]); len--);
	program_path[len + 1] = 0;
	if (len == 0) {
		free(program_path);
		return;
	}
	rhash_data.program_dir = program_path;
}

#ifdef USE_GETTEXT
/**
 * Check that the path points to an existing directory.
 *
 * @param path the path to check
 * @return 1 if the argument is a directory, 0 otherwise
 */
static int is_directory(const char* path)
{
	DWORD res = GetFileAttributesA(path);
	return (res != INVALID_FILE_ATTRIBUTES &&
		!!(res & FILE_ATTRIBUTE_DIRECTORY));
}

/**
 * Set the locale directory relative to ${PROGRAM_DIR}/LOCALEDIR.
 */
void setup_locale_dir(void)
{
	wchar_t* short_dir;
	char *program_dir = NULL;
	char *locale_dir;
	DWORD buf_size;
	DWORD res;
	
	if (!rhash_data.program_dir) return;
	buf_size = GetShortPathNameW(rhash_data.program_dir, NULL, 0);
	if (!buf_size) return;
	
	short_dir = (wchar_t*)rsh_malloc(sizeof(wchar_t) * buf_size);
	res = GetShortPathNameW(rhash_data.program_dir, short_dir, buf_size);
	if (res > 0 && res < buf_size)
		program_dir = w2c(short_dir);
	free(short_dir);
	if (!program_dir) return;
	
	locale_dir = make_path(program_dir, "locale");
	free(program_dir);
	if (!locale_dir) return;
	
	if (is_directory(locale_dir))
		bindtextdomain(TEXT_DOMAIN, locale_dir);
	free(locale_dir);
}
#endif /* USE_GETTEXT */

/**
 * Print formatted data to the specified file descriptor,
 * handling proper printing UTF-8 strings to Windows console.
 *
 * @param out file descriptor
 * @param format data format string
 * @param args list of arguments 
 */
int win_vfprintf(FILE* out, const char* format, va_list args)
{
	if ((out != stdout || !(rhash_data.output_flags & 1))
		&& (out != stderr || !(rhash_data.output_flags & 2)))
		return vfprintf(out, format, args);
	{
		/* because of using a static buffer, this function
                 * can be used only from a single-thread program */
		static char buffer[8192];
		wchar_t *wstr = NULL;
		int res = vsnprintf(buffer, 8192, format, args);
		if (res < 0 || res >= 8192)
		{
			errno = EINVAL;
			return -1;
		}
		wstr = cstr_to_wchar(buffer, CP_UTF8);
		res = fwprintf(out, L"%s", wstr);
		free(wstr);
		return res;
	}
}

/**
 * Print formatted data to the specified file descriptor,
 * handling proper printing UTF-8 strings to Windows console.
 *
 * @param out file descriptor
 * @param format data format string
 */
int win_fprintf(FILE* out, const char* format, ...)
{
	va_list args;
	va_start(args, format);
	return win_vfprintf(out, format, args);
}

size_t win_fwrite(const void *ptr, size_t size, size_t count, FILE *out)
{
	if ((out != stdout || !(rhash_data.output_flags & 1))
		&& (out != stderr || !(rhash_data.output_flags & 2)))
		return fwrite(ptr, size, count, out);
	{
		size_t i;
		const char* buf = (const char*)ptr;
		size *= count;
		if (!size)
			return 0;
		for (i = 0; i < size && buf[i] > 0; i++);
		if (i == size)
		{
			wchar_t* wstr = rsh_malloc(sizeof(wchar_t) * (size + 1));
			for (i = 0; i < size; i++)
				wstr[i] = (wchar_t)buf[i];
			wstr[size] = 0;
			fwprintf(out, L"%s", wstr);
			free(wstr);
			return count;
		}
		for (i = 0; (i + 8) <= size; i += 8)
			fwprintf(out, L"%C%C%C%C%C%C%C%C", buf[i], buf[i + 1], buf[i + 2],
				buf[i + 3], buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]);
		for (; i < size; i++)
			fwprintf(out, L"%C", buf[i]);
		return count;
	}
}

/****************************************************************************
 *                           Directory functions                            *
 ****************************************************************************/
struct WIN_DIR_t
{
	WIN32_FIND_DATAW findFileData;
	HANDLE hFind;
	struct win_dirent dir;
	int state; /* 0 - not started, -1 - ended, >=0 file index */
};

/**
 * Open directory iterator for reading the directory content.
 *
 * @param dir_path directory path
 * @return pointer to directory stream. On error, NULL is returned,
 *         and errno is set appropriately.
 */
WIN_DIR* win_opendir(const char* dir_path)
{
	WIN_DIR* d;
	wchar_t* wpath;

	/* append '\*' to the dir_path */
	size_t len = strlen(dir_path);
	char *path = (char*)malloc(len + 3);
	if (!path) return NULL; /* failed, malloc also set errno = ENOMEM */
	strcpy(path, dir_path);
	strcpy(path + len, "\\*");

	d = (WIN_DIR*)malloc(sizeof(WIN_DIR));
	if (!d) {
		free(path);
		return NULL;
	}
	memset(d, 0, sizeof(WIN_DIR));

	wpath = c2w_long_path(path, 0);
	d->hFind = (wpath != NULL ?
		FindFirstFileW(wpath, &d->findFileData) : INVALID_HANDLE_VALUE);
	free(wpath);

	if (d->hFind == INVALID_HANDLE_VALUE && GetLastError() != ERROR_ACCESS_DENIED) {
		wpath = c2w_long_path(path, 1); /* try to use secondary codepage */
		if (wpath) {
			d->hFind = FindFirstFileW(wpath, &d->findFileData);
			free(wpath);
		}
	}
	free(path);

	if (d->hFind == INVALID_HANDLE_VALUE && GetLastError() == ERROR_ACCESS_DENIED) {
		free(d);
		errno = EACCES;
		return NULL;
	}
	set_errno_from_last_file_error();

	d->state = (d->hFind == INVALID_HANDLE_VALUE ? -1 : 0);
	d->dir.d_name = NULL;
	return d;
}

/**
 * Open a directory for reading its content.
 * For simplicity the function supposes that dir_path points to an
 * existing directory and doesn't check for this error.
 * The Unicode version of the function.
 *
 * @param dir_path directory path
 * @return pointer to directory iterator
 */
WIN_DIR* win_wopendir(const wchar_t* dir_path)
{
	WIN_DIR* d;

	/* append '\*' to the dir_path */
	wchar_t *wpath = make_pathw(dir_path, (size_t)-1, L"*");
	d = (WIN_DIR*)rsh_malloc(sizeof(WIN_DIR));

	d->hFind = FindFirstFileW(wpath, &d->findFileData);
	free(wpath);
	if (d->hFind == INVALID_HANDLE_VALUE && GetLastError() == ERROR_ACCESS_DENIED) {
		free(d);
		errno = EACCES;
		return NULL;
	}

	/* note: we suppose if INVALID_HANDLE_VALUE was returned, then the file listing is empty */
	d->state = (d->hFind == INVALID_HANDLE_VALUE ? -1 : 0);
	d->dir.d_name = NULL;
	return d;
}

/**
 * Close a directory iterator.
 *
 * @param d pointer to the directory iterator
 */
void win_closedir(WIN_DIR* d)
{
	if (d->hFind != INVALID_HANDLE_VALUE) {
		FindClose(d->hFind);
	}
	free(d->dir.d_name);
	free(d);
}

/**
 * Read a directory content.
 *
 * @param d pointer to the directory iterator
 * @return directory entry or NULL if no entries left
 */
struct win_dirent* win_readdir(WIN_DIR* d)
{
	char* filename;
	int failed;

	if (d->state == -1) return NULL;
	if (d->dir.d_name != NULL) {
		free(d->dir.d_name);
		d->dir.d_name = NULL;
	}

	for (;;) {
		if (d->state > 0) {
			if ( !FindNextFileW(d->hFind, &d->findFileData) ) {
				/* the directory listing has ended */
				d->state = -1;
				return NULL;
			}
		}
		d->state++;

		if (d->findFileData.cFileName[0] == L'.' &&
			(d->findFileData.cFileName[1] == 0 ||
			(d->findFileData.cFileName[1] == L'.' &&
			d->findFileData.cFileName[2] == 0))) {
				/* simplified implementation, skips '.' and '..' names */
				continue;
		}

		d->dir.d_name = filename = wchar_to_cstr(d->findFileData.cFileName, WIN_DEFAULT_ENCODING, &failed);

		if (filename && !failed) {
			d->dir.d_wname = d->findFileData.cFileName;
			d->dir.d_isdir = (0 != (d->findFileData.dwFileAttributes &
				FILE_ATTRIBUTE_DIRECTORY));
			return &d->dir;
		}
		/* quietly skip an invalid filename and repeat the search */
		if (filename) {
			free(filename);
			d->dir.d_name = NULL;
		}
	}
}
#endif /* _WIN32 */
#else
typedef int dummy_declaration_required_by_strict_iso_c;
#endif /* defined(_WIN32) || defined(__CYGWIN__) */
