/* -----------------------------------------------------------------------------
	GSFramework
	Copyright 2001-2013 Emmanuel Julien. All Rights Reserved.
----------------------------------------------------------------------------- */


		#include "filesystem/io_cache.h"
		#include "hash/nsha1.h"
		#include "platform.h"
		#include "log/log.h"
		#include "ntypes.h"

		using namespace GS::IO;


//------------------------------------------------------------------------------
struct FreeEntry	// should be in ReserveOnStore but local type on template is prohibited until C++0x
{
		Cache::Entry *entry;
		int			score;

		FreeEntry(Cache::Entry *e, int s) : entry(e), score(s) {}

static	int		ComputeScore(const Cache::Entry *e, size_t request_size, const GS::Time &ctime)
		{
			int time_bonus = (int)(ctime - e->last_use).toSec();
			int size_bonus = e->size - request_size;

			return time_bonus + size_bonus / 8;
		}
static	int		CompareScore(FreeEntry *a, FreeEntry *b)
		{	return b->score - a->score;	}
};
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
GS::String	Cache::ReserveOnStore(size_t size)
{
	String id = store.Reserve(size);
	if	(!id.IsEmpty())
		return id;

	// Build a list of reference free entries.
	Time ctime = Platform::Get().GetTime();

	// No need to drop anything if the request can't fit anyway...
	size_t total_freeable_store = 0;
	ListForeachPtr(Entry *, e, entries)
		if	(e->refc == 0)
			total_freeable_store += e->size;

	if	(size > (store.GetFreeStore() + total_freeable_store))
		return "";

	// Drop entries until the request fits in the store.
	List <FreeEntry *> free_entries;
	ListForeachPtr(Entry *, e, entries)
		if	(e->refc == 0)
			free_entries.Add(new FreeEntry(e, FreeEntry::ComputeScore(e, size, ctime)));

	free_entries.MergeSort(FreeEntry::CompareScore);

	ListForeachPtr(FreeEntry *, e, free_entries)
	{
		__LOG_V__ << "IO::Cache: Disposing of cache entry '" << e->entry->path << "' (score: " << e->score << ").\n";

		DeleteCacheEntry(e->entry);

		id = store.Reserve(size);
		if	(!id.IsEmpty())
			break;
	}

	ListDeleteAllPtr(FreeEntry *, free_entries)

	store.Save();
	return id;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
Cache::Entry *Cache::GetCacheEntry(const char *path) const
{
	ListForeachPtr(Entry *, entry, entries)
		if	(entry->path == path)
			return entry;
	return NULL;
}
Cache::Entry *Cache::CreateCacheEntry(const char *path)
{
	__LOG_V__ << "Create cache entry for '" << path << "'.\n";

	Array <char> data;
	if	(!io->FileLoad(path, data))
		return NULL;

	AutoPtr <Entry> entry(new Entry);
	if	(entry.IsNull())
		return NULL;

	entry->path = path;
	entry->id = ReserveOnStore(data.GetSize());
	if	(entry->id.IsEmpty())
		return NULL; // store full

	Entry *e = entry.Detach();
	entries.Add(e);

	return UpdateCacheEntry(e, &data) ? e : NULL;
}
bool		Cache::UpdateCacheEntry(Entry *entry, GS::Array <char> *preloaded_data)
{
	__LOG_V__ << "Update cache entry for '" << entry->path << "'.\n";

	Array <char> data;
	if	(preloaded_data)
		data.Transfer(*preloaded_data);
	else
		if	(!io->FileLoad(entry->path, data))
			return false;

	// Check current store entry size.
	if	(store.GetEntrySize(entry->id) != data.GetSize())
	{
		store.Free(entry->id);
		entry->id = ReserveOnStore(data.GetSize());

		if	(entry->id.IsEmpty())
			return false; // store is full
	}

	// Update store data.
	if	(!store.Store(entry->id, data.c_ptr(), data.GetSize(), entry->path))
		return false;
	store.Save();

	entry->hash = SHA1::ComputeHexa(data);
	entry->size = data.GetSize();
	return true;
}
bool		Cache::DeleteCacheEntry(Entry *entry)
{
	if	(entry->refc > 0)
		return false;

	store.Free(entry->id);
	store.Save();

	return entries.Remove(entry);
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
uint		Cache::GetCaps() const { return io->GetCaps(); }

Handle		*Cache::Open(const char *path, Mode mode)
{
	if	(mode == ModeRead)
	{
		Threading::MutexLock lock(&mutex);

		// Update cache.
		Entry *entry = GetCacheEntry(path);

		if	(entry)
		{
			__LOG_V__ << "Entry match for '" << path << "'.\n";

			if	(entry->hash != io->Hash(path))
			{
				__LOG_V__ << "Hash mismatch for '" << path << "'.\n";

				if	(entry->refc > 0)
					__LOG_W__ << "IO::Cache: Support file '" << entry->path << "' has changed but its cached version is in use and cannot be updated.\n";
				else
				{
					if	(!UpdateCacheEntry(entry))	// contention risk here due to the mutex lock and a potentially long update (eg. networked fs)
						DeleteCacheEntry(entry);

					// Check for a match in updated cache.
					entry = GetCacheEntry(path);
				}
			}
		}
		else
			entry = CreateCacheEntry(path);

		// Return handler to store fs.
		if	(entry)
		{
			__LOG_V__ << "Opening '" << path << "' from cache.\n";

			entry->last_use = Platform::Get().GetTime();
			entry->refc++;

			return new CacheHandle(this, path, store.GetIO()->Open(entry->id, mode));
		}
	}

	// Direct access to the underlying fs.
	return io->Open(path, mode);
}
void		Cache::Close(Handle *h)
{
	if	(CacheHandle *c_h = (CacheHandle *)h)
	{
		Threading::MutexLock lock(&mutex);

		if	(Entry *entry = GetCacheEntry(c_h->path))
			entry->refc--;
		c_h->handle = NULL;
	}
}

bool		Cache::Delete(const char *path)
{
	/*
		[EJ] Minor synchronization issue warning.

		If a cached handle is already in use and the support fs file is deleted
		the cached handle will remain valid. Further open requests will then
		unexpectedly succeed as long as a single cached handler remains open.

		The correct fix would be to prevent deleting a support file as long as
		a cached entry with a non-zero reference count exists for it.
	*/
	return io->Delete(path);
}

size_t		Cache::Tell(Handle *h)
{	return ((CacheHandle *)h)->handle->Tell();	}
size_t		Cache::Seek(Handle *h, ptrdiff_t offset, SeekRef seek)
{	return ((CacheHandle *)h)->handle->Seek(offset, seek);	}

size_t		Cache::Read(Handle *h, void *data, size_t size)
{	return ((CacheHandle *)h)->handle->Read(data, size);	}
size_t		Cache::Write(Handle *h, const void *data, size_t size)
{	return ((CacheHandle *)h)->handle->Write(data, size);	}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool		Cache::SynchronizeWithStore(const char *store_path)
{
	if	(!store.Load(store_path))
		return false;

	entries.Clear();

	__LOG_H__ << "IO::Cache: Synchronizing with store.\n";

	ListForeachPtr(DataStore::Entry *, e, store.GetEntries())
	{
		Entry *entry = new Entry;

		entry->id = e->id;
		entry->size = e->size;
		entry->path = e->user;
		entry->hash = store.GetIO()->Hash(entry->id);

		entries.Add(entry);
	}

	__LOG__ << "Done, " << entries.GetCount() << " entries synchronized.\n";
	return true;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool		Cache::MkDir(const char *path)
{	return io->MkDir(path);	}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
Cache::Cache(Base *base, Base *store_io, size_t store_size) : io(base), store(store_io, store_size) {}
//------------------------------------------------------------------------------

//-----------------------------------------------------------------------------
CacheHandle::~CacheHandle()
{	GetIOSystem()->Close(this);	}
//-----------------------------------------------------------------------------
