/*****************************************************************************/
/* SListFile.cpp                          Copyright (c) Ladislav Zezula 2004 */
/*---------------------------------------------------------------------------*/
/* Description:                                                              */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 12.06.04  1.00  Lad  The first version of SListFile.cpp                   */
/*****************************************************************************/

#define __STORMLIB_SELF__
#include "StormLib.h"
#include "StormCommon.h"
#include <assert.h>

//-----------------------------------------------------------------------------
// Listfile entry structure

#define CACHE_BUFFER_SIZE  0x1000       // Size of the cache buffer
#define MAX_LISTFILE_SIZE  0x8000000    // Maximum accepted listfile size is 128 MB

union TListFileHandle
{
    TFileStream * pStream;              // Opened local file
    HANDLE hFile;                       // Opened MPQ file
};

struct TListFileCache
{
    char * szWildCard;                  // Self-relative pointer to file mask
    LPBYTE pBegin;                      // The begin of the listfile cache
    LPBYTE pPos;                        // Current position in the cache
    LPBYTE pEnd;                        // The last character in the file cache
    DWORD dwFlags;                      // Flags from TMPQArchive

//  char szWildCard[wildcard_length];   // Followed by the name mask (if any)
//  char szListFile[listfile_length];   // Followed by the listfile (if any)
};

typedef bool (*LOAD_LISTFILE)(TListFileHandle * pHandle, void * pvBuffer, DWORD cbBuffer, LPDWORD pdwBytesRead);

//-----------------------------------------------------------------------------
// Local functions (cache)

// In SFileFindFile.cll
bool SFileCheckWildCard(const char * szString, const char * szWildCard);

static char * CopyListLine(char * szListLine, const char * szFileName)
{
    // Copy the string
    while(szFileName[0] != 0)
        *szListLine++ = *szFileName++;

    // Append the end-of-line
    *szListLine++ = 0x0D;
    *szListLine++ = 0x0A;
    return szListLine;
}

static bool LoadListFile_Stream(TListFileHandle * pHandle, void * pvBuffer, DWORD cbBuffer, LPDWORD pdwBytesRead)
{
    ULONGLONG ByteOffset = 0;
    bool bResult;

    bResult = FileStream_Read(pHandle->pStream, &ByteOffset, pvBuffer, cbBuffer);
    if(bResult)
        *pdwBytesRead = cbBuffer;
    return bResult;
}

static bool LoadListFile_MPQ(TListFileHandle * pHandle, void * pvBuffer, DWORD cbBuffer, LPDWORD pdwBytesRead)
{
    return SFileReadFile(pHandle->hFile, pvBuffer, cbBuffer, pdwBytesRead, NULL);
}

static bool FreeListFileCache(TListFileCache * pCache)
{
    // Valid parameter check
    if(pCache != NULL)
        STORM_FREE(pCache);
    return true;
}

static TListFileCache * CreateListFileCache(
    LOAD_LISTFILE PfnLoadFile,
    TListFileHandle * pHandle,
    const char * szWildCard,
    DWORD dwFileSize,
    DWORD dwMaxSize,
    DWORD dwFlags)
{
    TListFileCache * pCache = NULL;
    size_t cchWildCardAligned = 0;
    size_t cchWildCard = 0;
    DWORD dwBytesRead = 0;

    // Get the amount of bytes that need to be allocated
    if(dwFileSize == 0 || dwFileSize > dwMaxSize)
        return NULL;

    // Append buffer for name mask, if any
    if(szWildCard != NULL)
    {
        cchWildCard = strlen(szWildCard) + 1;
        cchWildCardAligned = (cchWildCard + 3) & 0xFFFFFFFC;
    }

    // Allocate cache for one file block
    pCache = (TListFileCache *)STORM_ALLOC(BYTE, sizeof(TListFileCache) + cchWildCardAligned + dwFileSize + 1);
    if(pCache != NULL)
    {
        // Clear the entire structure
        memset(pCache, 0, sizeof(TListFileCache) + cchWildCard);
        pCache->dwFlags = dwFlags;

        // Shall we copy the mask?
        if(cchWildCard != 0)
        {
            pCache->szWildCard = (char *)(pCache + 1);
            memcpy(pCache->szWildCard, szWildCard, cchWildCard);
        }

        // Fill-in the rest of the cache pointers
        pCache->pBegin = (LPBYTE)(pCache + 1) + cchWildCardAligned;

        // Load the entire listfile to the cache
        PfnLoadFile(pHandle, pCache->pBegin, dwFileSize, &dwBytesRead);
        if(dwBytesRead != 0)
        {
            // Allocate pointers
            pCache->pPos = pCache->pBegin;
            pCache->pEnd = pCache->pBegin + dwBytesRead;
        }
        else
        {
            FreeListFileCache(pCache);
            pCache = NULL;
        }
    }

    // Return the cache
    return pCache;
}

static TListFileCache * CreateListFileCache(
    HANDLE hMpq,
    const TCHAR * szListFile,
    const char * szWildCard,
    DWORD dwMaxSize,
    DWORD dwFlags)
{
    TListFileCache * pCache = NULL;
    TListFileHandle ListHandle = {NULL};

    // Put default value to dwMaxSize
    if(dwMaxSize == 0)
        dwMaxSize = MAX_LISTFILE_SIZE;

    // Internal listfile: hMPQ must be non NULL and szListFile must be NULL.
    // We load the MPQ::(listfile) file
    if(hMpq != NULL && szListFile == NULL)
    {
        DWORD dwFileSize = 0;

        // Open the file from the MPQ
        if(SFileOpenFileEx(hMpq, LISTFILE_NAME, 0, &ListHandle.hFile))
        {
            // Get the file size and create the listfile cache
            dwFileSize = SFileGetFileSize(ListHandle.hFile, NULL);
            pCache = CreateListFileCache(LoadListFile_MPQ, &ListHandle, szWildCard, dwFileSize, dwMaxSize, dwFlags);

            // Close the MPQ file
            SFileCloseFile(ListHandle.hFile);
        }

        // Return the loaded cache
        return pCache;
    }

    // External listfile: hMpq must be NULL and szListFile must be non-NULL.
    // We load the file using TFileStream
    if(hMpq == NULL && szListFile != NULL)
    {
        ULONGLONG FileSize = 0;

        // Open the local file
        ListHandle.pStream = FileStream_OpenFile(szListFile, STREAM_FLAG_READ_ONLY);
        if(ListHandle.pStream != NULL)
        {
            // Verify the file size
            FileStream_GetSize(ListHandle.pStream, &FileSize);
            if(0 < FileSize && FileSize < dwMaxSize)
            {
                pCache = CreateListFileCache(LoadListFile_Stream, &ListHandle, szWildCard, (DWORD)FileSize, dwMaxSize, dwFlags);
            }

            // Close the stream
            FileStream_Close(ListHandle.pStream);
        }

        // Return the loaded cache
        return pCache;
    }

    // This combination should never happen
    SetLastError(ERROR_INVALID_PARAMETER);
    assert(false);
    return NULL;
}

#ifdef _DEBUG
/*
TMPQNameCache * CreateNameCache(HANDLE hListFile, const char * szSearchMask)
{
    TMPQNameCache * pNameCache;
    char * szCachePointer;
    size_t cbToAllocate;
    size_t nMaskLength = 1;
    DWORD dwBytesRead = 0;
    DWORD dwFileSize;

    // Get the size of the listfile. Ignore zero or too long ones
    dwFileSize = SFileGetFileSize(hListFile, NULL);
    if(dwFileSize == 0 || dwFileSize > MAX_LISTFILE_SIZE)
        return NULL;

    // Get the length of the search mask
    if(szSearchMask == NULL)
        szSearchMask = "*";
    nMaskLength = strlen(szSearchMask) + 1;

    // Allocate the name cache
    cbToAllocate = sizeof(TMPQNameCache) + nMaskLength + dwFileSize + 1;
    pNameCache = (TMPQNameCache *)STORM_ALLOC(BYTE, cbToAllocate);
    if(pNameCache != NULL)
    {
        // Initialize the name cache
        memset(pNameCache, 0, sizeof(TMPQNameCache));
        pNameCache->TotalCacheSize = (DWORD)(nMaskLength + dwFileSize + 1);
        szCachePointer = (char *)(pNameCache + 1);

        // Copy the search mask, if any
        memcpy(szCachePointer, szSearchMask, nMaskLength);
        pNameCache->FirstNameOffset = (DWORD)nMaskLength;
        pNameCache->FreeSpaceOffset = (DWORD)nMaskLength;

        // Read the listfile itself
        SFileSetFilePointer(hListFile, 0, NULL, FILE_BEGIN);
        SFileReadFile(hListFile, szCachePointer + nMaskLength, dwFileSize, &dwBytesRead, NULL);

        // If nothing has been read from the listfile, clear the cache
        if(dwBytesRead == 0)
        {
            STORM_FREE(pNameCache);
            return NULL;
        }

        // Move the free space offset
        pNameCache->FreeSpaceOffset = pNameCache->FirstNameOffset + dwBytesRead + 1;
        szCachePointer[nMaskLength + dwBytesRead] = 0;
    }

    return pNameCache;
}

static void FreeNameCache(TMPQNameCache * pNameCache)
{
    if(pNameCache != NULL)
        STORM_FREE(pNameCache);
    pNameCache = NULL;
}
*/
#endif  // _DEBUG

static char * ReadListFileLine(TListFileCache * pCache, size_t * PtrLength)
{
    LPBYTE pbLineBegin;
    LPBYTE pbLineEnd;

    // Skip newlines. Keep spaces and tabs, as they can be a legal part of the file name
    while(pCache->pPos < pCache->pEnd && (pCache->pPos[0] == 0x0A || pCache->pPos[0] == 0x0D))
        pCache->pPos++;

    // Set the line begin and end
    if(pCache->pPos >= pCache->pEnd)
        return NULL;
    pbLineBegin = pbLineEnd = pCache->pPos;

    // Find the end of the line
    while(pCache->pPos < pCache->pEnd && pCache->pPos[0] != 0x0A && pCache->pPos[0] != 0x0D)
        pCache->pPos++;

    // Remember the end of the line
    pbLineEnd = pCache->pPos++;
    pbLineEnd[0] = 0;

    // Give the line to the caller
    if(PtrLength != NULL)
        PtrLength[0] = (size_t)(pbLineEnd - pbLineBegin);
    return (char *)pbLineBegin;
}

static int STORMLIB_CDECL CompareFileNodes(const void * p1, const void * p2)
{
    char * szFileName1 = *(char **)p1;
    char * szFileName2 = *(char **)p2;

    return _stricmp(szFileName1, szFileName2);
}

static LPBYTE CreateListFile(TMPQArchive * ha, DWORD * pcbListFile)
{
    TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
    TFileEntry * pFileEntry;
    char ** SortTable = NULL;
    char * szListFile = NULL;
    char * szListLine;
    size_t nFileNodes = 0;
    size_t cbListFile = 0;
    size_t nIndex0;
    size_t nIndex1;

    // Allocate the table for sorting listfile
    SortTable = STORM_ALLOC(char*, ha->dwFileTableSize);
    if(SortTable == NULL)
        return NULL;

    // Construct the sort table
    // Note: in MPQs with multiple locale versions of the same file,
    // this code causes adding multiple listfile entries.
    // They will get removed after the listfile sorting
    for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
    {
        // Only take existing items
        if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) && pFileEntry->szFileName != NULL)
        {
            // Ignore pseudo-names and internal names
            if(!IsPseudoFileName(pFileEntry->szFileName, NULL) && !IsInternalMpqFileName(pFileEntry->szFileName))
            {
                SortTable[nFileNodes++] = pFileEntry->szFileName;
            }
        }
    }

    // Remove duplicities
    if(nFileNodes > 0)
    {
        // Sort the table
        qsort(SortTable, nFileNodes, sizeof(char *), CompareFileNodes);

        // Count the 0-th item
        cbListFile += strlen(SortTable[0]) + 2;

        // Walk through the items and only use the ones that are not duplicated
        for(nIndex0 = 0, nIndex1 = 1; nIndex1 < nFileNodes; nIndex1++)
        {
            // If the next file node is different, we will include it to the result listfile
            if(_stricmp(SortTable[nIndex1], SortTable[nIndex0]) != 0)
            {
                cbListFile += strlen(SortTable[nIndex1]) + 2;
                nIndex0 = nIndex1;
            }
        }

        // Now allocate buffer for the entire listfile
        szListFile = szListLine = STORM_ALLOC(char, cbListFile + 1);
        if(szListFile != NULL)
        {
            // Copy the 0-th item
            szListLine = CopyListLine(szListLine, SortTable[0]);

            // Walk through the items and only use the ones that are not duplicated
            for(nIndex0 = 0, nIndex1 = 1; nIndex1 < nFileNodes; nIndex1++)
            {
                // If the next file node is different, we will include it to the result listfile
                if(_stricmp(SortTable[nIndex1], SortTable[nIndex0]) != 0)
                {
                    // Copy the listfile line
                    szListLine = CopyListLine(szListLine, SortTable[nIndex1]);
                    nIndex0 = nIndex1;
                }
            }

            // Sanity check - does the size match?
            assert((size_t)(szListLine - szListFile) == cbListFile);
        }
    }
    else
    {
        szListFile = STORM_ALLOC(char, 1);
        cbListFile = 0;
    }

    // Free the sort table
    STORM_FREE(SortTable);

    // Give away the listfile
    if(pcbListFile != NULL)
        *pcbListFile = (DWORD)cbListFile;
    return (LPBYTE)szListFile;
}

//-----------------------------------------------------------------------------
// Local functions (listfile nodes)

// Adds a name into the list of all names. For each locale in the MPQ,
// one entry will be created
// If the file name is already there, does nothing.
static DWORD SListFileCreateNodeForAllLocales(TMPQArchive * ha, const char * szFileName)
{
    TFileEntry * pFileEntry;
    TMPQHash * pFirstHash;
    TMPQHash * pHashEnd;
    TMPQHash * pHash;
    DWORD dwName1;
    DWORD dwName2;

    // If we have HET table, use that one
    if(ha->pHetTable != NULL)
    {
        pFileEntry = GetFileEntryLocale(ha, szFileName, 0);
        if(pFileEntry != NULL)
        {
            // Allocate file name for the file entry
            AllocateFileName(ha, pFileEntry, szFileName);
        }

        return ERROR_SUCCESS;
    }

    // If we have hash table, we use it
    if(ha->pHashTable != NULL)
    {
        // Get the end of the hash table and both names
        pHashEnd = ha->pHashTable + ha->pHeader->dwHashTableSize;
        dwName1 = ha->pfnHashString(szFileName, MPQ_HASH_NAME_A);
        dwName2 = ha->pfnHashString(szFileName, MPQ_HASH_NAME_B);

        // Some protectors set very high hash table size (0x00400000 items or more)
        // in order to make this process very slow. We will ignore items
        // in the hash table that would be beyond the end of the file.
        // Example MPQ: MPQ_2022_v1_Sniper.scx
        if(ha->dwFlags & MPQ_FLAG_HASH_TABLE_CUT)
            pHashEnd = ha->pHashTable + (ha->dwRealHashTableSize / sizeof(TMPQHash));

        // Go through the hash table and put the name in each item that has the same name pair
        //for(pHash = ha->pHashTable; pHash < pHashEnd; pHash++)
        //{
        //    if(pHash->dwName1 == dwName1 && pHash->dwName2 == dwName2 && MPQ_BLOCK_INDEX(pHash) < ha->dwFileTableSize)
        //    {
        //        // Allocate file name for the file entry
        //        AllocateFileName(ha, ha->pFileTable + MPQ_BLOCK_INDEX(pHash), szFileName);
        //    }
        //}

        // Go while we found something
        pFirstHash = pHash = GetFirstHashEntry(ha, szFileName);
        while(pHash != NULL)
        {
           // Allocate file name for the file entry
           AllocateFileName(ha, ha->pFileTable + MPQ_BLOCK_INDEX(pHash), szFileName);

           // Now find the next language version of the file
           pHash = GetNextHashEntry(ha, pFirstHash, pHash);
        }

        return ERROR_SUCCESS;
    }

    return ERROR_CAN_NOT_COMPLETE;
}

// Saves the whole listfile to the MPQ
DWORD SListFileSaveToMpq(TMPQArchive * ha)
{
    TMPQFile * hf = NULL;
    LPBYTE pbListFile;
    DWORD cbListFile = 0;
    DWORD dwErrCode = ERROR_SUCCESS;

    // Only save the listfile if we should do so
    if(ha->dwFileFlags1 != 0)
    {
        // At this point, we expect to have at least one reserved entry in the file table
        assert(ha->dwFlags & MPQ_FLAG_LISTFILE_NEW);
        assert(ha->dwReservedFiles > 0);

        // Create the raw data that is to be written to (listfile)
        // Note: Creating the raw data before the (listfile) has been created in the MPQ
        // causes that the name of the listfile will not be included in the listfile itself.
        // That is OK, because (listfile) in Blizzard MPQs does not contain it either.
        pbListFile = CreateListFile(ha, &cbListFile);
        if(pbListFile != NULL)
        {
            // Determine the real flags for (listfile)
            if(ha->dwFileFlags1 == MPQ_FILE_DEFAULT_INTERNAL)
                ha->dwFileFlags1 = GetDefaultSpecialFileFlags(cbListFile, ha->pHeader->wFormatVersion);

            // Create the listfile in the MPQ
            dwErrCode = SFileAddFile_Init(ha, LISTFILE_NAME,
                                           0,
                                           cbListFile,
                                           LANG_NEUTRAL,
                                           ha->dwFileFlags1 | MPQ_FILE_REPLACEEXISTING,
                                          &hf);

            // Write the listfile raw data to it
            if(dwErrCode == ERROR_SUCCESS)
            {
                // Write the content of the listfile to the MPQ
                dwErrCode = SFileAddFile_Write(hf, pbListFile, cbListFile, MPQ_COMPRESSION_ZLIB);
                SFileAddFile_Finish(hf);
            }

            // Clear the listfile flags
            ha->dwFlags &= ~(MPQ_FLAG_LISTFILE_NEW | MPQ_FLAG_LISTFILE_NONE);
            ha->dwReservedFiles--;

            // Free the listfile buffer
            STORM_FREE(pbListFile);
        }
        else
        {
            // If the (listfile) file would be empty, its OK
            dwErrCode = (cbListFile == 0) ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_MEMORY;
        }
    }

    return dwErrCode;
}

static DWORD SFileAddArbitraryListFile(
    TMPQArchive * ha,
    HANDLE hMpq,
    const TCHAR * szListFile,
    DWORD dwMaxSize)
{
    TListFileCache * pCache = NULL;

    // Create the listfile cache for that file
    pCache = CreateListFileCache(hMpq, szListFile, NULL, dwMaxSize, ha->dwFlags);
    if(pCache != NULL)
    {
        char * szFileName;
        size_t nLength = 0;

        // Get the next line
        while((szFileName = ReadListFileLine(pCache, &nLength)) != NULL)
        {
            // Add the line to the MPQ
            if(nLength != 0)
                SListFileCreateNodeForAllLocales(ha, szFileName);
        }

        // Delete the cache
        FreeListFileCache(pCache);
    }

    return (pCache != NULL) ? ERROR_SUCCESS : ERROR_FILE_CORRUPT;
}

static DWORD SFileAddInternalListFile(
    TMPQArchive * ha,
    HANDLE hMpq)
{
    TMPQHash * pFirstHash;
    TMPQHash * pHash;
    LCID lcSaveLocale = g_lcFileLocale;
    DWORD dwMaxSize = MAX_LISTFILE_SIZE;
    DWORD dwErrCode = ERROR_SUCCESS;

    // If there is hash table, we need to support multiple listfiles
    // with different locales (BrooDat.mpq)
    if(ha->pHashTable != NULL)
    {
        // If the archive is a malformed map, ignore too large listfiles
        if(ha->dwFlags & MPQ_FLAG_MALFORMED)
            dwMaxSize = 0x40000;

        pFirstHash = pHash = GetFirstHashEntry(ha, LISTFILE_NAME);
        while(dwErrCode == ERROR_SUCCESS && pHash != NULL)
        {
            // Set the prefered locale to that from list file
            SFileSetLocale(SFILE_MAKE_LCID(pHash->Locale, pHash->Platform));

            // Add that listfile
            dwErrCode = SFileAddArbitraryListFile(ha, hMpq, NULL, dwMaxSize);

            // Move to the next hash
            pHash = GetNextHashEntry(ha, pFirstHash, pHash);
        }

        // Restore the original locale
        SFileSetLocale(lcSaveLocale);
    }
    else
    {
        // Add the single listfile
        dwErrCode = SFileAddArbitraryListFile(ha, hMpq, NULL, dwMaxSize);
    }

    // Return the result of the operation
    return dwErrCode;
}

static bool DoListFileSearch(TListFileCache * pCache, SFILE_FIND_DATA * lpFindFileData)
{
    // Check for the valid search handle
    if(pCache != NULL)
    {
        char * szFileName;
        size_t nLength = 0;

        // Get the next line
        while((szFileName = ReadListFileLine(pCache, &nLength)) != NULL)
        {
            // Check search mask
            if(nLength != 0 && SFileCheckWildCard(szFileName, pCache->szWildCard))
            {
                if(nLength >= sizeof(lpFindFileData->cFileName))
                    nLength = sizeof(lpFindFileData->cFileName) - 1;

                memcpy(lpFindFileData->cFileName, szFileName, nLength);
                lpFindFileData->cFileName[nLength] = 0;
                return true;
            }
        }
    }

    // No more files
    memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA));
    SetLastError(ERROR_NO_MORE_FILES);
    return false;
}

//-----------------------------------------------------------------------------
// File functions

// Adds a listfile into the MPQ archive.
DWORD WINAPI SFileAddListFile(HANDLE hMpq, const TCHAR * szListFile)
{
    TMPQArchive * ha = (TMPQArchive *)hMpq;
    DWORD dwErrCode = ERROR_SUCCESS;

    // Add the listfile for each MPQ in the patch chain
    while(ha != NULL)
    {
        if(szListFile != NULL)
            dwErrCode = SFileAddArbitraryListFile(ha, NULL, szListFile, MAX_LISTFILE_SIZE);
        else
            dwErrCode = SFileAddInternalListFile(ha, hMpq);

        // Also, add three special files to the listfile:
        // (listfile) itself, (attributes) and (signature)
        SListFileCreateNodeForAllLocales(ha, LISTFILE_NAME);
        SListFileCreateNodeForAllLocales(ha, SIGNATURE_NAME);
        SListFileCreateNodeForAllLocales(ha, ATTRIBUTES_NAME);

        // Move to the next archive in the chain
        ha = ha->haPatch;
    }

    return dwErrCode;
}

//-----------------------------------------------------------------------------
// Enumerating files in listfile

HANDLE WINAPI SListFileFindFirstFile(HANDLE hMpq, const TCHAR * szListFile, const char * szMask, SFILE_FIND_DATA * lpFindFileData)
{
    TListFileCache * pCache = NULL;

    // Initialize the structure with zeros
    memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA));

    // Open the local/internal listfile
    pCache = CreateListFileCache(hMpq, szListFile, szMask, 0, 0);
    if(pCache != NULL)
    {
        if(!DoListFileSearch(pCache, lpFindFileData))
        {
            memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA));
            SetLastError(ERROR_NO_MORE_FILES);
            FreeListFileCache(pCache);
            pCache = NULL;
        }
    }

    // Return the listfile cache as handle
    return (HANDLE)pCache;
}

bool WINAPI SListFileFindNextFile(HANDLE hFind, SFILE_FIND_DATA * lpFindFileData)
{
    return DoListFileSearch((TListFileCache *)hFind, lpFindFileData);
}

bool WINAPI SListFileFindClose(HANDLE hFind)
{
    TListFileCache * pCache = (TListFileCache *)hFind;

    return FreeListFileCache(pCache);
}

