/* vifm
 * Copyright (C) 2014 xaizek.
 *
 * 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 "os.h"

#ifdef _WIN32

#include <windows.h>
#include <ntdef.h>
#include <winioctl.h>

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

#include <errno.h> /* errno */
#include <stddef.h> /* NULL wchar_t */
#include <stdlib.h> /* free() malloc() */
#include <string.h> /* memset() strcpy() strdup() strlen() */
#include <stdio.h> /* FILE */
#include <wchar.h> /* _waccess() _wchmod() _wmkdir() _wrename() _wsystem() */

#include "../utils/fs.h"
#include "../utils/str.h"
#include "../utils/path.h"
#include "../utils/utf8.h"

/* Fake DIR structure for directory traversing functions. */
typedef struct
{
	_WDIR *dirp;         /* Handle returned by opendir(). */
	struct dirent entry; /* Buffer for modified return value of readdir(). */
}
os_dir_t;

static char * resolve_mount_points(const char path[]);

int
os_access(const char pathname[], int mode)
{
	wchar_t *const utf16_pathname = utf8_to_utf16(pathname);
	const int result = _waccess(utf16_pathname, mode);
	free(utf16_pathname);
	return result;
}

int
os_chdir(const char path[])
{
	wchar_t *const utf16_path = utf8_to_utf16(path);
	const int result = _wchdir(utf16_path);
	free(utf16_path);
	return result;
}

int
os_chmod(const char path[], int mode)
{
	wchar_t *const utf16_path = utf8_to_utf16(path);
	const int result = _wchmod(utf16_path, mode);
	free(utf16_path);
	return result;
}

int
os_closedir(DIR *dirp)
{
	os_dir_t *const os_dir = (os_dir_t *)dirp;
	const int result = _wclosedir(os_dir->dirp);
	free(os_dir);
	return result;
}

FILE *
os_fopen(const char path[], const char mode[])
{
	wchar_t *const utf16_path = utf8_to_utf16(path);
	wchar_t *const utf16_mode = utf8_to_utf16(mode);
	FILE *const result = _wfopen(utf16_path, utf16_mode);
	free(utf16_mode);
	free(utf16_path);
	return result;
}

DIR *
os_opendir(const char name[])
{
	os_dir_t *os_dir;
	wchar_t *const utf16_name = utf8_to_utf16(name);
	_WDIR *const d = _wopendir(utf16_name);
	free(utf16_name);

	if(d == NULL)
	{
		return NULL;
	}

	os_dir = malloc(sizeof(*os_dir));
	if(os_dir == NULL)
	{
		(void)_wclosedir(d);
		return NULL;
	}

	os_dir->dirp = d;
	return (DIR *)os_dir;
}

struct dirent *
os_readdir(DIR *dirp)
{
	char *utf8_name;
	os_dir_t *const os_dir = (os_dir_t *)dirp;
	struct _wdirent *const entry = _wreaddir(os_dir->dirp);
	if(entry == NULL)
	{
		return NULL;
	}

	utf8_name = utf8_from_utf16(entry->d_name);
	copy_str(os_dir->entry.d_name, sizeof(os_dir->entry.d_name), utf8_name);
	free(utf8_name);

	os_dir->entry.d_ino = 0;
	os_dir->entry.d_reclen  = 0;
	os_dir->entry.d_namlen = strlen(os_dir->entry.d_name);

	return &os_dir->entry;
}

int
os_rename(const char oldpath[], const char newpath[])
{
	wchar_t *const utf16_oldpath = utf8_to_utf16(oldpath);
	wchar_t *const utf16_newpath = utf8_to_utf16(newpath);
	const int result = _wrename(utf16_oldpath, utf16_newpath);
	free(utf16_oldpath);
	free(utf16_newpath);
	return result;
}

int
os_rmdir(const char path[])
{
	wchar_t *const utf16_path = utf8_to_utf16(path);
	const int result = _wrmdir(utf16_path);
	free(utf16_path);
	return result;
}

int
os_mkdir(const char pathname[], int mode)
{
	wchar_t *const utf16_pathname = utf8_to_utf16(pathname);
	const int result = _wmkdir(utf16_pathname);
	free(utf16_pathname);
	return result;
}

char *
os_realpath(const char path[], char resolved_path[])
{
	if(!path_exists(path, NODEREF))
	{
		errno = ENOENT;
		return NULL;
	}

	char base_path[PATH_MAX + 1];
	if(get_cwd(base_path, sizeof(base_path)) != base_path)
	{
		errno = ENOENT;
		return NULL;
	}

	char curr_path[PATH_MAX + 1];
	to_canonic_path(path, base_path, curr_path, sizeof(curr_path));

	char next_path[PATH_MAX + 1];

	while(1)
	{
		DWORD type = win_get_reparse_point_type(curr_path);
		if(type == IO_REPARSE_TAG_MOUNT_POINT)
		{
			char *resolved = resolve_mount_points(curr_path);
			if(resolved == NULL)
			{
				errno = ENOENT;
				return NULL;
			}

			to_canonic_path(resolved, base_path, next_path, sizeof(next_path));

			free(resolved);
		}
		else if(type == IO_REPARSE_TAG_SYMLINK)
		{
			if(get_link_target_abs(curr_path, base_path, next_path,
						sizeof(next_path)) != 0)
			{
				errno = ENOENT;
				return NULL;
			}
		}
		else
		{
			break;
		}

		copy_str(curr_path, sizeof(curr_path), next_path);

		copy_str(base_path, sizeof(base_path), curr_path);
		if(!is_root_dir(base_path))
		{
			remove_last_path_component(base_path);
		}
	}

	copy_str(resolved_path, PATH_MAX, curr_path);
	return resolved_path;
}

/* Resolves path to its destination.  Returns pointer to a newly allocated
 * memory or NULL on error. */
static char *
resolve_mount_points(const char path[])
{
	char resolved_path[PATH_MAX + 1];

	if(win_get_reparse_point_type(path) != IO_REPARSE_TAG_MOUNT_POINT)
	{
		return NULL;
	}

	char rdb[2048];
	if(win_reparse_point_read(path, rdb, sizeof(rdb)) != 0)
	{
		return NULL;
	}

	REPARSE_DATA_BUFFER *rdbp = (REPARSE_DATA_BUFFER *)rdb;
	char *mb = to_multibyte(rdbp->MountPointReparseBuffer.PathBuffer);

	int offset = starts_with_lit(mb, "\\??\\") ? 4 : 0;
	strcpy(resolved_path, mb + offset);

	free(mb);

	return strdup(resolved_path);
}

int
os_stat(const char path[], struct stat *buf)
{
	HANDLE hfile;
	struct _stat st;
	wchar_t *const utf16_path = utf8_to_utf16(path);
	const int result = _wstat(utf16_path, &st);

	memset(buf, 0, sizeof(*buf));
	buf->st_dev = st.st_dev;
	buf->st_ino = st.st_ino;
	buf->st_mode = st.st_mode;
	buf->st_nlink = st.st_nlink;
	buf->st_uid = st.st_uid;
	buf->st_gid = st.st_gid;
	buf->st_rdev = st.st_rdev;
	buf->st_size = st.st_size;
	buf->st_atime = st.st_atime;
	buf->st_mtime = st.st_mtime;
	buf->st_ctime = st.st_ctime;

	/* M$ CRT stands for Crappiest Run-Time, _wstat returns invalid time for files
	 * that were created when DST had different value and time adjustment setting
	 * is enabled. */
	hfile = CreateFileW(utf16_path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
			OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
			NULL);
	free(utf16_path);
	if(hfile != INVALID_HANDLE_VALUE)
	{
		FILETIME ctime, atime, mtime;
		if(GetFileTime(hfile, &ctime, &atime, &mtime))
		{
			buf->st_ctime = win_to_unix_time(ctime);
			buf->st_atime = win_to_unix_time(atime);
			buf->st_mtime = win_to_unix_time(mtime);
		}
		CloseHandle(hfile);
	}

	return result;
}

int
os_system(const char command[])
{
	wchar_t *const utf16_command = utf8_to_utf16(command);
	const int result = _wsystem(utf16_command);
	free(utf16_command);
	return result;
}

char *
os_getcwd(char buf[], size_t size)
{
	char *utf8_buf;
	wchar_t wbuf[size];
	if(_wgetcwd(wbuf, sizeof(wbuf)) == NULL)
	{
		return NULL;
	}

	utf8_buf = utf8_from_utf16(wbuf);
	if(utf8_buf == NULL)
	{
		return NULL;
	}

	if(strlen(utf8_buf) + 1U > size)
	{
		free(utf8_buf);
		return NULL;
	}

	copy_str(buf, size, utf8_buf);
	free(utf8_buf);

	system_to_internal_slashes(buf);
	return buf;
}

#else

#include <fcntl.h> /* F_FULLFSYNC */
#include <unistd.h> /* fcntl() fdatasync() */

int
os_fdatasync(int fd)
{
#ifdef F_FULLFSYNC
	/* OS X system might not have fdatasync(). */
	return fcntl(fd, F_FULLFSYNC);
#elif defined(HAVE_FDATASYNC)
	return fdatasync(fd);
#else
	/* At least Haiku lacks fdatasync(). */
	return fsync(fd);
#endif
}

#endif

#include <stdio.h> /* FILE */

#include "../utils/fs.h"
#include "../utils/path.h"
#include "fs_limits.h"

FILE *
os_tmpfile(void)
{
	char tmp_path[PATH_MAX + 1];
	return make_file_in_tmp("vifm.tmp", 0600, /*auto_delete=*/1, tmp_path,
			sizeof(tmp_path));
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
