/** 
 * @file llvocache.cpp
 * @brief Cache of objects on the viewer.
 *
 * $LicenseInfo:firstyear=2003&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"
#include "llvocache.h"
#include "llerror.h"
#include "llregionhandle.h"
#include "llviewercontrol.h"

BOOL check_read(LLAPRFile* apr_file, void* src, S32 n_bytes) 
{
	return apr_file->read(src, n_bytes) == n_bytes ;
}

BOOL check_write(LLAPRFile* apr_file, void* src, S32 n_bytes) 
{
	return apr_file->write(src, n_bytes) == n_bytes ;
}


//---------------------------------------------------------------------------
// LLVOCacheEntry
//---------------------------------------------------------------------------

LLVOCacheEntry::LLVOCacheEntry(U32 local_id, U32 crc, LLDataPackerBinaryBuffer &dp)
	:
	mLocalID(local_id),
	mCRC(crc),
	mHitCount(0),
	mDupeCount(0),
	mCRCChangeCount(0)
{
	mBuffer = new U8[dp.getBufferSize()];
	mDP.assignBuffer(mBuffer, dp.getBufferSize());
	mDP = dp;
}

LLVOCacheEntry::LLVOCacheEntry()
	:
	mLocalID(0),
	mCRC(0),
	mHitCount(0),
	mDupeCount(0),
	mCRCChangeCount(0),
	mBuffer(NULL)
{
	mDP.assignBuffer(mBuffer, 0);
}

LLVOCacheEntry::LLVOCacheEntry(LLAPRFile* apr_file)
{
	S32 size = -1;
	BOOL success;

	success = check_read(apr_file, &mLocalID, sizeof(U32));
	if(success)
	{
		success = check_read(apr_file, &mCRC, sizeof(U32));
	}
	if(success)
	{
		success = check_read(apr_file, &mHitCount, sizeof(S32));
	}
	if(success)
	{
		success = check_read(apr_file, &mDupeCount, sizeof(S32));
	}
	if(success)
	{
		success = check_read(apr_file, &mCRCChangeCount, sizeof(S32));
	}
	if(success)
	{
		success = check_read(apr_file, &size, sizeof(S32));

		// Corruption in the cache entries
		if ((size > 10000) || (size < 1))
		{
			// We've got a bogus size, skip reading it.
			// We won't bother seeking, because the rest of this file
			// is likely bogus, and will be tossed anyway.
			llwarns << "Bogus cache entry, size " << size << ", aborting!" << llendl;
			success = FALSE;
		}
	}
	if(success && size > 0)
	{
		mBuffer = new U8[size];
		success = check_read(apr_file, mBuffer, size);

		if(success)
		{
			mDP.assignBuffer(mBuffer, size);
		}
		else
		{
			delete[] mBuffer ;
			mBuffer = NULL ;
		}
	}

	if(!success)
	{
		mLocalID = 0;
		mCRC = 0;
		mHitCount = 0;
		mDupeCount = 0;
		mCRCChangeCount = 0;
		mBuffer = NULL;
	}
}

LLVOCacheEntry::~LLVOCacheEntry()
{
	delete [] mBuffer;
}


// New CRC means the object has changed.
void LLVOCacheEntry::assignCRC(U32 crc, LLDataPackerBinaryBuffer &dp)
{
	if (  (mCRC != crc)
		||(mDP.getBufferSize() == 0))
	{
		mCRC = crc;
		mHitCount = 0;
		mCRCChangeCount++;

		mDP.freeBuffer();
		mBuffer = new U8[dp.getBufferSize()];
		mDP.assignBuffer(mBuffer, dp.getBufferSize());
		mDP = dp;
	}
}

LLDataPackerBinaryBuffer *LLVOCacheEntry::getDP(U32 crc)
{
	if (  (mCRC != crc)
		||(mDP.getBufferSize() == 0))
	{
		//llinfos << "Not getting cache entry, invalid!" << llendl;
		return NULL;
	}
	mHitCount++;
	return &mDP;
}


void LLVOCacheEntry::recordHit()
{
	mHitCount++;
}


void LLVOCacheEntry::dump() const
{
	llinfos << "local " << mLocalID
		<< " crc " << mCRC
		<< " hits " << mHitCount
		<< " dupes " << mDupeCount
		<< " change " << mCRCChangeCount
		<< llendl;
}

BOOL LLVOCacheEntry::writeToFile(LLAPRFile* apr_file) const
{
	BOOL success;
	success = check_write(apr_file, (void*)&mLocalID, sizeof(U32));
	if(success)
	{
		success = check_write(apr_file, (void*)&mCRC, sizeof(U32));
	}
	if(success)
	{
		success = check_write(apr_file, (void*)&mHitCount, sizeof(S32));
	}
	if(success)
	{
		success = check_write(apr_file, (void*)&mDupeCount, sizeof(S32));
	}
	if(success)
	{
		success = check_write(apr_file, (void*)&mCRCChangeCount, sizeof(S32));
	}
	if(success)
	{
		S32 size = mDP.getBufferSize();
		success = check_write(apr_file, (void*)&size, sizeof(S32));
	
		if(success)
		{
			success = check_write(apr_file, (void*)mBuffer, size);
		}
	}

	return success ;
}

//-------------------------------------------------------------------
//LLVOCache
//-------------------------------------------------------------------
// Format string used to construct filename for the object cache
static const char OBJECT_CACHE_FILENAME[] = "objects_%d_%d.slc";

// Throw out 1/20 (5%) of our cache entries if we run out of room.
const U32 ENTRIES_PURGE_FACTOR = 20;
const char* object_cache_dirname = "objectcache";
const char* header_filename = "object.cache";

LLVOCache* LLVOCache::sInstance = NULL;

//static 
LLVOCache* LLVOCache::getInstance() 
{	
	if(!sInstance)
	{
		sInstance = new LLVOCache() ;
	}
	return sInstance ;
}

//static 
BOOL LLVOCache::hasInstance() 
{
	return sInstance != NULL ;
}

//static 
void LLVOCache::destroyClass() 
{
	if(sInstance)
	{
		delete sInstance ;
		sInstance = NULL ;
	}
}

LLVOCache::LLVOCache():
	mInitialized(FALSE),
	mReadOnly(TRUE),
	mCacheSize(1)
{
	mEnabled = gSavedSettings.getBOOL("ObjectCacheEnabled");
	mLocalAPRFilePoolp = new LLVolatileAPRPool() ;
}

LLVOCache::~LLVOCache()
{
	if(mEnabled)
	{
		writeCacheHeader();
		clearCacheInMemory();
	}
	delete mLocalAPRFilePoolp;
}

void LLVOCache::setDirNames(ELLPath location)
{
	std::string delem = gDirUtilp->getDirDelimiter();

	mHeaderFileName = gDirUtilp->getExpandedFilename(location, object_cache_dirname, header_filename);
	mObjectCacheDirName = gDirUtilp->getExpandedFilename(location, object_cache_dirname);
}

void LLVOCache::initCache(ELLPath location, U32 size, U32 cache_version)
{
	if(!mEnabled)
	{
		llwarns << "Not initializing cache: Cache is currently disabled." << llendl;
		return ;
	}

	if(mInitialized)
	{
		llwarns << "Cache already initialized." << llendl;
		return ;
	}

	setDirNames(location);
	if (!mReadOnly)
	{
		LLFile::mkdir(mObjectCacheDirName);
	}

	mCacheSize = size;

	readCacheHeader();
	mInitialized = TRUE ;

	if(mMetaInfo.mVersion != cache_version) 
	{
		mMetaInfo.mVersion = cache_version ;
		if(mReadOnly) //disable cache
		{
			clearCacheInMemory();
		}
		else //delete the current cache if the format does not match.
		{			
			removeCache();
		}
	}	
}
	
void LLVOCache::removeCache(ELLPath location) 
{
	if(mReadOnly)
	{
		llwarns << "Not removing cache at " << location << ": Cache is currently in read-only mode." << llendl;
		return ;
	}

	std::string delem = gDirUtilp->getDirDelimiter();
	std::string mask = delem + "*";
	std::string cache_dir = gDirUtilp->getExpandedFilename(location, object_cache_dirname);
	llinfos << "Removing cache at " << cache_dir << llendl;
	gDirUtilp->deleteFilesInDir(cache_dir, mask); //delete all files
	LLFile::rmdir(cache_dir);

	clearCacheInMemory();
	mInitialized = FALSE ;
}

void LLVOCache::removeCache() 
{
	llassert_always(mInitialized) ;
	if(mReadOnly)
	{
		llwarns << "Not clearing object cache: Cache is currently in read-only mode." << llendl;
		return ;
	}

	std::string delem = gDirUtilp->getDirDelimiter();
	std::string mask = delem + "*";
	llinfos << "Removing cache at " << mObjectCacheDirName << llendl;
	gDirUtilp->deleteFilesInDir(mObjectCacheDirName, mask); 

	clearCacheInMemory() ;
	writeCacheHeader();
}

void LLVOCache::clearCacheInMemory()
{
	std::for_each(mHandleEntryMap.begin(), mHandleEntryMap.end(), DeletePairedPointer());
	mHandleEntryMap.clear();
}

void LLVOCache::getObjectCacheFilename(U64 handle, std::string& filename) 
{
	U32 region_x, region_y;

	grid_from_region_handle(handle, &region_x, &region_y);
	filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE, object_cache_dirname,
			   llformat(OBJECT_CACHE_FILENAME, region_x, region_y));

	return ;
}

void LLVOCache::removeFromCache(U64 handle)
{
	if(mReadOnly)
	{
		llwarns << "Not removing cache for handle " << handle << ": Cache is currently in read-only mode." << llendl;
		return ;
	}

	std::string filename;
	getObjectCacheFilename(handle, filename);
	LLAPRFile::remove(filename, mLocalAPRFilePoolp);	
}

BOOL LLVOCache::checkRead(LLAPRFile* apr_file, void* src, S32 n_bytes, bool remove_cache_on_error)
{
	if(!check_read(apr_file, src, n_bytes))
	{
		if (remove_cache_on_error)
		{
			removeCache() ;
		}
		return FALSE ;
	}

	return TRUE ;
}

BOOL LLVOCache::checkWrite(LLAPRFile* apr_file, void* src, S32 n_bytes, bool remove_cache_on_error) 
{
	if(!check_write(apr_file, src, n_bytes))
	{
		if (remove_cache_on_error)
		{
			removeCache() ;
		}
		return FALSE ;
	}

	return TRUE ;
}

void LLVOCache::readCacheHeader()
{
	if(!mEnabled)
	{
		llwarns << "Not reading cache header: Cache is currently disabled." << llendl;
		return;
	}

	//clear stale info.
	clearCacheInMemory();	

	if (LLAPRFile::isExist(mHeaderFileName, mLocalAPRFilePoolp))
	{
		LLAPRFile* apr_file = new LLAPRFile(mHeaderFileName, APR_FOPEN_READ|APR_FOPEN_BINARY, mLocalAPRFilePoolp);		
		
		//read the meta element
		bool remove_cache_on_error = false;
		if(!checkRead(apr_file, &mMetaInfo, sizeof(HeaderMetaInfo), remove_cache_on_error))
		{
			llwarns << "Error reading meta information from cache header." << llendl;
			delete apr_file;
			return;
		}

		HeaderEntryInfo* entry ;
		for(U32 entry_index = 0; entry_index < mCacheSize; ++entry_index)
		{
			entry = new HeaderEntryInfo() ;
			if(!checkRead(apr_file, entry, sizeof(HeaderEntryInfo), remove_cache_on_error))
			{
				llwarns << "Error reading cache header entry. (entry_index=" << entry_index << ")" << llendl;
				delete entry ;			
				break;
			}
			else if(!entry->mTime) //end of the cache.
			{
				delete entry ;
				break;
			}

			entry->mIndex = entry_index;
			mHandleEntryMap[entry->mHandle] = entry;
		}

		delete apr_file ;
	}
	else
	{
		writeCacheHeader() ;
	}
}

void LLVOCache::writeCacheHeader()
{
	if (!mEnabled)
	{
		llwarns << "Not writing cache header: Cache is currently disabled." << llendl;
		return;
	}

	if(mReadOnly)
	{
		llwarns << "Not writing cache header: Cache is currently in read-only mode." << llendl;
		return;
	}

	LLAPRFile* apr_file = new LLAPRFile(mHeaderFileName, APR_FOPEN_CREATE|APR_FOPEN_WRITE|APR_FOPEN_BINARY|APR_FOPEN_TRUNCATE, mLocalAPRFilePoolp);

	//write the meta element
	if(!checkWrite(apr_file, &mMetaInfo, sizeof(HeaderMetaInfo)))
	{
		llwarns << "Error writing meta information to cache header." << llendl;
		delete apr_file;
		return;
	}

	U32 entry_index = 0;
	handle_entry_map_t::iterator iter_end = mHandleEntryMap.end();
	for(handle_entry_map_t::iterator iter = mHandleEntryMap.begin();
		iter != iter_end;
		++iter)
	{
		HeaderEntryInfo* entry = iter->second;
		entry->mIndex = entry_index++;
		if(!checkWrite(apr_file, (void*)entry, sizeof(HeaderEntryInfo)))
		{
			llwarns << "Failed to write cache header for entry " << entry->mHandle << " (entry_index = " << entry_index << ")" << llendl;
			delete apr_file;
			return;
		}
	}

	// Why do we need to fill the cache header with default entries?  DK 2010-12-14
	// It looks like we currently rely on the file being pre-allocated so we can seek during updateEntry().
	if(entry_index < mCacheSize)
	{
		HeaderEntryInfo* entry = new HeaderEntryInfo() ;
		for(; entry_index < mCacheSize; ++entry_index)
		{
			//fill the cache with the default entry.
			if(!checkWrite(apr_file, entry, sizeof(HeaderEntryInfo)))
			{
				llwarns << "Failed to fill cache header with default entries (entry_index = " << entry_index << ").  Switching to read-only mode." << llendl;
				mReadOnly = TRUE ; //disable the cache.
				break;
			}
		}
		delete entry ;
	}
	delete apr_file ;
}

BOOL LLVOCache::updateEntry(const HeaderEntryInfo* entry)
{
	LLAPRFile* apr_file = new LLAPRFile(mHeaderFileName, APR_FOPEN_WRITE|APR_FOPEN_BINARY, mLocalAPRFilePoolp);
	apr_file->seek(APR_SET, entry->mIndex * sizeof(HeaderEntryInfo) + sizeof(HeaderMetaInfo)) ;

	BOOL result = checkWrite(apr_file, (void*)entry, sizeof(HeaderEntryInfo)) ;
	delete apr_file;
	return result;
}

void LLVOCache::readFromCache(U64 handle, const LLUUID& id, LLVOCacheEntry::vocache_entry_map_t& cache_entry_map) 
{
	if(!mEnabled)
	{
		llwarns << "Not reading cache for handle " << handle << "): Cache is currently disabled." << llendl;
		return ;
	}
	llassert_always(mInitialized);

	handle_entry_map_t::iterator iter = mHandleEntryMap.find(handle) ;
	if(iter == mHandleEntryMap.end()) //no cache
	{
		llwarns << "No handle map entry for " << handle << llendl;
		return ;
	}

	std::string filename;
	getObjectCacheFilename(handle, filename);
	LLAPRFile* apr_file = new LLAPRFile(filename, APR_FOPEN_READ|APR_FOPEN_BINARY, mLocalAPRFilePoolp);

	LLUUID cache_id ;
	if(!checkRead(apr_file, cache_id.mData, UUID_BYTES))
	{
		llwarns << "Error reading cache_id from " << filename << llendl;
		delete apr_file;
		return ;
	}
	if(cache_id != id)
	{
		llwarns << "Cache ID (" << cache_id << ") doesn't match id for this region (" << id << "), discarding.  handle = " << handle << llendl;
		delete apr_file ;
		return ;
	}

	S32 num_entries;
	if(!checkRead(apr_file, &num_entries, sizeof(S32)))
	{
		llwarns << "Error reading num_entries from " << filename << llendl;
		delete apr_file;
		return ;
	}
	
	for (S32 i = 0; i < num_entries; i++)
	{
		LLVOCacheEntry* entry = new LLVOCacheEntry(apr_file);
		if (!entry->getLocalID())
		{
			llwarns << "Aborting cache file load for " << filename << ", cache file corruption! (entry number = " << i << ")" << llendl;
			delete entry ;
			break;
		}
		cache_entry_map[entry->getLocalID()] = entry;
	}

	delete apr_file ;
	return ;
}
	
void LLVOCache::purgeEntries()
{
	U32 limit = mCacheSize - (mCacheSize / ENTRIES_PURGE_FACTOR);
	limit = llclamp(limit, (U32)1, mCacheSize);
	// Construct a vector of entries out of the map so we can sort by time.
	std::vector<HeaderEntryInfo*> header_vector;
	handle_entry_map_t::iterator iter_end = mHandleEntryMap.end();
	for (handle_entry_map_t::iterator iter = mHandleEntryMap.begin();
		iter != iter_end;
		++iter)
	{
		header_vector.push_back(iter->second);
	}
	// Sort by time, oldest first.
	std::sort(header_vector.begin(), header_vector.end(), header_entry_less());
	while(header_vector.size() > limit)
	{
		HeaderEntryInfo* entry = header_vector.front();
		
		removeFromCache(entry->mHandle);
		mHandleEntryMap.erase(entry->mHandle);
		header_vector.erase(header_vector.begin());
		delete entry;
	}

	writeCacheHeader() ;
	// *TODO: Verify that we can avoid re-reading the cache header.  DK 2010-12-14
	readCacheHeader() ;
}

void LLVOCache::writeToCache(U64 handle, const LLUUID& id, const LLVOCacheEntry::vocache_entry_map_t& cache_entry_map, BOOL dirty_cache) 
{
	if(!mEnabled)
	{
		llwarns << "Not writing cache for handle " << handle << "): Cache is currently disabled." << llendl;
		return ;
	}
	llassert_always(mInitialized);

	if(mReadOnly)
	{
		llwarns << "Not writing cache for handle " << handle << "): Cache is currently in read-only mode." << llendl;
		return ;
	}

	U32 num_handle_entries = mHandleEntryMap.size();
	
	HeaderEntryInfo* entry;
	handle_entry_map_t::iterator iter = mHandleEntryMap.find(handle) ;
	if(iter == mHandleEntryMap.end()) //new entry
	{
		if(num_handle_entries >= mCacheSize)
		{
			purgeEntries() ;
			num_handle_entries = mHandleEntryMap.size();
		}
		
		entry = new HeaderEntryInfo();
		entry->mHandle = handle ;
		entry->mTime = time(NULL) ;
		entry->mIndex = num_handle_entries++;
		mHandleEntryMap[handle] = entry ;
	}
	else
	{
		// Update access time.
		entry = iter->second ;
		entry->mTime = time(NULL) ;
	}

	//update cache header
	if(!updateEntry(entry))
	{
		llwarns << "Failed to update cache header index " << entry->mIndex << ". handle = " << handle << llendl;
		return ; //update failed.
	}

	if(!dirty_cache)
	{
		llwarns << "Skipping write to cache for handle " << handle << ": cache not dirty" << llendl;
		return ; //nothing changed, no need to update.
	}

	//write to cache file
	std::string filename;
	getObjectCacheFilename(handle, filename);
	LLAPRFile* apr_file = new LLAPRFile(filename, APR_FOPEN_CREATE|APR_FOPEN_WRITE|APR_FOPEN_BINARY|APR_FOPEN_TRUNCATE, mLocalAPRFilePoolp);
	
	if(!checkWrite(apr_file, (void*)id.mData, UUID_BYTES))
	{
		llwarns << "Error writing id to " << filename << llendl;
		delete apr_file;
		return ;
	}

	S32 num_entries = cache_entry_map.size() ;
	if(!checkWrite(apr_file, &num_entries, sizeof(S32)))
	{
		llwarns << "Error writing num_entries to " << filename << llendl;
		delete apr_file;
		return ;
	}

	for (LLVOCacheEntry::vocache_entry_map_t::const_iterator iter = cache_entry_map.begin(); iter != cache_entry_map.end(); ++iter)
	{
		if(!iter->second->writeToFile(apr_file))
		{
			llwarns << "Aborting cache file write for " << filename << ", error writing to file!" << llendl;
			//failed
			removeCache() ;
			break;
		}
	}

	delete apr_file ;
	return ;
}