/*
 *
 * (C) COPYRIGHT 2008-2014 ARM Limited. All rights reserved.
 *
 * This program is free software and is provided to you under the terms of the
 * GNU General Public License version 2 as published by the Free Software
 * Foundation, and any use by you of this program is subject to the terms
 * of such GNU licence.
 *
 * A copy of the licence is included with the program, and can also be obtained
 * from Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 *
 */





#include <linux/ump-ioctl.h>
#include <linux/ump.h>

#include <asm/uaccess.h>	         /* copy_*_user */
#include <linux/compat.h>
#include <linux/module.h>            /* kernel module definitions */
#include <linux/fs.h>                /* file system operations */
#include <linux/cdev.h>              /* character device definitions */
#include <linux/ioport.h>            /* request_mem_region */
#include <linux/device.h>            /* class registration support */

#include <common/ump_kernel_core.h>

#include "ump_kernel_linux_mem.h"
#include <ump_arch.h>


struct ump_linux_device
{
	struct cdev cdev;
	struct class * ump_class;
};

/* Name of the UMP device driver */
static char ump_dev_name[] = "ump"; /* should be const, but the functions we call requires non-cost */

/* Module parameter to control log level */
int ump_debug_level = 2;
module_param(ump_debug_level, int, S_IRUSR | S_IWUSR | S_IWGRP | S_IRGRP | S_IROTH); /* rw-rw-r-- */
MODULE_PARM_DESC(ump_debug_level, "Higher number, more dmesg output");

/* By default the module uses any available major, but it's possible to set it at load time to a specific number */
int ump_major = 0;
module_param(ump_major, int, S_IRUGO); /* r--r--r-- */
MODULE_PARM_DESC(ump_major, "Device major number");

#define UMP_REV_STRING "1.0"

char * ump_revision = UMP_REV_STRING;
module_param(ump_revision, charp, S_IRUGO); /* r--r--r-- */
MODULE_PARM_DESC(ump_revision, "Revision info");

static int umpp_linux_open(struct inode *inode, struct file *filp);
static int umpp_linux_release(struct inode *inode, struct file *filp);
#ifdef HAVE_UNLOCKED_IOCTL
static long umpp_linux_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
#else
static int umpp_linux_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
#endif

/* This variable defines the file operations this UMP device driver offers */
static struct file_operations ump_fops =
{
	.owner   = THIS_MODULE,
	.open    = umpp_linux_open,
	.release = umpp_linux_release,
#ifdef HAVE_UNLOCKED_IOCTL
	.unlocked_ioctl   = umpp_linux_ioctl,
#else
	.ioctl   = umpp_linux_ioctl,
#endif
	.compat_ioctl = umpp_linux_ioctl,
	.mmap = umpp_linux_mmap
};

/* import module handling */
DEFINE_MUTEX(import_list_lock);
struct ump_import_handler *  import_handlers[UMPP_EXTERNAL_MEM_COUNT];

/* The global variable containing the global device data */
static struct ump_linux_device ump_linux_device;

#define DBG_MSG(level, ...) do { \
if ((level) <=  ump_debug_level)\
{\
printk(KERN_DEBUG "UMP<" #level ">:\n" __VA_ARGS__);\
} \
} while (0)

#define MSG_ERR(...) do{ \
printk(KERN_ERR "UMP: ERR: %s\n           %s()%4d\n", __FILE__, __func__  , __LINE__) ; \
printk(KERN_ERR __VA_ARGS__); \
printk(KERN_ERR "\n"); \
} while(0)

#define MSG(...) do{ \
printk(KERN_INFO "UMP: " __VA_ARGS__);\
} while (0)

/*
 * This function is called by Linux to initialize this module.
 * All we do is initialize the UMP device driver.
 */
static int __init umpp_linux_initialize_module(void)
{
	ump_result err;

	err = umpp_core_constructor();
	if (UMP_OK != err)
	{
		MSG_ERR("UMP device driver init failed\n");
		return -ENOTTY;
	}

	MSG("UMP device driver %s loaded\n", UMP_REV_STRING);
	return 0;
}



/*
 * This function is called by Linux to unload/terminate/exit/cleanup this module.
 * All we do is terminate the UMP device driver.
 */
static void __exit umpp_linux_cleanup_module(void)
{
	DBG_MSG(2, "Unloading UMP device driver\n");
	umpp_core_destructor();
	DBG_MSG(2, "Module unloaded\n");
}



/*
 * Initialize the UMP device driver.
 */
ump_result umpp_device_initialize(void)
{
	int err;
	dev_t dev = 0;

	if (0 == ump_major)
	{
		/* auto select a major */
		err = alloc_chrdev_region(&dev, 0, 1, ump_dev_name);
		ump_major = MAJOR(dev);
	}
	else
	{
		/* use load time defined major number */
		dev = MKDEV(ump_major, 0);
		err = register_chrdev_region(dev, 1, ump_dev_name);
	}

	if (0 == err)
	{
		memset(&ump_linux_device, 0, sizeof(ump_linux_device));

		/* initialize our char dev data */
		cdev_init(&ump_linux_device.cdev, &ump_fops);
		ump_linux_device.cdev.owner = THIS_MODULE;
		ump_linux_device.cdev.ops = &ump_fops;

		/* register char dev with the kernel */
		err = cdev_add(&ump_linux_device.cdev, dev, 1/*count*/);
		if (0 == err)
		{

			ump_linux_device.ump_class = class_create(THIS_MODULE, ump_dev_name);
			if (IS_ERR(ump_linux_device.ump_class))
			{
				err = PTR_ERR(ump_linux_device.ump_class);
			}
			else
			{
				struct device * mdev;
				mdev = device_create(ump_linux_device.ump_class, NULL, dev, NULL, ump_dev_name);
				if (!IS_ERR(mdev))
				{
					return UMP_OK;
				}

				err = PTR_ERR(mdev);
				class_destroy(ump_linux_device.ump_class);
			}
			cdev_del(&ump_linux_device.cdev);
		}

		unregister_chrdev_region(dev, 1);
	}

	return UMP_ERROR;
}



/*
 * Terminate the UMP device driver
 */
void umpp_device_terminate(void)
{
	dev_t dev = MKDEV(ump_major, 0);

	device_destroy(ump_linux_device.ump_class, dev);
	class_destroy(ump_linux_device.ump_class);

	/* unregister char device */
	cdev_del(&ump_linux_device.cdev);

	/* free major */
	unregister_chrdev_region(dev, 1);
}


static int umpp_linux_open(struct inode *inode, struct file *filp)
{
	umpp_session *session;
	
	session = umpp_core_session_start();
	if (NULL == session)
	{
		return -EFAULT;
	}
	
	filp->private_data = session;

	return 0;
}

static int umpp_linux_release(struct inode *inode, struct file *filp)
{
	umpp_session *session;
	
	session = filp->private_data;

	umpp_core_session_end(session);

	filp->private_data = NULL;

	return 0;
}

/**************************/
/*ioctl specific functions*/
/**************************/
static int do_ump_dd_allocate(umpp_session * session, ump_k_allocate * params)
{
	ump_dd_handle new_allocation;
	new_allocation = ump_dd_allocate_64(params->size, params->alloc_flags, NULL, NULL, NULL);

	if (UMP_DD_INVALID_MEMORY_HANDLE != new_allocation)
	{
		umpp_session_memory_usage * tracker;

		tracker = kmalloc(sizeof(*tracker), GFP_KERNEL | __GFP_HARDWALL);
		if (NULL != tracker)
		{
			/* update the return struct with the new ID */
			params->secure_id = ump_dd_secure_id_get(new_allocation);

			tracker->mem = new_allocation;
			tracker->id = params->secure_id;
			atomic_set(&tracker->process_usage_count, 1);

			/* link it into the session in-use list */
			mutex_lock(&session->session_lock);
			list_add(&tracker->link, &session->memory_usage);
			mutex_unlock(&session->session_lock);

			return 0;
		}
		ump_dd_release(new_allocation);
	}

	printk(KERN_WARNING "UMP: Allocation FAILED\n");
	return -ENOMEM;
}

static int do_ump_dd_retain(umpp_session * session, ump_k_retain * params)
{
	umpp_session_memory_usage * it;

	mutex_lock(&session->session_lock);

	/* try to find it on the session usage list */
	list_for_each_entry(it, &session->memory_usage, link)
	{
		if (it->id == params->secure_id)
		{
			/* found to already be in use */
			/* check for overflow */
			while(1)
			{
				int refcnt = atomic_read(&it->process_usage_count);
				if (refcnt + 1 > 0)
				{
					/* add a process local ref */
					if(atomic_cmpxchg(&it->process_usage_count, refcnt, refcnt + 1) == refcnt)
					{
						mutex_unlock(&session->session_lock);
						return 0;
					}
				}
				else
				{
					/* maximum usage cap reached */
					mutex_unlock(&session->session_lock);
					return -EBUSY;
				}
			}
		}
	}
	/* try to look it up globally */

	it = kmalloc(sizeof(*it), GFP_KERNEL);

	if (NULL != it)
	{
		it->mem = ump_dd_from_secure_id(params->secure_id);
		if (UMP_DD_INVALID_MEMORY_HANDLE != it->mem)
		{
			/* found, add it to the session usage list */
			it->id = params->secure_id;
			atomic_set(&it->process_usage_count, 1);
			list_add(&it->link, &session->memory_usage);
		}
		else
		{
			/* not found */
			kfree(it);
			it = NULL;
		}
	}

	mutex_unlock(&session->session_lock);

	return (NULL != it) ? 0 : -ENODEV;
}


static int do_ump_dd_release(umpp_session * session, ump_k_release * params)
{
	umpp_session_memory_usage * it;
	int result = -ENODEV;

	mutex_lock(&session->session_lock);

	/* only do a release if found on the session list */
	list_for_each_entry(it, &session->memory_usage, link)
	{
		if (it->id == params->secure_id)
		{
			/* found, a valid call */
			result = 0;

			if (0 == atomic_sub_return(1, &it->process_usage_count))
			{
				/* last ref in this process remove from the usage list and remove the underlying ref */
				list_del(&it->link);
				ump_dd_release(it->mem);
				kfree(it);
			}

			break;
		}
	}
	mutex_unlock(&session->session_lock);

	return result;
}

static int do_ump_dd_sizequery(umpp_session * session, ump_k_sizequery * params)
{
	umpp_session_memory_usage * it;
	int result = -ENODEV;

	mutex_lock(&session->session_lock);

	/* only valid if found on the session list */
	list_for_each_entry(it, &session->memory_usage, link)
	{
		if (it->id == params->secure_id)
		{
			/* found, a valid call */
			params->size = ump_dd_size_get_64(it->mem);
			result = 0;
			break;
		}

	}
	mutex_unlock(&session->session_lock);

	return result;
}

static int do_ump_dd_allocation_flags_get(umpp_session * session, ump_k_allocation_flags * params)
{
	umpp_session_memory_usage * it;
	int result = -ENODEV;

	mutex_lock(&session->session_lock);

	/* only valid if found on the session list */
	list_for_each_entry(it, &session->memory_usage, link)
	{
		if (it->id == params->secure_id)
		{
			/* found, a valid call */
			params->alloc_flags = ump_dd_allocation_flags_get(it->mem);
			result = 0;
			break;
		}

	}
	mutex_unlock(&session->session_lock);

	return result;
}

static int do_ump_dd_msync_now(umpp_session * session, ump_k_msync * params)
{
	umpp_session_memory_usage * it;
	int result = -ENODEV;

	mutex_lock(&session->session_lock);

	/* only valid if found on the session list */
	list_for_each_entry(it, &session->memory_usage, link)
	{
		if (it->id == params->secure_id)
		{
			/* found, do the cache op */
#ifdef CONFIG_COMPAT
			if (is_compat_task())
			{
				umpp_dd_cpu_msync_now(it->mem, params->cache_operation, compat_ptr(params->mapped_ptr.compat_value), params->size);
				result = 0;
			}
			else
			{
#endif
				umpp_dd_cpu_msync_now(it->mem, params->cache_operation, params->mapped_ptr.value, params->size);
				result = 0;
#ifdef CONFIG_COMPAT
			}
#endif
			break;
		}
	}
	mutex_unlock(&session->session_lock);

	return result;
}


void umpp_import_handlers_init(umpp_session * session)
{
	int i;
	mutex_lock(&import_list_lock);
	for ( i = 1; i < UMPP_EXTERNAL_MEM_COUNT; i++ )
	{
		if (import_handlers[i])
		{
			import_handlers[i]->session_begin(&session->import_handler_data[i]);
			/* It is OK if session_begin returned an error.
			 * We won't do any import calls if so */
		}
	}
	mutex_unlock(&import_list_lock);
}

void umpp_import_handlers_term(umpp_session * session)
{
	int i;
	mutex_lock(&import_list_lock);
	for ( i = 1; i < UMPP_EXTERNAL_MEM_COUNT; i++ )
	{
		/* only call if session_begin succeeded */
		if (session->import_handler_data[i] != NULL)
		{
			/* if session_beging succeeded the handler
			 * should not have unregistered with us */
			BUG_ON(!import_handlers[i]);
			import_handlers[i]->session_end(session->import_handler_data[i]);
			session->import_handler_data[i] = NULL;
		}
	}
	mutex_unlock(&import_list_lock);
}

int ump_import_module_register(enum ump_external_memory_type type, struct ump_import_handler * handler)
{
	int res = -EEXIST;

	/* validate input */
	BUG_ON(type == 0 || type >= UMPP_EXTERNAL_MEM_COUNT);
	BUG_ON(!handler);
	BUG_ON(!handler->linux_module);
	BUG_ON(!handler->session_begin);
	BUG_ON(!handler->session_end);
	BUG_ON(!handler->import);

	mutex_lock(&import_list_lock);

	if (!import_handlers[type])
	{
		import_handlers[type] = handler;
		res = 0;
	}

	mutex_unlock(&import_list_lock);

	return res;
}

void ump_import_module_unregister(enum ump_external_memory_type type)
{
	BUG_ON(type == 0 || type >= UMPP_EXTERNAL_MEM_COUNT);

	mutex_lock(&import_list_lock);
	/* an error to call this if ump_import_module_register didn't succeed */
	BUG_ON(!import_handlers[type]);
	import_handlers[type] = NULL;
	mutex_unlock(&import_list_lock);
}

static struct ump_import_handler * import_handler_get(unsigned int type_id)
{
	enum ump_external_memory_type type;
	struct ump_import_handler * handler;

	/* validate and convert input */
	/* handle bad data here, not just BUG_ON */
	if (type_id == 0 || type_id >= UMPP_EXTERNAL_MEM_COUNT)
		return NULL;

	type = (enum ump_external_memory_type)type_id;

	/* find the handler */
	mutex_lock(&import_list_lock);

	handler = import_handlers[type];

	if (handler)
	{
		if (!try_module_get(handler->linux_module))
		{
			handler = NULL;
		}
	}

	mutex_unlock(&import_list_lock);

	return handler;
}

static void import_handler_put(struct ump_import_handler * handler)
{
	module_put(handler->linux_module);
}

static int do_ump_dd_import(umpp_session * session, ump_k_import * params)
{
	ump_dd_handle new_allocation = UMP_DD_INVALID_MEMORY_HANDLE;
	struct ump_import_handler * handler;

	handler = import_handler_get(params->type);

	if (handler)
	{
		/* try late binding if not already bound */
		if (!session->import_handler_data[params->type])
		{
			handler->session_begin(&session->import_handler_data[params->type]);
		}

		/* do we have a bound session? */
		if (session->import_handler_data[params->type])
		{
			new_allocation = handler->import( session->import_handler_data[params->type],
		                                      params->phandle.value,
		                                      params->alloc_flags);
		}

		/* done with the handler */
		import_handler_put(handler);
	}

	/* did the import succeed? */
	if (UMP_DD_INVALID_MEMORY_HANDLE != new_allocation)
	{
		umpp_session_memory_usage * tracker;

		tracker = kmalloc(sizeof(*tracker), GFP_KERNEL | __GFP_HARDWALL);
		if (NULL != tracker)
		{
			/* update the return struct with the new ID */
			params->secure_id = ump_dd_secure_id_get(new_allocation);

			tracker->mem = new_allocation;
			tracker->id = params->secure_id;
			atomic_set(&tracker->process_usage_count, 1);

			/* link it into the session in-use list */
			mutex_lock(&session->session_lock);
			list_add(&tracker->link, &session->memory_usage);
			mutex_unlock(&session->session_lock);

			return 0;
		}
		ump_dd_release(new_allocation);
	}

	return -ENOMEM;

}

#ifdef HAVE_UNLOCKED_IOCTL
static long umpp_linux_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
#else
static int umpp_linux_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
#endif
{
	int ret;
	uint64_t msg[(UMP_CALL_MAX_SIZE+7)>>3]; /* alignment fixup */
	uint32_t size = _IOC_SIZE(cmd);
	struct umpp_session *session = filp->private_data;

#ifndef HAVE_UNLOCKED_IOCTL
	(void)inode; /* unused arg */
#endif

	/*
	 * extract the type and number bitfields, and don't decode
	 * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok()
	 */
	if (_IOC_TYPE(cmd) != UMP_IOC_MAGIC)
	{
		return -ENOTTY;

	}
	if (_IOC_NR(cmd) > UMP_IOC_MAXNR)
	{
		return -ENOTTY;
	}

	switch(cmd)
	{
		case UMP_FUNC_ALLOCATE:
			if (size != sizeof(ump_k_allocate))
			{
				return -ENOTTY;
			}
			if (copy_from_user(&msg, (void __user *)arg, size))
			{
				return -EFAULT;
			}
			ret = do_ump_dd_allocate(session, (ump_k_allocate *)&msg);
			if (ret)
			{
				return ret;
			}
			if (copy_to_user((void *)arg, &msg, size))
			{
				return -EFAULT;
			}
			return 0;
		case UMP_FUNC_SIZEQUERY:
			if (size != sizeof(ump_k_sizequery))
			{
				return -ENOTTY;
			}
			if (copy_from_user(&msg, (void __user *)arg, size))
			{
				return -EFAULT;
			}
			ret = do_ump_dd_sizequery(session,(ump_k_sizequery*) &msg);
			if (ret)
			{
				return ret;
			}
			if (copy_to_user((void *)arg, &msg, size))
			{
				return -EFAULT;
			}
			return 0;
		case UMP_FUNC_MSYNC:
			if (size != sizeof(ump_k_msync))
			{
				return -ENOTTY;
			}
			if (copy_from_user(&msg, (void __user *)arg, size))
			{
				return -EFAULT;
			}
			ret = do_ump_dd_msync_now(session,(ump_k_msync*) &msg);
			if (ret)
			{
				return ret;
			}
			if (copy_to_user((void *)arg, &msg, size))
			{
				return -EFAULT;
			}
			return 0;
		case UMP_FUNC_IMPORT:
			if (size != sizeof(ump_k_import))
			{
				return -ENOTTY;
			}
			if (copy_from_user(&msg, (void __user*)arg, size))
			{
				return -EFAULT;
			}
			ret = do_ump_dd_import(session, (ump_k_import*) &msg);
			if (ret)
			{
				return ret;
			}
			if (copy_to_user((void *)arg, &msg, size))
			{
				return -EFAULT;
			}
			return 0;
		/* used only by v1 API */
		case UMP_FUNC_ALLOCATION_FLAGS_GET:
			if (size != sizeof(ump_k_allocation_flags))
			{
				return -ENOTTY;
			}
			if (copy_from_user(&msg, (void __user *)arg, size))
			{
				return -EFAULT;
			}
			ret = do_ump_dd_allocation_flags_get(session,(ump_k_allocation_flags*) &msg);
			if (ret)
			{
				return ret;
			}
			if (copy_to_user((void *)arg, &msg, size))
			{
				return -EFAULT;
			}
			return 0;
		case UMP_FUNC_RETAIN:
			if (size != sizeof(ump_k_retain))
			{
				return -ENOTTY;
			}
			if (copy_from_user(&msg, (void __user *)arg, size))
			{
				return -EFAULT;
			}
			ret = do_ump_dd_retain(session,(ump_k_retain*) &msg);
			if (ret)
			{
				return ret;
			}
			return 0;
		case UMP_FUNC_RELEASE:
			if (size != sizeof(ump_k_release))
			{
				return -ENOTTY;
			}
			if (copy_from_user(&msg, (void __user *)arg, size))
			{
				return -EFAULT;
			}
			ret = do_ump_dd_release(session,(ump_k_release*) &msg);
			if (ret)
			{
				return ret;
			}
			return 0;
		default:
			/* not ours */
			return -ENOTTY;
	}
	/*redundant below*/
	return -ENOTTY;
}


/* Export UMP kernel space API functions */
EXPORT_SYMBOL(ump_dd_allocate_64);
EXPORT_SYMBOL(ump_dd_allocation_flags_get);
EXPORT_SYMBOL(ump_dd_secure_id_get);
EXPORT_SYMBOL(ump_dd_from_secure_id);
EXPORT_SYMBOL(ump_dd_phys_blocks_get_64);
EXPORT_SYMBOL(ump_dd_size_get_64);
EXPORT_SYMBOL(ump_dd_retain);
EXPORT_SYMBOL(ump_dd_release);
EXPORT_SYMBOL(ump_dd_create_from_phys_blocks_64);
#ifdef CONFIG_KDS
EXPORT_SYMBOL(ump_dd_kds_resource_get);
#endif

/* import API */
EXPORT_SYMBOL(ump_import_module_register);
EXPORT_SYMBOL(ump_import_module_unregister);



/* V1 API */
EXPORT_SYMBOL(ump_dd_handle_create_from_secure_id);
EXPORT_SYMBOL(ump_dd_phys_block_count_get);
EXPORT_SYMBOL(ump_dd_phys_block_get);
EXPORT_SYMBOL(ump_dd_phys_blocks_get);
EXPORT_SYMBOL(ump_dd_size_get);
EXPORT_SYMBOL(ump_dd_reference_add);
EXPORT_SYMBOL(ump_dd_reference_release);
EXPORT_SYMBOL(ump_dd_handle_create_from_phys_blocks);


/* Setup init and exit functions for this module */
module_init(umpp_linux_initialize_module);
module_exit(umpp_linux_cleanup_module);

/* And some module informatio */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ARM Ltd.");
MODULE_VERSION(UMP_REV_STRING);
