/*****************************************************************************/
/* SFilePatchArchives.cpp                 Copyright (c) Ladislav Zezula 2010 */
/*---------------------------------------------------------------------------*/
/* Description:                                                              */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 18.08.10  1.00  Lad  The first version of SFilePatchArchives.cpp          */
/*****************************************************************************/

#define __STORMLIB_SELF__
#include "StormLib.h"
#include "StormCommon.h"

//-----------------------------------------------------------------------------
// Local structures

#define MAX_SC2_PATCH_PREFIX   0x80

#define PATCH_SIGNATURE_HEADER 0x48435450
#define PATCH_SIGNATURE_MD5    0x5f35444d
#define PATCH_SIGNATURE_XFRM   0x4d524658

#define SIZE_OF_XFRM_HEADER  0x0C

// Header for incremental patch files
typedef struct _MPQ_PATCH_HEADER
{
    //-- PATCH header -----------------------------------
    DWORD dwSignature;                          // 'PTCH'
    DWORD dwSizeOfPatchData;                    // Size of the entire patch (decompressed)
    DWORD dwSizeBeforePatch;                    // Size of the file before patch
    DWORD dwSizeAfterPatch;                     // Size of file after patch

    //-- MD5 block --------------------------------------
    DWORD dwMD5;                                // 'MD5_'
    DWORD dwMd5BlockSize;                       // Size of the MD5 block, including the signature and size itself
    BYTE md5_before_patch[0x10];                // MD5 of the original (unpached) file
    BYTE md5_after_patch[0x10];                 // MD5 of the patched file

    //-- XFRM block -------------------------------------
    DWORD dwXFRM;                               // 'XFRM'
    DWORD dwXfrmBlockSize;                      // Size of the XFRM block, includes XFRM header and patch data
    DWORD dwPatchType;                          // Type of patch ('BSD0' or 'COPY')

    // Followed by the patch data
} MPQ_PATCH_HEADER, *PMPQ_PATCH_HEADER;

typedef struct _BLIZZARD_BSDIFF40_FILE
{
    ULONGLONG Signature;
    ULONGLONG CtrlBlockSize;
    ULONGLONG DataBlockSize;
    ULONGLONG NewFileSize;
} BLIZZARD_BSDIFF40_FILE, *PBLIZZARD_BSDIFF40_FILE;

typedef struct _BSDIFF_CTRL_BLOCK
{
    DWORD dwAddDataLength;
    DWORD dwMovDataLength;
    DWORD dwOldMoveLength;

} BSDIFF_CTRL_BLOCK, *PBSDIFF_CTRL_BLOCK;

typedef struct _LOCALIZED_MPQ_INFO
{
    const char * szNameTemplate;            // Name template
    size_t nLangOffset;                     // Offset of the language
    size_t nLength;                         // Length of the name template
} LOCALIZED_MPQ_INFO, *PLOCALIZED_MPQ_INFO;

//-----------------------------------------------------------------------------
// Local variables

// 4-byte groups for all languages
static const char * LanguageList = "baseteenenUSenGBenCNenTWdeDEesESesMXfrFRitITkoKRptBRptPTruRUzhCNzhTW";

// List of localized MPQs for World of Warcraft
static LOCALIZED_MPQ_INFO LocaleMpqs_WoW[] =
{
    {"expansion1-locale-####", 18, 22},
    {"expansion1-speech-####", 18, 22},
    {"expansion2-locale-####", 18, 22},
    {"expansion2-speech-####", 18, 22},
    {"expansion3-locale-####", 18, 22},
    {"expansion3-speech-####", 18, 22},
    {"locale-####",             7, 11},
    {"speech-####",             7, 11},
    {NULL, 0, 0}
};

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

static inline bool IsPatchMetadataFile(TFileEntry * pFileEntry)
{
    // The file must ave a name
    if(pFileEntry->szFileName != NULL && (pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) == 0)
    {
        // The file must be small
        if(0 < pFileEntry->dwFileSize && pFileEntry->dwFileSize < 0x40)
        {
            // Compare the plain name
            return (_stricmp(GetPlainFileName(pFileEntry->szFileName), PATCH_METADATA_NAME) == 0);
        }
    }

    // Not a patch_metadata
    return false;
}

static void Decompress_RLE(LPBYTE pbDecompressed, DWORD cbDecompressed, LPBYTE pbCompressed, DWORD cbCompressed)
{
    LPBYTE pbDecompressedEnd = pbDecompressed + cbDecompressed;
    LPBYTE pbCompressedEnd = pbCompressed + cbCompressed;
    BYTE RepeatCount;
    BYTE OneByte;

    // Cut the initial DWORD from the compressed chunk
    pbCompressed += sizeof(DWORD);

    // Pre-fill decompressed buffer with zeros
    memset(pbDecompressed, 0, cbDecompressed);

    // Unpack
    while(pbCompressed < pbCompressedEnd && pbDecompressed < pbDecompressedEnd)
    {
        OneByte = *pbCompressed++;

        // Is it a repetition byte ?
        if(OneByte & 0x80)
        {
            RepeatCount = (OneByte & 0x7F) + 1;
            for(BYTE i = 0; i < RepeatCount; i++)
            {
                if(pbDecompressed == pbDecompressedEnd || pbCompressed == pbCompressedEnd)
                    break;

                *pbDecompressed++ = *pbCompressed++;
            }
        }
        else
        {
            pbDecompressed += (OneByte + 1);
        }
    }
}

static DWORD LoadFilePatch_COPY(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch)
{
    DWORD cbBytesToRead = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER);
    DWORD cbBytesRead = 0;

    // Simply load the rest of the patch
    SFileReadFile((HANDLE)hf, (pFullPatch + 1), cbBytesToRead, &cbBytesRead, NULL);
    return (cbBytesRead == cbBytesToRead) ? ERROR_SUCCESS : ERROR_FILE_CORRUPT;
}

static DWORD LoadFilePatch_BSD0(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch)
{
    LPBYTE pbDecompressed = (LPBYTE)(pFullPatch + 1);
    LPBYTE pbCompressed = NULL;
    DWORD cbDecompressed = 0;
    DWORD cbCompressed = 0;
    DWORD dwBytesRead = 0;
    DWORD dwErrCode = ERROR_SUCCESS;

    // Calculate the size of compressed data
    cbDecompressed = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER);
    cbCompressed = pFullPatch->dwXfrmBlockSize - SIZE_OF_XFRM_HEADER;

    // Is that file compressed?
    if(cbCompressed < cbDecompressed)
    {
        pbCompressed = STORM_ALLOC(BYTE, cbCompressed);
        if(pbCompressed == NULL)
            dwErrCode = ERROR_NOT_ENOUGH_MEMORY;

        // Read the compressed patch data
        if(dwErrCode == ERROR_SUCCESS)
        {
            SFileReadFile((HANDLE)hf, pbCompressed, cbCompressed, &dwBytesRead, NULL);
            if(dwBytesRead != cbCompressed)
                dwErrCode = ERROR_FILE_CORRUPT;
        }

        // Decompress the data
        if(dwErrCode == ERROR_SUCCESS)
            Decompress_RLE(pbDecompressed, cbDecompressed, pbCompressed, cbCompressed);

        if(pbCompressed != NULL)
            STORM_FREE(pbCompressed);
    }
    else
    {
        SFileReadFile((HANDLE)hf, pbDecompressed, cbDecompressed, &dwBytesRead, NULL);
        if(dwBytesRead != cbDecompressed)
            dwErrCode = ERROR_FILE_CORRUPT;
    }

    return dwErrCode;
}

static DWORD ApplyFilePatch_COPY(
    TMPQPatcher * pPatcher,
    PMPQ_PATCH_HEADER pFullPatch,
    LPBYTE pbTarget,
    LPBYTE pbSource)
{
    // Sanity checks
    assert(pPatcher->cbMaxFileData >= pPatcher->cbFileData);
    pFullPatch = pFullPatch;

    // Copy the patch data as-is
    memcpy(pbTarget, pbSource, pPatcher->cbFileData);
    return ERROR_SUCCESS;
}

static DWORD ApplyFilePatch_BSD0(
    TMPQPatcher * pPatcher,
    PMPQ_PATCH_HEADER pFullPatch,
    LPBYTE pbTarget,
    LPBYTE pbSource)
{
    PBLIZZARD_BSDIFF40_FILE pBsdiff;
    PBSDIFF_CTRL_BLOCK pCtrlBlock;
    LPBYTE pbPatchData = (LPBYTE)(pFullPatch + 1);
    LPBYTE pDataBlock;
    LPBYTE pExtraBlock;
    LPBYTE pbOldData = pbSource;
    LPBYTE pbNewData = pbTarget;
    DWORD dwCombineSize;
    DWORD dwNewOffset = 0;                          // Current position to patch
    DWORD dwOldOffset = 0;                          // Current source position
    DWORD dwNewSize;                                // Patched file size
    DWORD dwOldSize = pPatcher->cbFileData;         // File size before patch

    // Get pointer to the patch header
    // Format of BSDIFF header corresponds to original BSDIFF, which is:
    // 0000   8 bytes   signature "BSDIFF40"
    // 0008   8 bytes   size of the control block
    // 0010   8 bytes   size of the data block
    // 0018   8 bytes   new size of the patched file
    pBsdiff = (PBLIZZARD_BSDIFF40_FILE)pbPatchData;
    pbPatchData += sizeof(BLIZZARD_BSDIFF40_FILE);

    // Get pointer to the 32-bit BSDIFF control block
    // The control block follows immediately after the BSDIFF header
    // and consists of three 32-bit integers
    // 0000   4 bytes   Length to copy from the BSDIFF data block the new file
    // 0004   4 bytes   Length to copy from the BSDIFF extra block
    // 0008   4 bytes   Size to increment source file offset
    pCtrlBlock = (PBSDIFF_CTRL_BLOCK)pbPatchData;
    pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->CtrlBlockSize);

    // Get the pointer to the data block
    pDataBlock = (LPBYTE)pbPatchData;
    pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->DataBlockSize);

    // Get the pointer to the extra block
    pExtraBlock = (LPBYTE)pbPatchData;
    dwNewSize = (DWORD)BSWAP_INT64_UNSIGNED(pBsdiff->NewFileSize);

    // Now patch the file
    while(dwNewOffset < dwNewSize)
    {
        DWORD dwAddDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwAddDataLength);
        DWORD dwMovDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwMovDataLength);
        DWORD dwOldMoveLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwOldMoveLength);
        DWORD i;

        // Sanity check
        if((dwNewOffset + dwAddDataLength) > dwNewSize)
            return ERROR_FILE_CORRUPT;

        // Read the diff string to the target buffer
        memcpy(pbNewData + dwNewOffset, pDataBlock, dwAddDataLength);
        pDataBlock += dwAddDataLength;

        // Get the longest block that we can combine
        dwCombineSize = ((dwOldOffset + dwAddDataLength) >= dwOldSize) ? (dwOldSize - dwOldOffset) : dwAddDataLength;
        if((dwNewOffset + dwCombineSize) > dwNewSize || (dwNewOffset + dwCombineSize) < dwNewOffset)
            return ERROR_FILE_CORRUPT;

        // Now combine the patch data with the original file
        for(i = 0; i < dwCombineSize; i++)
            pbNewData[dwNewOffset + i] = pbNewData[dwNewOffset + i] + pbOldData[dwOldOffset + i];

        // Move the offsets
        dwNewOffset += dwAddDataLength;
        dwOldOffset += dwAddDataLength;

        // Sanity check
        if((dwNewOffset + dwMovDataLength) > dwNewSize)
            return ERROR_FILE_CORRUPT;

        // Copy the data from the extra block in BSDIFF patch
        memcpy(pbNewData + dwNewOffset, pExtraBlock, dwMovDataLength);
        pExtraBlock += dwMovDataLength;
        dwNewOffset += dwMovDataLength;

        // Move the old offset
        if(dwOldMoveLength & 0x80000000)
            dwOldMoveLength = 0x80000000 - dwOldMoveLength;
        dwOldOffset += dwOldMoveLength;
        pCtrlBlock++;
    }

    // The size after patch must match
    if(dwNewOffset != pFullPatch->dwSizeAfterPatch)
        return ERROR_FILE_CORRUPT;

    // Update the new data size
    pPatcher->cbFileData = dwNewOffset;
    return ERROR_SUCCESS;
}

static PMPQ_PATCH_HEADER LoadFullFilePatch(TMPQFile * hf, MPQ_PATCH_HEADER & PatchHeader)
{
    PMPQ_PATCH_HEADER pFullPatch;
    DWORD dwErrCode = ERROR_SUCCESS;

    // BSWAP the entire header, if needed
    BSWAP_ARRAY32_UNSIGNED(&PatchHeader, sizeof(DWORD) * 6);
    BSWAP_ARRAY32_UNSIGNED(&PatchHeader.dwXFRM, sizeof(DWORD) * 3);

    // Verify the signatures in the patch header
    if(PatchHeader.dwSignature != PATCH_SIGNATURE_HEADER || PatchHeader.dwMD5 != PATCH_SIGNATURE_MD5 || PatchHeader.dwXFRM != PATCH_SIGNATURE_XFRM)
        return NULL;

    // Allocate space for patch header and compressed data
    pFullPatch = (PMPQ_PATCH_HEADER)STORM_ALLOC(BYTE, PatchHeader.dwSizeOfPatchData);
    if(pFullPatch != NULL)
    {
        // Copy the patch header
        memcpy(pFullPatch, &PatchHeader, sizeof(MPQ_PATCH_HEADER));

        // Read the patch, depending on patch type
        if(dwErrCode == ERROR_SUCCESS)
        {
            switch(PatchHeader.dwPatchType)
            {
                case 0x59504f43:    // 'COPY'
                    dwErrCode = LoadFilePatch_COPY(hf, pFullPatch);
                    break;

                case 0x30445342:    // 'BSD0'
                    dwErrCode = LoadFilePatch_BSD0(hf, pFullPatch);
                    break;

                default:
                    dwErrCode = ERROR_FILE_CORRUPT;
                    break;
            }
        }

        // If something failed, free the patch buffer
        if(dwErrCode != ERROR_SUCCESS)
        {
            STORM_FREE(pFullPatch);
            pFullPatch = NULL;
        }
    }

    // Give the result to the caller
    return pFullPatch;
}

static DWORD ApplyFilePatch(
    TMPQPatcher * pPatcher,
    PMPQ_PATCH_HEADER pFullPatch)
{
    LPBYTE pbSource = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData2 : pPatcher->pbFileData1;
    LPBYTE pbTarget = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData1 : pPatcher->pbFileData2;
    DWORD dwErrCode;

    // Sanity checks
    assert(pFullPatch->dwSizeAfterPatch <= pPatcher->cbMaxFileData);

    // Apply the patch according to the type
    switch(pFullPatch->dwPatchType)
    {
        case 0x59504f43:    // 'COPY'
            dwErrCode = ApplyFilePatch_COPY(pPatcher, pFullPatch, pbTarget, pbSource);
            break;

        case 0x30445342:    // 'BSD0'
            dwErrCode = ApplyFilePatch_BSD0(pPatcher, pFullPatch, pbTarget, pbSource);
            break;

        default:
            dwErrCode = ERROR_FILE_CORRUPT;
            break;
    }

    // Verify MD5 after patch
    if(dwErrCode == ERROR_SUCCESS && pFullPatch->dwSizeAfterPatch != 0)
    {
        // Verify the patched file
        if(!VerifyDataBlockHash(pbTarget, pFullPatch->dwSizeAfterPatch, pFullPatch->md5_after_patch))
            dwErrCode = ERROR_FILE_CORRUPT;

        // Copy the MD5 of the new block
        memcpy(pPatcher->this_md5, pFullPatch->md5_after_patch, MD5_DIGEST_SIZE);
    }

    return dwErrCode;
}

//-----------------------------------------------------------------------------
// Local functions (patch prefix matching)

static bool CreatePatchPrefix(TMPQArchive * ha, const char * szFileName, size_t nLength)
{
    TMPQNamePrefix * pNewPrefix;

    // If the length of the patch prefix was not entered, find it
    // Not that the patch prefix must always begin with backslash
    if(szFileName != NULL && nLength == 0)
        nLength = strlen(szFileName);

    // Create the patch prefix
    pNewPrefix = (TMPQNamePrefix *)STORM_ALLOC(BYTE, sizeof(TMPQNamePrefix) + nLength + 1);
    if(pNewPrefix != NULL)
    {
        // Fill the name prefix. Also add the backslash
        if(szFileName && nLength)
        {
            memcpy(pNewPrefix->szPatchPrefix, szFileName, nLength);
            if(pNewPrefix->szPatchPrefix[nLength - 1] != '\\')
                pNewPrefix->szPatchPrefix[nLength++] = '\\';
        }

        // Terminate the string and fill the length
        pNewPrefix->szPatchPrefix[nLength] = 0;
        pNewPrefix->nLength = nLength;
    }

    ha->pPatchPrefix = pNewPrefix;
    return (pNewPrefix != NULL);
}

static bool CheckAndCreatePatchPrefix(TMPQArchive * ha, const char * szPatchPrefix, size_t nLength)
{
    char szTempName[MAX_SC2_PATCH_PREFIX + 0x41];
    bool bResult = false;

    // Prepare the patch file name
    if(nLength > MAX_SC2_PATCH_PREFIX)
        return false;

    // Prepare the patched file name
    memcpy(szTempName, szPatchPrefix, nLength);
    memcpy(&szTempName[nLength], "\\(patch_metadata)", 18);

    // Verifywhether that file exists
    if(GetFileEntryLocale(ha, szTempName, 0) != NULL)
        bResult = CreatePatchPrefix(ha, szPatchPrefix, nLength);

    return bResult;
}

static bool IsMatchingPatchFile(
    TMPQArchive * ha,
    const char * szFileName,
    LPBYTE pbBaseFileMd5)
{
    MPQ_PATCH_HEADER PatchHeader = {0};
    HANDLE hFile = NULL;
    DWORD dwTransferred = 0;
    DWORD dwFlags = 0;
    bool bResult = false;

    // Open the file and load the patch header
    if(SFileOpenFileEx((HANDLE)ha, szFileName, SFILE_OPEN_BASE_FILE, &hFile))
    {
        // Retrieve the flags. We need to know whether the file is a patch or not
        SFileGetFileInfo(hFile, SFileInfoFlags, &dwFlags, sizeof(DWORD), &dwTransferred);
        if(dwFlags & MPQ_FILE_PATCH_FILE)
        {
            // Load the patch header
            SFileReadFile(hFile, &PatchHeader, sizeof(MPQ_PATCH_HEADER), &dwTransferred, NULL);
            BSWAP_ARRAY32_UNSIGNED(&PatchHeader, sizeof(DWORD) * 6);

            // If the file contains an incremental patch,
            // compare the "MD5 before patching" with the base file MD5
            if(dwTransferred == sizeof(MPQ_PATCH_HEADER) && PatchHeader.dwSignature == PATCH_SIGNATURE_HEADER)
                bResult = (!memcmp(PatchHeader.md5_before_patch, pbBaseFileMd5, MD5_DIGEST_SIZE));
        }
        else
        {
            // TODO: How to match it if it's not an incremental patch?
            // Example: StarCraft II\Updates\enGB\s2-update-enGB-23258.MPQ:
            //          Mods\Core.SC2Mod\enGB.SC2Assets\StreamingBuckets.txt"
            bResult = false;
        }

        // Close the file
        SFileCloseFile(hFile);
    }

    return bResult;
}

static const char * FindArchiveLanguage(TMPQArchive * ha, PLOCALIZED_MPQ_INFO pMpqInfo)
{
    TFileEntry * pFileEntry;
    const char * szLanguage = LanguageList;
    char szFileName[0x40];

    // Iterate through all localized languages
    while(pMpqInfo->szNameTemplate != NULL)
    {
        // Iterate through all languages
        for(szLanguage = LanguageList; szLanguage[0] != 0; szLanguage += 4)
        {
            // Construct the file name
            memcpy(szFileName, pMpqInfo->szNameTemplate, pMpqInfo->nLength);
            szFileName[pMpqInfo->nLangOffset + 0] = szLanguage[0];
            szFileName[pMpqInfo->nLangOffset + 1] = szLanguage[1];
            szFileName[pMpqInfo->nLangOffset + 2] = szLanguage[2];
            szFileName[pMpqInfo->nLangOffset + 3] = szLanguage[3];

            // Append the suffix
            memcpy(szFileName + pMpqInfo->nLength, "-md5.lst", 9);

            // Check whether the name exists
            pFileEntry = GetFileEntryLocale(ha, szFileName, 0);
            if(pFileEntry != NULL)
                return szLanguage;
        }

        // Move to the next language name
        pMpqInfo++;
    }

    // Not found
    return NULL;
}

//-----------------------------------------------------------------------------
// Finding ratch prefix for an temporary build of WoW (Pre-Cataclysm)

static bool FindPatchPrefix_WoW_13164_13623(TMPQArchive * haBase, TMPQArchive * haPatch)
{
    const char * szPatchPrefix;
    char szNamePrefix[0x08];

    // Try to find the language of the MPQ archive
    szPatchPrefix = FindArchiveLanguage(haBase, LocaleMpqs_WoW);
    if(szPatchPrefix == NULL)
        szPatchPrefix = "Base";

    // Format the patch prefix
    szNamePrefix[0] = szPatchPrefix[0];
    szNamePrefix[1] = szPatchPrefix[1];
    szNamePrefix[2] = szPatchPrefix[2];
    szNamePrefix[3] = szPatchPrefix[3];
    szNamePrefix[4] = '\\';
    szNamePrefix[5] = 0;
    return CreatePatchPrefix(haPatch, szNamePrefix, 5);
}

//-----------------------------------------------------------------------------
// Finding patch prefix for Starcraft II (Pre-Legacy of the Void)

//
// This method tries to match the patch by placement of the archive (in the game subdirectory)
//
// Archive Path: %GAME_DIR%\Mods\SwarmMulti.SC2Mod\Base.SC2Data
// Patch Prefix: Mods\SwarmMulti.SC2Mod\Base.SC2Data
//
// Archive Path: %ANY_DIR%\MPQ_2013_v4_Mods#Liberty.SC2Mod#enGB.SC2Data
// Patch Prefix: Mods\Liberty.SC2Mod\enGB.SC2Data
//

static bool CheckPatchPrefix_SC2_ArchiveName(
    TMPQArchive * haPatch,
    const TCHAR * szPathPtr,
    const TCHAR * szSeparator,
    const TCHAR * szPathEnd,
    const TCHAR * szExpectedString,
    size_t cchExpectedString)
{
    char szPatchPrefix[MAX_SC2_PATCH_PREFIX+0x41];
    size_t nLength = 0;
    bool bResult = false;

    // Check whether the length is equal to the length of the expected string
    if((size_t)(szSeparator - szPathPtr) == cchExpectedString)
    {
        // Now check the string itself
        if(!_tcsnicmp(szPathPtr, szExpectedString, szSeparator - szPathPtr))
        {
            // Copy the name string
            for(; szPathPtr < szPathEnd; szPathPtr++)
            {
                if(szPathPtr[0] != _T('/') && szPathPtr[0] != _T('#'))
                    szPatchPrefix[nLength++] = (char)szPathPtr[0];
                else
                    szPatchPrefix[nLength++] = '\\';
            }

            // Check and create the patch prefix
            bResult = CheckAndCreatePatchPrefix(haPatch, szPatchPrefix, nLength);
        }
    }

    return bResult;
}

static bool FindPatchPrefix_SC2_ArchiveName(TMPQArchive * haBase, TMPQArchive * haPatch)
{
    const TCHAR * szPathBegin = FileStream_GetFileName(haBase->pStream);
    const TCHAR * szSeparator = NULL;
    const TCHAR * szPathEnd = szPathBegin + _tcslen(szPathBegin);
    const TCHAR * szPathPtr;
    int nSlashCount = 0;
    int nDotCount = 0;

    // Skip the part where the patch prefix would be too long
    if((szPathEnd - szPathBegin) > MAX_SC2_PATCH_PREFIX)
        szPathBegin = szPathEnd - MAX_SC2_PATCH_PREFIX;

    // Search for the file extension
    for(szPathPtr = szPathEnd; szPathPtr > szPathBegin; szPathPtr--)
    {
        if(szPathPtr[0] == _T('.'))
        {
            nDotCount++;
            break;
        }
    }

    // Search for the possible begin of the prefix name
    for(/* NOTHING */; szPathPtr > szPathBegin; szPathPtr--)
    {
        // Check the slashes, backslashes and hashes
        if(szPathPtr[0] == _T('\\') || szPathPtr[0] == _T('/') || szPathPtr[0] == _T('#'))
        {
            if(nDotCount == 0)
                return false;
            szSeparator = szPathPtr;
            nSlashCount++;
        }

        // Check the path parts
        if(szSeparator != NULL && nSlashCount >= nDotCount)
        {
            if(CheckPatchPrefix_SC2_ArchiveName(haPatch, szPathPtr, szSeparator, szPathEnd, _T("Battle.net"), 10))
                return true;
            if(CheckPatchPrefix_SC2_ArchiveName(haPatch, szPathPtr, szSeparator, szPathEnd, _T("Campaigns"), 9))
                return true;
            if(CheckPatchPrefix_SC2_ArchiveName(haPatch, szPathPtr, szSeparator, szPathEnd, _T("Mods"), 4))
                return true;
        }
    }

    // Not matched, sorry
    return false;
}

//
// This method tries to read the patch prefix from a helper file
//
// Example
// =========================================================
// MPQ File Name: MPQ_2013_v4_Base1.SC2Data
// Helper File  : MPQ_2013_v4_Base1.SC2Data-PATCH
// File Contains: PatchPrefix=Mods\Core.SC2Mod\Base.SC2Data
// Patch Prefix : Mods\Core.SC2Mod\Base.SC2Data
//

static bool ExtractPatchPrefixFromFile(const TCHAR * szHelperFile, char * szPatchPrefix, size_t nMaxChars, size_t * PtrLength)
{
    TFileStream * pStream;
    ULONGLONG FileSize = 0;
    size_t nLength;
    char szFileData[MAX_PATH+1];
    bool bResult = false;

    pStream = FileStream_OpenFile(szHelperFile, STREAM_FLAG_READ_ONLY);
    if(pStream != NULL)
    {
        // Retrieve and check the file size
        FileStream_GetSize(pStream, &FileSize);
        if(12 <= FileSize && FileSize < MAX_PATH)
        {
            // Read the entire file to memory
            if(FileStream_Read(pStream, NULL, szFileData, (DWORD)FileSize))
            {
                // Terminate the buffer with zero
                szFileData[(DWORD)FileSize] = 0;

                // The file data must begin with the "PatchPrefix" variable
                if(!_strnicmp(szFileData, "PatchPrefix", 11))
                {
                    char * szLinePtr = szFileData + 11;
                    char * szLineEnd;

                    // Skip spaces or '='
                    while(szLinePtr[0] == ' ' || szLinePtr[0] == '=')
                        szLinePtr++;
                    szLineEnd = szLinePtr;

                    // Find the end
                    while(szLineEnd[0] != 0 && szLineEnd[0] != 0x0A && szLineEnd[0] != 0x0D)
                        szLineEnd++;
                    nLength = (size_t)(szLineEnd - szLinePtr);

                    // Copy the variable
                    if(szLineEnd > szLinePtr && nLength <= nMaxChars)
                    {
                        memcpy(szPatchPrefix, szLinePtr, nLength);
                        szPatchPrefix[nLength] = 0;
                        PtrLength[0] = nLength;
                        bResult = true;
                    }
                }
            }
        }

        // Close the stream
        FileStream_Close(pStream);
    }

    return bResult;
}


static bool FindPatchPrefix_SC2_HelperFile(TMPQArchive * haBase, TMPQArchive * haPatch)
{
    TCHAR szHelperFile[MAX_PATH+1];
    char szPatchPrefix[MAX_SC2_PATCH_PREFIX+0x41];
    size_t nLength = 0;
    bool bResult = false;

    // Create the name of the patch helper file
    _tcscpy(szHelperFile, FileStream_GetFileName(haBase->pStream));
    if(_tcslen(szHelperFile) + 6 > MAX_PATH)
        return false;
    _tcscat(szHelperFile, _T("-PATCH"));

    // Open the patch helper file and read the line
    if(ExtractPatchPrefixFromFile(szHelperFile, szPatchPrefix, MAX_SC2_PATCH_PREFIX, &nLength))
        bResult = CheckAndCreatePatchPrefix(haPatch, szPatchPrefix, nLength);

    return bResult;
}

//
// Find match in Starcraft II patch MPQs
// Match a LST file in the root directory if the MPQ with any of the file in subdirectories
//
// The problem:
// File in the base MPQ:  enGB-md5.lst
// File in the patch MPQ: Campaigns\Liberty.SC2Campaign\enGB.SC2Assets\enGB-md5.lst
//                        Campaigns\Liberty.SC2Campaign\enGB.SC2Data\enGB-md5.lst
//                        Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst
//                        Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst Mods\Core.SC2Mod\enGB.SC2Assets\enGB-md5.lst
//                        Mods\Core.SC2Mod\enGB.SC2Data\enGB-md5.lst
//                        Mods\Liberty.SC2Mod\enGB.SC2Assets\enGB-md5.lst
//                        Mods\Liberty.SC2Mod\enGB.SC2Data\enGB-md5.lst
//                        Mods\LibertyMulti.SC2Mod\enGB.SC2Data\enGB-md5.lst
//
// Solution:
// We need to match the file by its MD5
//

static bool FindPatchPrefix_SC2_MatchFiles(TMPQArchive * haBase, TMPQArchive * haPatch, TFileEntry * pBaseEntry)
{
    TMPQNamePrefix * pPatchPrefix;
    char * szPatchFileName;
    char * szPlainName;
    size_t cchWorkBuffer = 0x400;
    bool bResult = false;

    // First-level patches: Find the same file within the patch archive
    // and verify by MD5-before-patch
    if(haBase->haPatch == NULL)
    {
        TFileEntry * pFileTableEnd = haPatch->pFileTable + haPatch->dwFileTableSize;
        TFileEntry * pFileEntry;

        // Allocate working buffer for merging LST file
        szPatchFileName = STORM_ALLOC(char, cchWorkBuffer);
        if(szPatchFileName != NULL)
        {
            // Parse the entire file table
            for(pFileEntry = haPatch->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
            {
                // Look for "patch_metadata" file
                if(IsPatchMetadataFile(pFileEntry))
                {
                    // Construct the name of the MD5 file
                    strcpy(szPatchFileName, pFileEntry->szFileName);
                    szPlainName = (char *)GetPlainFileName(szPatchFileName);
                    strcpy(szPlainName, pBaseEntry->szFileName);

                    // Check for matching MD5 file
                    if(IsMatchingPatchFile(haPatch, szPatchFileName, pBaseEntry->md5))
                    {
                        bResult = CreatePatchPrefix(haPatch, szPatchFileName, (size_t)(szPlainName - szPatchFileName));
                        break;
                    }
                }
            }

            // Delete the merge buffer
            STORM_FREE(szPatchFileName);
        }
    }

    // For second-level patches, just take the patch prefix from the lower level patch
    else
    {
        // There must be at least two patches in the chain
        assert(haBase->haPatch->pPatchPrefix != NULL);
        pPatchPrefix = haBase->haPatch->pPatchPrefix;

        // Copy the patch prefix
        bResult = CreatePatchPrefix(haPatch,
                                    pPatchPrefix->szPatchPrefix,
                                    pPatchPrefix->nLength);
    }

    return bResult;
}

// Note: pBaseEntry is the file entry of the base version of "StreamingBuckets.txt"
static bool FindPatchPrefix_SC2(TMPQArchive * haBase, TMPQArchive * haPatch, TFileEntry * pBaseEntry)
{
    // Method 1: Try it by the placement of the archive.
    // Works when someone is opening an archive in the game (sub)directory
    if(FindPatchPrefix_SC2_ArchiveName(haBase, haPatch))
        return true;

    // Method 2: Try to locate the Name.Ext-PATCH file and read the patch prefix from it
    if(FindPatchPrefix_SC2_HelperFile(haBase, haPatch))
        return true;

    // Method 3: Try to pair any version of "StreamingBuckets.txt" from the patch MPQ
    // with the "StreamingBuckets.txt" in the base MPQ. Does not always work
    if(FindPatchPrefix_SC2_MatchFiles(haBase, haPatch, pBaseEntry))
        return true;

    return false;
}

//
// Patch prefix is the path subdirectory where the patched files are within MPQ.
//
// Example 1:
// Main MPQ:  locale-enGB.MPQ
// Patch MPQ: wow-update-12694.MPQ
// File in main MPQ: DBFilesClient\Achievement.dbc
// File in patch MPQ: enGB\DBFilesClient\Achievement.dbc
// Path prefix: enGB
//
// Example 2:
// Main MPQ:  expansion1.MPQ
// Patch MPQ: wow-update-12694.MPQ
// File in main MPQ: DBFilesClient\Achievement.dbc
// File in patch MPQ: Base\DBFilesClient\Achievement.dbc
// Path prefix: Base
//
// Example 3:
// Main MPQ:  %GAME%\Battle.net\Battle.net.MPQ
// Patch MPQ: s2-update-base-26147.MPQ
// File in main MPQ: Battle.net\i18n\deDE\String\CLIENT_ACHIEVEMENTS.xml
// File in patch MPQ: Battle.net\Battle.net.MPQ\Battle.net\i18n\deDE\String\CLIENT_ACHIEVEMENTS.xml
// Path prefix: Battle.net\Battle.net.MPQ
//
// Example 4:
// Main MPQ:  %GAME%\Campaigns\Liberty.SC2Campaign\enGB.SC2Data
//   *OR*     %ANY_DIR%\%ANY_NAME%Campaigns#Liberty.SC2Campaign#enGB.SC2Data
// Patch MPQ: s2-update-enGB-23258.MPQ
// File in main MPQ: LocalizedData\GameHotkeys.txt
// File in patch MPQ: Campaigns\Liberty.SC2Campaign\enGB.SC2Data\LocalizedData\GameHotkeys.txt
// Patch Prefix: Campaigns\Liberty.SC2Campaign\enGB.SC2Data
//

static bool FindPatchPrefix(TMPQArchive * haBase, TMPQArchive * haPatch, const char * szPatchPathPrefix)
{
    TFileEntry * pFileEntry;

    // If the patch prefix was explicitly entered, we use that one
    if(szPatchPathPrefix != NULL)
        return CreatePatchPrefix(haPatch, szPatchPathPrefix, 0);

    // Patches for World of Warcraft - they mostly do not use prefix.
    // All patches that use patch prefix have the "base\\(patch_metadata) file present
    if(GetFileEntryLocale(haPatch, "base\\" PATCH_METADATA_NAME, 0))
        return FindPatchPrefix_WoW_13164_13623(haBase, haPatch);

    // Updates for Starcraft II
    // Match: LocalizedData\GameHotkeys.txt <==> Campaigns\Liberty.SC2Campaign\enGB.SC2Data\LocalizedData\GameHotkeys.txt
    // All Starcraft II base archives seem to have the file "StreamingBuckets.txt" present
    pFileEntry = GetFileEntryLocale(haBase, "StreamingBuckets.txt", 0);
    if(pFileEntry != NULL)
        return FindPatchPrefix_SC2(haBase, haPatch, pFileEntry);

    // Diablo III patch MPQs don't use patch prefix
    // Hearthstone MPQs don't use patch prefix
    CreatePatchPrefix(haPatch, NULL, 0);
    return true;
}

//-----------------------------------------------------------------------------
// Public functions (StormLib internals)

bool IsIncrementalPatchFile(const void * pvData, DWORD cbData, LPDWORD pdwPatchedFileSize)
{
    PMPQ_PATCH_HEADER pPatchHeader = (PMPQ_PATCH_HEADER)pvData;
    BLIZZARD_BSDIFF40_FILE DiffFile;
    DWORD dwPatchType;

    if(cbData >= sizeof(MPQ_PATCH_HEADER) + sizeof(BLIZZARD_BSDIFF40_FILE))
    {
        dwPatchType = BSWAP_INT32_UNSIGNED(pPatchHeader->dwPatchType);
        if(dwPatchType == 0x30445342)
        {
            // Give the caller the patch file size
            if(pdwPatchedFileSize != NULL)
            {
                Decompress_RLE((LPBYTE)&DiffFile, sizeof(BLIZZARD_BSDIFF40_FILE), (LPBYTE)(pPatchHeader + 1), sizeof(BLIZZARD_BSDIFF40_FILE));
                DiffFile.NewFileSize = BSWAP_INT64_UNSIGNED(DiffFile.NewFileSize);
                *pdwPatchedFileSize = (DWORD)DiffFile.NewFileSize;
                return true;
            }
        }
    }

    return false;
}

DWORD Patch_InitPatcher(TMPQPatcher * pPatcher, TMPQFile * hf)
{
    DWORD cbMaxFileData = 0;

    // Overflow check
    if((cbMaxFileData + (DWORD)sizeof(MPQ_PATCH_HEADER)) < cbMaxFileData)
        return ERROR_NOT_ENOUGH_MEMORY;
    if(hf->hfPatch == NULL)
        return ERROR_INVALID_PARAMETER;

    // Initialize the entire structure with zeros
    memset(pPatcher, 0, sizeof(TMPQPatcher));

    // Copy the MD5 of the current file
    memcpy(pPatcher->this_md5, hf->pFileEntry->md5, MD5_DIGEST_SIZE);

    // Find out the biggest data size needed during the patching process
    while(hf != NULL)
    {
        if(hf->pFileEntry->dwFileSize > cbMaxFileData)
            cbMaxFileData = hf->pFileEntry->dwFileSize;
        hf = hf->hfPatch;
    }

    // Allocate primary and secondary buffer
    pPatcher->pbFileData1 = STORM_ALLOC(BYTE, cbMaxFileData);
    pPatcher->pbFileData2 = STORM_ALLOC(BYTE, cbMaxFileData);
    if(!pPatcher->pbFileData1 || !pPatcher->pbFileData2)
        return ERROR_NOT_ENOUGH_MEMORY;

    pPatcher->cbMaxFileData = cbMaxFileData;
    return ERROR_SUCCESS;
}

//
// Note: The patch may either be applied to the base file or to the previous version
// In Starcraft II, Mods\Core.SC2Mod\Base.SC2Data, file StreamingBuckets.txt:
//
// Base file MD5: 31376b0344b6df59ad009d4296125539
//
// s2-update-base-23258: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824
// s2-update-base-24540: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824
// s2-update-base-26147: from 31376b0344b6df59ad009d4296125539 to d5d5253c762fac6b9761240288a0771a
// s2-update-base-28522: from 31376b0344b6df59ad009d4296125539 to 5a76c4b356920aab7afd22e0e1913d7a
// s2-update-base-30508: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671
// s2-update-base-32283: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671
//
// We don't keep all intermediate versions in memory, as it would cause massive
// memory usage during patching process. A prime example is the file
// DBFilesClient\\Item-Sparse.db2 from locale-enGB.MPQ (WoW 16965), which has
// 9 patches in a row, each requiring 70 MB memory (35 MB patch data + 35 MB work buffer)
//

DWORD Patch_Process(TMPQPatcher * pPatcher, TMPQFile * hf)
{
    PMPQ_PATCH_HEADER pFullPatch;
    MPQ_PATCH_HEADER PatchHeader1;
    MPQ_PATCH_HEADER PatchHeader2 = {0};
    TMPQFile * hfBase = hf;
    DWORD cbBytesRead = 0;
    DWORD dwErrCode = ERROR_SUCCESS;

    // Move to the first patch
    assert(hfBase->pbFileData == NULL);
    assert(hfBase->cbFileData == 0);
    hf = hf->hfPatch;

    // Read the header of the current patch
    SFileReadFile((HANDLE)hf, &PatchHeader1, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL);
    if(cbBytesRead != sizeof(MPQ_PATCH_HEADER))
        return ERROR_FILE_CORRUPT;

    // Perform the patching process
    while(dwErrCode == ERROR_SUCCESS && hf != NULL)
    {
        // Try to read the next patch header. If the md5_before_patch
        // still matches we go directly to the next one and repeat
        while(hf->hfPatch != NULL)
        {
            // Attempt to read the patch header
            SFileReadFile((HANDLE)hf->hfPatch, &PatchHeader2, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL);
            if(cbBytesRead != sizeof(MPQ_PATCH_HEADER))
                return ERROR_FILE_CORRUPT;

            // Compare the md5_before_patch
            if(memcmp(PatchHeader2.md5_before_patch, pPatcher->this_md5, MD5_DIGEST_SIZE))
                break;

            // Move one patch fuhrter
            PatchHeader1 = PatchHeader2;
            hf = hf->hfPatch;
        }

        // Allocate memory for the patch data
        pFullPatch = LoadFullFilePatch(hf, PatchHeader1);
        if(pFullPatch != NULL)
        {
            // Apply the patch
            dwErrCode = ApplyFilePatch(pPatcher, pFullPatch);
            STORM_FREE(pFullPatch);
        }
        else
        {
            dwErrCode = ERROR_FILE_CORRUPT;
        }

        // Move to the next patch
        PatchHeader1 = PatchHeader2;
        pPatcher->nCounter++;
        hf = hf->hfPatch;
    }

    // Put the result data to the file structure
    if(dwErrCode == ERROR_SUCCESS)
    {
        // Swap the pointer to the file data structure
        if(pPatcher->nCounter & 0x01)
        {
            hfBase->pbFileData = pPatcher->pbFileData2;
            pPatcher->pbFileData2 = NULL;
        }
        else
        {
            hfBase->pbFileData = pPatcher->pbFileData1;
            pPatcher->pbFileData1 = NULL;
        }

        // Also supply the data size
        hfBase->cbFileData = pPatcher->cbFileData;
    }

    return ERROR_SUCCESS;
}

void Patch_Finalize(TMPQPatcher * pPatcher)
{
    if(pPatcher != NULL)
    {
        if(pPatcher->pbFileData1 != NULL)
            STORM_FREE(pPatcher->pbFileData1);
        if(pPatcher->pbFileData2 != NULL)
            STORM_FREE(pPatcher->pbFileData2);

        memset(pPatcher, 0, sizeof(TMPQPatcher));
    }
}


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

bool WINAPI SFileOpenPatchArchive(
    HANDLE hMpq,
    const TCHAR * szPatchMpqName,
    const char * szPatchPathPrefix,
    DWORD dwFlags)
{
    TMPQArchive * haPatch;
    TMPQArchive * ha = (TMPQArchive *)hMpq;
    HANDLE hPatchMpq = NULL;
    DWORD dwErrCode = ERROR_SUCCESS;

    // Keep compiler happy
    dwFlags = dwFlags;

    // Verify input parameters
    if(!IsValidMpqHandle(hMpq))
        dwErrCode = ERROR_INVALID_HANDLE;
    if(szPatchMpqName == NULL || *szPatchMpqName == 0)
        dwErrCode = ERROR_INVALID_PARAMETER;

    //
    // We don't allow adding patches to archives that have been open for write
    //
    // Error scenario:
    //
    // 1) Open archive for writing
    // 2) Modify or replace a file
    // 3) Add patch archive to the opened MPQ
    // 4) Read patched file
    // 5) Now what ?
    //

    if(dwErrCode == ERROR_SUCCESS)
    {
        if(!(ha->dwFlags & MPQ_FLAG_READ_ONLY))
            dwErrCode = ERROR_ACCESS_DENIED;
    }

    // Open the archive like it is normal archive
    if(dwErrCode == ERROR_SUCCESS)
    {
        if(SFileOpenArchive(szPatchMpqName, 0, MPQ_OPEN_READ_ONLY | MPQ_OPEN_PATCH, &hPatchMpq))
        {
            // Cast the archive handle to structure pointer
            haPatch = (TMPQArchive *)hPatchMpq;

            // We need to remember the proper patch prefix to match names of patched files
            if(FindPatchPrefix(ha, (TMPQArchive *)hPatchMpq, szPatchPathPrefix))
            {
                // Now add the patch archive to the list of patches to the original MPQ
                while(ha != NULL)
                {
                    if(ha->haPatch == NULL)
                    {
                        haPatch->haBase = ha;
                        ha->haPatch = haPatch;
                        return true;
                    }

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

            // Close the archive
            SFileCloseArchive(hPatchMpq);
            dwErrCode = ERROR_CANT_FIND_PATCH_PREFIX;
        }
        else
        {
            dwErrCode = GetLastError();
        }
    }

    SetLastError(dwErrCode);
    return false;
}

bool WINAPI SFileIsPatchedArchive(HANDLE hMpq)
{
    TMPQArchive * ha = (TMPQArchive *)hMpq;

    // Verify input parameters
    if(!IsValidMpqHandle(hMpq))
        return false;

    return (ha->haPatch != NULL);
}
