/*
 * io-portability.c:  Optional filename mangling to try to cope with
 *			badly-written non-portable windows apps
 *
 * Author:
 *	Dick Porter (dick@ximian.com)
 *
 * Copyright (c) 2006 Novell, Inc.
 */

#include <config.h>
#include <glib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <dirent.h>
#include <utime.h>
#include <sys/stat.h>

#include <mono/io-layer/mono-mutex.h>
#include <mono/io-layer/io-portability.h>

#undef DEBUG

enum {
	PORTABILITY_NONE	= 0x00,
	PORTABILITY_UNKNOWN	= 0x01,
	PORTABILITY_DRIVE	= 0x02,
	PORTABILITY_CASE	= 0x04,
};

static mono_once_t options_once = MONO_ONCE_INIT;
static int portability_helpers = PORTABILITY_UNKNOWN;

static void options_init (void)
{
	const gchar *env;
	
	portability_helpers = PORTABILITY_NONE;
	
	env = g_getenv ("MONO_IOMAP");
	if (env != NULL) {
		/* parse the environment setting and set up some vars
		 * here
		 */
		gchar **options = g_strsplit (env, ":", 0);
		int i;
		
		if (options == NULL) {
			/* This shouldn't happen */
			return;
		}
		
		for (i = 0; options[i] != NULL; i++) {
#ifdef DEBUG
			g_message ("%s: Setting option [%s]", __func__,
				   options[i]);
#endif
			if (!strncasecmp (options[i], "drive", 5)) {
				portability_helpers |= PORTABILITY_DRIVE;
			} else if (!strncasecmp (options[i], "case", 4)) {
				portability_helpers |= PORTABILITY_CASE;
			} else if (!strncasecmp (options[i], "all", 3)) {
				portability_helpers |= (PORTABILITY_DRIVE |
							PORTABILITY_CASE);
			}
		}
	}
}

/* Returns newly allocated string, or NULL on failure */
static gchar *find_in_dir (DIR *current, const gchar *name)
{
	struct dirent *entry;

#ifdef DEBUG
	g_message ("%s: looking for [%s]\n", __func__, name);
#endif
	
	while((entry = readdir (current)) != NULL) {
#ifdef DEBUGX
		g_message ("%s: found [%s]\n", __func__, entry->d_name);
#endif
		
		if (!g_ascii_strcasecmp (name, entry->d_name)) {
#ifdef DEBUG
			g_message ("%s: matched [%s] to [%s]\n", __func__,
				   entry->d_name, name);
#endif
			
			closedir (current);
			return(g_strdup (entry->d_name));
		}
	}
	
#ifdef DEBUG
	g_message ("%s: returning NULL\n", __func__);
#endif
	
	closedir (current);
	
	return(NULL);
}

/* Returns newly-allocated string or NULL on failure */
static gchar *find_file (const gchar *pathname, gboolean last_exists)
{
	gchar *new_pathname, **components, **new_components;
	int num_components = 0, component = 0;
	DIR *scanning;
	
	mono_once (&options_once, options_init);

	if (portability_helpers == PORTABILITY_NONE) {
		return(NULL);
	}

	new_pathname = g_strdup (pathname);
	
#ifdef DEBUG
	g_message ("%s: Finding [%s] last_exists: %s\n", __func__, pathname,
		   last_exists?"TRUE":"FALSE");
#endif
	
	if (last_exists &&
	    access (new_pathname, F_OK) == 0) {
#ifdef DEBUG
		g_message ("%s: Found it without doing anything\n", __func__);
#endif
		return(new_pathname);
	}
	
	/* First turn '\' into '/' and strip any drive letters */
	g_strdelimit (new_pathname, "\\", '/');

#ifdef DEBUG
	g_message ("%s: Fixed slashes, now have [%s]\n", __func__,
		   new_pathname);
#endif
	
	if (portability_helpers & PORTABILITY_DRIVE &&
	    g_ascii_isalpha (new_pathname[0]) &&
	    (new_pathname[1] == ':')) {
		int len = strlen (new_pathname);
		
		g_memmove (new_pathname, new_pathname+2, len - 2);
		new_pathname[len - 2] = '\0';
		
#ifdef DEBUG
		g_message ("%s: Stripped drive letter, now looking for [%s]\n",
			   __func__, new_pathname);
#endif
	}
	
	if (last_exists &&
	    access (new_pathname, F_OK) == 0) {
#ifdef DEBUG
		g_message ("%s: Found it\n", __func__);
#endif
		
		return(new_pathname);
	}

	/* OK, have to work harder.  Take each path component in turn
	 * and do a case-insensitive directory scan for it
	 */

	if (!(portability_helpers & PORTABILITY_CASE)) {
		g_free (new_pathname);
		return(NULL);
	}

	components = g_strsplit (new_pathname, "/", 0);
	if (components == NULL) {
		/* This shouldn't happen */
		g_free (new_pathname);
		return(NULL);
	}
	
	while(components[num_components] != NULL) {
		num_components++;
	}
	g_assert (num_components > 0);
	
	g_free (new_pathname);

	new_components = (gchar **)g_new0 (gchar **, num_components + 1);

	if (num_components > 1) {
		if (strcmp (components[0], "") == 0) {
			/* first component blank, so start at / */
			scanning = opendir ("/");
			if (scanning == NULL) {
#ifdef DEBUG
				g_message ("%s: opendir 1 error: %s", __func__,
					   g_strerror (errno));
#endif
				g_strfreev (new_components);
				g_strfreev (components);
				return(NULL);
			}
		
			new_components[component++] = g_strdup ("");
		} else {
			DIR *current;
			gchar *entry;
		
			current = opendir (".");
			if (current == NULL) {
#ifdef DEBUG
				g_message ("%s: opendir 2 error: %s", __func__,
					   g_strerror (errno));
#endif
				g_strfreev (new_components);
				g_strfreev (components);
				return(NULL);
			}
		
			entry = find_in_dir (current, components[0]);
			if (entry == NULL) {
				g_strfreev (new_components);
				g_strfreev (components);
				return(NULL);
			}
		
			scanning = opendir (entry);
			if (scanning == NULL) {
#ifdef DEBUG
				g_message ("%s: opendir 3 error: %s", __func__,
					   g_strerror (errno));
#endif
				g_free (entry);
				g_strfreev (new_components);
				g_strfreev (components);
				return(NULL);
			}
		
			new_components[component++] = entry;
		}
	} else {
		if (last_exists) {
			if (strcmp (components[0], "") == 0) {
				/* First and only component blank */
				new_components[component++] = g_strdup ("");
			} else {
				DIR *current;
				gchar *entry;
				
				current = opendir (".");
				if (current == NULL) {
#ifdef DEBUG
					g_message ("%s: opendir 4 error: %s",
						   __func__,
						   g_strerror (errno));
#endif
					g_strfreev (new_components);
					g_strfreev (components);
					return(NULL);
				}
				
				entry = find_in_dir (current, components[0]);
				if (entry == NULL) {
					g_strfreev (new_components);
					g_strfreev (components);
					return(NULL);
				}
				
				new_components[component++] = entry;
			}
		} else {
				new_components[component++] = g_strdup (components[0]);
		}
	}

#ifdef DEBUG
	g_message ("%s: Got first entry: [%s]\n", __func__, new_components[0]);
#endif

	g_assert (component == 1);
	
	for(; component < num_components; component++) {
		gchar *entry;
		gchar *path_so_far;
		
		if (!last_exists &&
		    component == num_components -1) {
			entry = g_strdup (components[component]);
			closedir (scanning);
		} else {
			entry = find_in_dir (scanning, components[component]);
			if (entry == NULL) {
				g_strfreev (new_components);
				g_strfreev (components);
				return(NULL);
			}
		}
		
		new_components[component] = entry;
		
		if (component < num_components -1) {
			path_so_far = g_strjoinv ("/", new_components);

			scanning = opendir (path_so_far);
			g_free (path_so_far);
			if (scanning == NULL) {
				g_strfreev (new_components);
				g_strfreev (components);
				return(NULL);
			}
		}
	}
	
	g_strfreev (components);

	new_pathname = g_strjoinv ("/", new_components);

#ifdef DEBUG
	g_message ("%s: pathname [%s] became [%s]\n", __func__, pathname,
		   new_pathname);
#endif
	
	g_strfreev (new_components);

	if ((last_exists &&
	     access (new_pathname, F_OK) == 0) ||
	    (!last_exists)) {
		return(new_pathname);
	}
	
	g_free (new_pathname);
	return(NULL);
}

int _wapi_open (const char *pathname, int flags, mode_t mode)
{
	int fd;
	gchar *located_filename;
	
	if (flags & O_CREAT) {
		located_filename = find_file (pathname, FALSE);
		if (located_filename == NULL) {
			fd = open (pathname, flags, mode);
		} else {
			fd = open (located_filename, flags, mode);
			g_free (located_filename);
		}
	} else {
		fd = open (pathname, flags, mode);
		if (fd == -1 &&
		    (errno == ENOENT || errno == ENOTDIR) &&
		    portability_helpers > 0) {
			int saved_errno = errno;
			located_filename = find_file (pathname, TRUE);
			
			if (located_filename == NULL) {
				errno = saved_errno;
				return (-1);
			}
			
			fd = open (located_filename, flags, mode);
			g_free (located_filename);
		}
	}
	
	
	return(fd);
}

int _wapi_access (const char *pathname, int mode)
{
	int ret;
	
	ret = access (pathname, mode);
	if (ret == -1 &&
	    (errno == ENOENT || errno == ENOTDIR) &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (pathname, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = access (located_filename, mode);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_chmod (const char *pathname, mode_t mode)
{
	int ret;
	
	ret = chmod (pathname, mode);
	if (ret == -1 &&
	    (errno == ENOENT || errno == ENOTDIR) &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (pathname, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = chmod (located_filename, mode);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_utime (const char *filename, const struct utimbuf *buf)
{
	int ret;
	
	ret = utime (filename, buf);
	if (ret == -1 &&
	    errno == ENOENT &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (filename, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = utime (located_filename, buf);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_unlink (const char *pathname)
{
	int ret;
	
	ret = unlink (pathname);
	if (ret == -1 &&
	    (errno == ENOENT || errno == ENOTDIR || errno == EISDIR) &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (pathname, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = unlink (located_filename);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_rename (const char *oldpath, const char *newpath)
{
	int ret;
	gchar *located_newpath = find_file (newpath, FALSE);
	
	if (located_newpath == NULL) {
		ret = rename (oldpath, newpath);
	} else {
		ret = rename (oldpath, located_newpath);
	
		if (ret == -1 &&
		    (errno == EISDIR || errno == ENAMETOOLONG ||
		     errno == ENOENT || errno == ENOTDIR || errno == EXDEV) &&
		    portability_helpers > 0) {
			int saved_errno = errno;
			gchar *located_oldpath = find_file (oldpath, TRUE);
			
			if (located_oldpath == NULL) {
				g_free (located_oldpath);
				g_free (located_newpath);
			
				errno = saved_errno;
				return(-1);
			}
			
			ret = rename (located_oldpath, located_newpath);
			g_free (located_oldpath);
		}
		g_free (located_newpath);
	}
	
	return(ret);
}

int _wapi_stat (const char *path, struct stat *buf)
{
	int ret;
	
	ret = stat (path, buf);
	if (ret == -1 &&
	    (errno == ENOENT || errno == ENOTDIR) &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (path, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = stat (located_filename, buf);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_lstat (const char *path, struct stat *buf)
{
	int ret;
	
	ret = lstat (path, buf);
	if (ret == -1 &&
	    (errno == ENOENT || errno == ENOTDIR) &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (path, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = lstat (located_filename, buf);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_mkdir (const char *pathname, mode_t mode)
{
	int ret;
	gchar *located_filename = find_file (pathname, FALSE);
	
	if (located_filename == NULL) {
		ret = mkdir (pathname, mode);
	} else {
		ret = mkdir (located_filename, mode);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_rmdir (const char *pathname)
{
	int ret;
	
	ret = rmdir (pathname);
	if (ret == -1 &&
	    (errno == ENOENT || errno == ENOTDIR || errno == ENAMETOOLONG) &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (pathname, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = rmdir (located_filename);
		g_free (located_filename);
	}
	
	return(ret);
}

int _wapi_chdir (const char *path)
{
	int ret;
	
	ret = chdir (path);
	if (ret == -1 &&
	    (errno == ENOENT || errno == ENOTDIR || errno == ENAMETOOLONG) &&
	    portability_helpers > 0) {
		int saved_errno = errno;
		gchar *located_filename = find_file (path, TRUE);
		
		if (located_filename == NULL) {
			errno = saved_errno;
			return(-1);
		}
		
		ret = chdir (located_filename);
		g_free (located_filename);
	}
	
	return(ret);
}

gchar *_wapi_basename (const gchar *filename)
{
	gchar *new_filename = g_strdup (filename), *ret;

	mono_once (&options_once, options_init);
	
	g_strdelimit (new_filename, "\\", '/');

	if (portability_helpers & PORTABILITY_DRIVE &&
	    g_ascii_isalpha (new_filename[0]) &&
	    (new_filename[1] == ':')) {
		int len = strlen (new_filename);
		
		g_memmove (new_filename, new_filename + 2, len - 2);
		new_filename[len - 2] = '\0';
	}
	
	ret = g_path_get_basename (new_filename);
	g_free (new_filename);
	
	return(ret);
}

gchar *_wapi_dirname (const gchar *filename)
{
	gchar *new_filename = g_strdup (filename), *ret;

	mono_once (&options_once, options_init);
	
	g_strdelimit (new_filename, "\\", '/');

	if (portability_helpers & PORTABILITY_DRIVE &&
	    g_ascii_isalpha (new_filename[0]) &&
	    (new_filename[1] == ':')) {
		int len = strlen (new_filename);
		
		g_memmove (new_filename, new_filename + 2, len - 2);
		new_filename[len - 2] = '\0';
	}
	
	ret = g_path_get_dirname (new_filename);
	g_free (new_filename);
	
	return(ret);
}

GDir *_wapi_g_dir_open (const gchar *path, guint flags, GError **error)
{
	GDir *ret;
	
	ret = g_dir_open (path, flags, error);
	if (ret == NULL &&
	    ((*error)->code == G_FILE_ERROR_NOENT ||
	     (*error)->code == G_FILE_ERROR_NOTDIR ||
	     (*error)->code == G_FILE_ERROR_NAMETOOLONG) &&
	    portability_helpers > 0) {
		gchar *located_filename = find_file (path, TRUE);
		GError *tmp_error;
		
		if (located_filename == NULL) {
			return(NULL);
		}
		
		ret = g_dir_open (located_filename, flags, &tmp_error);
		g_free (located_filename);
		if (tmp_error != NULL) {
			g_propagate_error (error, tmp_error);
		}
	}
	
	return(ret);
}
