/*****************************************************************************/
/* CascFindFile.cpp                       Copyright (c) Ladislav Zezula 2014 */
/*---------------------------------------------------------------------------*/
/* System-dependent directory functions for CascLib                          */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 10.05.14  1.00  Lad  The first version of CascFindFile.cpp                */
/*****************************************************************************/

#define __CASCLIB_SELF__
#include "CascLib.h"
#include "CascCommon.h"

//-----------------------------------------------------------------------------
// Local functions

// Reset the search structure. Called before each search
static void ResetFindData(PCASC_FIND_DATA pFindData)
{
    // Reset the variables
    ZeroMemory16(pFindData->CKey);
    ZeroMemory16(pFindData->EKey);
    pFindData->szFileName[0] = 0;
    pFindData->szPlainName = pFindData->szFileName;
    pFindData->TagBitMask = 0;
    pFindData->FileSize = CASC_INVALID_SIZE64;
    pFindData->dwFileDataId = CASC_INVALID_ID;
    pFindData->dwLocaleFlags = CASC_INVALID_ID;
    pFindData->dwContentFlags = CASC_INVALID_ID;
    pFindData->dwSpanCount = 1;
    pFindData->NameType = CascNameFull;
    pFindData->bFileAvailable = false;
}

static void SupplyFakeFileName(PCASC_FIND_DATA pFindData, PCASC_CKEY_ENTRY pCKeyEntry)
{
    // If there is a file data ID, create fake file name
    if(pFindData->dwFileDataId != CASC_INVALID_ID)
    {
        CascStrPrintf(pFindData->szFileName, _countof(pFindData->szFileName), CASC_FILEID_FORMAT, pFindData->dwFileDataId);
        pFindData->NameType = CascNameDataId;
        return;
    }

    // If there is a CKey, convert the CKey to file name
    if(pCKeyEntry->Flags & CASC_CE_HAS_CKEY)
    {
        StringFromBinary(pFindData->CKey, MD5_HASH_SIZE, pFindData->szFileName);
        pFindData->NameType = CascNameCKey;
        return;
    }

    // EKey should be always present
    if(pCKeyEntry->Flags & CASC_CE_HAS_EKEY)
    {
        StringFromBinary(pFindData->EKey, MD5_HASH_SIZE, pFindData->szFileName);
        pFindData->NameType = CascNameEKey;
        return;
    }

    assert(false);
}

static bool CopyCKeyEntryToFindData(PCASC_FIND_DATA pFindData, PCASC_CKEY_ENTRY pCKeyEntry)
{
    ULONGLONG ContentSize = 0;
    ULONGLONG EncodedSize = 0;

    // Supply both keys
    CopyMemory16(pFindData->CKey, pCKeyEntry->CKey);
    CopyMemory16(pFindData->EKey, pCKeyEntry->EKey);

    // Supply the tag mask
    pFindData->TagBitMask = pCKeyEntry->TagBitMask;
    
    // Supply the plain name. Only do that if the found name is not a CKey/EKey
    if(pFindData->szFileName[0] != 0)
        pFindData->szPlainName = (char *)GetPlainFileName(pFindData->szFileName);

    // If we retrieved the file size directly from the root provider, use it
    // Otherwise, supply EncodedSize or ContentSize, whichever is available (but ContentSize > EncodedSize)
    pFindData->dwSpanCount = GetFileSpanInfo(pCKeyEntry, &ContentSize, &EncodedSize);
    if(pFindData->FileSize == CASC_INVALID_SIZE64)
    {
        if(ContentSize != CASC_INVALID_SIZE64)
            pFindData->FileSize = ContentSize;
        else 
            pFindData->FileSize = EncodedSize;
    }

    // Set flag indicating that the file is locally available
    pFindData->bFileAvailable = (pCKeyEntry->Flags & CASC_CE_FILE_IS_LOCAL);

    // Supply a fake file name, if there is none supplied by the root handler
    if(pFindData->szFileName[0] == 0)
        SupplyFakeFileName(pFindData, pCKeyEntry);
    return true;
}

// Perform searching using root-specific provider.
// The provider may need the listfile
static bool DoStorageSearch_RootFile(TCascSearch * pSearch, PCASC_FIND_DATA pFindData)
{
    PCASC_CKEY_ENTRY pCKeyEntry;
    TCascStorage * hs = pSearch->hs;

    // Reset the search structure
    ResetFindData(pFindData);

    // Enumerate over all files
    for(;;)
    {
        // Attempt to find (the next) file from the root handler
        pCKeyEntry = hs->pRootHandler->Search(pSearch, pFindData);
        if(pCKeyEntry == NULL)
            return false;

        // The entry is expected to be referenced by the root directory
        assert(pCKeyEntry->RefCount != 0);

        // Copy the CKey entry to the find data and return it
        return CopyCKeyEntryToFindData(pFindData, pCKeyEntry);
    }
}

static bool DoStorageSearch_CKey(TCascSearch * pSearch, PCASC_FIND_DATA pFindData)
{
    PCASC_CKEY_ENTRY pCKeyEntry;
    TCascStorage * hs = pSearch->hs;
    size_t nTotalItems = hs->CKeyArray.ItemCount();

    // Reset the find data structure
    ResetFindData(pFindData);

    // Check for CKeys that haven't been found yet
    while(pSearch->nFileIndex < nTotalItems)
    {
        // Locate the n-th CKey entry.
        pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.ItemAt(pSearch->nFileIndex++);
        //BREAK_ON_XKEY3(pCKeyEntry->CKey, 0x2B, 0xfc, 0xe4);

        // Only report files that are unreferenced by the ROOT handler
        if(pCKeyEntry->IsFile() && pCKeyEntry->RefCount == 0)
        {
            return CopyCKeyEntryToFindData(pFindData, pCKeyEntry);
        }
    }

    // Nameless search ended
    return false;
}

static bool DoStorageSearch(TCascSearch * pSearch, PCASC_FIND_DATA pFindData)
{
    // State 0: No search done yet
    if(pSearch->nSearchState == 0)
    {
        // Does the search specify listfile?
        if(pSearch->szListFile != NULL)
            pSearch->pCache = ListFile_OpenExternal(pSearch->szListFile);

        // Move the search phase to the listfile searching
        pSearch->nSearchState = 1;
        pSearch->nFileIndex = 0;
    }

    // State 1: Searching the list file
    if(pSearch->nSearchState == 1)
    {
        if(DoStorageSearch_RootFile(pSearch, pFindData))
            return true;

        // Move to the nameless search state
        pSearch->nSearchState = 2;
        pSearch->nFileIndex = 0;
    }

    // State 2: Searching the remaining entries by CKey
    if(pSearch->nSearchState == 2 && (pSearch->szMask == NULL || !strcmp(pSearch->szMask, "*")))
    {
        if(DoStorageSearch_CKey(pSearch, pFindData))
            return true;

        // Move to the final search state
        pSearch->nSearchState++;
        pSearch->nFileIndex = 0;
    }

    return false;
}

//-----------------------------------------------------------------------------
// Public functions

HANDLE WINAPI CascFindFirstFile(
    HANDLE hStorage,
    LPCSTR szMask,
    PCASC_FIND_DATA pFindData,
    LPCTSTR szListFile)
{
    TCascStorage * hs;
    TCascSearch * pSearch = NULL;
    DWORD dwErrCode = ERROR_SUCCESS;

    // Check parameters
    if((hs = TCascStorage::IsValid(hStorage)) == NULL)
        dwErrCode = ERROR_INVALID_HANDLE;
    if(pFindData == NULL)
        dwErrCode = ERROR_INVALID_PARAMETER;

    // Supply default mask, if needed
    if(szMask == NULL || szMask[0] == 0)
        szMask = "*";

    // Init the search structure and search handle
    if(dwErrCode == ERROR_SUCCESS)
    {
        // Allocate the search handle
        pSearch = new TCascSearch(hs, szListFile, szMask);
        if(pSearch == NULL)
            dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
    }

    // Perform search
    if(dwErrCode == ERROR_SUCCESS)
    {
        if(!DoStorageSearch(pSearch, pFindData))
            dwErrCode = ERROR_NO_MORE_FILES;
    }

    if(dwErrCode != ERROR_SUCCESS)
    {
        delete pSearch;
        pSearch = (TCascSearch *)INVALID_HANDLE_VALUE;
    }

    return (HANDLE)pSearch;
}

bool WINAPI CascFindNextFile(
    HANDLE hFind,
    PCASC_FIND_DATA pFindData)
{
    TCascSearch * pSearch;

    pSearch = TCascSearch::IsValid(hFind);
    if(pSearch == NULL || pFindData == NULL)
    {
        SetCascError(ERROR_INVALID_PARAMETER);
        return false;
    }

    // Perform search
    return DoStorageSearch(pSearch, pFindData);
}

bool WINAPI CascFindClose(HANDLE hFind)
{
    TCascSearch * pSearch;

    pSearch = TCascSearch::IsValid(hFind);
    if(pSearch == NULL)
    {
        SetCascError(ERROR_INVALID_PARAMETER);
        return false;
    }

    delete pSearch;
    return true;
}
