package vefontcache

/*
The choice was made to keep the LRU cache implementation as close to the original as possible.
*/

import "base:runtime"

Pool_ListIter  :: i32
Pool_ListValue :: u64

Pool_List_Item :: struct {
	prev  : Pool_ListIter,
	next  : Pool_ListIter,
	value : Pool_ListValue,
}

Pool_List :: struct {
	items     : [dynamic]Pool_List_Item,
	free_list : [dynamic]Pool_ListIter,
	front     : Pool_ListIter,
	back      : Pool_ListIter,
	size      : i32,
	capacity  : i32,
	dbg_name  : string,
}

pool_list_init :: proc( pool : ^Pool_List, capacity : i32, dbg_name : string = "" )
{
	error : Allocator_Error
	pool.items, error = make( [dynamic]Pool_List_Item, int(capacity) )
	assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate items array")
	resize( & pool.items, capacity )

	pool.free_list, error = make( [dynamic]Pool_ListIter, len = 0, cap = int(capacity) )
	assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate free_list array")
	resize( & pool.free_list, capacity )

	pool.capacity = capacity

	pool.dbg_name = dbg_name
	using pool

	for id in 0 ..< capacity {
		free_list[id] = i32(id)
		items[id] = {
			prev = -1,
			next = -1,
		}
	}

	front = -1
	back  = -1
}

pool_list_free :: proc( pool : ^Pool_List ) {
	delete( pool.items)
	delete( pool.free_list)
}

pool_list_reload :: proc( pool : ^Pool_List, allocator : Allocator ) {
	reload_array( & pool.items, allocator )
	reload_array( & pool.free_list, allocator )
}

pool_list_push_front :: proc( pool : ^Pool_List, value : Pool_ListValue )
{
	using pool
	if size >= capacity do return

	length := len(free_list)
	assert( length > 0 )
	assert( length == int(capacity - size) )

	id := free_list[ len(free_list) - 1 ]
	if pool.dbg_name != "" {
		logf("pool_list: back %v", id)
	}
	pop( & free_list )
	items[ id ].prev  = -1
	items[ id ].next  = front
	items[ id ].value = value
	if pool.dbg_name != "" {
		logf("pool_list: pushed %v into id %v", value, id)
	}

	if front != -1 do items[ front ].prev = id
	if back  == -1 do back = id
	front  = id
	size  += 1
}

pool_list_erase :: proc( pool : ^Pool_List, iter : Pool_ListIter )
{
	using pool
	if size <= 0 do return
	assert( iter >= 0 && iter < i32(capacity) )
	assert( len(free_list) == int(capacity - size) )

	iter_node := & items[ iter ]
	prev := iter_node.prev
	next := iter_node.next

	if iter_node.prev != -1 do items[ prev ].next = iter_node.next
	if iter_node.next != -1 do items[ next ].prev = iter_node.prev

	if front == iter do front = iter_node.next
	if back  == iter do back  = iter_node.prev

	iter_node.prev  = -1
	iter_node.next  = -1
	iter_node.value = 0
	append( & free_list, iter )

	size -= 1
	if size == 0 {
		back  = -1
		front = -1
	}
}

pool_list_move_to_front :: #force_inline proc( pool : ^Pool_List, iter : Pool_ListIter )
{
	using pool

	if front == iter do return

	item := & items[iter]
	if item.prev != -1   do items[ item.prev ].next = item.next
	if item.next != -1   do items[ item.next ].prev = item.prev
	if back      == iter do back = item.prev

	item.prev           = -1
	item.next           = front
	items[ front ].prev = iter
	front               = iter
}

pool_list_peek_back :: #force_inline proc ( pool : ^Pool_List ) -> Pool_ListValue {
	assert( pool.back != - 1 )
	value := pool.items[ pool.back ].value
	return value
}

pool_list_pop_back :: #force_inline proc( pool : ^Pool_List ) -> Pool_ListValue {
	if pool.size <= 0 do return 0
	assert( pool.back != -1 )

	value := pool.items[ pool.back ].value
	pool_list_erase( pool, pool.back )
	return value
}

LRU_Link :: struct {
	pad_top : u64,

	value : i32,
	ptr   : Pool_ListIter,

	pad_bottom : u64,
}

LRU_Cache :: struct {
	capacity  : i32,
	num       : i32,
	table     :  map[u64]LRU_Link,
	key_queue : Pool_List,
}

lru_init :: proc( cache : ^LRU_Cache, capacity : i32, dbg_name : string = "" ) {
	error : Allocator_Error
	cache.capacity     = capacity
	cache.table, error = make( map[u64]LRU_Link, uint(capacity) )
	assert( error == .None, "VEFontCache.lru_init : Failed to allocate cache's table")

	pool_list_init( & cache.key_queue, capacity, dbg_name = dbg_name )
}

lru_free :: proc( cache : ^LRU_Cache ) {
	pool_list_free( & cache.key_queue )
	delete( cache.table )
}

lru_reload :: #force_inline proc( cache : ^LRU_Cache, allocator : Allocator ) {
	reload_map( & cache.table, allocator )
	pool_list_reload( & cache.key_queue, allocator )
}

lru_find :: #force_inline proc "contextless" ( cache : ^LRU_Cache, key : u64, must_find := false ) -> (LRU_Link, bool) {
	link, success := cache.table[key]
	return link, success
}

lru_get :: #force_inline proc( cache: ^LRU_Cache, key : u64 ) -> i32 {
	if link, ok := &cache.table[ key ]; ok {
			pool_list_move_to_front(&cache.key_queue, link.ptr)
			return link.value
	}
	return -1
}

lru_get_next_evicted :: #force_inline proc ( cache : ^LRU_Cache ) -> u64 {
	if cache.key_queue.size >= cache.capacity {
		evict := pool_list_peek_back( & cache.key_queue )
		return evict
	}
	return 0xFFFFFFFFFFFFFFFF
}

lru_peek :: #force_inline proc ( cache : ^LRU_Cache, key : u64, must_find := false ) -> i32 {
	iter, success := lru_find( cache, key, must_find )
	if success == false {
		return -1
	}
	return iter.value
}

lru_put :: #force_inline proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64
{
	if link, ok := & cache.table[ key ]; ok {
		pool_list_move_to_front( & cache.key_queue, link.ptr )
		link.value = value
		return key
	}

	evict := key
	if cache.key_queue.size >= cache.capacity {
		evict = pool_list_pop_back(&cache.key_queue)
		delete_key(&cache.table, evict)
		cache.num -= 1
	}

	pool_list_push_front(&cache.key_queue, key)
	cache.table[key] = LRU_Link{
			value = value,
			ptr   = cache.key_queue.front,
	}
	cache.num += 1
	return evict
}

lru_refresh :: proc( cache : ^LRU_Cache, key : u64 ) {
	link, success := lru_find( cache, key )
	pool_list_erase( & cache.key_queue, link.ptr )
	pool_list_push_front( & cache.key_queue, key )
	link.ptr = cache.key_queue.front
}
