/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "module-context.h"
#include "eacces-error.h"
#include "mail-index-private.h"
#include "mail-index-alloc-cache.h"

#define MAIL_INDEX_ALLOC_CACHE_CONTEXT(obj) \
	MODULE_CONTEXT(obj, mail_index_alloc_cache_index_module)

/* How many seconds to keep index opened for reuse after it's been closed */
#define INDEX_CACHE_TIMEOUT 10
/* How many closed indexes to keep */
#define INDEX_CACHE_MAX 3

struct mail_index_alloc_cache_list {
	union mail_index_module_context module_ctx;
	struct mail_index_alloc_cache_list *next;

	struct mail_index *index;
	char *mailbox_path;
	int refcount;
	bool referenced;

	dev_t index_dir_dev;
	ino_t index_dir_ino;

	time_t destroy_time;
};

static MODULE_CONTEXT_DEFINE_INIT(mail_index_alloc_cache_index_module,
				  &mail_index_module_register);
static struct mail_index_alloc_cache_list *indexes = NULL;
static unsigned int indexes_cache_references_count = 0;
static struct timeout *to_index = NULL;

static struct mail_index_alloc_cache_list *
mail_index_alloc_cache_add(struct mail_index *index,
			   const char *mailbox_path, struct stat *st)
{
	struct mail_index_alloc_cache_list *list;

	list = i_new(struct mail_index_alloc_cache_list, 1);
	list->refcount = 1;
	list->index = index;

	list->mailbox_path = i_strdup(mailbox_path);
	list->index_dir_dev = st->st_dev;
	list->index_dir_ino = st->st_ino;

	list->next = indexes;
	indexes = list;

	MODULE_CONTEXT_SET(index, mail_index_alloc_cache_index_module, list);
	return list;
}

static void
mail_index_alloc_cache_list_unref(struct mail_index_alloc_cache_list *list)
{
	i_assert(list->referenced);
	i_assert(indexes_cache_references_count > 0);

	indexes_cache_references_count--;
	mail_index_close(list->index);
	list->referenced = FALSE;
}

static void
mail_index_alloc_cache_list_free(struct mail_index_alloc_cache_list *list)
{
	i_assert(list->refcount == 0);

	if (list->referenced)
		mail_index_alloc_cache_list_unref(list);
	mail_index_free(&list->index);
	i_free(list->mailbox_path);
	i_free(list);
}

static struct mail_index_alloc_cache_list *
mail_index_alloc_cache_find_and_expire(const char *mailbox_path,
				       const char *index_dir,
				       const struct stat *index_st)
{
	struct mail_index_alloc_cache_list **indexp, *rec, *match;
	unsigned int destroy_count;
	struct stat st;

	destroy_count = 0; match = NULL;
	for (indexp = &indexes; *indexp != NULL;) {
		rec = *indexp;

		if (match != NULL) {
			/* already found the index. we're just going through
			   the rest of them to drop 0 refcounts */
		} else if (rec->refcount == 0 && rec->index->open_count == 0) {
			/* index is already closed. don't even try to
			   reuse it. */
		} else if (index_dir != NULL && rec->index_dir_ino != 0) {
			if (index_st->st_ino == rec->index_dir_ino &&
			    CMP_DEV_T(index_st->st_dev, rec->index_dir_dev)) {
				/* make sure the directory still exists.
				   it might have been renamed and we're trying
				   to access it via its new path now. */
				if (stat(rec->index->dir, &st) < 0 ||
				    st.st_ino != index_st->st_ino ||
				    !CMP_DEV_T(st.st_dev, index_st->st_dev))
					rec->destroy_time = 0;
				else
					match = rec;
			}
		} else if (mailbox_path != NULL && rec->mailbox_path != NULL &&
			   index_dir == NULL && rec->index_dir_ino == 0) {
			if (strcmp(mailbox_path, rec->mailbox_path) == 0)
				match = rec;
		}

		if (rec->refcount == 0 && rec != match) {
			if (rec->destroy_time <= ioloop_time ||
			    destroy_count >= INDEX_CACHE_MAX) {
				*indexp = rec->next;
				mail_index_alloc_cache_list_free(rec);
				continue;
			} else {
				destroy_count++;
			}
		}

                indexp = &(*indexp)->next;
	}
	return match;
}

struct mail_index *
mail_index_alloc_cache_get(struct event *parent_event, const char *mailbox_path,
			   const char *index_dir, const char *prefix)
{
	struct mail_index_alloc_cache_list *match;
	struct stat st;

	/* compare index_dir inodes so we don't break even with symlinks.
	   if index_dir doesn't exist yet or if using in-memory indexes, just
	   compare mailbox paths */
	i_zero(&st);
	if (index_dir == NULL) {
		/* in-memory indexes */
	} else if (stat(index_dir, &st) < 0) {
		if (errno == ENOENT) {
			/* it'll be created later */
		} else if (ENOACCESS(errno)) {
			e_error(parent_event, "%s",
				eacces_error_get("stat", index_dir));
		} else {
			e_error(parent_event, "stat(%s) failed: %m", index_dir);
		}
	}

	match = mail_index_alloc_cache_find_and_expire(mailbox_path,
						       index_dir, &st);
	if (match == NULL) {
		struct mail_index *index =
			mail_index_alloc(parent_event, index_dir, prefix);
		match = mail_index_alloc_cache_add(index, mailbox_path, &st);
	} else {
		match->refcount++;
	}
	i_assert(match->index != NULL);
	return match->index;
}

struct mail_index *
mail_index_alloc_cache_find(const char *index_dir)
{
	struct mail_index_alloc_cache_list *rec;
	struct stat st;

	if (stat(index_dir, &st) < 0) {
		if (errno != ENOENT)
			i_error("stat(%s) failed: %m", index_dir);
		return NULL;
	}

	for (rec = indexes; rec != NULL; rec = rec->next) {
		if (st.st_ino == rec->index_dir_ino &&
		    CMP_DEV_T(st.st_dev, rec->index_dir_dev))
			return rec->index;
	}
	return NULL;
}

static bool destroy_unrefed(unsigned int min_destroy_count)
{
	struct mail_index_alloc_cache_list **list, *rec;
	bool destroyed = FALSE;
	bool seen_ref0 = FALSE;

	for (list = &indexes; *list != NULL;) {
		rec = *list;

		if (rec->refcount == 0 &&
		    (min_destroy_count > 0 || rec->destroy_time <= ioloop_time)) {
			*list = rec->next;
			destroyed = TRUE;
			mail_index_alloc_cache_list_free(rec);
			if (min_destroy_count > 0)
				min_destroy_count--;
		} else {
			if (rec->refcount == 0)
				seen_ref0 = TRUE;
			if (min_destroy_count > 0 &&
			    rec->index->open_count == 1 &&
			    rec->referenced) {
				/* we're the only one keeping this index open.
				   we might be here, because the caller is
				   deleting this mailbox and wants its indexes
				   to be closed. so close it. */
				destroyed = TRUE;
				mail_index_alloc_cache_list_unref(rec);
			}
			list = &(*list)->next;
		}
	}

	if (!seen_ref0 && to_index != NULL)
		timeout_remove(&to_index);
	return destroyed;
}

static void ATTR_NULL(1)
index_removal_timeout(void *context ATTR_UNUSED)
{
	destroy_unrefed(0);
}

void mail_index_alloc_cache_unref(struct mail_index **_index)
{
	struct mail_index *index = *_index;
	struct mail_index_alloc_cache_list *list, **listp;

	*_index = NULL;
	list = NULL;
	for (listp = &indexes; *listp != NULL; listp = &(*listp)->next) {
		if ((*listp)->index == index) {
			list = *listp;
			break;
		}
	}

	i_assert(list != NULL);
	i_assert(list->refcount > 0);

	list->refcount--;
	list->destroy_time = ioloop_time + INDEX_CACHE_TIMEOUT;

	if (list->refcount == 0 && index->open_count == 0) {
		/* index was already closed. don't even try to cache it. */
		*listp = list->next;
		mail_index_alloc_cache_list_free(list);
	} else if (to_index == NULL) {
		/* Add to root ioloop in case we got here from an inner
		   ioloop which gets destroyed too early. */
		to_index = timeout_add_to(io_loop_get_root(),
					  INDEX_CACHE_TIMEOUT*1000/2,
					  index_removal_timeout, NULL);
	}
}

void mail_index_alloc_cache_destroy_unrefed(void)
{
	destroy_unrefed(UINT_MAX);
}

void mail_index_alloc_cache_index_opened(struct mail_index *index)
{
	struct mail_index_alloc_cache_list *list =
		MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);
	struct stat st;

	if (list != NULL && list->index_dir_ino == 0 &&
	    !MAIL_INDEX_IS_IN_MEMORY(index)) {
		/* newly created index directory. update its stat. */
		if (stat(index->dir, &st) == 0) {
			list->index_dir_ino = st.st_ino;
			list->index_dir_dev = st.st_dev;
		}
	}
}

void mail_index_alloc_cache_index_closing(struct mail_index *index)
{
	struct mail_index_alloc_cache_list *list =
		MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);

	i_assert(index->open_count > 0);
	if (index->open_count > 1 || list == NULL)
		return;

	if (list->referenced) {
		/* we're closing our referenced index */
		return;
	}
	while (indexes_cache_references_count > INDEX_CACHE_MAX) {
		if (!destroy_unrefed(1)) {
			/* our cache is full already, don't keep more */
			return;
		}
	}
	/* keep the index referenced for caching */
	indexes_cache_references_count++;
	list->referenced = TRUE;
	index->open_count++;
}
