//////////////////////////////////////////////////////////////////////////////
//
// Asura_Hash.cpp
//
// Copyright (c) 2002 Rebellion. All rights reserved.
//
// Description:
//
//     none
//
// History:
//
//     (MH - 18.10.2002) - Created.
//
//////////////////////////////////////////////////////////////////////////////

#include "Core/Asura.h"

// This file's header.
#include "Core/Asura_Hash.h"

// Stdlib independent.

// Engine independent.
#ifdef ASURA_ENABLE_HASH_TO_FILENAME_LOOKUPS
#include "Core/Asura_HashTree.h"
#include "Core/Asura_NameList.h"
#include "Core/Asura_String.h"
#include "Core/Asura_StrippedHashedFilename.h"
#include "Debugging/Asura_Logfile.h"
#endif

// ARC - 12 September 2006 14:34:34 - Universal hash lookup
#if defined(ASURA_UNIVERSAL_HASH_LOOKUP)
#include "Core/Memory/Asura_Mem.h"
#include "Core/Asura_HashTree.h"
#endif

// Engine platform-specific.
#include "Core/Asura_PlatformBasics.h"

//Tools
#ifdef ASURA_TOOLS
#include "Mesh/Tools_PlatformMesh_System.h"
#endif



// M A C R O S ///////////////////////////////////////////////////////////////

#define	ASURA_MKLOWER(c) ( static_cast<char>(( c ) - 'A' + 'a') )

// C L A S S   I M P L E M E N T A T I O N S /////////////////////////////////

#ifdef ASURA_ENABLE_HASH_TO_FILENAME_LOOKUPS

bool Asura_HashedFilename_System::s_bInitialised = false;
Asura_HashTree* Asura_HashedFilename_System::s_pxFullFilenameHashTree;
Asura_NameList* Asura_HashedFilename_System::s_pxFullFilenameList;
Asura_TemplateHashTree<Asura_StrippedHashedFilename>* Asura_HashedFilename_System::s_pxStrippedFilenameTree;
Asura_NameList* Asura_HashedFilename_System::s_pxBackupFullFilenameList;
Asura_TemplateHashTree<Asura_StrippedHashedFilename>* Asura_HashedFilename_System::s_pxBackupStrippedFilenameTree;
int Asura_HashedFilename_System::s_iNumDuplicateStrippedNames;
bool Asura_HashedFilename_System::s_bUsingSavedHashedFilenames;

static const char* g_szDuplicateFilenameLogfile = "LogFiles\\DuplicateFilenames.txt";


void Asura_HashedFilename_System::Initialise()
{
    s_pxFullFilenameHashTree = new Asura_HashTree;
    s_pxFullFilenameList = new Asura_NameList;
	s_pxStrippedFilenameTree = new Asura_TemplateHashTree<Asura_StrippedHashedFilename>;
    s_pxBackupFullFilenameList = 0;
    s_pxBackupStrippedFilenameTree = 0;

    s_iNumDuplicateStrippedNames = 0;
    s_bInitialised = true;

	s_bUsingSavedHashedFilenames = Platform_LoadHashedFilenames();
	if(!s_bUsingSavedHashedFilenames)
	{
    	Platform_BuildHashToFilenameLookupTree();
	}
}

void Asura_HashedFilename_System::Shutdown()
{
    delete s_pxFullFilenameHashTree;
    delete s_pxFullFilenameList;
	delete s_pxStrippedFilenameTree;
    delete s_pxBackupFullFilenameList;
    delete s_pxBackupStrippedFilenameTree;
    s_bInitialised = false;
}

void Asura_HashedFilename_System::AddFile(const char* szFilename)
{
    if (!s_bInitialised) 
    {
        Asura_Assert(false, "Asura_HashedFilename_System not initialised");
        return;
    }
    
    Asura_Assertf(!strstr(szFilename, "\\\\"), ("Error with filename %s.\nShouldn't have network paths or '\\\\' in filenames", szFilename));
        

    int iIndex;
	int iFullNameIndex;
    Asura_String xFullFilename = szFilename;
    Asura_String xStrippedFilename;
    xFullFilename.SplitFilename(0, &xStrippedFilename, 0);

    Asura_Hash_ID uFullFilenameHashID = Asura_GetHashID(xFullFilename.GetString());
    Asura_Hash_ID uStrippedFilenameHashID = Asura_GetHashID(xStrippedFilename.GetString());
    
    bool bExists = s_pxFullFilenameHashTree->FindNodeIndexByHash(uFullFilenameHashID, true, iIndex);
    if (!bExists)
    {
        iFullNameIndex = s_pxFullFilenameList->AddName(xFullFilename);
        Asura_Assert(iFullNameIndex == iIndex, "MH error");
    }
    if (bExists)
    {
        // OK if full filename is identical, have found same file twice, once on network, once locally.
		#if defined ASURA_ASSERT
			const Asura_String& xExistingFileName = s_pxFullFilenameList->GetName(iIndex);
	        bool bDuplicateOK = stricmp(xExistingFileName.GetString(), szFilename) == 0;
		#endif

        Asura_Assertf(bDuplicateOK, ("MH: duplicate full filename hash - should rename.\nFilenames are '%s' and '%s'", xExistingFileName.GetString(), szFilename));
		return;
    }


	//find or add entry to the stripped filename tree
	Asura_StrippedHashedFilename* pxStrippedEntry = s_pxStrippedFilenameTree->GetDataByHash(uStrippedFilenameHashID);
	if(pxStrippedEntry)
	{
		//already exists, make sure the name is the same
		if(pxStrippedEntry->m_xStrippedFilename != xStrippedFilename)
		{
        	Asura_Assertf(pxStrippedEntry->m_xStrippedFilename == xStrippedFilename, 
        	    ("duplicate stripped filename hash for distinct strings - should rename.\nFilenames are '%s' and '%s'",
        	    pxStrippedEntry->m_xStrippedFilename.GetString(), xStrippedFilename.GetString()));

			return;
		}
        s_iNumDuplicateStrippedNames++;
	}
	else
	{
		//add entry to the tree
		pxStrippedEntry = s_pxStrippedFilenameTree->AddNode(uStrippedFilenameHashID);
		Asura_Assert(pxStrippedEntry,"Failed to add entry to stripped filename tree");

		pxStrippedEntry->m_xStrippedFilename = xStrippedFilename;
	}

	//fill in the full filename index
	#ifdef ASURA_SUPPORT_PLATFORM_HASHED_FILENAMES
	static bool s_bLogCleared = false;

	//Is the file in a platform specific directory?
	int iPlatform = Tools_Platform::GetPlatformFromPath(szFilename);
	if(iPlatform < 0)
	{
		if(pxStrippedEntry->m_iFullIndex != -1)
		{
			if(!s_bLogCleared)
			{
				Asura_SimpleLogfileClear(g_szDuplicateFilenameLogfile);
				s_bLogCleared = true;
			}
			Asura_SimpleLogfileOutput((g_szDuplicateFilenameLogfile,"%s\n",s_pxFullFilenameList->GetName(pxStrippedEntry->m_iFullIndex).GetString()));
			Asura_SimpleLogfileOutput((g_szDuplicateFilenameLogfile,"%s\n\n",s_pxFullFilenameList->GetName(iFullNameIndex).GetString()));
		}
		
		pxStrippedEntry->m_iFullIndex = iFullNameIndex;
	}
	else
	{
		if(pxStrippedEntry->m_aiPlatformIndex[iPlatform] != -1)
		{
			if(!s_bLogCleared)
			{
				Asura_SimpleLogfileClear(g_szDuplicateFilenameLogfile);
				s_bLogCleared = true;
			}
			Asura_SimpleLogfileOutput((g_szDuplicateFilenameLogfile,"%s\n",s_pxFullFilenameList->GetName(pxStrippedEntry->m_aiPlatformIndex[iPlatform]).GetString()));
			Asura_SimpleLogfileOutput((g_szDuplicateFilenameLogfile,"%s\n\n",s_pxFullFilenameList->GetName(iFullNameIndex).GetString()));
		}
		
		pxStrippedEntry->m_aiPlatformIndex[iPlatform] = iFullNameIndex;
		pxStrippedEntry->m_bHasPlatformVersions = true;
	}
	#else
	pxStrippedEntry->m_iFullIndex = iFullNameIndex;
	#endif
}

const char* Asura_HashedFilename_System::GetFilename(Asura_Hash_ID uHashID,bool bAssertOnFail /*= true*/)
{
    Asura_Assert(uHashID != ASURA_HASH_ID_UNSET, "invalid hash ID");

    int iIndex;
    // first try the full filenames
    // search only, don't add
    bool bExists = s_pxFullFilenameHashTree->FindNodeIndexByHash(uHashID, false, iIndex);
    if (bExists)
    {
        // check this doesn't exist in the stripped list

        Asura_Assert(!s_pxStrippedFilenameTree->GetDataByHash(uHashID), "found same hash ID in full and stripped filename lists");
        return s_pxFullFilenameList->GetName(iIndex).GetString();
    }

	Asura_StrippedHashedFilename* pxStrippedEntry = s_pxStrippedFilenameTree->GetDataByHash(uHashID);
	if(pxStrippedEntry)
	{
		return pxStrippedEntry->m_xStrippedFilename.GetString();	
	}

	if(s_bUsingSavedHashedFilenames)
	{
		//failed to find file  , but are using saved values for filenames.
		//try updating the filename list , and see if we can find it now
    	Platform_BuildHashToFilenameLookupTree();
		return GetFilename(uHashID,bAssertOnFail);
	}

	if(bAssertOnFail)
	{
		Asura_Assertf(false, ("failed to resolve filename from hash ID (0x%08x)", uHashID));
	}
	
    return NULL;
}

const char* Asura_HashedFilename_System::GetFullFilename(Asura_Hash_ID uHashID,bool bAssertOnFail /*= true*/)
{
    Asura_Assert(uHashID != ASURA_HASH_ID_UNSET, "invalid hash ID");

    int iIndex;
    // first try the full filenames
    // search only, don't add
    bool bExists = s_pxFullFilenameHashTree->FindNodeIndexByHash(uHashID, false, iIndex);
    if (bExists)
    {
        // check this doesn't exist in the stripped list
        Asura_Assert(!s_pxStrippedFilenameTree->GetDataByHash(uHashID), "found same hash ID in full and stripped filename lists");
        return s_pxFullFilenameList->GetName(iIndex).GetString();
    }


	Asura_StrippedHashedFilename* pxStrippedEntry = s_pxStrippedFilenameTree->GetDataByHash(uHashID);
	if(pxStrippedEntry)
	{
		#ifdef ASURA_SUPPORT_PLATFORM_HASHED_FILENAMES
		//Check for a platform specific file
		int iPlatform = Tools_PlatformMesh_System::GetPlatform();
		if(iPlatform < 0) iPlatform = TOOLS_PLATFORM_PC;//Use PC if showing default meshes
		if(pxStrippedEntry->m_aiPlatformIndex[iPlatform] != -1)
		{
			return s_pxFullFilenameList->GetName(pxStrippedEntry->m_aiPlatformIndex[iPlatform]).GetString();
		}
		#endif

		if(pxStrippedEntry->m_iFullIndex != -1)
		{
			return s_pxFullFilenameList->GetName(pxStrippedEntry->m_iFullIndex).GetString();
		}
	}


	if(s_bUsingSavedHashedFilenames)
	{
		//failed to find file  , but are using saved values for filenames.
		//try updating the filename list , and see if we can find it now
    	Platform_BuildHashToFilenameLookupTree();
		return GetFullFilename(uHashID,bAssertOnFail);
	}

	if( bAssertOnFail )
	{
		Asura_Assertf( false, ("Failed to get filename for hash ID 0x%08X.", uHashID) );
	}

    return NULL;
}

bool Asura_HashedFilename_System::DoesStrippedFilenameHavePlatformVersions(Asura_Hash_ID uHashID)
{
	#ifdef ASURA_SUPPORT_PLATFORM_HASHED_FILENAMES
	if(!uHashID) return false;

	Asura_StrippedHashedFilename* pxStrippedEntry = s_pxStrippedFilenameTree->GetDataByHash(uHashID);
	if(pxStrippedEntry)
	{
		return pxStrippedEntry->m_bHasPlatformVersions;
	}


	if(s_bUsingSavedHashedFilenames)
	{
		//failed to find file  , but are using saved values for filenames.
		//try updating the filename list , and see if we can find it now
    	Platform_BuildHashToFilenameLookupTree();
		return DoesStrippedFilenameHavePlatformVersions(uHashID);
	}

	#endif //ASURA_SUPPORT_PLATFORM_HASHED_FILENAMES

	return false;	
}

bool Asura_HashedFilename_System::DoesStrippedFilenameHaveVersionForPlatform(Asura_Hash_ID uHashID,int iPlatform)
{
	#ifdef ASURA_SUPPORT_PLATFORM_HASHED_FILENAMES
	if(!uHashID) return false;
	Asura_Assert(iPlatform>=0 && iPlatform<TOOLS_PLATFORM_NUM_PLATFORMS,"");

	Asura_StrippedHashedFilename* pxStrippedEntry = s_pxStrippedFilenameTree->GetDataByHash(uHashID);
	if(pxStrippedEntry)
	{
		return pxStrippedEntry->m_aiPlatformIndex[iPlatform] != -1;
	}

	if(s_bUsingSavedHashedFilenames)
	{
		//failed to find file  , but are using saved values for filenames.
		//try updating the filename list , and see if we can find it now
    	Platform_BuildHashToFilenameLookupTree();
		return DoesStrippedFilenameHaveVersionForPlatform(uHashID,iPlatform);
	}

	#endif //ASURA_SUPPORT_PLATFORM_HASHED_FILENAMES

	return false;	
}

void Asura_HashedFilename_System::RebuildTreeIfUsingSavedData()
{
	if(s_bUsingSavedHashedFilenames)
	{
    	Platform_BuildHashToFilenameLookupTree();
	}
}

#endif // #ifdef ASURA_ENABLE_HASH_TO_FILENAME_LOOKUPS

// F U N C T I O N   I M P L E M E N T A T I O N S ///////////////////////////

Asura_Hash_ID Asura_ConcatenateHashID( const Asura_Hash_ID uInitialHash, const char* szString )
{
	if(!szString) return uInitialHash;
	Asura_Hash_ID uHash = uInitialHash;

	char cThisChar;
	for( ; *szString ; ++szString )
	{
		cThisChar = *szString;

		if( cThisChar >= 'A' && cThisChar <= 'Z' )
		{
			cThisChar = ASURA_MKLOWER( cThisChar );
		} 
		else if( cThisChar == '\\' )
		{
			// Convert back slash '\\' to forward slash '/'.
			cThisChar = '/';
		}

		uHash = ( uHash << 5 ) - uHash + cThisChar;
	}

	return uHash;
}

Asura_Hash_ID Asura_GetHashID( const char* szString )
{
	if(!szString) return ASURA_HASH_ID_UNSET;

#if defined(ASURA_UNIVERSAL_HASH_LOOKUP)
	// ARC - 12 September 2006 12:04:15 - make a copy of the string pointer
	const char* szOriginalString = szString;
#endif // #if defined(ASURA_UNIVERSAL_HASH_LOOKUP)

	Asura_Hash_ID uHash = Asura_ConcatenateHashID( 0, szString );

#if defined(ASURA_UNIVERSAL_HASH_LOOKUP)
	// ARC - 12 September 2006 15:30:01 - Store the hashes/strings at runtime
	Asura_Universal_HashLookup::AddName(szOriginalString, uHash);
#endif // #if defined(ASURA_UNIVERSAL_HASH_LOOKUP)
	return uHash;
}

Asura_Hash_ID Asura_GetHashID_IgnoreExtension( const char* szString )
{
	if(!szString) return ASURA_HASH_ID_UNSET;

#if defined(ASURA_UNIVERSAL_HASH_LOOKUP)
	// ARC - 12 September 2006 12:04:15 - make a copy of the string pointer
	const char* szOriginalString = szString;
#endif // #if defined(ASURA_UNIVERSAL_HASH_LOOKUP)

	// get a pointer to the final dot
    const char* pcFinalDot   = NULL;
    const char* pcStringIter = szString;
    while ( *pcStringIter )
    {
        if ( *pcStringIter == '.' )
        {
            pcFinalDot = pcStringIter;
        }
        ++pcStringIter;
    }

	Asura_Hash_ID uHash = 0;
	char cThisChar;
	for( ; *szString && szString != pcFinalDot; ++szString )
	{
		cThisChar = *szString;
		
		if( cThisChar >= 'A' && cThisChar <= 'Z' )
		{
			cThisChar = ASURA_MKLOWER( cThisChar );
		} 
		else if( cThisChar == '\\' )
		{
			// Convert back slash '\\' to forward slash '/'.
			cThisChar = '/';
		}

		uHash = ( uHash << 5 ) - uHash + cThisChar;
	}

#if defined(ASURA_UNIVERSAL_HASH_LOOKUP)
	// ARC - 12 September 2006 15:30:01 - Store the hashes/strings at runtime
	char szBuffer[256];
	Asura_Mem::Set(szBuffer, 0, sizeof(szBuffer));
	int iLen = ( pcFinalDot != NULL ) ? pcFinalDot - szOriginalString : strlen( szOriginalString );
	iLen = Asura_Maths::Min( iLen, 255 );
	Asura_PrintfToBfr( szBuffer, "%s", iLen, false, szOriginalString);
	Asura_Universal_HashLookup::AddName(szBuffer, uHash);
#endif // #if defined(ASURA_UNIVERSAL_HASH_LOOKUP)

	return uHash;
}

Asura_Hash_ID Asura_GetHashID_Binary( const void* pvData, u_int uNumBytes )
{
    // Start with an arbitary value. This deals with the case of the input
    // data being all zeros, since otherwise such data would always hash to
    // an output value of zero regardless of length. This is the reason for
    // the inconsistency between this function and Asura_GetHashID(), as
    // noted in the header.
	Asura_Hash_ID uHash = 0x9E3779B9u;

    if( pvData && uNumBytes )
    {
        const u_char* pucData = static_cast<const u_char*>( pvData );
	    while( uNumBytes-- )
	    {
		    uHash = (uHash << 5u) - uHash + *pucData++;
	    }
    }

	return uHash;
}

#if defined(ASURA_UNIVERSAL_HASH_LOOKUP)
#include "Core/Asura_HashTree.h"

// DLH - 27 April 2007 13:08:36 - Global instance that gets destoryed on shutdown. In batch mode the System Init and DeInit routines are not run so the memory that this has allocated never gets deallocated. This makes sure that the Shutdown is called.
class Asura_Universal_HashLookup_Instance
{
public:
	~Asura_Universal_HashLookup_Instance();
};
static Asura_Universal_HashLookup_Instance s_xUniversalHashLookup;

Asura_Universal_HashLookup_Instance::~Asura_Universal_HashLookup_Instance()
{
	Asura_Universal_HashLookup::Shutdown();
}

// ARC - 12 September 2006 12:58:08 - Universal hash lookup
Asura_TemplateHashTree<const char*>* Asura_Universal_HashLookup::s_pxHashTree = NULL;
bool Asura_Universal_HashLookup::s_bInitialised = false;
// ARC - 20 April 2007 - If the system has been shut down, *don't* re-init or you'll get memory leaks
bool Asura_Universal_HashLookup::s_bHasShutDown = false;

void Asura_Universal_HashLookup::Initialise()
{
	if (s_bInitialised == true || s_bHasShutDown == true)
	{
		return;
	}

	s_pxHashTree = new Asura_TemplateHashTree<const char*>;
	s_bInitialised = true;
}

void Asura_Universal_HashLookup::Shutdown()
{
	if( true == Asura_Universal_HashLookup::s_bHasShutDown)
	{
		return;
	}
	
	s_pxHashTree->Traverse(DeleteStrings);
	delete s_pxHashTree;
	s_pxHashTree = NULL;
	s_bInitialised = false;
	s_bHasShutDown = true;
}

void Asura_Universal_HashLookup::AddName(const char* szString, Asura_Hash_ID uHashID)
{
	if (s_bHasShutDown == true)
	{
		return;
	}

	if (s_bInitialised != true)
	{
		Initialise();
	}

	if (strlen(szString) == 0 || uHashID == 0)
	{
		return;
	}

	const char** pszString = s_pxHashTree->GetDataByHash(uHashID);
	if (pszString != NULL)
	{
		return;
	}

	int iLen = strlen(szString) + 1;
	char* szNewString = new char[iLen];
	Asura_Assert(szNewString != NULL, "Asura_Universal_HashLookup::AddName() No more memory to add hash strings!");
	Asura_PrintfToBfr(szNewString, "%s", iLen, true, szString);

	s_pxHashTree->AddNode(uHashID, szNewString);
}

const char* Asura_Universal_HashLookup::GetName(Asura_Hash_ID uHashID, bool bAssertOnFail /* = true */)
{
	if (s_bInitialised != true)
	{
		return NULL;
	}

	const char** pszString = s_pxHashTree->GetDataByHash(uHashID);
	if (pszString != NULL)
	{
		return *pszString;
	}

	if (pszString == NULL && bAssertOnFail == true)
	{
		Asura_Assert(false, "Asura_Universal_HashLookup::GetName() Unable to resolve hash");
	}

	return NULL;
}

void Asura_Universal_HashLookup::DeleteStrings(Asura_Hash_ID uHash, const char** pxData)
{
	const char* szString = *pxData;
	delete[] szString;
}
#endif // ASURA_UNIVERSAL_HASH_LOOKUP
