/* file.c - file abstraction layer */

/* use 64-bit off_t.
 * these macros must be defined before any included file */
#undef _LARGEFILE64_SOURCE
#undef _FILE_OFFSET_BITS
#define _LARGEFILE64_SOURCE
#define _FILE_OFFSET_BITS 64

#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "file.h"
#include "common_func.h"
#include "win_utils.h"

#if defined( _WIN32) || defined(__CYGWIN__)
# include <windows.h>
# include <share.h> /* for _SH_DENYWR */
# include <fcntl.h>  /* _O_RDONLY, _O_BINARY */
# include <io.h>
#endif

#ifdef __cplusplus
extern "C" {
#endif

/*=========================================================================
 * Path functions
 *=========================================================================*/

/**
 * Return file name without path.
 *
 * @param path file path
 * @return file name
 */
const char* get_basename(const char* path)
{
	const char *p = path + strlen(path) - 1;
	for (; p >= path && !IS_PATH_SEPARATOR(*p); p--);
	return (p+1);
}

/**
 * Return allocated buffer with the directory part of the path.
 * The buffer must be freed by calling free().
 *
 * @param path file path
 * @return directory
 */
char* get_dirname(const char* path)
{
	const char *p = path + strlen(path) - 1;
	char *res;
	for (; p > path && !IS_PATH_SEPARATOR(*p); p--);
	if ((p - path) > 1) {
		res = (char*)rsh_malloc(p-path+1);
		memcpy(res, path, p-path);
		res[p-path] = 0;
		return res;
	} else {
		return rsh_strdup(".");
	}
}

/**
 * Assemble a filepath from its directory and filename.
 *
 * @param dir_path directory path
 * @param filename the file name
 * @return assembled file path
 */
char* make_path(const char* dir_path, const char* filename)
{
	char* buf;
	size_t len;
	assert(dir_path);
	assert(filename);

	/* remove leading path separators from filename */
	while (IS_PATH_SEPARATOR(*filename)) filename++;

	if (dir_path[0] == '.' && dir_path[1] == 0) {
		/* do not extend filename for dir_path="." */
		return rsh_strdup(filename);
	}

	/* copy directory path */
	len = strlen(dir_path);
	buf = (char*)rsh_malloc(len + strlen(filename) + 2);
	strcpy(buf, dir_path);

	/* separate directory from filename */
	if (len > 0 && !IS_PATH_SEPARATOR(buf[len-1])) {
		buf[len++] = SYS_PATH_SEPARATOR;
	}

	/* append filename */
	strcpy(buf+len, filename);
	return buf;
}

#define IS_ANY_SLASH(c) ((c) == RSH_T('/') || (c) == RSH_T('\\'))

/**
 * Compare paths.
 *
 * @param a the first path
 * @param b the second path
 * @return 1 if paths a equal, 0 otherwise
 */
int are_paths_equal(ctpath_t a, ctpath_t b)
{
	if (!a || !b) return 0;
	if (a[0] == RSH_T('.') && IS_ANY_SLASH(a[1])) a += 2;
	if (b[0] == RSH_T('.') && IS_ANY_SLASH(b[1])) b += 2;
	
	for (; *a; ++a, ++b) {
		if (*a != *b && (!IS_ANY_SLASH(*b) || !IS_ANY_SLASH(*a))) {
			/* paths are different */
			return 0;
		}
	}
	/* check if both paths terminated */
	return (*a == *b);
}

/**
 * Check if a path points to a regular file.
 *
 * @param path the path to check
 * @return 1 if file exists an is a regular file, 0 otherwise
 */
int is_regular_file(const char* path)
{
	int is_regular = 0;
	file_t file;
	file_init(&file, path, FILE_OPT_DONT_FREE_PATH);
	if (file_stat(&file, 0) >= 0) {
		is_regular = FILE_ISREG(&file);
	}
	file_cleanup(&file);
	return is_regular;
}

/**
 * Check if a file exists at the specified path.
 *
 * @param path the path to check
 * @return 1 if file exists, 0 otherwise.
 */
int if_file_exists(const char* path)
{
	int exists;
	file_t file;
	file_init(&file, path, FILE_OPT_DONT_FREE_PATH);
	exists = (file_stat(&file, 0) >= 0);
	file_cleanup(&file);
	return exists;
}

/*=========================================================================
 * file_t functions
 *=========================================================================*/

void file_init(file_t* file, const char* path, int finit_flags)
{
	memset(file, 0, sizeof(*file));
	if ((finit_flags & FILE_OPT_DONT_FREE_PATH) != 0) {
		file->path = (char*)path;
		file->mode = (unsigned)finit_flags;
	} else {
		file->path = rsh_strdup(path);
	}
}

/**
 * Free the memory allocated by the fields of the file_t structure.
 *
 * @param file the file_t structure to clean
 */
void file_cleanup(file_t* file)
{
	if ((file->mode & FILE_OPT_DONT_FREE_PATH) == 0)
		free(file->path);
	file->path = NULL;

#ifdef _WIN32
	if ((file->mode & FILE_OPT_DONT_FREE_WPATH) == 0)
		free(file->wpath);
	file->wpath = NULL;
#endif /* _WIN32 */

	file->mtime = file->size = 0;
	file->mode = 0;
}

/**
 * Append the specified suffix to the src file path.
 *
 * @param dst result of appending
 * @param src the path to append the suffix to
 * @param suffix the suffix to append
 */
void file_path_append(file_t* dst, file_t* src, const char* suffix)
{
	memset(dst, 0, sizeof(*dst));
#ifdef _WIN32
	if (src->wpath)
	{
		wchar_t* wsuffix = c2w(suffix, 0);
		assert(wsuffix != 0);
		size_t src_len = wcslen(src->wpath);
		size_t dst_len = src_len + wcslen(wsuffix) + 1;
		dst->wpath = (wchar_t*)rsh_malloc(dst_len * sizeof(wchar_t));
		wcscpy(dst->wpath, src->wpath);
		wcscpy(dst->wpath + src_len, wsuffix);
		dst->path = w2c(dst->wpath); /* for legacy file handling */
		return;
	}
#endif
	assert(!!file->path);
	size_t src_len = strlen(src->path);
	size_t dst_len = src_len + strlen(suffix) + 1;
	dst->path = (char*)rsh_malloc(dst_len);
	strcpy(dst->path, src->path);
	strcpy(dst->path + src_len, suffix);
}

#ifdef _WIN32
/**
 * Retrieve file information (type, size, mtime) into file_t fields.
 *
 * @param file the file information
 * @return 0 on success, -1 on error
 */
static int file_statw(file_t* file)
{
	WIN32_FILE_ATTRIBUTE_DATA data;
	wchar_t* long_path = get_long_path_if_needed(file->wpath);

	/* read file attributes */
	if (GetFileAttributesExW((long_path ? long_path : file->wpath), GetFileExInfoStandard, &data)) {
		uint64_t u;
		file->size  = (((uint64_t)data.nFileSizeHigh) << 32) + data.nFileSizeLow;
		file->mode |= (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ? FILE_IFDIR : FILE_IFREG);
		if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
			file->mode |= FILE_IFLNK;

		/* the number of 100-nanosecond intervals since January 1, 1601 */
		u = (((uint64_t)data.ftLastWriteTime.dwHighDateTime) << 32) + data.ftLastWriteTime.dwLowDateTime;
		/* convert to second and subtract the epoch difference */
		file->mtime = u / 10000000 - 11644473600LL;
		free(long_path);
		return 0;
	}
	free(long_path);
	set_errno_from_last_file_error();
	return -1;
}
#endif

/**
 * Retrieve file information (type, size, mtime) into file_t fields.
 *
 * @param file the file information
 * @param fstat_flags bitmask consisting of FileStatModes bits
 * @return 0 on success, -1 on error
 */
int file_stat(file_t* file, int fstat_flags)
{
#ifdef _WIN32
	int i;
	(void)fstat_flags; /* ignore on windows */

	file->size  = 0;
	file->mtime = 0;
	file->mode &= (FILE_OPT_DONT_FREE_PATH | FILE_OPT_DONT_FREE_WPATH | FILE_IFROOT | FILE_IFSTDIN);
	if (file->wpath)
		return file_statw(file);

	for (i = 0; i < 2; i++) {
		file->wpath = c2w_long_path(file->path, i);
		if (file->wpath == NULL) continue;
		if (file_statw(file) == 0) return 0; /* success */
		free(file->wpath);
		file->wpath = NULL;
	}
	assert(errno != 0);
	return -1;
#else
	struct stat st;
	int res = 0;
	file->size  = 0;
	file->mtime = 0;
	file->mode  &= (FILE_OPT_DONT_FREE_PATH | FILE_IFROOT | FILE_IFSTDIN);

	if ((fstat_flags & FUseLstat) != 0) {
		if (lstat(file->path, &st) < 0) return -1;
		if (S_ISLNK(st.st_mode))
			file->mode |= FILE_IFLNK; /* it's a symlink */
	}
	else
		res = stat(file->path, &st);

	if (res == 0) {
		file->size  = st.st_size;
		file->mtime = st.st_mtime;

		if (S_ISDIR(st.st_mode)) {
			file->mode |= FILE_IFDIR;
		} else if (S_ISREG(st.st_mode)) {
			/* it's a regular file or a symlink pointing to a regular file */
			file->mode |= FILE_IFREG;
		}
	}
	return res;
#endif
}


/**
 * Retrieve file information (type, size, mtime) into file_t fields.
 *
 * @param file the file information
 * @param fopen_flags bitmask consisting of FileFOpenModes bits
 * @return 0 on success, -1 on error
 */
FILE* file_fopen(file_t* file, int fopen_flags)
{
	const file_tchar* possible_modes[8] = { 0, RSH_T("r"), RSH_T("w"), RSH_T("r+"),
		0, RSH_T("rb"), RSH_T("wb"), RSH_T("r+b") };
	const file_tchar* mode = possible_modes[fopen_flags & FOpenMask];
	assert((fopen_flags & FOpenRW) != 0);
	assert((fopen_flags & FOpenRW) != 0);
#ifdef _WIN32
	if (!file->wpath)
	{
		int i;
		FILE* fd = 0;
		for (i = 0; i < 2; i++) {
			file->wpath = c2w_long_path(file->path, i);
			if (file->wpath == NULL) continue;
			fd = _wfsopen(file->wpath, mode, _SH_DENYNO);
			if (fd || errno != ENOENT) break;
			free(file->wpath);
			file->wpath = 0;
		}
		return fd;
	}
	return _wfsopen(file->wpath, mode, _SH_DENYNO);
#else
	return fopen(file->path, mode);
#endif
}

#ifdef _WIN32
FILE* rsh_tfopen(ctpath_t tpath, file_tchar* tmode)
{
	return _wfsopen(tpath, tmode, _SH_DENYNO);
}
#endif

/**
 * Rename or move the file. The source and destination paths should be on the same device.
 *
 * @param from the source file
 * @param to the destination path
 * @return 0 on success, -1 on error
 */
int file_rename(file_t* from, file_t* to)
{
#ifdef _WIN32
	if (from->wpath && to->wpath)
	{
		/* Windows: file must be removed before overwriting it */
		_wunlink(to->wpath);
		return _wrename(from->wpath, to->wpath);
	}
	assert(from->path && to->path);
	unlink(to->path);
#endif
	return rename(from->path, to->path);
}

#ifdef _WIN32
static int win_can_open_exclusive(wchar_t* wpath)
{
	int fd = _wsopen(wpath, _O_RDONLY | _O_BINARY, _SH_DENYWR, 0);
	if (fd < 0) return 0;
	_close(fd);
	return 1;
}

/**
 * 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 file_is_write_locked(file_t* file)
{
	int i, res = 0;
	if (file->wpath)
		return win_can_open_exclusive(file->wpath);
	for (i = 0; i < 2 && !res; i++) {
		file->wpath = c2w_long_path(file->path, i);
		if(file->wpath && win_can_open_exclusive(file->wpath)) return 1;
		free(file->wpath);
	}
	file->wpath = NULL;
	return 0;
}
#endif


/*=========================================================================
 * file-list functions
 *=========================================================================*/

/**
 * Open a file, containing a list of file paths, to iterate over those paths
 * using the file_list_read() function.
 *
 * @param list the file_list_t structure to initialize
 * @param file_path the file to open
 * @return 0 on success, -1 on error and set errno
 */
int file_list_open(file_list_t* list, file_t* file_path)
{
	memset(list, 0, sizeof(file_list_t));
	list->fd = file_fopen(file_path, FOpenRead | FOpenBin);
	return (list->fd ? 0 : -1);
}

/**
 * Close file_list_t and free allocated memory.
 */
void file_list_close(file_list_t* list)
{
	if (list->fd) {
		fclose(list->fd);
		list->fd = 0;
	}
	file_cleanup(&list->current_file);
}

enum FileListStateBits {
	NotFirstLine = 1
};

/**
 * Iterate over file list.
 *
 * @param list the file list to iterate over
 * @return 1 if next file have been obtained, 0 on EOF or error
 */
int file_list_read(file_list_t* list)
{
	char buf[2048];
	file_cleanup(&list->current_file);
	while(fgets(buf, 2048, list->fd)) {
		char *p;
		char* line = buf;
		char *buf_back = buf + sizeof(buf) - 1;
		/* detect and skip BOM */
		if (buf[0] == (char)0xEF && buf[1] == (char)0xBB && buf[2] == (char)0xBF && !(list->state & NotFirstLine))
			line += 3;
		list->state |= NotFirstLine;
		for (p = line; p < buf_back && *p && *p != '\r' && *p != '\n'; p++);
		*p = 0;
		if (*line == '\0') continue; /* skip empty lines */
		file_init(&list->current_file, line, 0);
		return 1;
	}
	return 0;
}

#ifdef __cplusplus
} /* extern "C" */
#endif /* __cplusplus */

