/*
 * Copyright (C) 2010-2011, 2013-2014, 2016 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.
 */

/* needed to detect kernel version specific code */
#include <linux/version.h>

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,26)
#include <linux/semaphore.h>
#else /* pre 2.6.26 the file was in the arch specific location */
#include <asm/semaphore.h>
#endif

#include <linux/mm.h>
#include <linux/slab.h>
#include <asm/atomic.h>
#include <linux/vmalloc.h>
#include "ump_kernel_common.h"
#include "ump_kernel_memory_backend.h"



#define UMP_BLOCK_SIZE (256UL * 1024UL)  /* 256kB, remember to keep the ()s */



typedef struct block_info {
	struct block_info *next;
} block_info;



typedef struct block_allocator {
	struct semaphore mutex;
	block_info *all_blocks;
	block_info *first_free;
	u32 base;
	u32 num_blocks;
	u32 num_free;
} block_allocator;


static void block_allocator_shutdown(ump_memory_backend *backend);
static int block_allocator_allocate(void *ctx, ump_dd_mem *mem);
static void block_allocator_release(void *ctx, ump_dd_mem *handle);
static inline u32 get_phys(block_allocator *allocator, block_info *block);
static u32 block_allocator_stat(struct ump_memory_backend *backend);



/*
 * Create dedicated memory backend
 */
ump_memory_backend *ump_block_allocator_create(u32 base_address, u32 size)
{
	ump_memory_backend *backend;
	block_allocator *allocator;
	u32 usable_size;
	u32 num_blocks;

	usable_size = (size + UMP_BLOCK_SIZE - 1) & ~(UMP_BLOCK_SIZE - 1);
	num_blocks = usable_size / UMP_BLOCK_SIZE;

	if (0 == usable_size) {
		DBG_MSG(1, ("Memory block of size %u is unusable\n", size));
		return NULL;
	}

	DBG_MSG(5, ("Creating dedicated UMP memory backend. Base address: 0x%08x, size: 0x%08x\n", base_address, size));
	DBG_MSG(6, ("%u usable bytes which becomes %u blocks\n", usable_size, num_blocks));

	backend = kzalloc(sizeof(ump_memory_backend), GFP_KERNEL);
	if (NULL != backend) {
		allocator = kmalloc(sizeof(block_allocator), GFP_KERNEL);
		if (NULL != allocator) {
			allocator->all_blocks = kmalloc(sizeof(block_info) * num_blocks, GFP_KERNEL);
			if (NULL != allocator->all_blocks) {
				int i;

				allocator->first_free = NULL;
				allocator->num_blocks = num_blocks;
				allocator->num_free = num_blocks;
				allocator->base = base_address;
				sema_init(&allocator->mutex, 1);

				for (i = 0; i < num_blocks; i++) {
					allocator->all_blocks[i].next = allocator->first_free;
					allocator->first_free = &allocator->all_blocks[i];
				}

				backend->ctx = allocator;
				backend->allocate = block_allocator_allocate;
				backend->release = block_allocator_release;
				backend->shutdown = block_allocator_shutdown;
				backend->stat = block_allocator_stat;
				backend->pre_allocate_physical_check = NULL;
				backend->adjust_to_mali_phys = NULL;

				return backend;
			}
			kfree(allocator);
		}
		kfree(backend);
	}

	return NULL;
}



/*
 * Destroy specified dedicated memory backend
 */
static void block_allocator_shutdown(ump_memory_backend *backend)
{
	block_allocator *allocator;

	BUG_ON(!backend);
	BUG_ON(!backend->ctx);

	allocator = (block_allocator *)backend->ctx;

	DBG_MSG_IF(1, allocator->num_free != allocator->num_blocks, ("%u blocks still in use during shutdown\n", allocator->num_blocks - allocator->num_free));

	kfree(allocator->all_blocks);
	kfree(allocator);
	kfree(backend);
}



static int block_allocator_allocate(void *ctx, ump_dd_mem *mem)
{
	block_allocator *allocator;
	u32 left;
	block_info *last_allocated = NULL;
	int i = 0;

	BUG_ON(!ctx);
	BUG_ON(!mem);

	allocator = (block_allocator *)ctx;
	left = mem->size_bytes;

	BUG_ON(!left);
	BUG_ON(!&allocator->mutex);

	mem->nr_blocks = ((left + UMP_BLOCK_SIZE - 1) & ~(UMP_BLOCK_SIZE - 1)) / UMP_BLOCK_SIZE;
	mem->block_array = (ump_dd_physical_block *)vmalloc(sizeof(ump_dd_physical_block) * mem->nr_blocks);
	if (NULL == mem->block_array) {
		MSG_ERR(("Failed to allocate block array\n"));
		return 0;
	}

	if (down_interruptible(&allocator->mutex)) {
		MSG_ERR(("Could not get mutex to do block_allocate\n"));
		return 0;
	}

	mem->size_bytes = 0;

	while ((left > 0) && (allocator->first_free)) {
		block_info *block;

		block = allocator->first_free;
		allocator->first_free = allocator->first_free->next;
		block->next = last_allocated;
		last_allocated = block;
		allocator->num_free--;

		mem->block_array[i].addr = get_phys(allocator, block);
		mem->block_array[i].size = UMP_BLOCK_SIZE;
		mem->size_bytes += UMP_BLOCK_SIZE;

		i++;

		if (left < UMP_BLOCK_SIZE) left = 0;
		else left -= UMP_BLOCK_SIZE;
	}

	if (left) {
		block_info *block;
		/* release all memory back to the pool */
		while (last_allocated) {
			block = last_allocated->next;
			last_allocated->next = allocator->first_free;
			allocator->first_free = last_allocated;
			last_allocated = block;
			allocator->num_free++;
		}

		vfree(mem->block_array);
		mem->backend_info = NULL;
		mem->block_array = NULL;

		DBG_MSG(4, ("Could not find a mem-block for the allocation.\n"));
		up(&allocator->mutex);

		return 0;
	}

	mem->backend_info = last_allocated;

	up(&allocator->mutex);
	mem->is_cached = 0;

	return 1;
}



static void block_allocator_release(void *ctx, ump_dd_mem *handle)
{
	block_allocator *allocator;
	block_info *block, * next;

	BUG_ON(!ctx);
	BUG_ON(!handle);

	allocator = (block_allocator *)ctx;
	block = (block_info *)handle->backend_info;
	BUG_ON(!block);

	if (down_interruptible(&allocator->mutex)) {
		MSG_ERR(("Allocator release: Failed to get mutex - memory leak\n"));
		return;
	}

	while (block) {
		next = block->next;

		BUG_ON((block < allocator->all_blocks) || (block > (allocator->all_blocks + allocator->num_blocks)));

		block->next = allocator->first_free;
		allocator->first_free = block;
		allocator->num_free++;

		block = next;
	}
	DBG_MSG(3, ("%d blocks free after release call\n", allocator->num_free));
	up(&allocator->mutex);

	vfree(handle->block_array);
	handle->block_array = NULL;
}



/*
 * Helper function for calculating the physical base adderss of a memory block
 */
static inline u32 get_phys(block_allocator *allocator, block_info *block)
{
	return allocator->base + ((block - allocator->all_blocks) * UMP_BLOCK_SIZE);
}

static u32 block_allocator_stat(struct ump_memory_backend *backend)
{
	block_allocator *allocator;
	BUG_ON(!backend);
	allocator = (block_allocator *)backend->ctx;
	BUG_ON(!allocator);

	return (allocator->num_blocks - allocator->num_free) * UMP_BLOCK_SIZE;
}
