/*
 * Copyright (C) the libgit2 contributors. All rights reserved.
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */
#include "../posix.h"
#include "../fileops.h"
#include "path.h"
#include "utf-conv.h"
#include "repository.h"
#include <errno.h>
#include <io.h>
#include <fcntl.h>
#include <ws2tcpip.h>

int p_unlink(const char *path)
{
	git_win32_path buf;
	git_win32_path_from_c(buf, path);
	_wchmod(buf, 0666);
	return _wunlink(buf);
}

int p_fsync(int fd)
{
	HANDLE fh = (HANDLE)_get_osfhandle(fd);

	if (fh == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	if (!FlushFileBuffers(fh)) {
		DWORD code = GetLastError();

		if (code == ERROR_INVALID_HANDLE)
			errno = EINVAL;
		else
			errno = EIO;

		return -1;
	}

	return 0;
}

GIT_INLINE(time_t) filetime_to_time_t(const FILETIME *ft)
{
	long long winTime = ((long long)ft->dwHighDateTime << 32) + ft->dwLowDateTime;
	winTime -= 116444736000000000LL; /* Windows to Unix Epoch conversion */
	winTime /= 10000000;		 /* Nano to seconds resolution */
	return (time_t)winTime;
}

#define WIN32_IS_WSEP(CH) ((CH) == L'/' || (CH) == L'\\')

static int do_lstat(
	const char *file_name, struct stat *buf, int posix_enotdir)
{
	WIN32_FILE_ATTRIBUTE_DATA fdata;
	git_win32_path fbuf;
	wchar_t lastch;
	int flen;

	flen = git_win32_path_from_c(fbuf, file_name);

	/* truncate trailing slashes */
	for (; flen > 0; --flen) {
		lastch = fbuf[flen - 1];
		if (WIN32_IS_WSEP(lastch))
			fbuf[flen - 1] = L'\0';
		else if (lastch != L'\0')
			break;
	}

	if (GetFileAttributesExW(fbuf, GetFileExInfoStandard, &fdata)) {
		int fMode = S_IREAD;

		if (!buf)
			return 0;

		if (fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
			fMode |= S_IFDIR;
		else
			fMode |= S_IFREG;

		if (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
			fMode |= S_IWRITE;

		if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
			fMode |= S_IFLNK;

		if ((fMode & (S_IFDIR | S_IFLNK)) == (S_IFDIR | S_IFLNK)) // junction
			fMode ^= S_IFLNK;

		buf->st_ino = 0;
		buf->st_gid = 0;
		buf->st_uid = 0;
		buf->st_nlink = 1;
		buf->st_mode = (mode_t)fMode;
		buf->st_size = ((git_off_t)fdata.nFileSizeHigh << 32) + fdata.nFileSizeLow;
		buf->st_dev = buf->st_rdev = (_getdrive() - 1);
		buf->st_atime = filetime_to_time_t(&(fdata.ftLastAccessTime));
		buf->st_mtime = filetime_to_time_t(&(fdata.ftLastWriteTime));
		buf->st_ctime = filetime_to_time_t(&(fdata.ftCreationTime));

		/* Windows symlinks have zero file size, call readlink to determine
		 * the length of the path pointed to, which we expect everywhere else
		 */
		if (S_ISLNK(fMode)) {
			git_win32_path_as_utf8 target;
			int readlink_result;

			readlink_result = p_readlink(file_name, target, sizeof(target));

			if (readlink_result == -1)
				return -1;

			buf->st_size = strlen(target);
		}

		return 0;
	}

	errno = ENOENT;

	/* To match POSIX behavior, set ENOTDIR when any of the folders in the
	 * file path is a regular file, otherwise set ENOENT.
	 */
	if (posix_enotdir) {
		/* scan up path until we find an existing item */
		while (1) {
			/* remove last directory component */
			for (--flen; flen > 0 && !WIN32_IS_WSEP(fbuf[flen]); --flen);

			if (flen <= 0)
				break;

			fbuf[flen] = L'\0';

			if (GetFileAttributesExW(fbuf, GetFileExInfoStandard, &fdata)) {
				if (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
					errno = ENOTDIR;
				break;
			}
		}
	}

	return -1;
}

int p_lstat(const char *filename, struct stat *buf)
{
	return do_lstat(filename, buf, 0);
}

int p_lstat_posixly(const char *filename, struct stat *buf)
{
	return do_lstat(filename, buf, 1);
}


/*
 * Parts of the The p_readlink function are heavily inspired by the php 
 * readlink function in link_win32.c
 *
 * Copyright (c) 1999 - 2012 The PHP Group. All rights reserved.
 *
 * For details of the PHP license see http://www.php.net/license/3_01.txt
 */
int p_readlink(const char *link, char *target, size_t target_len)
{
	typedef DWORD (WINAPI *fpath_func)(HANDLE, LPWSTR, DWORD, DWORD);
	static fpath_func pGetFinalPath = NULL;
	HANDLE hFile;
	DWORD dwRet;
	git_win32_path link_w;
	wchar_t* target_w;
	int error = 0;

	assert(link && target && target_len > 0);

	/*
	 * Try to load the pointer to pGetFinalPath dynamically, because
	 * it is not available in platforms older than Vista
	 */
	if (pGetFinalPath == NULL) {
		HMODULE module = GetModuleHandle("kernel32");

		if (module != NULL)
			pGetFinalPath = (fpath_func)GetProcAddress(module, "GetFinalPathNameByHandleW");

		if (pGetFinalPath == NULL) {
			giterr_set(GITERR_OS,
				"'GetFinalPathNameByHandleW' is not available in this platform");
			return -1;
		}
	}

	git_win32_path_from_c(link_w, link);

	hFile = CreateFileW(link_w,			// file to open
			GENERIC_READ,			// open for reading
			FILE_SHARE_READ,		// share for reading
			NULL,					// default security
			OPEN_EXISTING,			// existing file only
			FILE_FLAG_BACKUP_SEMANTICS, // normal file
			NULL);					// no attr. template

	if (hFile == INVALID_HANDLE_VALUE) {
		giterr_set(GITERR_OS, "Cannot open '%s' for reading", link);
		return -1;
	}

	target_w = (wchar_t*)git__malloc(target_len * sizeof(wchar_t));
	GITERR_CHECK_ALLOC(target_w);

	dwRet = pGetFinalPath(hFile, target_w, (DWORD)target_len, 0x0);
	if (dwRet == 0 ||
		dwRet >= target_len ||
		!WideCharToMultiByte(CP_UTF8, 0, target_w, -1, target,
			(int)(target_len * sizeof(char)), NULL, NULL))
		error = -1;

	git__free(target_w);
	CloseHandle(hFile);

	if (error)
		return error;

	/* Skip first 4 characters if they are "\\?\" */
	if (dwRet > 4 &&
		target[0] == '\\' && target[1] == '\\' &&
		target[2] == '?' && target[3] == '\\')
	{
		unsigned int offset = 4;
		dwRet -= 4;

		/* \??\UNC\ */
		if (dwRet > 7 &&
			target[4] == 'U' && target[5] == 'N' && target[6] == 'C')
		{
			offset += 2;
			dwRet -= 2;
			target[offset] = '\\';
		}

		memmove(target, target + offset, dwRet);
	}

	target[dwRet] = '\0';

	return dwRet;
}

int p_symlink(const char *old, const char *new)
{
	/* Real symlinks on NTFS require admin privileges. Until this changes,
	 * libgit2 just creates a text file with the link target in the contents.
	 */
	return git_futils_fake_symlink(old, new);
}

int p_open(const char *path, int flags, ...)
{
	git_win32_path buf;
	mode_t mode = 0;

	git_win32_path_from_c(buf, path);

	if (flags & O_CREAT) {
		va_list arg_list;

		va_start(arg_list, flags);
		mode = (mode_t)va_arg(arg_list, int);
		va_end(arg_list);
	}

	return _wopen(buf, flags | _O_BINARY, mode);
}

int p_creat(const char *path, mode_t mode)
{
	git_win32_path buf;
	git_win32_path_from_c(buf, path);
	return _wopen(buf, _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, mode);
}

int p_getcwd(char *buffer_out, size_t size)
{
	int ret;
	wchar_t *buf;

	if ((size_t)((int)size) != size)
		return -1;

	buf = (wchar_t*)git__malloc(sizeof(wchar_t) * (int)size);
	GITERR_CHECK_ALLOC(buf);

	_wgetcwd(buf, (int)size);

	ret = WideCharToMultiByte(
		CP_UTF8, 0, buf, -1, buffer_out, (int)size, NULL, NULL);

	git__free(buf);
	return !ret ? -1 : 0;
}

int p_stat(const char* path, struct stat* buf)
{
	git_win32_path_as_utf8 target;
	int error = 0;

	error = do_lstat(path, buf, 0);

	/* We need not do this in a loop to unwind chains of symlinks since
	 * p_readlink calls GetFinalPathNameByHandle which does it for us. */
	if (error >= 0 && S_ISLNK(buf->st_mode) &&
		(error = p_readlink(path, target, sizeof(target))) >= 0)
		error = do_lstat(target, buf, 0);

	return error;
}

int p_chdir(const char* path)
{
	git_win32_path buf;
	git_win32_path_from_c(buf, path);
	return _wchdir(buf);
}

int p_chmod(const char* path, mode_t mode)
{
	git_win32_path buf;
	git_win32_path_from_c(buf, path);
	return _wchmod(buf, mode);
}

int p_rmdir(const char* path)
{
	int error;
	git_win32_path buf;
	git_win32_path_from_c(buf, path);

	error = _wrmdir(buf);

	/* _wrmdir() is documented to return EACCES if "A program has an open
	 * handle to the directory."  This sounds like what everybody else calls
	 * EBUSY.  Let's convert appropriate error codes.
	 */
	if (GetLastError() == ERROR_SHARING_VIOLATION)
		errno = EBUSY;

	return error;
}

int p_hide_directory__w32(const char *path)
{
	git_win32_path buf;
	git_win32_path_from_c(buf, path);
	return (SetFileAttributesW(buf, FILE_ATTRIBUTE_HIDDEN) != 0) ? 0 : -1;
}

char *p_realpath(const char *orig_path, char *buffer)
{
	int ret;
	git_win32_path orig_path_w;
	git_win32_path buffer_w;

	git_win32_path_from_c(orig_path_w, orig_path);

	/* Implicitly use GetCurrentDirectory which can be a threading issue */
	ret = GetFullPathNameW(orig_path_w, GIT_WIN_PATH_UTF16, buffer_w, NULL);

	/* According to MSDN, a return value equals to zero means a failure. */
	if (ret == 0 || ret > GIT_WIN_PATH_UTF16)
		buffer = NULL;

	else if (GetFileAttributesW(buffer_w) == INVALID_FILE_ATTRIBUTES) {
		buffer = NULL;
		errno = ENOENT;
	}

	else if (buffer == NULL) {
		int buffer_sz = WideCharToMultiByte(
			CP_UTF8, 0, buffer_w, -1, NULL, 0, NULL, NULL);

		if (!buffer_sz ||
			!(buffer = (char *)git__malloc(buffer_sz)) ||
			!WideCharToMultiByte(
				CP_UTF8, 0, buffer_w, -1, buffer, buffer_sz, NULL, NULL))
		{
			git__free(buffer);
			buffer = NULL;
		}
	}

	else if (!WideCharToMultiByte(
		CP_UTF8, 0, buffer_w, -1, buffer, GIT_PATH_MAX, NULL, NULL))
		buffer = NULL;

	if (buffer)
		git_path_mkposix(buffer);

	return buffer;
}

int p_vsnprintf(char *buffer, size_t count, const char *format, va_list argptr)
{
#ifdef _MSC_VER
	int len;

	if (count == 0 ||
		(len = _vsnprintf_s(buffer, count, _TRUNCATE, format, argptr)) < 0)
		return _vscprintf(format, argptr);

	return len;
#else /* MinGW */
	return vsnprintf(buffer, count, format, argptr);
#endif
}

int p_snprintf(char *buffer, size_t count, const char *format, ...)
{
	va_list va;
	int r;

	va_start(va, format);
	r = p_vsnprintf(buffer, count, format, va);
	va_end(va);

	return r;
}

extern int p_creat(const char *path, mode_t mode);

int p_mkstemp(char *tmp_path)
{
#if defined(_MSC_VER)
	if (_mktemp_s(tmp_path, strlen(tmp_path) + 1) != 0)
		return -1;
#else
	if (_mktemp(tmp_path) == NULL)
		return -1;
#endif

	return p_creat(tmp_path, 0744); //-V536
}

int p_setenv(const char* name, const char* value, int overwrite)
{
	if (overwrite != 1)
		return -1;

	return (SetEnvironmentVariableA(name, value) == 0 ? -1 : 0);
}

int p_access(const char* path, mode_t mode)
{
	git_win32_path buf;
	git_win32_path_from_c(buf, path);
	return _waccess(buf, mode);
}

int p_rename(const char *from, const char *to)
{
	git_win32_path wfrom;
	git_win32_path wto;

	git_win32_path_from_c(wfrom, from);
	git_win32_path_from_c(wto, to);
	return MoveFileExW(wfrom, wto, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) ? 0 : -1;
}

int p_recv(GIT_SOCKET socket, void *buffer, size_t length, int flags)
{
	if ((size_t)((int)length) != length)
		return -1; /* giterr_set will be done by caller */

	return recv(socket, buffer, (int)length, flags);
}

int p_send(GIT_SOCKET socket, const void *buffer, size_t length, int flags)
{
	if ((size_t)((int)length) != length)
		return -1; /* giterr_set will be done by caller */

	return send(socket, buffer, (int)length, flags);
}

/**
 * Borrowed from http://old.nabble.com/Porting-localtime_r-and-gmtime_r-td15282276.html
 * On Win32, `gmtime_r` doesn't exist but `gmtime` is threadsafe, so we can use that
 */
struct tm *
p_localtime_r (const time_t *timer, struct tm *result)
{
	struct tm *local_result;
	local_result = localtime (timer);

	if (local_result == NULL || result == NULL)
		return NULL;

	memcpy (result, local_result, sizeof (struct tm));
	return result;
}
struct tm *
p_gmtime_r (const time_t *timer, struct tm *result)
{
	struct tm *local_result;
	local_result = gmtime (timer);

	if (local_result == NULL || result == NULL)
		return NULL;

	memcpy (result, local_result, sizeof (struct tm));
	return result;
}

int p_inet_pton(int af, const char *src, void *dst)
{
	struct sockaddr_storage sin;
	void *addr;
	int sin_len = sizeof(struct sockaddr_storage), addr_len;
	int error = 0;

	if (af == AF_INET) {
		addr = &((struct sockaddr_in *)&sin)->sin_addr;
		addr_len = sizeof(struct in_addr);
	} else if (af == AF_INET6) {
		addr = &((struct sockaddr_in6 *)&sin)->sin6_addr;
		addr_len = sizeof(struct in6_addr);
	} else {
		errno = EAFNOSUPPORT;
		return -1;
	}

	if ((error = WSAStringToAddressA((LPSTR)src, af, NULL, (LPSOCKADDR)&sin, &sin_len)) == 0) {
		memcpy(dst, addr, addr_len);
		return 1;
	}

	switch(WSAGetLastError()) {
	case WSAEINVAL:
		return 0;
	case WSAEFAULT:
		errno = ENOSPC;
		return -1;
	case WSA_NOT_ENOUGH_MEMORY:
		errno = ENOMEM;
		return -1;
	}

	errno = EINVAL;
	return -1;
}
