/**
 * cache.c : deal with LRU caches
 *
 * Copyright (c) 2008-2010 Jean-Pierre Andre
 *
 * This program/include file is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program/include file is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program (in the main directory of the NTFS-3G
 * distribution in the file COPYING); if not, write to the Free Software
 * Foundation,Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef HAVE_CONFIG_H
#include "../config.h"
#endif

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif

#include "types.h"
#include "security.h"
#include "cache.h"
#include "misc.h"
#include "logging.h"

/*
 *		General functions to deal with LRU caches
 *
 *	The cached data have to be organized in a structure in which
 *	the first fields must follow a mandatory pattern and further
 *	fields may contain any fixed size data. They are stored in an
 *	LRU list.
 *
 *	A compare function must be provided for finding a wanted entry
 *	in the cache. Another function may be provided for invalidating
 *	an entry to facilitate multiple invalidation.
 *
 *	These functions never return error codes. When there is a
 *	shortage of memory, data is simply not cached.
 *	When there is a hashing bug, hashing is dropped, and sequential
 *	searches are used.
 */

/*
 *		Enter a new hash index, after a new record has been inserted
 *
 *	Do not call when a record has been modified (with no key change)
 */

static void inserthashindex(struct CACHE_HEADER *cache,
			struct CACHED_GENERIC *current)
{
	int h;
	struct HASH_ENTRY *link;
	struct HASH_ENTRY *first;

	if (cache->dohash) {
		h = cache->dohash(current);
		if ((h >= 0) && (h < cache->max_hash)) {
			/* get a free link and insert at top of hash list */
			link = cache->free_hash;
			if (link) {
				cache->free_hash = link->next;
				first = cache->first_hash[h];
				if (first)
					link->next = first;
				else
					link->next = NULL;
				link->entry = current;
				cache->first_hash[h] = link;
			} else {
				ntfs_log_error("No more hash entries,"
						" cache %s hashing dropped\n",
						cache->name);
				cache->dohash = (cache_hash)NULL;
			}
		} else {
			ntfs_log_error("Illegal hash value,"
						" cache %s hashing dropped\n",
						cache->name);
			cache->dohash = (cache_hash)NULL;
		}
	}
}

/*
 *		Drop a hash index when a record is about to be deleted
 */

static void drophashindex(struct CACHE_HEADER *cache,
			const struct CACHED_GENERIC *current, int hash)
{
	struct HASH_ENTRY *link;
	struct HASH_ENTRY *previous;

	if (cache->dohash) {
		if ((hash >= 0) && (hash < cache->max_hash)) {
			/* find the link and unlink */
			link = cache->first_hash[hash];
			previous = (struct HASH_ENTRY*)NULL;
			while (link && (link->entry != current)) {
				previous = link;
				link = link->next;
			}
			if (link) {
				if (previous)
					previous->next = link->next;
				else
					cache->first_hash[hash] = link->next;
				link->next = cache->free_hash;
				cache->free_hash = link;
			} else {
				ntfs_log_error("Bad hash list,"
						" cache %s hashing dropped\n",
						cache->name);
				cache->dohash = (cache_hash)NULL;
			}
		} else {
			ntfs_log_error("Illegal hash value,"
					" cache %s hashing dropped\n",
					cache->name);
			cache->dohash = (cache_hash)NULL;
		}
	}
}

/*
 *		Fetch an entry from cache
 *
 *	returns the cache entry, or NULL if not available
 *	The returned entry may be modified, but not freed
 */

struct CACHED_GENERIC *ntfs_fetch_cache(struct CACHE_HEADER *cache,
		const struct CACHED_GENERIC *wanted, cache_compare compare)
{
	struct CACHED_GENERIC *current;
	struct CACHED_GENERIC *previous;
	struct HASH_ENTRY *link;
	int h;

	current = (struct CACHED_GENERIC*)NULL;
	if (cache) {
		if (cache->dohash) {
			/*
			 * When possible, use the hash table to
			 * locate the entry if present
			 */
			h = cache->dohash(wanted);
		        link = cache->first_hash[h];
			while (link && compare(link->entry, wanted))
				link = link->next;
			if (link)
				current = link->entry;
		}
		if (!cache->dohash) {
			/*
			 * Search sequentially in LRU list if no hash table
			 * or if hashing has just failed
			 */
			current = cache->most_recent_entry;
			while (current
				   && compare(current, wanted)) {
				current = current->next;
				}
		}
		if (current) {
			previous = current->previous;
			cache->hits++;
			if (previous) {
			/*
			 * found and not at head of list, unlink from current
			 * position and relink as head of list
			 */
				previous->next = current->next;
				if (current->next)
					current->next->previous
						= current->previous;
				else
					cache->oldest_entry
						= current->previous;
				current->next = cache->most_recent_entry;
				current->previous
						= (struct CACHED_GENERIC*)NULL;
				cache->most_recent_entry->previous = current;
				cache->most_recent_entry = current;
			}
		}
		cache->reads++;
	}
	return (current);
}

/*
 *		Enter an inode number into cache
 *	returns the cache entry or NULL if not possible
 */

struct CACHED_GENERIC *ntfs_enter_cache(struct CACHE_HEADER *cache,
			const struct CACHED_GENERIC *item,
			cache_compare compare)
{
	struct CACHED_GENERIC *current;
	struct CACHED_GENERIC *before;
	struct HASH_ENTRY *link;
	int h;

	current = (struct CACHED_GENERIC*)NULL;
	if (cache) {
		if (cache->dohash) {
			/*
			 * When possible, use the hash table to
			 * find out whether the entry if present
			 */
			h = cache->dohash(item);
		        link = cache->first_hash[h];
			while (link && compare(link->entry, item))
				link = link->next;
			if (link) {
				current = link->entry;
			}
		}
		if (!cache->dohash) {
			/*
			 * Search sequentially in LRU list to locate the end,
			 * and find out whether the entry is already in list
			 * As we normally go to the end, no statistics is
			 * kept.
		 	 */
			current = cache->most_recent_entry;
			while (current
			   && compare(current, item)) {
				current = current->next;
				}
		}

		if (!current) {
			/*
			 * Not in list, get a free entry or reuse the
			 * last entry, and relink as head of list
			 * Note : we assume at least three entries, so
			 * before, previous and first are different when
			 * an entry is reused.
			 */

			if (cache->free_entry) {
				current = cache->free_entry;
				cache->free_entry = cache->free_entry->next;
				if (item->varsize) {
					current->variable = ntfs_malloc(
						item->varsize);
				} else
					current->variable = (void*)NULL;
				current->varsize = item->varsize;
				if (!cache->oldest_entry)
					cache->oldest_entry = current;
			} else {
				/* reusing the oldest entry */
				current = cache->oldest_entry;
				before = current->previous;
				before->next = (struct CACHED_GENERIC*)NULL;
				if (cache->dohash)
					drophashindex(cache,current,
						cache->dohash(current));
				if (cache->dofree)
					cache->dofree(current);
				cache->oldest_entry = current->previous;
				if (item->varsize) {
					if (current->varsize)
						current->variable = realloc(
							current->variable,
							item->varsize);
					else
						current->variable = ntfs_malloc(
							item->varsize);
				} else {
					if (current->varsize)
						free(current->variable);
					current->variable = (void*)NULL;
				}
				current->varsize = item->varsize;
			}
			current->next = cache->most_recent_entry;
			current->previous = (struct CACHED_GENERIC*)NULL;
			if (cache->most_recent_entry)
				cache->most_recent_entry->previous = current;
			cache->most_recent_entry = current;
			memcpy(current->payload, item->payload, cache->fixed_size);
			if (item->varsize) {
				if (current->variable) {
					memcpy(current->variable,
						item->variable, item->varsize);
				} else {
					/*
					 * no more memory for variable part
					 * recycle entry in free list
					 * not an error, just uncacheable
					 */
					cache->most_recent_entry = current->next;
					current->next = cache->free_entry;
					cache->free_entry = current;
					current = (struct CACHED_GENERIC*)NULL;
				}
			} else {
				current->variable = (void*)NULL;
				current->varsize = 0;
			}
			if (cache->dohash && current)
				inserthashindex(cache,current);
		}
		cache->writes++;
	}
	return (current);
}

/*
 *		Invalidate a cache entry
 *	The entry is moved to the free entry list
 *	A specific function may be called for entry deletion
 */

static void do_invalidate(struct CACHE_HEADER *cache,
		struct CACHED_GENERIC *current, int flags)
{
	struct CACHED_GENERIC *previous;

	previous = current->previous;
	if ((flags & CACHE_FREE) && cache->dofree)
		cache->dofree(current);
	/*
	 * Relink into free list
	 */
	if (current->next)
		current->next->previous = current->previous;
	else
		cache->oldest_entry = current->previous;
	if (previous)
		previous->next = current->next;
	else
		cache->most_recent_entry = current->next;
	current->next = cache->free_entry;
	cache->free_entry = current;
	if (current->variable)
		free(current->variable);
	current->varsize = 0;
   }


/*
 *		Invalidate entries in cache
 *
 *	Several entries may have to be invalidated (at least for inodes
 *	associated to directories which have been renamed), a different
 *	compare function may be provided to select entries to invalidate
 *
 *	Returns the number of deleted entries, this can be used by
 *	the caller to signal a cache corruption if the entry was
 *	supposed to be found.
 */

int ntfs_invalidate_cache(struct CACHE_HEADER *cache,
		const struct CACHED_GENERIC *item, cache_compare compare,
		int flags)
{
	struct CACHED_GENERIC *current;
	struct CACHED_GENERIC *next;
	struct HASH_ENTRY *link;
	int count;
	int h;

	current = (struct CACHED_GENERIC*)NULL;
	count = 0;
	if (cache) {
		if (!(flags & CACHE_NOHASH) && cache->dohash) {
			/*
			 * When possible, use the hash table to
			 * find out whether the entry if present
			 */
			h = cache->dohash(item);
		        link = cache->first_hash[h];
			while (link) {
				if (compare(link->entry, item))
					link = link->next;
				else {
					current = link->entry;
					link = link->next;
					if (current) {
						drophashindex(cache,current,h);
						do_invalidate(cache,
							current,flags);
						count++;
					}
				}
			}
		}
		if ((flags & CACHE_NOHASH) || !cache->dohash) {
				/*
				 * Search sequentially in LRU list
				 */
			current = cache->most_recent_entry;
			while (current) {
				if (!compare(current, item)) {
					next = current->next;
					if (cache->dohash)
						drophashindex(cache,current,
						    cache->dohash(current));
					do_invalidate(cache,current,flags);
					current = next;
					count++;
				} else {
					current = current->next;
				}
			}
		}
	}
	return (count);
}

int ntfs_remove_cache(struct CACHE_HEADER *cache,
		struct CACHED_GENERIC *item, int flags)
{
	int count;

	count = 0;
	if (cache) {
		if (cache->dohash)
			drophashindex(cache,item,cache->dohash(item));
		do_invalidate(cache,item,flags);
		count++;
	}
	return (count);
}

/*
 *		Free memory allocated to a cache
 */

static void ntfs_free_cache(struct CACHE_HEADER *cache)
{
	struct CACHED_GENERIC *entry;

	if (cache) {
		for (entry=cache->most_recent_entry; entry; entry=entry->next) {
			if (cache->dofree)
				cache->dofree(entry);
			if (entry->variable)
				free(entry->variable);
		}
		free(cache);
	}
}

/*
 *		Create a cache
 *
 *	Returns the cache header, or NULL if the cache could not be created
 */

static struct CACHE_HEADER *ntfs_create_cache(const char *name,
			cache_free dofree, cache_hash dohash,
			int full_item_size,
			int item_count, int max_hash)
{
	struct CACHE_HEADER *cache;
	struct CACHED_GENERIC *pc;
	struct CACHED_GENERIC *qc;
	struct HASH_ENTRY *ph;
	struct HASH_ENTRY *qh;
	struct HASH_ENTRY **px;
	size_t size;
	int i;

	size = sizeof(struct CACHE_HEADER) + item_count*full_item_size;
	if (max_hash)
		size += item_count*sizeof(struct HASH_ENTRY)
			 + max_hash*sizeof(struct HASH_ENTRY*);
	cache = (struct CACHE_HEADER*)ntfs_malloc(size);
	if (cache) {
				/* header */
		cache->name = name;
		cache->dofree = dofree;
		if (dohash && max_hash) {
			cache->dohash = dohash;
			cache->max_hash = max_hash;
		} else {
			cache->dohash = (cache_hash)NULL;
			cache->max_hash = 0;
		}
		cache->fixed_size = full_item_size - sizeof(struct CACHED_GENERIC);
		cache->reads = 0;
		cache->writes = 0;
		cache->hits = 0;
		/* chain the data entries, and mark an invalid entry */
		cache->most_recent_entry = (struct CACHED_GENERIC*)NULL;
		cache->oldest_entry = (struct CACHED_GENERIC*)NULL;
		cache->free_entry = &cache->entry[0];
		pc = &cache->entry[0];
		for (i=0; i<(item_count - 1); i++) {
			qc = (struct CACHED_GENERIC*)((char*)pc
							 + full_item_size);
			pc->next = qc;
			pc->variable = (void*)NULL;
			pc->varsize = 0;
			pc = qc;
		}
			/* special for the last entry */
		pc->next =  (struct CACHED_GENERIC*)NULL;
		pc->variable = (void*)NULL;
		pc->varsize = 0;

		if (max_hash) {
				/* chain the hash entries */
			ph = (struct HASH_ENTRY*)(((char*)pc) + full_item_size);
			cache->free_hash = ph;
			for (i=0; i<(item_count - 1); i++) {
				qh = &ph[1];
				ph->next = qh;
				ph = qh;
			}
				/* special for the last entry */
			if (item_count) {
				ph->next =  (struct HASH_ENTRY*)NULL;
			}
				/* create and initialize the hash indexes */
			px = (struct HASH_ENTRY**)&ph[1];
			cache->first_hash = px;
			for (i=0; i<max_hash; i++)
				px[i] = (struct HASH_ENTRY*)NULL;
		} else {
			cache->free_hash = (struct HASH_ENTRY*)NULL;
			cache->first_hash = (struct HASH_ENTRY**)NULL;
		}
	}
	return (cache);
}

/*
 *		Create all LRU caches
 *
 *	No error return, if creation is not possible, cacheing will
 *	just be not available
 */

void ntfs_create_lru_caches(ntfs_volume *vol)
{
#if CACHE_INODE_SIZE
		 /* inode cache */
	vol->xinode_cache = ntfs_create_cache("inode",(cache_free)NULL,
		ntfs_dir_inode_hash, sizeof(struct CACHED_INODE),
		CACHE_INODE_SIZE, 2*CACHE_INODE_SIZE);
#endif
#if CACHE_NIDATA_SIZE
		 /* idata cache */
	vol->nidata_cache = ntfs_create_cache("nidata",
		ntfs_inode_nidata_free, ntfs_inode_nidata_hash,
		sizeof(struct CACHED_NIDATA),
		CACHE_NIDATA_SIZE, 2*CACHE_NIDATA_SIZE);
#endif
#if CACHE_LOOKUP_SIZE
		 /* lookup cache */
	vol->lookup_cache = ntfs_create_cache("lookup",
		(cache_free)NULL, ntfs_dir_lookup_hash,
		sizeof(struct CACHED_LOOKUP),
		CACHE_LOOKUP_SIZE, 2*CACHE_LOOKUP_SIZE);
#endif
	vol->securid_cache = ntfs_create_cache("securid",(cache_free)NULL,
		(cache_hash)NULL,sizeof(struct CACHED_SECURID), CACHE_SECURID_SIZE, 0);
#if CACHE_LEGACY_SIZE
	vol->legacy_cache = ntfs_create_cache("legacy",(cache_free)NULL,
		(cache_hash)NULL, sizeof(struct CACHED_PERMISSIONS_LEGACY), CACHE_LEGACY_SIZE, 0);
#endif
}

/*
 *		Free all LRU caches
 */

void ntfs_free_lru_caches(ntfs_volume *vol)
{
#if CACHE_INODE_SIZE
	ntfs_free_cache(vol->xinode_cache);
#endif
#if CACHE_NIDATA_SIZE
	ntfs_free_cache(vol->nidata_cache);
#endif
#if CACHE_LOOKUP_SIZE
	ntfs_free_cache(vol->lookup_cache);
#endif
	ntfs_free_cache(vol->securid_cache);
#if CACHE_LEGACY_SIZE
	ntfs_free_cache(vol->legacy_cache);
#endif
}
