/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "dict.h"
#include "dict-private.h"
#include "dict-init-cache.h"
#include "llist.h"

/* How many seconds to keep dict opened for reuse after it's been closed */
#define DICT_CACHE_TIMEOUT_SECS 30
/* How many closed dicts to keep */
#define DICT_CACHE_MAX_COUNT 10

struct dict_init_cache_list {
	struct dict_init_cache_list *prev, *next;

	struct dict *dict;
	char *dict_name;
	int refcount;

	time_t destroy_time;
};

static struct dict_init_cache_list *dicts = NULL;
static struct timeout *to_dict = NULL;

static struct dict_init_cache_list *
dict_init_cache_add(const char *dict_name, struct dict *dict)
{
	struct dict_init_cache_list *list;

	list = i_new(struct dict_init_cache_list, 1);
	list->refcount = 1;
	list->dict = dict;
	list->dict_name = i_strdup(dict_name);

	DLLIST_PREPEND(&dicts, list);

	return list;
}

static void dict_init_cache_list_free(struct dict_init_cache_list *list)
{
	i_assert(list->refcount == 0);

	DLLIST_REMOVE(&dicts, list);
	dict_deinit(&list->dict);
	i_free(list->dict_name);
	i_free(list);
}

static struct dict_init_cache_list *dict_init_cache_find(const char *dict_name)
{
	struct dict_init_cache_list *listp = dicts, *next = NULL, *match = NULL;
	unsigned int ref0_count = 0;

	while (listp != NULL) {
                next = listp->next;
		if (match != NULL) {
			/* already found the dict. we're just going through
			   the rest of them to drop 0 refcounts */
		} else if (strcmp(dict_name, listp->dict_name) == 0)
			match = listp;

		if (listp->refcount == 0 && listp != match) {
			if (listp->destroy_time <= ioloop_time ||
			    ref0_count >= DICT_CACHE_MAX_COUNT - 1)
				dict_init_cache_list_free(listp);
			else
				ref0_count++;
		}
                listp = next;
	}
	return match;
}

int dict_init_cache_get(struct event *event, const char *dict_name,
			struct dict **dict_r, const char **error_r)
{
	struct dict_init_cache_list *match;
	int ret = 0;

	match = dict_init_cache_find(dict_name);
	if (match == NULL) {
		if (dict_init_filter_auto(event, dict_name, dict_r, error_r) <= 0)
			return -1;
		match = dict_init_cache_add(dict_name, *dict_r);
	} else {
		match->refcount++;
		*dict_r = match->dict;
	}
	i_assert(match->dict != NULL);
	return ret;
}

static void destroy_unrefed(void)
{
	struct dict_init_cache_list *listp, *next = NULL;
	bool seen_ref0 = FALSE;

	for (listp = dicts; listp != NULL; listp = next) {
		next = listp->next;

		i_assert(listp->refcount >= 0);
		if (listp->refcount > 0)
			;
		else if (listp->destroy_time <= ioloop_time)
			dict_init_cache_list_free(listp);
		else
			seen_ref0 = TRUE;
	}

	if (!seen_ref0 && to_dict != NULL)
		timeout_remove(&to_dict);
}

static void dict_removal_timeout(void *context ATTR_UNUSED)
{
	destroy_unrefed();
}

void dict_init_cache_unref(struct dict **_dict)
{
	struct dict *dict = *_dict;
	struct dict_init_cache_list *listp;

	if (dict == NULL)
		return;

	*_dict = NULL;
	for (listp = dicts; listp != NULL; listp = listp->next) {
		if (listp->dict == dict)
			break;
	}

	i_assert(listp != NULL && listp->dict == dict);
	i_assert(listp->refcount > 0);

	listp->refcount--;
	listp->destroy_time = ioloop_time + DICT_CACHE_TIMEOUT_SECS;

	if (to_dict == NULL) {
		to_dict = timeout_add_to(io_loop_get_root(),
					 DICT_CACHE_TIMEOUT_SECS*1000/2,
					 dict_removal_timeout, NULL);
	}
}

void dict_init_cache_wait_all(void)
{
	struct dict_init_cache_list *listp;

	for (listp = dicts; listp != NULL; listp = listp->next)
		dict_wait(listp->dict);
}

void dict_init_cache_destroy_all(void)
{
	timeout_remove(&to_dict);
	while (dicts != NULL)
		dict_init_cache_list_free(dicts);
}
