/**
 * \file
 * File mmap internal calls
 *
 * Author:
 *	Rodrigo Kumpera
 *
 * Copyright 2014 Xamarin Inc (http://www.xamarin.com)
 * Licensed under the MIT license. See LICENSE file in the project root for full license information.
 */

#include <config.h>

#ifndef HOST_WIN32

#include <glib.h>
#include <string.h>
#include <errno.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#if HAVE_SYS_MMAN_H
#include <sys/mman.h>
#endif

#include <fcntl.h>


#include <mono/metadata/object.h>
#include <mono/metadata/w32file.h>
#include <mono/metadata/file-mmap.h>
#include <mono/utils/atomic.h>
#include <mono/utils/mono-memory-model.h>
#include <mono/utils/mono-mmap.h>
#include <mono/utils/mono-coop-mutex.h>
#include <mono/utils/mono-threads.h>

typedef struct {
	int kind;
	int ref_count;
	size_t capacity;
	char *name;
	int fd;
} MmapHandle;

typedef struct {
	void *address;
	void *free_handle;
	size_t length;
} MmapInstance;

enum {
	BAD_CAPACITY_FOR_FILE_BACKED = 1,
	CAPACITY_SMALLER_THAN_FILE_SIZE,
	FILE_NOT_FOUND,
	FILE_ALREADY_EXISTS,
	PATH_TOO_LONG,
	COULD_NOT_OPEN,
	CAPACITY_MUST_BE_POSITIVE,
	INVALID_FILE_MODE,
	COULD_NOT_MAP_MEMORY,
	ACCESS_DENIED,
	CAPACITY_LARGER_THAN_LOGICAL_ADDRESS_SPACE
};

enum {
	FILE_MODE_CREATE_NEW = 1,
	FILE_MODE_CREATE = 2,
	FILE_MODE_OPEN = 3,
	FILE_MODE_OPEN_OR_CREATE = 4,
	FILE_MODE_TRUNCATE = 5,
	FILE_MODE_APPEND = 6,
};

enum {
	MMAP_FILE_ACCESS_READ_WRITE = 0,
	MMAP_FILE_ACCESS_READ = 1,
	MMAP_FILE_ACCESS_WRITE = 2,
	MMAP_FILE_ACCESS_COPY_ON_WRITE = 3,
	MMAP_FILE_ACCESS_READ_EXECUTE = 4,
	MMAP_FILE_ACCESS_READ_WRITE_EXECUTE = 5,
};

#ifdef DEFFILEMODE
#define DEFAULT_FILEMODE DEFFILEMODE
#else
#define DEFAULT_FILEMODE 0666
#endif

static int mmap_init_state;
static MonoCoopMutex named_regions_mutex;
static GHashTable *named_regions;


static gint64
align_up_to_page_size (gint64 size)
{
	gint64 page_size = mono_pagesize ();
	return (size + page_size - 1) & ~(page_size - 1);
}

static gint64
align_down_to_page_size (gint64 size)
{
	gint64 page_size = mono_pagesize ();
	return size & ~(page_size - 1);
}

static void
file_mmap_init (void)
{
retry:	
	switch (mmap_init_state) {
	case  0:
		if (mono_atomic_cas_i32 (&mmap_init_state, 1, 0) != 0)
			goto retry;
		named_regions = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);
		mono_coop_mutex_init (&named_regions_mutex);

		mono_atomic_store_release (&mmap_init_state, 2);
		break;

	case 1:
		do {
			mono_thread_info_sleep (1, NULL); /* Been init'd by other threads, this is very rare. */
		} while (mmap_init_state != 2);
		break;
	case 2:
		break;
	default:
		g_error ("Invalid init state %d", mmap_init_state);
	}
}

static void
named_regions_lock (void)
{
	file_mmap_init ();
	mono_coop_mutex_lock (&named_regions_mutex);
}

static void
named_regions_unlock (void)
{
	mono_coop_mutex_unlock (&named_regions_mutex);
}


static int
file_mode_to_unix (int mode)
{
	switch (mode) {
	case FILE_MODE_CREATE_NEW:
        return O_CREAT | O_EXCL; 
	case FILE_MODE_CREATE:
        return O_CREAT | O_TRUNC;
	case FILE_MODE_OPEN:
		return 0;
	case FILE_MODE_OPEN_OR_CREATE:
        return O_CREAT;
	case FILE_MODE_TRUNCATE:
        return O_TRUNC;
	case FILE_MODE_APPEND:
		return O_APPEND;
	default:
		g_error ("unknown FileMode %d", mode);
	}
}

static int
access_mode_to_unix (int access)
{
	switch (access) {
	case MMAP_FILE_ACCESS_READ_WRITE:
	case MMAP_FILE_ACCESS_COPY_ON_WRITE:
	case MMAP_FILE_ACCESS_READ_WRITE_EXECUTE:
		return O_RDWR;
	case MMAP_FILE_ACCESS_READ:
	case MMAP_FILE_ACCESS_READ_EXECUTE:
		return O_RDONLY;
	case MMAP_FILE_ACCESS_WRITE:
		return O_WRONLY;
	default:
		g_error ("unknown MemoryMappedFileAccess %d", access);
	}
}

static int
acess_to_mmap_flags (int access)
{
	switch (access) {
	case MMAP_FILE_ACCESS_READ_WRITE:
        return MONO_MMAP_WRITE | MONO_MMAP_READ | MONO_MMAP_SHARED;
        
	case MMAP_FILE_ACCESS_WRITE:
        return MONO_MMAP_WRITE | MONO_MMAP_SHARED;
        
	case MMAP_FILE_ACCESS_COPY_ON_WRITE:
        return MONO_MMAP_WRITE | MONO_MMAP_READ | MONO_MMAP_PRIVATE;
        
	case MMAP_FILE_ACCESS_READ_EXECUTE:
        return MONO_MMAP_EXEC | MONO_MMAP_PRIVATE | MONO_MMAP_SHARED;
        
	case MMAP_FILE_ACCESS_READ_WRITE_EXECUTE:
        return MONO_MMAP_WRITE | MONO_MMAP_READ | MONO_MMAP_EXEC | MONO_MMAP_SHARED;
        
	case MMAP_FILE_ACCESS_READ:
        return MONO_MMAP_READ | MONO_MMAP_SHARED;
	default:
		g_error ("unknown MemoryMappedFileAccess %d", access);
	}
}

/*
This allow us to special case zero size files that can be arbitrarily mapped.
*/
static gboolean
is_special_zero_size_file (struct stat *buf)
{
	return buf->st_size == 0 && (buf->st_mode & (S_IFCHR | S_IFBLK | S_IFIFO | S_IFSOCK)) != 0;
}

/*
XXX implement options
*/
static void*
open_file_map (const char *c_path, int input_fd, int mode, gint64 *capacity, int access, int options, int *ioerror)
{
	struct stat buf;
	MmapHandle *handle = NULL;
	int result, fd;

	if (c_path)
		result = stat (c_path, &buf);
	else
		result = fstat (input_fd, &buf);

	if (mode == FILE_MODE_TRUNCATE || mode == FILE_MODE_APPEND || mode == FILE_MODE_OPEN) {
		if (result == -1) { //XXX translate errno?
			*ioerror = FILE_NOT_FOUND;
			goto done;
		}
	}

	if (mode == FILE_MODE_CREATE_NEW && result == 0) {
		*ioerror = FILE_ALREADY_EXISTS;
		goto done;
	}

	if (result == 0) {
		if (*capacity == 0) {
			/**
			 * Special files such as FIFOs, sockets, and devices can have a size of 0. Specifying a capacity for these
			 * also makes little sense, so don't do the check if th file is one of these.
			 */
			if (buf.st_size == 0 && !is_special_zero_size_file (&buf)) {
				*ioerror = CAPACITY_SMALLER_THAN_FILE_SIZE;
				goto done;
			}
			*capacity = buf.st_size;
		} else if (*capacity < buf.st_size) {
			*ioerror = CAPACITY_SMALLER_THAN_FILE_SIZE;
			goto done;
		}
	} else {
		if (mode == FILE_MODE_CREATE_NEW && *capacity == 0) {
			*ioerror = CAPACITY_SMALLER_THAN_FILE_SIZE;
			goto done;
		}
	}

	if (c_path) //FIXME use io portability?
		fd = open (c_path, file_mode_to_unix (mode) | access_mode_to_unix (access), DEFAULT_FILEMODE);
	else
		fd = dup (input_fd);

	if (fd == -1) { //XXX translate errno?
		*ioerror = COULD_NOT_OPEN;
		goto done;
	}

	if (result != 0 || *capacity > buf.st_size) {
		int unused G_GNUC_UNUSED = ftruncate (fd, (off_t)*capacity);
	}

	handle = g_new0 (MmapHandle, 1);
	handle->ref_count = 1;
	handle->capacity = *capacity;
	handle->fd = fd;

done:
	return (void*)handle;
}

#define MONO_ANON_FILE_TEMPLATE "/mono.anonmap.XXXXXXXXX"
static void*
open_memory_map (const char *c_mapName, int mode, gint64 *capacity, int access, int options, int *ioerror)
{
	MmapHandle *handle;
	if (*capacity <= 0 && mode != FILE_MODE_OPEN) {
		*ioerror = CAPACITY_MUST_BE_POSITIVE;
		return NULL;
	}
#if SIZEOF_VOID_P == 4
	if (*capacity > UINT32_MAX) {
		*ioerror = CAPACITY_LARGER_THAN_LOGICAL_ADDRESS_SPACE;
		return NULL;
	}
#endif

	if (!(mode == FILE_MODE_CREATE_NEW || mode == FILE_MODE_OPEN_OR_CREATE || mode == FILE_MODE_OPEN)) {
		*ioerror = INVALID_FILE_MODE;
		return NULL;
	}

	named_regions_lock ();
	handle = (MmapHandle*)g_hash_table_lookup (named_regions, c_mapName);
	if (handle) {
		if (mode == FILE_MODE_CREATE_NEW) {
			*ioerror = FILE_ALREADY_EXISTS;
			goto done;
		}

		handle->ref_count++;
		//XXX should we ftruncate if the file is smaller than capacity?
	} else {
		int fd;
		char *file_name;
		const char *tmp_dir;
		int unused G_GNUC_UNUSED, alloc_size;

		if (mode == FILE_MODE_OPEN) {
			*ioerror = FILE_NOT_FOUND;
			goto done;
		}
		*capacity = align_up_to_page_size (*capacity);

		tmp_dir = g_get_tmp_dir ();
		alloc_size = strlen (tmp_dir) + strlen (MONO_ANON_FILE_TEMPLATE) + 1;
		if (alloc_size > 1024) {//rather fail that stack overflow
			*ioerror = COULD_NOT_MAP_MEMORY;
			goto done;
		}
		file_name = (char *)alloca (alloc_size);
		strcpy (file_name, tmp_dir);
		strcat (file_name, MONO_ANON_FILE_TEMPLATE);

		fd = mkstemp (file_name);
		if (fd == -1) {
			*ioerror = COULD_NOT_MAP_MEMORY;
			goto done;
		}

		unlink (file_name);
		unused = ftruncate (fd, (off_t)*capacity);

		handle = g_new0 (MmapHandle, 1);
		handle->ref_count = 1;
		handle->capacity = *capacity;
		handle->fd = fd;
		handle->name = g_strdup (c_mapName);

		g_hash_table_insert (named_regions, handle->name, handle);

	}

done:
	named_regions_unlock ();

	return handle;
}


/* This is an icall */
void *
mono_mmap_open_file (const gunichar2 *path, gint path_length, int mode, const gunichar2 *mapName, gint mapName_length, gint64 *capacity, int access, int options, int *ioerror, MonoError *error)
{
	MmapHandle *handle = NULL;
	g_assert (path || mapName);

	if (!mapName) {
		char * c_path = mono_utf16_to_utf8 (path, path_length, error);
		return_val_if_nok (error, NULL);
		handle = open_file_map (c_path, -1, mode, capacity, access, options, ioerror);
		g_free (c_path);
		return handle;
	}

	char *c_mapName = mono_utf16_to_utf8 (mapName, mapName_length, error);
	return_val_if_nok (error, NULL);

	if (path) {
		named_regions_lock ();
		handle = (MmapHandle*)g_hash_table_lookup (named_regions, c_mapName);
		if (handle) {
			*ioerror = FILE_ALREADY_EXISTS;
			handle = NULL;
		} else {
			char *c_path = mono_utf16_to_utf8 (path, path_length, error);
			if (is_ok (error)) {
				handle = (MmapHandle *)open_file_map (c_path, -1, mode, capacity, access, options, ioerror);
				if (handle) {
					handle->name = g_strdup (c_mapName);
					g_hash_table_insert (named_regions, handle->name, handle);
				}
			} else {
				handle = NULL;
			}
			g_free (c_path);
		}
		named_regions_unlock ();
	} else
		handle = open_memory_map (c_mapName, mode, capacity, access, options, ioerror);

	g_free (c_mapName);
	return handle;
}

/* this is an icall */
void *
mono_mmap_open_handle (void *input_fd, const mono_unichar2 *mapName, gint mapName_length, gint64 *capacity, int access, int options, int *ioerror, MonoError *error)
{
	MmapHandle *handle;
	if (!mapName) {
		handle = (MmapHandle *)open_file_map (NULL, GPOINTER_TO_INT (input_fd), FILE_MODE_OPEN, capacity, access, options, ioerror);
	} else {
		char *c_mapName = mono_utf16_to_utf8 (mapName, mapName_length, error);
		return_val_if_nok (error, NULL);

		named_regions_lock ();
		handle = (MmapHandle*)g_hash_table_lookup (named_regions, c_mapName);
		if (handle) {
			*ioerror = FILE_ALREADY_EXISTS;
			handle = NULL;
		} else {
			//XXX we're exploiting wapi HANDLE == FD equivalence. THIS IS FRAGILE, create a _wapi_handle_to_fd call
			handle = (MmapHandle *)open_file_map (NULL, GPOINTER_TO_INT (input_fd), FILE_MODE_OPEN, capacity, access, options, ioerror);
			handle->name = g_strdup (c_mapName);
			g_hash_table_insert (named_regions, handle->name, handle);
		}
		named_regions_unlock ();

		g_free (c_mapName);
	}
	return handle;
}

void
mono_mmap_close (void *mmap_handle, MonoError *error)
{
	MmapHandle *handle = (MmapHandle *)mmap_handle;

	named_regions_lock ();
	--handle->ref_count;
	if (handle->ref_count == 0) {
		if (handle->name)
			g_hash_table_remove (named_regions, handle->name);

		g_free (handle->name);
		close (handle->fd);
		g_free (handle);
	}
	named_regions_unlock ();
}

void
mono_mmap_configure_inheritability (void *mmap_handle, gboolean inheritability, MonoError *error)
{
	MmapHandle *h = (MmapHandle *)mmap_handle;
	int fd, flags;

	fd = h->fd;
	flags = fcntl (fd, F_GETFD, 0);
	if (inheritability)
		flags &= ~FD_CLOEXEC;
	else
		flags |= FD_CLOEXEC;
	fcntl (fd, F_SETFD, flags);	
}

void
mono_mmap_flush (void *mmap_handle, MonoError *error)
{
	MmapInstance *h = (MmapInstance *)mmap_handle;

	if (h)
		msync (h->address, h->length, MS_SYNC);
}

int
mono_mmap_map (void *handle, gint64 offset, gint64 *size, int access, void **mmap_handle, void **base_address, MonoError *error)
{
	gint64 mmap_offset = 0;
	MmapHandle *fh = (MmapHandle *)handle;
	MmapInstance res = { 0 };
	size_t eff_size = *size;
	struct stat buf = { 0 };
	fstat (fh->fd, &buf); //FIXME error handling

	*mmap_handle = NULL;
	*base_address = NULL;

	if (offset > buf.st_size || ((eff_size + offset) > buf.st_size && !is_special_zero_size_file (&buf)))
		return ACCESS_DENIED;
	/**
	  * We use the file size if one of the following conditions is true:
	  *  -input size is zero
	  *  -input size is bigger than the file and the file is not a magical zero size file such as /dev/mem.
	  */
	if (eff_size == 0)
		eff_size = align_up_to_page_size (buf.st_size) - offset;
	*size = eff_size;

	mmap_offset = align_down_to_page_size (offset);
	eff_size += (offset - mmap_offset);
	//FIXME translate some interesting errno values
	res.address = mono_file_map ((size_t)eff_size, acess_to_mmap_flags (access), fh->fd, mmap_offset, &res.free_handle);
	res.length = eff_size;

	if (res.address) {
		*mmap_handle = g_memdup (&res, sizeof (MmapInstance));
		*base_address = (char*)res.address + (offset - mmap_offset);
		return 0;
	}

	return COULD_NOT_MAP_MEMORY;
}

gboolean
mono_mmap_unmap (void *mmap_handle, MonoError *error)
{
	int res = 0;
	MmapInstance *h = (MmapInstance *)mmap_handle;

	res = mono_file_unmap (h->address, h->free_handle);

	g_free (h);
	return res == 0;
}

#endif
