module cache{Key, Value, SIZE};

/* LRU Cache
 * The cache uses a pool (array) to store all the elements, each element has
 * a key (id) and a value. A HashMap correlates the ids to an index in the pool.
 * To keep track of which items were recently used two bit arrays are kept, one
 * stores the "used" flag for each index and anothe the "present" flag.
 * Every NCYCLES operations the present and used arrays are updated to free up
 * the elements that were not recently used.
 */

import std::core::mem;
import std::core::mem::allocator;
import std::collections::bitset;
import std::collections::map;

alias BitArr = bitset::BitSet{SIZE};
alias IdTable = map::HashMap{Key, usz};
alias IdTableEntry = map::Entry{Key, usz};

const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);

struct Cache {
	Allocator allocator;
	BitArr present, used;
	IdTable table;
	Value[] pool;
	usz cycle_count;
}

// Every CACHE_CYCLES operations mark as not-present the unused elements
macro Cache.cycle(&cache) @private {
    cache.cycle_count++;
	if (cache.cycle_count > CACHE_NCYCLES) {
		for (usz i = 0; i < cache.present.data.len; i++) {
			cache.present.data[i] &= cache.used.data[i];
			cache.used.data[i] = 0;
		}
		cache.cycle_count = 0;
	}
}

fn void? Cache.init(&cache, Allocator allocator)
{
	cache.allocator = allocator;
	cache.table.init(allocator, capacity: SIZE);
	// FIXME: this shit is SLOW
	foreach (idx, bit : cache.used) { cache.used[idx] = false; }
	foreach (idx, bit : cache.present) { cache.present[idx] = false; }
	cache.pool = allocator::new_array(allocator, Value, SIZE);
}

fn void Cache.free(&cache)
{
	(void)cache.table.free();
	(void)allocator::free(cache.allocator, cache.pool);
}

fn Value*? Cache.search(&cache, Key id)
{
	// get_entry() faults on miss
	IdTableEntry* entry = cache.table.get_entry(id)!;

	/* MISS, wrong key */
	if (entry.key != id) {
		cache.table.remove(id)!;
		return NOT_FOUND?;
	}

	/* MISS, the data is not valid (not present) */
	if (!cache.present[entry.value]) {
		// if the data is not present but it is still in the table, remove it
		cache.table.remove(id)!;
		return NOT_FOUND?;
	}

	/* HIT, set as recently used */
	//io::printfn("HIT: %d [%d]", entry.value, entry.key);
	cache.used[entry.value] = true;
	return &(cache.pool[entry.value]);
}

fn void Cache.remove(&cache, Key id)
{
	IdTableEntry*? entry = cache.table.get_entry(id);
	if (catch entry) {
		return;
	}

	// found, remove it
	cache.present[entry.value] = false;
	(void)cache.table.remove(id);
}

/* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */
fn usz Cache.get_free_spot(&cache) @private
{
	// TODO: in the upgrade to c3 1.7.5 use @bitsof()
	const BITS = $typeof(cache.present.data[0]).sizeof*8;
	foreach (idx, d: cache.present.data) {
		if (d != $typeof(d).max) {
			usz spot = idx*BITS + BITS-d.clz();
			if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot);
			return spot;
		}
	}
	return 0;
}

fn Value*? Cache.insert_at(&cache, Value* g, Key id, usz index) @private
{
	// TODO: verify index, g and id
	Value* spot;

	/* Set used and present */
	cache.present.set(index);
	cache.used.set(index);
	cache.cycle();

	spot = &(cache.pool[index]);
	*spot = *g;
	cache.table.set(id, index);
	return spot;
}

// Insert an element in the cache, returns the index
fn Value*? Cache.insert_new(&cache, Value* g, Key id)
{
	usz index = cache.get_free_spot();
	return cache.insert_at(g, id, index);
}

fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null)
{
	Value*? c = cache.search(id);
	if (catch e = c) {
		if (e != NOT_FOUND) {
			return e?;
		} else {
			// if the element is new (inserted) set the is_new flag
			if (is_new) *is_new = true;
			return cache.insert_new(g, id);
		}
	} else {
		if (is_new) *is_new = false;
		return c;
	}
}
