/**
@file     lru_hashtable.c
@brief    LRU hashtable implementation
@details  Copyright (c) 2026 Acronis International GmbH
@author   Bruce Wang (bruce.wang@acronis.com)
@since    $Id: $
*/

#include "lru_hashtable.h"

#include "hash_fast.h"
#include "path_tools.h"
#include "memory.h"

#include <linux/jiffies.h>
#include <linux/list.h>

#define LRU_TABLE_SIZE (1 << (LRU_TABLE_SIZE_BITS - 1)) // 16384
#define LRU_TABLE_CLEAN_SIZE 256

#ifndef list_first_entry_or_null
#define list_first_entry_or_null(ptr, type, member) (list_empty(ptr) ? NULL : list_first_entry(ptr, type, member))
#endif

#ifdef KERNEL_MOCK
#include "mock/mock.h"
#endif

// 30 seconds
#define TTL msecs_to_jiffies(30000)

typedef struct
{
	atomic_t refcount;

	struct hlist_node hash_node;

	struct list_head lru_list_node;
	bool lru_list_inserted;
	unsigned long lru_deadline;

	struct rcu_head rcu;

	lru_hashtable_key_t key;

} lru_hashtable_node_t;

// MARK: lru hashtable node
static lru_hashtable_node_t *node_alloc(const lru_hashtable_key_t *key, lru_hashtable_key_type_t key_type)
{
	lru_hashtable_node_t *node = mem_alloc(sizeof(lru_hashtable_node_t));
	if (!node)
		return NULL;

	atomic_set(&node->refcount, 1);
	node->lru_list_inserted = false;
	INIT_HLIST_NODE(&node->hash_node);
	INIT_LIST_HEAD(&node->lru_list_node);

	switch (key_type)
	{
	case LRU_HASHTABLE_KEY_TYPE_PROCFS:
		node->key.procfs_key = key->procfs_key;
		break;
	case LRU_HASHTABLE_KEY_TYPE_PTRACE:
		node->key.ptrace_key = key->ptrace_key;
		break;
	default:
		mem_free(node);
		return NULL;
	}
	return node;
}

static void node_rcu_free(struct rcu_head *rcu)
{
	lru_hashtable_node_t *node = container_of(rcu, lru_hashtable_node_t, rcu);
	mem_free(node);
}

static void node_put(lru_hashtable_node_t *node)
{
	if (atomic_dec_and_test(&node->refcount))
		call_rcu(&node->rcu, node_rcu_free);
}

static bool key_equal(const lru_hashtable_key_t *k1, const lru_hashtable_key_t *k2, lru_hashtable_key_type_t key_type)
{
	// TODO: merge access_types and access_modes
	switch (key_type)
	{
	case LRU_HASHTABLE_KEY_TYPE_PROCFS:
		return k1->procfs_key.access_type == k2->procfs_key.access_type &&
			   k1->procfs_key.caller_pid_version == k2->procfs_key.caller_pid_version &&
			   k1->procfs_key.target_pid_version == k2->procfs_key.target_pid_version;
	case LRU_HASHTABLE_KEY_TYPE_PTRACE:
		return k1->ptrace_key.access_mode == k2->ptrace_key.access_mode &&
			   k1->ptrace_key.caller_pid_version == k2->ptrace_key.caller_pid_version &&
			   k1->ptrace_key.target_pid_version == k2->ptrace_key.target_pid_version;
	default:
		return false;
	}
}

static int key_hash(const lru_hashtable_key_t *key, lru_hashtable_key_type_t key_type)
{
	switch (key_type)
	{
	case LRU_HASHTABLE_KEY_TYPE_PROCFS:
		return murmur_hash(&key->procfs_key.caller_pid_version, sizeof(key->procfs_key)) >> (64 - LRU_TABLE_SIZE_BITS);
	case LRU_HASHTABLE_KEY_TYPE_PTRACE:
		return murmur_hash(&key->ptrace_key.caller_pid_version, sizeof(key->ptrace_key)) >> (64 - LRU_TABLE_SIZE_BITS);
	default:
		return -1;
	}
}

static lru_hashtable_node_t *find_ref_rcu(struct lru_hashtable_manager *manager, int hash, const lru_hashtable_key_t *key, lru_hashtable_key_type_t key_type)
{
	lru_hashtable_node_t *search_node;
	hlist_for_each_entry_rcu(search_node, &manager->seen_entries_hashtable[hash], hash_node)
	{
		if (!key_equal(&search_node->key, key, key_type))
			continue;

		if (atomic_inc_not_zero(&search_node->refcount))
			return search_node;
		else
			return NULL;
	}

	return NULL;
}

static lru_hashtable_node_t *find(struct lru_hashtable_manager *manager, int hash, const lru_hashtable_key_t *key, lru_hashtable_key_type_t key_type)
{
	lru_hashtable_node_t *search_node;
	hlist_for_each_entry(search_node, &manager->seen_entries_hashtable[hash], hash_node)
	{
		if (key_equal(&search_node->key, key, key_type))
			return search_node;
	}

	return NULL;
}

static void erase_impl(struct lru_hashtable_manager *manager, lru_hashtable_node_t *node)
{
	hash_del_rcu(&node->hash_node);
	list_del(&node->lru_list_node);
	node->lru_list_inserted = false;
	manager->entries_count--;
	node_put(node);
}

static void refresh_impl(struct lru_hashtable_manager *manager, lru_hashtable_node_t *node)
{
	if (node->lru_list_inserted)
	{
		node->lru_deadline = jiffies + TTL;
		list_del(&node->lru_list_node);
		list_add_tail(&node->lru_list_node, &manager->seen_entries_lru_list);
	}
}

// MARK: lru hashtable manager
int lru_hashtable_manager_init(struct lru_hashtable_manager **out_manager)
{
	*out_manager = vmem_alloc(sizeof(lru_hashtable_manager_t));
	if (!*out_manager)
		return -ENOMEM;

	mutex_init(&(*out_manager)->table_writer_lock);
	(*out_manager)->active = false;
	(*out_manager)->entries_count = 0;
	hash_init((*out_manager)->seen_entries_hashtable);
	INIT_LIST_HEAD(&(*out_manager)->seen_entries_lru_list);

	return 0;
}

void lru_hashtable_manager_deinit(struct lru_hashtable_manager *manager)
{
	if (!manager)
		return;

	vmem_free(manager);
}

void lru_hashtable_manager_activate(struct lru_hashtable_manager *manager)
{
	mutex_lock(&manager->table_writer_lock);
	manager->active = true;
	mutex_unlock(&manager->table_writer_lock);
}

void lru_hashtable_manager_deactivate(struct lru_hashtable_manager *manager)
{
	mutex_lock(&manager->table_writer_lock);
	if (manager->active)
	{
		while (1)
		{
			lru_hashtable_node_t *node = list_first_entry_or_null(&manager->seen_entries_lru_list, lru_hashtable_node_t, lru_list_node);
			if (!node)
				break;

			erase_impl(manager, node);
		}

		manager->active = false;
	}
	mutex_unlock(&manager->table_writer_lock);
}

static void sweep_impl(struct lru_hashtable_manager *manager)
{
	while (1)
	{
		lru_hashtable_node_t *node = list_first_entry_or_null(&manager->seen_entries_lru_list, lru_hashtable_node_t, lru_list_node);
		if (!node)
			break;

		if (time_after(jiffies, node->lru_deadline))
			erase_impl(manager, node);
		else
			break;
	}

	while (manager->entries_count >= LRU_TABLE_SIZE - LRU_TABLE_CLEAN_SIZE)
	{
		lru_hashtable_node_t *node = list_first_entry_or_null(&manager->seen_entries_lru_list, lru_hashtable_node_t, lru_list_node);
		if (!node)
			break;

		erase_impl(manager, node);
	}
}

bool lru_hashtable_manager_key_exist(struct lru_hashtable_manager *manager, const lru_hashtable_key_t *key, lru_hashtable_key_type_t key_type)
{
	int hash;
	lru_hashtable_node_t *node;

	if (!manager || !manager->active)
	{
		return false;
	}

	hash = key_hash(key, key_type);

	if(hash == -1)
	{
		return false;
	}

	rcu_read_lock();
	node = find_ref_rcu(manager, hash, key, key_type);
	rcu_read_unlock();
	if (node)
	{
		mutex_lock(&manager->table_writer_lock);
		refresh_impl(manager, node);
		mutex_unlock(&manager->table_writer_lock);
		node_put(node);
		return true;
	}

	mutex_lock(&manager->table_writer_lock);
	if (!READ_ONCE(manager->active))
	{
		mutex_unlock(&manager->table_writer_lock);
		return false;
	}

	node = find(manager, hash, key, key_type);
	if (node)
	{
		refresh_impl(manager, node);
		mutex_unlock(&manager->table_writer_lock);
		return true;
	}

	sweep_impl(manager);

	node = node_alloc(key, key_type);
	if (!node)
	{
		mutex_unlock(&manager->table_writer_lock);
		return false;
	}

	hlist_add_head_rcu(&node->hash_node, &manager->seen_entries_hashtable[hash]);

	node->lru_deadline = jiffies + TTL;
	list_add_tail(&node->lru_list_node, &manager->seen_entries_lru_list);
	node->lru_list_inserted = true;

	manager->entries_count++;
	mutex_unlock(&manager->table_writer_lock);

	return false;
}
