/*
 * Copyright (c) 2015-2021 Hanspeter Portner (dev@open-music-kontrollers.ch)
 *
 * This is free software: you can redistribute it and/or modify
 * it under the terms of the Artistic License 2.0 as published by
 * The Perl Foundation.
 *
 * This source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * Artistic License 2.0 for more details.
 *
 * You should have received a copy of the Artistic License 2.0
 * along the source as a COPYING file. If not, obtain it from
 * http://www.perlfoundation.org/artistic_license_2_0.
 */

#if !defined(_WIN32)
# include <sys/mman.h>
#endif
#include <assert.h>
#include <stdlib.h>
#include <string.h>

#include <moony.h>
#include <api_vm.h>

#include <lualib.h>
#include <lauxlib.h>

#include <laes128.h>

//FIXME put those into a header
extern int luaopen_lpeg(lua_State *L);
extern int luaopen_base64(lua_State *L);
extern int luaopen_ascii85(lua_State *L);
extern int luaopen_mathx(lua_State *L);
extern int luaopen_complex(lua_State *L);
extern int luaopen_random(lua_State *L);

//#define MOONY_LOG_MEM
#ifdef MOONY_LOG_MEM
__realtime static inline void
_log_mem(moony_vm_t *vm, void *ptr, size_t osize, size_t nsize)
{
	moony_t *moony = vm->data;

	if(moony->log)
	{
		char suffix0 = ' ';
		size_t space = vm->space;
		if(space >= 1024)
		{
			suffix0 = 'K';
			space >>= 10;
		}
		if(space >= 1024)
		{
			suffix0 = 'M';
			space >>= 10;
		}

		char suffix1 = ' ';
		size_t used = vm->used;
		if(used >= 1024)
		{
			suffix1 = 'K';
			used >>= 10;
		}
		if(used >= 1024)
		{
			suffix1 = 'M';
			used >>= 10;
		}

		lv2_log_trace(&moony->logger, "space: %4zu%c, used: %4zu%c, old: %4zu, new: %4zu, data: %p\n",
			space, suffix0, used, suffix1, osize, nsize, ptr);
	}
}
#endif

__realtime inline void *
moony_rt_alloc(moony_vm_t *vm, size_t nsize)
{
	vm->used += nsize;
	if(vm->used > (vm->space >> 1))
		moony_vm_mem_extend(vm);

#ifdef MOONY_LOG_MEM
	_log_mem(vm, NULL, 0, nsize);
#endif

	return tlsf_malloc(vm->tlsf, nsize);
}

__realtime inline void *
moony_rt_realloc(moony_vm_t *vm, void *buf, size_t osize, size_t nsize)
{
	vm->used -= osize;
	vm->used += nsize;
	if(vm->used > (vm->space >> 1))
		moony_vm_mem_extend(vm);

#ifdef MOONY_LOG_MEM
	_log_mem(vm, buf, osize, nsize);
#endif

	return tlsf_realloc(vm->tlsf, buf, nsize);
}

__realtime inline void
moony_rt_free(moony_vm_t *vm, void *buf, size_t osize)
{
	vm->used -= osize;
	if(vm->used > (vm->space >> 1))
		moony_vm_mem_extend(vm);

#ifdef MOONY_LOG_MEM
	_log_mem(vm, buf, osize, 0);
#endif

	tlsf_free(vm->tlsf, buf);
}

__realtime static void *
lua_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
	moony_vm_t *vm = ud;

	if(nsize == 0)
	{
		if(ptr)
			moony_rt_free(vm, ptr, osize);
		return NULL;
	}
	else
	{
		if(ptr)
			return moony_rt_realloc(vm, ptr, osize, nsize);
		else
			return moony_rt_alloc(vm, nsize);
	}
}

__non_realtime moony_vm_t *
moony_vm_new(size_t mem_size, bool testing, void *data)
{
	moony_vm_t *vm = calloc(1, sizeof(moony_vm_t));
	if(!vm)
		return NULL;

	vm->data = data;

	// initialize array of increasing pool sizes
	vm->size[0] = mem_size;

	// allocate first pool
	vm->area[0] = moony_vm_mem_alloc(vm->size[0]);
	if(!vm->area[0])
	{
		free(vm);
		return NULL;
	}
	
	vm->tlsf = tlsf_create_with_pool(vm->area[0], vm->size[0]);
	if(!vm->tlsf)
	{
		moony_vm_mem_free(vm->area[0], vm->size[0]);
		free(vm);
		return NULL;
	}

	vm->pool[0] = tlsf_get_pool(vm->tlsf);
	vm->space += vm->size[0];

	lua_State *L = lua_newstate(lua_alloc, vm);
	if(!L)
	{
		free(vm);
		return NULL;
	}

	vm->L = L;

	const int n = lua_gettop(L);

	luaL_requiref(L, "base", luaopen_base, 0);

	luaL_requiref(L, "coroutine", luaopen_coroutine, 1);
	luaL_requiref(L, "table", luaopen_table, 1);
	luaL_requiref(L, "string", luaopen_string, 1);
	luaL_requiref(L, "math", luaopen_math, 1);
	luaL_requiref(L, "utf8", luaopen_utf8, 1);
	luaL_requiref(L, "debug", luaopen_debug, 1);

	luaL_requiref(L, "lpeg", luaopen_lpeg, 1);
	luaL_requiref(L, "base64", luaopen_base64, 1);
	luaL_requiref(L, "ascii85", luaopen_ascii85, 1);
	luaL_requiref(L, "aes128", luaopen_aes128, 1);
	luaL_requiref(L, "mathx", luaopen_mathx, 1);
	luaL_requiref(L, "complex", luaopen_complex, 1);
	luaL_requiref(L, "random", luaopen_random, 1);

	if(testing)
	{
		luaL_requiref(L, "io", luaopen_io, 1);
		luaL_requiref(L, "package", luaopen_package, 1);
		//luaL_requiref(L, "os", luaopen_os, 1);
		//luaL_requiref(L, "bit32", luaopen_bit32, 1);
	}

	lua_settop(L, n);

	if(!testing)
	{
		// clear dofile
		lua_pushnil(L);
		lua_setglobal(L, "dofile");

		// clear loadfile
		lua_pushnil(L);
		lua_setglobal(L, "loadfile");
	}

	// clear math.random[seed]
	lua_getglobal(L, "math");

	lua_pushnil(L);
	lua_setfield(L, -2, "random");

	lua_pushnil(L);
	lua_setfield(L, -2, "randomseed");

	lua_pop(L, 1); // math

#if USE_MANUAL_GC
	// manual garbage collector
	lua_gc(L, LUA_GCSTOP, 0); // disable automatic garbage collection
	// set step size to run 'as fast as memory allocation'
	lua_gc(L, LUA_GCINC, 0, 100, 13);
#elif USE_INCREMENTAL_GC
	// incremental garbage collector
	lua_gc(L, LUA_GCRESTART, 0); // enable automatic garbage collection
	// next step when memory increased by 5%
	// run 5% faster than memory allocation
	lua_gc(L, LUA_GCINC, 105, 105, 13);
#elif USE_GENERATIONAL_GC
	// generational garbage collector
	lua_gc(L, LUA_GCRESTART, 0); // enable automatic garbage collection
	// next minor collection when memory increased by 5%
	// next major collection when memory increased by 100% 
	lua_gc(L, LUA_GCGEN, 5, 100);
#else
#	error "GC method invalid"
#endif

	return vm;
}

__non_realtime void
moony_vm_free(moony_vm_t *vm)
{
	if(vm->L)
		lua_close(vm->L);

	if(vm->ser.buf)
		moony_rt_free(vm, vm->ser.buf, vm->ser.size);

	vm->used = 0;

	for(int i=(MOONY_POOL_NUM-1); i>=0; i--)
	{
		if(!vm->area[i])
			continue; // this memory slot is unused, skip it

		tlsf_remove_pool(vm->tlsf, vm->pool[i]);
		moony_vm_mem_free(vm->area[i], vm->size[i]);
		vm->space -= vm->size[i];

		vm->area[i] = NULL;
		vm->pool[i] = NULL;
		vm->size[i] = 0;
	}

	assert(vm->space == 0);
	tlsf_destroy(vm->tlsf);
	vm->tlsf = NULL;

	free(vm);
}

__non_realtime void *
moony_vm_mem_alloc(size_t size)
{
	void *area = NULL;

	//printf("moony_vm_mem_alloc: %zu\n", size);

#if defined(_WIN32)
	area = _aligned_malloc(size, 8);
#else
	posix_memalign(&area, 8, size);
#endif
	if(!area)
		return NULL;

	mlock(area, size);
	memset(area, 0x0, size);
	return area;
}

__non_realtime void
moony_vm_mem_free(void *area, size_t size)
{
	if(!area)
		return;
	
	//printf("moony_vm_mem_free: %zu\n", size);

	munlock(area, size);
	free(area);
}

__realtime int
moony_vm_mem_extend(moony_vm_t *vm)
{
	moony_t *moony = vm->data;

	// request processing or fully extended?
	if(vm->allocating || vm->fully_extended)
		return -1;

	for(int i=1; i<MOONY_POOL_NUM; i++)
	{
		if(vm->area[i]) // pool already allocated/in-use
			continue;

		if(vm->nrt)
		{
			vm->size[i] = vm->size[i-1] * 2;
			vm->area[i] = moony_vm_mem_alloc(vm->size[i]);
			if(vm->area[i])
			{
				vm->pool[i] = tlsf_add_pool(vm->tlsf,
					vm->area[i], vm->size[i]); //FIXME stoat complains about printf

				if(vm->pool[i])
				{
					vm->space += vm->size[i];
				}
				else
				{
					moony_vm_mem_free(vm->area[i], vm->size[i]);
					vm->size[i] = 0;
					vm->area[i] = NULL;
				}
			}
		}
		else
		{
			moony_job_t *req;
			if((req = varchunk_write_request(moony->from_dsp, sizeof(moony_job_t))))
			{
				req->type = MOONY_JOB_MEM_ALLOC;
				req->mem.size = vm->size[i-1] * 2;
				req->mem.ptr = NULL;

				varchunk_write_advance(moony->from_dsp, sizeof(moony_job_t));
				if(moony_wake_worker(moony->sched) == LV2_WORKER_SUCCESS)
					vm->allocating = true; // toggle working flag
			}
		}

		return 0;
	}

	vm->fully_extended = true;

	return -1;
}

void 
moony_vm_nrt_enter(moony_vm_t *vm)
{
	vm->nrt = true;
}

void
moony_vm_nrt_leave(moony_vm_t *vm)
{
	vm->nrt = false;
}
