/*
 *
 * (C) COPYRIGHT 2008-2013 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.h>
#include <linux/ump-ioctl.h>

#include <linux/version.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/mm.h> /* memory mananger definitions */
#include <linux/pfn.h>
#include <linux/highmem.h> /*kmap*/

#include <linux/compat.h> /* is_compat_task */

#include <common/ump_kernel_core.h>
#include <ump_arch.h>
#include <common/ump_kernel_priv.h>

static void umpp_vm_close(struct vm_area_struct *vma)
{
	umpp_cpu_mapping * mapping;
	umpp_session * session;
	ump_dd_handle handle;

	mapping = (umpp_cpu_mapping*)vma->vm_private_data;
	UMP_ASSERT(mapping);
	
	session = mapping->session;
	handle = mapping->handle;

	umpp_dd_remove_cpu_mapping(mapping->handle, mapping); /* will free the mapping object */
	ump_dd_release(handle);
}


static const struct vm_operations_struct umpp_vm_ops = {
	.close = umpp_vm_close
};

int umpp_phys_commit(umpp_allocation * alloc)
{
	uint64_t i;

	/* round up to a page boundary */
	alloc->size = (alloc->size + PAGE_SIZE - 1) & ~((uint64_t)PAGE_SIZE-1) ;
	/* calculate number of pages */
	alloc->blocksCount = alloc->size >> PAGE_SHIFT;

	if( (sizeof(ump_dd_physical_block_64) * alloc->blocksCount) > ((size_t)-1))
	{
		printk(KERN_WARNING "UMP: umpp_phys_commit - trying to allocate more than possible\n");
		return -ENOMEM;
	}

	alloc->block_array = kmalloc(sizeof(ump_dd_physical_block_64) * alloc->blocksCount, __GFP_HARDWALL | GFP_KERNEL | __GFP_NORETRY | __GFP_NOWARN);
	if (NULL == alloc->block_array)
	{
		return -ENOMEM;
	}

	for (i = 0; i < alloc->blocksCount; i++)
	{
		void * mp;
		struct page * page = alloc_page(GFP_HIGHUSER | __GFP_NORETRY | __GFP_NOWARN | __GFP_COLD);
		if (NULL == page)
		{
			break;
		}

		alloc->block_array[i].addr = page_to_pfn(page) << PAGE_SHIFT;
		alloc->block_array[i].size = PAGE_SIZE;

		mp = kmap(page);
		if (NULL == mp)
		{
			__free_page(page);
			break;
		}

		memset(mp, 0x00, PAGE_SIZE); /* instead of __GFP_ZERO, so we can do cache maintenance */
		ump_sync_to_memory(PFN_PHYS(page_to_pfn(page)), mp, PAGE_SIZE);
		kunmap(page);
	}

	if (i == alloc->blocksCount)
	{
		return 0;
	}
	else
	{
		uint64_t j;
		for (j = 0; j < i; j++)
		{
			struct page * page;
			page = pfn_to_page(alloc->block_array[j].addr >> PAGE_SHIFT);
			__free_page(page);
		}
		
		kfree(alloc->block_array);

		return -ENOMEM;
	}
}

void umpp_phys_free(umpp_allocation * alloc)
{
	uint64_t i;

	for (i = 0; i < alloc->blocksCount; i++)
	{
		__free_page(pfn_to_page(alloc->block_array[i].addr >> PAGE_SHIFT));
	}

	kfree(alloc->block_array);
}

int umpp_linux_mmap(struct file * filp, struct vm_area_struct * vma)
{
	ump_secure_id id;
	ump_dd_handle h;
	size_t offset;
	int err = -EINVAL;
	size_t length = vma->vm_end - vma->vm_start;

	umpp_cpu_mapping * map = NULL;
	umpp_session *session = filp->private_data;

	if ( 0 == length )
	{
		return -EINVAL;
	}

	map = kzalloc(sizeof(*map), GFP_KERNEL);
	if (NULL == map)
	{
		WARN_ON(1);
		err = -ENOMEM;
		goto out;
	}

	/* unpack our arg */
#if defined CONFIG_64BIT && CONFIG_64BIT
	if (is_compat_task())
	{
#endif
		id = vma->vm_pgoff >> UMP_LINUX_OFFSET_BITS_32;
		offset = vma->vm_pgoff & UMP_LINUX_OFFSET_MASK_32;
#if defined CONFIG_64BIT && CONFIG_64BIT
	}
	else
	{
		id = vma->vm_pgoff >> UMP_LINUX_OFFSET_BITS_64;
		offset = vma->vm_pgoff & UMP_LINUX_OFFSET_MASK_64;
	}
#endif

	h = ump_dd_from_secure_id(id);
	if (UMP_DD_INVALID_MEMORY_HANDLE != h)
	{
		uint64_t i;
		uint64_t block_idx;
		uint64_t block_offset;
		uint64_t paddr;
		umpp_allocation * alloc;
		uint64_t last_byte;

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0))
		vma->vm_flags |= VM_DONTCOPY | VM_DONTEXPAND | VM_IO | VM_MIXEDMAP | VM_DONTDUMP;
#else
		vma->vm_flags |= VM_DONTCOPY | VM_DONTEXPAND | VM_RESERVED | VM_IO | VM_MIXEDMAP;
#endif
		vma->vm_ops = &umpp_vm_ops;
		vma->vm_private_data = map;

		alloc = (umpp_allocation*)h;

		if( (alloc->flags & UMP_CONSTRAINT_UNCACHED) != 0)
		{
			/* cache disabled flag set, disable caching for cpu mappings */
			vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
		}

		last_byte = length + (offset << PAGE_SHIFT) - 1;
		if (last_byte >= alloc->size || last_byte < (offset << PAGE_SHIFT))
		{
			goto err_out;
		}

		if (umpp_dd_find_start_block(alloc, offset << PAGE_SHIFT, &block_idx, &block_offset))
		{
			goto err_out;
		}

		paddr = alloc->block_array[block_idx].addr + block_offset;

		for (i = 0; i < (length >> PAGE_SHIFT); i++)
		{
			/* check if we've overrrun the current block, if so move to the next block */
			if (paddr >= (alloc->block_array[block_idx].addr + alloc->block_array[block_idx].size))
			{
				block_idx++;
				UMP_ASSERT(block_idx < alloc->blocksCount);
				paddr = alloc->block_array[block_idx].addr;
			}

			err = vm_insert_mixed(vma, vma->vm_start + (i << PAGE_SHIFT), paddr >> PAGE_SHIFT);
			paddr += PAGE_SIZE;
		}

		map->vaddr_start = (void*)vma->vm_start;
		map->nr_pages = length >> PAGE_SHIFT;
		map->page_off = offset;
		map->handle = h;
		map->session = session;

		umpp_dd_add_cpu_mapping(h, map);

		return 0;

		err_out:

		ump_dd_release(h);
	}

	kfree(map);

out:

	return err;
}

