/*****************************************************************************/
/* SBaseFileTable.cpp                     Copyright (c) Ladislav Zezula 2010 */
/*---------------------------------------------------------------------------*/
/* Description: Common handler for classic and new hash&block tables         */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 06.09.10  1.00  Lad  The first version of SBaseFileTable.cpp              */
/*****************************************************************************/

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

//-----------------------------------------------------------------------------
// Local defines

#define MAX_FLAG_INDEX  256

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

// Structure for HET table header
typedef struct _HET_TABLE_HEADER
{
    DWORD dwTableSize;                      // Size of the entire HET table, including HET_TABLE_HEADER (in bytes)
    DWORD dwMaxFileCount;                   // Maximum number of files in the MPQ
    DWORD dwHashTableSize;                  // Size of the hash table (in bytes)
    DWORD dwHashEntrySize;                  // Effective size of the hash entry (in bits)
    DWORD dwIndexSizeTotal;                 // Total size of file index (in bits)
    DWORD dwIndexSizeExtra;                 // Extra bits in the file index
    DWORD dwIndexSize;                      // Effective size of the file index (in bits)
    DWORD dwIndexTableSize;                 // Size of the block index subtable (in bytes)

} HET_TABLE_HEADER, *PHET_TABLE_HEADER;

// Structure for BET table header
typedef struct _BET_TABLE_HEADER
{
    DWORD dwTableSize;                      // Size of the entire BET table, including the header (in bytes)
    DWORD dwFileCount;                      // Number of files in the BET table
    DWORD dwUnknown08;
    DWORD dwTableEntrySize;                 // Size of one table entry (in bits)
    DWORD dwBitIndex_FilePos;               // Bit index of the file position (within the entry record)
    DWORD dwBitIndex_FileSize;              // Bit index of the file size (within the entry record)
    DWORD dwBitIndex_CmpSize;               // Bit index of the compressed size (within the entry record)
    DWORD dwBitIndex_FlagIndex;             // Bit index of the flag index (within the entry record)
    DWORD dwBitIndex_Unknown;               // Bit index of the ??? (within the entry record)
    DWORD dwBitCount_FilePos;               // Bit size of file position (in the entry record)
    DWORD dwBitCount_FileSize;              // Bit size of file size (in the entry record)
    DWORD dwBitCount_CmpSize;               // Bit size of compressed file size (in the entry record)
    DWORD dwBitCount_FlagIndex;             // Bit size of flags index (in the entry record)
    DWORD dwBitCount_Unknown;               // Bit size of ??? (in the entry record)
    DWORD dwBetHashSizeTotal;               // Total size of the BET hash
    DWORD dwBetHashSizeExtra;               // Extra bits in the BET hash
    DWORD dwBetHashSize;                    // Effective size of BET hash (in bits)
    DWORD dwBetHashArraySize;               // Size of BET hashes array, in bytes
    DWORD dwFlagCount;                      // Number of flags in the following array

} BET_TABLE_HEADER, *PBET_TABLE_HEADER;

//-----------------------------------------------------------------------------
// Support for calculating bit sizes

static DWORD GetNecessaryBitCount(ULONGLONG MaxValue)
{
    DWORD dwBitCount = 0;

    while(MaxValue > 0)
    {
        MaxValue >>= 1;
        dwBitCount++;
    }
    
    return dwBitCount;
}

static BYTE GetFlagsComboIndex(DWORD dwFlags)
{
    BYTE FlagComboIndex = 0;

    //
    // We only use 8 different bits. This allows us to
    // construct 256 unique values for every possible combination of MPQ file flags.
    //

    if(dwFlags & MPQ_FILE_IMPLODE)
        FlagComboIndex |= 0x01;
    dwFlags &= ~MPQ_FILE_IMPLODE;

    if(dwFlags & MPQ_FILE_COMPRESS)
        FlagComboIndex |= 0x02;
    dwFlags &= ~MPQ_FILE_COMPRESS;

    if(dwFlags & MPQ_FILE_ENCRYPTED)
        FlagComboIndex |= 0x04;
    dwFlags &= ~MPQ_FILE_ENCRYPTED;

    if(dwFlags & MPQ_FILE_FIX_KEY)
        FlagComboIndex |= 0x08;
    dwFlags &= ~MPQ_FILE_FIX_KEY;

    if(dwFlags & MPQ_FILE_PATCH_FILE)
        FlagComboIndex |= 0x10;
    dwFlags &= ~MPQ_FILE_PATCH_FILE;

    if(dwFlags & MPQ_FILE_SINGLE_UNIT)
        FlagComboIndex |= 0x20;
    dwFlags &= ~MPQ_FILE_SINGLE_UNIT;

    if(dwFlags & MPQ_FILE_DELETE_MARKER)
        FlagComboIndex |= 0x40;
    dwFlags &= ~MPQ_FILE_DELETE_MARKER;

    if(dwFlags & MPQ_FILE_SECTOR_CRC)
        FlagComboIndex |= 0x80;
    dwFlags &= ~MPQ_FILE_SECTOR_CRC;

    // Sanity check - the flags must now be zero
    assert((dwFlags & 0x7FFFFFFF) == 0);
    return FlagComboIndex;
}

static DWORD GetFileFlagIndex(LPDWORD FlagArray, DWORD dwFlags)
{
    // Find free or equal entry in the flag array
    for(DWORD dwFlagIndex = 0; dwFlagIndex < MAX_FLAG_INDEX; dwFlagIndex++)
    {
        if(FlagArray[dwFlagIndex] == 0 || FlagArray[dwFlagIndex] == dwFlags)
        {
            FlagArray[dwFlagIndex] = dwFlags;
            return dwFlagIndex;
        }
    }

    // This should never happen
    assert(false);
    return 0xFFFFFFFF;
}

//-----------------------------------------------------------------------------
// Support functions for BIT_ARRAY

static USHORT SetBitsMask[] = {0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF};

static TBitArray * CreateBitArray(
    DWORD NumberOfBits,
    BYTE FillValue)
{
    TBitArray * pBitArray;
    size_t nSize = sizeof(TBitArray) + (NumberOfBits + 7) / 8;

    // Allocate the bit array
    pBitArray = (TBitArray *)ALLOCMEM(BYTE, nSize);
    if(pBitArray != NULL)
    {
        memset(pBitArray, FillValue, nSize);
        pBitArray->NumberOfBits = NumberOfBits;
    }

    return pBitArray;
}

void TBitArray::GetBits(
    unsigned int nBitPosition,
    unsigned int nBitLength,
    void * pvBuffer,
    int nResultByteSize)
{
    unsigned char * pbBuffer = (unsigned char *)pvBuffer;
    unsigned int nBytePosition0 = (nBitPosition / 8);
    unsigned int nBytePosition1 = nBytePosition0 + 1;
    unsigned int nByteLength = (nBitLength / 8);
    unsigned int nBitOffset = (nBitPosition & 0x07);
    unsigned char BitBuffer;

    // Keep compiler happy for platforms where nResultByteSize is not used
    nResultByteSize = nResultByteSize;

#ifdef _DEBUG
    // Check if the target is properly zeroed
    for(int i = 0; i < nResultByteSize; i++)
        assert(pbBuffer[i] == 0);
#endif

#ifndef PLATFORM_LITTLE_ENDIAN
    // Adjust the buffer pointer for big endian platforms
    pbBuffer += (nResultByteSize - 1);
#endif    

    // Copy whole bytes, if any
    while(nByteLength > 0)
    {
        // Is the current position in the Elements byte-aligned?
        if(nBitOffset != 0)
        {
            BitBuffer = (unsigned char)((Elements[nBytePosition0] >> nBitOffset) | (Elements[nBytePosition1] << (0x08 - nBitOffset)));
        }
        else
        {
            BitBuffer = Elements[nBytePosition0];
        }

#ifdef PLATFORM_LITTLE_ENDIAN
        *pbBuffer++ = BitBuffer;
#else
        *pbBuffer-- = BitBuffer;
#endif

        // Move byte positions and lengths
        nBytePosition1++;
        nBytePosition0++;
        nByteLength--;
    }

    // Get the rest of the bits
    nBitLength = (nBitLength & 0x07);
    if(nBitLength != 0)
    {
        *pbBuffer = (unsigned char)(Elements[nBytePosition0] >> nBitOffset);

        if(nBitLength > (8 - nBitOffset))
            *pbBuffer = (unsigned char)((Elements[nBytePosition1] << (8 - nBitOffset)) | (Elements[nBytePosition0] >> nBitOffset));

        *pbBuffer &= (0x01 << nBitLength) - 1;
    }
}

void TBitArray::SetBits(
    unsigned int nBitPosition,
    unsigned int nBitLength,
    void * pvBuffer,
    int nResultByteSize)
{
    unsigned char * pbBuffer = (unsigned char *)pvBuffer;
    unsigned int nBytePosition = (nBitPosition / 8);
    unsigned int nBitOffset = (nBitPosition & 0x07);
    unsigned short BitBuffer = 0;
    unsigned short AndMask = 0;
    unsigned short OneByte = 0;

    // Keep compiler happy for platforms where nResultByteSize is not used
    nResultByteSize = nResultByteSize;

#ifndef PLATFORM_LITTLE_ENDIAN
    // Adjust the buffer pointer for big endian platforms
    pbBuffer += (nResultByteSize - 1);
#endif    

    // Copy whole bytes, if any
    while(nBitLength > 8)
    {
        // Reload the bit buffer
#ifdef PLATFORM_LITTLE_ENDIAN
        OneByte = *pbBuffer++;
#else
        OneByte = *pbBuffer--;
#endif
        // Update the BitBuffer and AndMask for the bit array
        BitBuffer = (BitBuffer >> 0x08) | (OneByte << nBitOffset);
        AndMask = (AndMask >> 0x08) | (0x00FF << nBitOffset);

        // Update the byte in the array
        Elements[nBytePosition] = (BYTE)((Elements[nBytePosition] & ~AndMask) | BitBuffer);

        // Move byte positions and lengths
        nBytePosition++;
        nBitLength -= 0x08;
    }

    if(nBitLength != 0)
    {
        // Reload the bit buffer
        OneByte = *pbBuffer;

        // Update the AND mask for the last bit
        BitBuffer = (BitBuffer >> 0x08) | (OneByte << nBitOffset);
        AndMask = (AndMask >> 0x08) | (SetBitsMask[nBitLength] << nBitOffset);

        // Update the byte in the array
        Elements[nBytePosition] = (BYTE)((Elements[nBytePosition] & ~AndMask) | BitBuffer);

        // Update the next byte, if needed
        if(AndMask & 0xFF00)
        {
            nBytePosition++;
            BitBuffer >>= 0x08;
            AndMask >>= 0x08;

            Elements[nBytePosition] = (BYTE)((Elements[nBytePosition] & ~AndMask) | BitBuffer);
        }
    }
}


//-----------------------------------------------------------------------------
// Support for hash table

// Returns a hash table entry in the following order:
// 1) A hash table entry with the neutral locale
// 2) A hash table entry with any other locale
// 3) NULL
static TMPQHash * GetHashEntryAny(TMPQArchive * ha, const char * szFileName)
{
    TMPQHash * pHashNeutral = NULL;
    TMPQHash * pFirstHash = GetFirstHashEntry(ha, szFileName);
    TMPQHash * pHashAny = NULL;
    TMPQHash * pHash = pFirstHash;

    // Parse the found hashes
    while(pHash != NULL)
    {
        // If we found neutral hash, remember it
        if(pHash->lcLocale == 0)
            pHashNeutral = pHash;
        if(pHashAny == NULL)
            pHashAny = pHash;

        // Get the next hash entry for that file
        pHash = GetNextHashEntry(ha, pFirstHash, pHash); 
    }

    // At the end, return neutral hash (if found), otherwise NULL
    return (pHashNeutral != NULL) ? pHashNeutral : pHashAny;
}

// Returns a hash table entry in the following order:
// 1) A hash table entry with the preferred locale
// 2) A hash table entry with the neutral locale
// 3) NULL
static TMPQHash * GetHashEntryLocale(TMPQArchive * ha, const char * szFileName, LCID lcLocale)
{
    TMPQHash * pHashNeutral = NULL;
    TMPQHash * pFirstHash = GetFirstHashEntry(ha, szFileName);
    TMPQHash * pHash = pFirstHash;

    // Parse the found hashes
    while(pHash != NULL)
    {
        // If the locales match, return it
        if(pHash->lcLocale == lcLocale)
            return pHash;
        
        // If we found neutral hash, remember it
        if(pHash->lcLocale == 0)
            pHashNeutral = pHash;

        // Get the next hash entry for that file
        pHash = GetNextHashEntry(ha, pFirstHash, pHash); 
    }

    // At the end, return neutral hash (if found), otherwise NULL
    return pHashNeutral;
}

// Returns a hash table entry in the following order:
// 1) A hash table entry with the preferred locale
// 2) NULL
static TMPQHash * GetHashEntryExact(TMPQArchive * ha, const char * szFileName, LCID lcLocale)
{
    TMPQHash * pFirstHash = GetFirstHashEntry(ha, szFileName);
    TMPQHash * pHash = pFirstHash;

    // Parse the found hashes
    while(pHash != NULL)
    {
        // If the locales match, return it
        if(pHash->lcLocale == lcLocale)
            return pHash;
        
        // Get the next hash entry for that file
        pHash = GetNextHashEntry(ha, pFirstHash, pHash); 
    }

    // Not found
    return NULL;
}

static TMPQHash * TranslateHashTable(
    TMPQArchive * ha,
    ULONGLONG * pcbTableSize)
{
    TMPQHash * pHashTable;
    size_t HashTableSize;

    // Allocate copy of the hash table
    pHashTable = ALLOCMEM(TMPQHash, ha->pHeader->dwHashTableSize);
    if(pHashTable != NULL)
    {
        // Copy the hash table
        HashTableSize = sizeof(TMPQHash) * ha->pHeader->dwHashTableSize;
        memcpy(pHashTable, ha->pHashTable, HashTableSize);

        // Give the size to the caller
        if(pcbTableSize != NULL)
        {
            *pcbTableSize = (ULONGLONG)HashTableSize;
        }
    }

    return pHashTable;
}

static TMPQBlock * TranslateBlockTable(
    TMPQArchive * ha,
    ULONGLONG * pcbTableSize,
    bool * pbNeedHiBlockTable)
{
    TFileEntry * pFileEntry = ha->pFileTable;
    TMPQBlock * pBlockTable;
    TMPQBlock * pBlock;
    size_t BlockTableSize;
    bool bNeedHiBlockTable = false;

    // Allocate copy of the hash table
    pBlockTable = pBlock = ALLOCMEM(TMPQBlock, ha->dwFileTableSize);
    if(pBlockTable != NULL)
    {
        // Copy the block table
        BlockTableSize = sizeof(TMPQBlock) * ha->dwFileTableSize;
        for(DWORD i = 0; i < ha->dwFileTableSize; i++)
        {
            if(pFileEntry->ByteOffset >> 32)
                bNeedHiBlockTable = true;
            pBlock->dwFilePos = (DWORD)pFileEntry->ByteOffset;
            pBlock->dwFSize   = pFileEntry->dwFileSize;
            pBlock->dwCSize   = pFileEntry->dwCmpSize;
            pBlock->dwFlags   = pFileEntry->dwFlags;

            pFileEntry++;
            pBlock++;
        }

        // Give the size to the caller
        if(pcbTableSize != NULL)
            *pcbTableSize = (ULONGLONG)BlockTableSize;

        if(pbNeedHiBlockTable != NULL)
            *pbNeedHiBlockTable = bNeedHiBlockTable;
    }

    return pBlockTable;
}

static USHORT * TranslateHiBlockTable(
    TMPQArchive * ha,
    ULONGLONG * pcbTableSize)
{
    TFileEntry * pFileEntry = ha->pFileTable;
    USHORT * pHiBlockTable;
    USHORT * pHiBlock;
    size_t HiBlockTableSize;

    // Allocate copy of the hash table
    pHiBlockTable = pHiBlock = ALLOCMEM(USHORT, ha->dwFileTableSize);
    if(pHiBlockTable != NULL)
    {
        // Copy the block table
        HiBlockTableSize = sizeof(USHORT) * ha->dwFileTableSize;
        for(DWORD i = 0; i < ha->dwFileTableSize; i++)
            pHiBlock[i] = (USHORT)(pFileEntry[i].ByteOffset >> 0x20);

        // Give the size to the caller
        if(pcbTableSize != NULL)
            *pcbTableSize = (ULONGLONG)HiBlockTableSize;
    }

    return pHiBlockTable;
}

//-----------------------------------------------------------------------------
// General EXT table functions

TMPQExtTable * LoadExtTable(
    TMPQArchive * ha,
    ULONGLONG ByteOffset,
    size_t Size,
    DWORD dwSignature,
    DWORD dwKey)
{
    TMPQExtTable * pCompressed = NULL;      // Compressed table
    TMPQExtTable * pExtTable = NULL;        // Uncompressed table

    // Do nothing if the size is zero
    if(ByteOffset != 0 && Size != 0)
    {
        // Allocate size for the compressed table
        pExtTable = (TMPQExtTable *)ALLOCMEM(BYTE, Size);
        if(pExtTable != NULL)
        {
            // Load the table from the MPQ
            ByteOffset += ha->MpqPos;
            if(!FileStream_Read(ha->pStream, &ByteOffset, pExtTable, (DWORD)Size))
            {
                FREEMEM(pExtTable);
                return NULL;
            }

            // Swap the ext table header
            BSWAP_ARRAY32_UNSIGNED(pExtTable, sizeof(TMPQExtTable));
            if(pExtTable->dwSignature != dwSignature)
            {
                FREEMEM(pExtTable);
                return NULL;
            }

            // Decrypt the block
            BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);
            DecryptMpqBlock(pExtTable + 1, (DWORD)(Size - sizeof(TMPQExtTable)), dwKey);
            BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);

            // If the table is compressed, decompress it
            if((pExtTable->dwDataSize + sizeof(TMPQExtTable)) > Size)
            {
                pCompressed = pExtTable;
                pExtTable = (TMPQExtTable *)ALLOCMEM(BYTE, sizeof(TMPQExtTable) + pCompressed->dwDataSize);
                if(pExtTable != NULL)
                {
                    int cbOutBuffer = (int)pCompressed->dwDataSize;
                    int cbInBuffer = (int)Size;

                    // Decompress the XXX block 
                    pExtTable->dwSignature = pCompressed->dwSignature;
                    pExtTable->dwVersion   = pCompressed->dwVersion;
                    pExtTable->dwDataSize  = pCompressed->dwDataSize;
                    SCompDecompress((char *)(pExtTable + 1), &cbOutBuffer, (char *)(pCompressed + 1), cbInBuffer);
                }

                // Free the compressed block
                FREEMEM(pCompressed);
            }
        }
    }

    // Return the decompressed table to the caller
    return pExtTable;
}

static int SaveMpqTable(
    TMPQArchive * ha,
    void * pMpqTable,
    ULONGLONG ByteOffset,
    size_t Size,
    unsigned char * md5_digest,
    DWORD dwKey,
    bool bCompress)
{
    ULONGLONG FileOffset;
    void * pCompressed = NULL;
    int nError = ERROR_SUCCESS;

    // Do we have to compress the table?
    if(bCompress)
    {
        int cbOutBuffer = (int)Size;
        int cbInBuffer = (int)Size;

        // Allocate extra space for compressed table
        pCompressed = ALLOCMEM(BYTE, Size);
        if(pCompressed == NULL)
            return ERROR_NOT_ENOUGH_MEMORY;

        // Compress the table
        SCompCompress((char *)pCompressed, &cbOutBuffer, (char *)pMpqTable, cbInBuffer, MPQ_COMPRESSION_ZLIB, 0, 0);

        // If the compression failed, revert it. Otherwise, swap the tables
        if(cbOutBuffer >= cbInBuffer)
        {
            FREEMEM(pCompressed);
            pCompressed = NULL;
        }
        else
        {
            pMpqTable = pCompressed;
        }
    }

    // Encrypt the table
    if(dwKey != 0)
    {
        BSWAP_ARRAY32_UNSIGNED(pMpqTable, Size);
        EncryptMpqBlock(pMpqTable, (DWORD)Size, dwKey);
        BSWAP_ARRAY32_UNSIGNED(pMpqTable, Size);
    }

    // Calculate the MD5
    if(md5_digest != NULL)
    {
        hash_state md5_state;

        md5_init(&md5_state);
        md5_process(&md5_state, (unsigned char *)pMpqTable, (DWORD)Size);
        md5_done(&md5_state, md5_digest);
    }

    // Save the table to the MPQ
    BSWAP_ARRAY32_UNSIGNED(pMpqTable, Size);
    FileOffset = ha->MpqPos + ByteOffset;
    if(!FileStream_Write(ha->pStream, &FileOffset, pMpqTable, (DWORD)Size))
        nError = GetLastError();

    // Free the compressed table, if any
    if(pCompressed != NULL)
        FREEMEM(pCompressed);
    return nError;
}

static int SaveExtTable(
    TMPQArchive * ha,
    TMPQExtTable * pExtTable,
    ULONGLONG ByteOffset,
    DWORD dwTableSize,
    unsigned char * md5_digest,
    DWORD dwKey,
    bool bCompress,
    LPDWORD pcbTotalSize)
{
    ULONGLONG FileOffset;
    TMPQExtTable * pCompressed = NULL;
    DWORD cbTotalSize = 0;
    int nError = ERROR_SUCCESS;

    // Do we have to compress the table?
    if(bCompress)
    {
        int cbOutBuffer = (int)dwTableSize;
        int cbInBuffer = (int)dwTableSize;

        // Allocate extra space for compressed table
        pCompressed = (TMPQExtTable *)ALLOCMEM(BYTE, dwTableSize);
        if(pCompressed == NULL)
            return ERROR_NOT_ENOUGH_MEMORY;

        // Compress the table
        pCompressed->dwSignature = pExtTable->dwSignature;
        pCompressed->dwVersion   = pExtTable->dwVersion;
        pCompressed->dwDataSize  = pExtTable->dwDataSize;
        SCompCompress((char *)(pCompressed + 1), &cbOutBuffer, (char *)(pExtTable + 1), cbInBuffer, MPQ_COMPRESSION_ZLIB, 0, 0);

        // If the compression failed, revert it. Otherwise, swap the tables
        if(cbOutBuffer >= cbInBuffer)
        {
            FREEMEM(pCompressed);
            pCompressed = NULL;
        }
        else
        {
            pExtTable = pCompressed;
        }
    }

    // Encrypt the table
    if(dwKey != 0)
    {
        BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);
        EncryptMpqBlock(pExtTable + 1, (DWORD)(dwTableSize - sizeof(TMPQExtTable)), dwKey);
        BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);
    }

    // Calculate the MD5 of the table after
    if(md5_digest != NULL)
    {
        hash_state md5_state;

        md5_init(&md5_state);
        md5_process(&md5_state, (unsigned char *)pExtTable, dwTableSize);
        md5_done(&md5_state, md5_digest);
    }

    // Save the table to the MPQ
    FileOffset = ha->MpqPos + ByteOffset;
    if(FileStream_Write(ha->pStream, &FileOffset, pExtTable, dwTableSize))
        cbTotalSize += dwTableSize;
    else
        nError = GetLastError();

    // We have to write raw data MD5
    if(nError == ERROR_SUCCESS && ha->pHeader->dwRawChunkSize != 0)
    {
        nError = WriteMemDataMD5(ha->pStream,
                                 FileOffset,
                                 pExtTable,
                                 dwTableSize,
                                 ha->pHeader->dwRawChunkSize,
                                &cbTotalSize);
    }

    // Give the total written size, if needed
    if(pcbTotalSize != NULL)
        *pcbTotalSize = cbTotalSize;

    // Free the compressed table, if any
    if(pCompressed != NULL)
        FREEMEM(pCompressed);
    return nError;
}

//-----------------------------------------------------------------------------
// Support for HET table

static void CreateHetHeader(
    TMPQHetTable * pHetTable,
    PHET_TABLE_HEADER pHetHeader)
{
    // Fill the BET header
    pHetHeader->dwMaxFileCount   = pHetTable->dwMaxFileCount;
    pHetHeader->dwHashTableSize  = pHetTable->dwHashTableSize;
    pHetHeader->dwHashEntrySize  = pHetTable->dwHashBitSize;
    pHetHeader->dwIndexSizeTotal = GetNecessaryBitCount(pHetTable->dwMaxFileCount);
    pHetHeader->dwIndexSizeExtra = 0;
    pHetHeader->dwIndexSize      = pHetHeader->dwIndexSizeTotal;
    pHetHeader->dwIndexTableSize = ((pHetHeader->dwIndexSizeTotal * pHetTable->dwHashTableSize) + 7) / 8;

    // Calculate the total size needed for holding HET table
    pHetHeader->dwTableSize = sizeof(HET_TABLE_HEADER) +
                              pHetHeader->dwHashTableSize +
                              pHetHeader->dwIndexTableSize;
}

TMPQHetTable * CreateHetTable(DWORD dwMaxFileCount, DWORD dwHashBitSize, bool bCreateEmpty)
{
    TMPQHetTable * pHetTable;

    pHetTable = ALLOCMEM(TMPQHetTable, 1);
    if(pHetTable != NULL)
    {
        pHetTable->dwIndexSizeTotal = 0;
        pHetTable->dwIndexSizeExtra = 0;
        pHetTable->dwIndexSize      = pHetTable->dwIndexSizeTotal;
        pHetTable->dwMaxFileCount   = dwMaxFileCount;
        pHetTable->dwHashTableSize  = (dwMaxFileCount * 4 / 3);
        pHetTable->dwHashBitSize    = dwHashBitSize;

        // Size of one index is calculated from max file count
        pHetTable->dwIndexSizeTotal = GetNecessaryBitCount(dwMaxFileCount);
        pHetTable->dwIndexSizeExtra = 0;
        pHetTable->dwIndexSize      = pHetTable->dwIndexSizeTotal;

        // Allocate hash table
        pHetTable->pHetHashes = ALLOCMEM(BYTE, pHetTable->dwHashTableSize);
        memset(pHetTable->pHetHashes, 0, pHetTable->dwHashTableSize);

        // If we shall create empty HET table, we have to allocate empty block index table as well
        if(bCreateEmpty)
            pHetTable->pBetIndexes = CreateBitArray(pHetTable->dwHashTableSize * pHetTable->dwIndexSizeTotal, 0xFF);

        // Calculate masks
        pHetTable->AndMask64 = 0;
        if(dwHashBitSize != 0x40)
            pHetTable->AndMask64 = (ULONGLONG)1 << dwHashBitSize;
        pHetTable->AndMask64--;

        pHetTable->OrMask64 = (ULONGLONG)1 << (dwHashBitSize - 1);
    }

    return pHetTable;
}

static TMPQHetTable * TranslateHetTable(TMPQExtTable * pExtTable)
{
    HET_TABLE_HEADER HetHeader;
    TMPQHetTable * pHetTable = NULL;
    LPBYTE pbSrcData = (LPBYTE)(pExtTable + 1);

    // Sanity check
    assert(pExtTable->dwSignature == HET_TABLE_SIGNATURE);
    assert(pExtTable->dwVersion == 1);

    // Verify size of the HET table
    if(pExtTable != NULL && pExtTable->dwDataSize >= sizeof(HET_TABLE_HEADER))
    {
        // Copy the table header in order to have it aligned and swapped
        memcpy(&HetHeader, pbSrcData, sizeof(HET_TABLE_HEADER));
        BSWAP_ARRAY32_UNSIGNED(&HetHeader, sizeof(HET_TABLE_HEADER));
        pbSrcData += sizeof(HET_TABLE_HEADER);

        // Verify the size of the table in the header
        if(HetHeader.dwTableSize == pExtTable->dwDataSize)
        {
            // Create translated table
            pHetTable = CreateHetTable(HetHeader.dwMaxFileCount, HetHeader.dwHashEntrySize, false);
            if(pHetTable != NULL)
            {
                // Copy the hash table size, index size and extra bits from the HET header
                pHetTable->dwHashTableSize  = HetHeader.dwHashTableSize;
                pHetTable->dwIndexSizeTotal = HetHeader.dwIndexSizeTotal;
                pHetTable->dwIndexSizeExtra = HetHeader.dwIndexSizeExtra;

                // Fill the hash table
                if(pHetTable->pHetHashes != NULL)
                    memcpy(pHetTable->pHetHashes, pbSrcData, pHetTable->dwHashTableSize);
                pbSrcData += pHetTable->dwHashTableSize;

                // Copy the block index table
                pHetTable->pBetIndexes = CreateBitArray(HetHeader.dwIndexTableSize * 8, 0xFF);
                if(pHetTable->pBetIndexes != NULL)
                    memcpy(pHetTable->pBetIndexes->Elements, pbSrcData, HetHeader.dwIndexTableSize);
                pbSrcData += HetHeader.dwIndexTableSize;
            }
        }
    }

    return pHetTable;
}

static TMPQExtTable * TranslateHetTable(TMPQHetTable * pHetTable, ULONGLONG * pcbHetTable)
{
    TMPQExtTable * pExtTable = NULL;
    HET_TABLE_HEADER HetHeader;
    LPBYTE pbLinearTable = NULL;
    LPBYTE pbTrgData;
    size_t HetTableSize;

    // Prepare header of the HET table
    CreateHetHeader(pHetTable, &HetHeader);

    // Calculate the total size needed for holding the encrypted HET table
    HetTableSize = HetHeader.dwTableSize;

    // Allocate space for the linear table
    pbLinearTable = ALLOCMEM(BYTE, sizeof(TMPQExtTable) + HetTableSize);
    if(pbLinearTable != NULL)
    {
        // Create the common ext table header
        pExtTable = (TMPQExtTable *)pbLinearTable;
        pExtTable->dwSignature = HET_TABLE_SIGNATURE;
        pExtTable->dwVersion   = 1;
        pExtTable->dwDataSize  = (DWORD)HetTableSize;
        pbTrgData = (LPBYTE)(pExtTable + 1);

        // Copy the HET table header
        memcpy(pbTrgData, &HetHeader, sizeof(HET_TABLE_HEADER));
        BSWAP_ARRAY32_UNSIGNED(pbTrgData, sizeof(HET_TABLE_HEADER));
        pbTrgData += sizeof(HET_TABLE_HEADER);

        // Copy the array of HET hashes
        memcpy(pbTrgData, pHetTable->pHetHashes, pHetTable->dwHashTableSize);
        pbTrgData += pHetTable->dwHashTableSize;

        // Copy the bit array of BET indexes
        memcpy(pbTrgData, pHetTable->pBetIndexes->Elements, HetHeader.dwIndexTableSize);

        // Calculate the total size of the table, including the TMPQExtTable
        if(pcbHetTable != NULL)
        {
            *pcbHetTable = (ULONGLONG)(sizeof(TMPQExtTable) + HetTableSize);
        }
    }

    return pExtTable;
}

DWORD GetFileIndex_Het(TMPQArchive * ha, const char * szFileName)
{
    TMPQHetTable * pHetTable = ha->pHetTable;
    ULONGLONG FileNameHash;
    ULONGLONG AndMask64;
    ULONGLONG OrMask64;
    ULONGLONG BetHash;
    DWORD StartIndex;
    DWORD Index;
    BYTE HetHash;                   // Upper 8 bits of the masked file name hash

    // Do nothing if the MPQ has no HET table
    assert(ha->pHetTable != NULL);

    // Calculate 64-bit hash of the file name
    AndMask64 = pHetTable->AndMask64;
    OrMask64 = pHetTable->OrMask64;
    FileNameHash = (HashStringJenkins(szFileName) & AndMask64) | OrMask64;

    // Split the file name hash into two parts:
    // Part 1: The highest 8 bits of the name hash
    // Part 2: The rest of the name hash (without the highest 8 bits)
    HetHash = (BYTE)(FileNameHash >> (pHetTable->dwHashBitSize - 8));
    BetHash = FileNameHash & (AndMask64 >> 0x08);

    // Calculate the starting index to the hash table
    StartIndex = Index = (DWORD)(FileNameHash % pHetTable->dwHashTableSize);

    // Go through HET table until we find a terminator
    while(pHetTable->pHetHashes[Index] != HET_ENTRY_FREE)
    {
        // Did we find match ?
        if(pHetTable->pHetHashes[Index] == HetHash)
        {
            DWORD dwFileIndex = 0;

            // Get the index of the BetHash
            pHetTable->pBetIndexes->GetBits(pHetTable->dwIndexSizeTotal * Index,
                                            pHetTable->dwIndexSize,
                                           &dwFileIndex,
                                            4);

            //
            // TODO: This condition only happens when we are opening a MPQ
            // where some files were deleted by StormLib. Perhaps 
            // we should not allow shrinking of the file table in MPQs v 4.0?
            // assert(dwFileIndex <= ha->dwFileTableSize);
            //

            // Verify the BetHash against the entry in the table of BET hashes
            if(dwFileIndex <= ha->dwFileTableSize && ha->pFileTable[dwFileIndex].BetHash == BetHash)
                return dwFileIndex;
        }

        // Move to the next entry in the primary search table
        // If we came to the start index again, we are done
        Index = (Index + 1) % pHetTable->dwHashTableSize;
        if(Index == StartIndex)
            break;
    }

    // File not found
    return HASH_ENTRY_FREE;
}

DWORD AllocateHetEntry(
    TMPQArchive * ha,
    TFileEntry * pFileEntry)
{
    TMPQHetTable * pHetTable = ha->pHetTable;
    ULONGLONG FileNameHash;
    ULONGLONG AndMask64;
    ULONGLONG OrMask64;
    ULONGLONG BetHash;
    DWORD FreeHetIndex = HASH_ENTRY_FREE;
    DWORD dwFileIndex;
    DWORD StartIndex;
    DWORD Index;
    BYTE HetHash;                   // Upper 8 bits of the masked file name hash

    // Do nothing if the MPQ has no HET table
    assert(ha->pHetTable != NULL);

    // Calculate 64-bit hash of the file name
    AndMask64 = pHetTable->AndMask64;
    OrMask64 = pHetTable->OrMask64;
    FileNameHash = (HashStringJenkins(pFileEntry->szFileName) & AndMask64) | OrMask64;

    // Calculate the starting index to the hash table
    StartIndex = Index = (DWORD)(FileNameHash % pHetTable->dwHashTableSize);

    // Split the file name hash into two parts:
    // Part 1: The highest 8 bits of the name hash
    // Part 2: The rest of the name hash (without the highest 8 bits)
    HetHash = (BYTE)(FileNameHash >> (pHetTable->dwHashBitSize - 8));
    BetHash = FileNameHash & (AndMask64 >> 0x08);

    // Go through HET table until we find a terminator
    for(;;)
    {
        // Check for entries that might have been deleted
        if(pHetTable->pHetHashes[Index] == HET_ENTRY_DELETED)
        {
            DWORD dwInvalidBetIndex = (1 << pHetTable->dwIndexSizeTotal) - 1;
            DWORD dwBetIndex = 0;

            // Verify the BET index. If it's really free, we can use it
            dwFileIndex = (DWORD)(pFileEntry - ha->pFileTable);
            pHetTable->pBetIndexes->GetBits(pHetTable->dwIndexSizeTotal * Index,
                                            pHetTable->dwIndexSize,
                                           &dwBetIndex,
                                            4);
            
            if(dwBetIndex == dwInvalidBetIndex)
            {
                FreeHetIndex = Index;
                break;
            }
        }

        // Is that entry free ?
        if(pHetTable->pHetHashes[Index] == HET_ENTRY_FREE)
        {
            FreeHetIndex = Index;
            break;
        }

        // Move to the next entry in the primary search table
        // If we came to the start index again, we are done
        Index = (Index + 1) % pHetTable->dwHashTableSize;
        if(Index == StartIndex)
            return HASH_ENTRY_FREE;
    }

    // Fill the HET table entry
    dwFileIndex = (DWORD)(pFileEntry - ha->pFileTable);
    pHetTable->pHetHashes[FreeHetIndex] = HetHash;
    pHetTable->pBetIndexes->SetBits(pHetTable->dwIndexSizeTotal * FreeHetIndex,
                                    pHetTable->dwIndexSize,
                                   &dwFileIndex,
                                    4);
    // Fill the file entry
    pFileEntry->BetHash    = BetHash;
    pFileEntry->dwHetIndex = FreeHetIndex;
    return FreeHetIndex;
}

void FreeHetTable(TMPQHetTable * pHetTable)
{
    if(pHetTable != NULL)
    {
        if(pHetTable->pHetHashes != NULL)
            FREEMEM(pHetTable->pHetHashes);
        if(pHetTable->pBetIndexes != NULL)
            FREEMEM(pHetTable->pBetIndexes);

        FREEMEM(pHetTable);
    }
}

//-----------------------------------------------------------------------------
// Support for BET table

static void CreateBetHeader(
    TMPQArchive * ha,
    PBET_TABLE_HEADER pBetHeader)
{
    TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
    TFileEntry * pFileEntry;
    ULONGLONG MaxByteOffset = 0;
    DWORD dwMaxFileSize = 0;
    DWORD dwMaxCmpSize = 0;
    DWORD dwFlagCount = 0;
    BYTE FlagComboArray[MAX_FLAG_INDEX];
    BYTE FlagComboIndex;

    // Initialize array of flag combinations
    memset(FlagComboArray, 0, sizeof(FlagComboArray));

    // Get the maximum values for the BET table
    for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
    {
        // We don't allow the file table to have free entries in the middle
        // This must be a bug in the library somewhere.
        if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) == 0)
        {
            pFileEntry->dwFlags = MPQ_FILE_EXISTS | MPQ_FILE_DELETE_MARKER;
            assert(false);
        }

        // Highest file position in the MPQ
        if(pFileEntry->ByteOffset > MaxByteOffset)
            MaxByteOffset = pFileEntry->ByteOffset;

        // Biggest file size
        if(pFileEntry->dwFileSize > dwMaxFileSize)
            dwMaxFileSize = pFileEntry->dwFileSize;

        // Biggest compressed size
        if(pFileEntry->dwCmpSize > dwMaxCmpSize)
            dwMaxCmpSize = pFileEntry->dwCmpSize;

        // Check if this flag was there before
        FlagComboIndex = GetFlagsComboIndex(pFileEntry->dwFlags);
        if(FlagComboArray[FlagComboIndex] == 0)
            dwFlagCount++;
        FlagComboArray[FlagComboIndex] = 1;
    }

    // Now save bit count for every piece of file information
    pBetHeader->dwBitIndex_FilePos   = 0;
    pBetHeader->dwBitCount_FilePos   = GetNecessaryBitCount(MaxByteOffset);

    pBetHeader->dwBitIndex_FileSize  = pBetHeader->dwBitIndex_FilePos + pBetHeader->dwBitCount_FilePos;
    pBetHeader->dwBitCount_FileSize  = GetNecessaryBitCount(dwMaxFileSize);

    pBetHeader->dwBitIndex_CmpSize   = pBetHeader->dwBitIndex_FileSize + pBetHeader->dwBitCount_FileSize;
    pBetHeader->dwBitCount_CmpSize   = GetNecessaryBitCount(dwMaxCmpSize);

    pBetHeader->dwBitIndex_FlagIndex = pBetHeader->dwBitIndex_CmpSize + pBetHeader->dwBitCount_CmpSize;
    pBetHeader->dwBitCount_FlagIndex = GetNecessaryBitCount(dwFlagCount);

    pBetHeader->dwBitIndex_Unknown   = pBetHeader->dwBitIndex_FlagIndex + pBetHeader->dwBitCount_FlagIndex;
    pBetHeader->dwBitCount_Unknown   = 0;

    // Calculate the total size of one entry
    pBetHeader->dwTableEntrySize     = pBetHeader->dwBitCount_FilePos +
                                       pBetHeader->dwBitCount_FileSize +
                                       pBetHeader->dwBitCount_CmpSize +
                                       pBetHeader->dwBitCount_FlagIndex +
                                       pBetHeader->dwBitCount_Unknown;

    // Save the file count and flag count
    pBetHeader->dwFileCount          = ha->dwFileTableSize;
    pBetHeader->dwFlagCount          = dwFlagCount;
    pBetHeader->dwUnknown08          = 0x10;

    // Save the total size of the BET hash
    pBetHeader->dwBetHashSizeTotal   = ha->pHetTable->dwHashBitSize - 0x08;
    pBetHeader->dwBetHashSizeExtra   = 0;
    pBetHeader->dwBetHashSize        = pBetHeader->dwBetHashSizeTotal;
    pBetHeader->dwBetHashArraySize   = ((pBetHeader->dwBetHashSizeTotal * pBetHeader->dwFileCount) + 7) / 8;

    // Save the total table size
    pBetHeader->dwTableSize          = sizeof(BET_TABLE_HEADER) +
                                       pBetHeader->dwFlagCount * sizeof(DWORD) +
                                     ((pBetHeader->dwTableEntrySize * pBetHeader->dwFileCount) + 7) / 8 +
                                      pBetHeader->dwBetHashArraySize;
}

TMPQBetTable * CreateBetTable(DWORD dwFileCount)
{
    TMPQBetTable * pBetTable;

    // Allocate BET table
    pBetTable = ALLOCMEM(TMPQBetTable, 1);
    if(pBetTable != NULL)
    {
        memset(pBetTable, 0, sizeof(TMPQBetTable));
        pBetTable->dwFileCount = dwFileCount;
    }

    return pBetTable;
}

static TMPQBetTable * TranslateBetTable(
    TMPQArchive * ha,
    TMPQExtTable * pExtTable)
{
    BET_TABLE_HEADER BetHeader;
    TMPQBetTable * pBetTable = NULL;
    LPBYTE pbSrcData = (LPBYTE)(pExtTable + 1);
    DWORD LengthInBytes;

    // Sanity check
    assert(pExtTable->dwSignature == BET_TABLE_SIGNATURE);
    assert(pExtTable->dwVersion == 1);
    assert(ha->pHetTable != NULL);
    ha = ha;

    // Verify size of the HET table
    if(pExtTable != NULL && pExtTable->dwDataSize >= sizeof(BET_TABLE_HEADER))
    {
        // Copy the table header in order to have it aligned and swapped
        memcpy(&BetHeader, pbSrcData, sizeof(BET_TABLE_HEADER));
        BSWAP_ARRAY32_UNSIGNED(&BetHeader, sizeof(BET_TABLE_HEADER));
        pbSrcData += sizeof(BET_TABLE_HEADER);

        // Some MPQs affected by a bug in StormLib have pBetTable->dwFileCount
        // greater than ha->dwMaxFileCount
        if(BetHeader.dwFileCount > ha->dwMaxFileCount)
            return NULL;

        // Verify the size of the table in the header
        if(BetHeader.dwTableSize == pExtTable->dwDataSize)
        {
            // Create translated table
            pBetTable = CreateBetTable(BetHeader.dwFileCount);
            if(pBetTable != NULL)
            {
                // Copy the variables from the header to the BetTable
                pBetTable->dwTableEntrySize     = BetHeader.dwTableEntrySize;
                pBetTable->dwBitIndex_FilePos   = BetHeader.dwBitIndex_FilePos;
                pBetTable->dwBitIndex_FileSize  = BetHeader.dwBitIndex_FileSize;
                pBetTable->dwBitIndex_CmpSize   = BetHeader.dwBitIndex_CmpSize;
                pBetTable->dwBitIndex_FlagIndex = BetHeader.dwBitIndex_FlagIndex;
                pBetTable->dwBitIndex_Unknown   = BetHeader.dwBitIndex_Unknown;
                pBetTable->dwBitCount_FilePos   = BetHeader.dwBitCount_FilePos;
                pBetTable->dwBitCount_FileSize  = BetHeader.dwBitCount_FileSize;
                pBetTable->dwBitCount_CmpSize   = BetHeader.dwBitCount_CmpSize;
                pBetTable->dwBitCount_FlagIndex = BetHeader.dwBitCount_FlagIndex;
                pBetTable->dwBitCount_Unknown   = BetHeader.dwBitCount_Unknown;

                // Since we don't know what the "unknown" is, we'll assert when it's nonzero
                assert(pBetTable->dwBitCount_Unknown == 0);

                // Allocate array for flags
                if(BetHeader.dwFlagCount != 0)
                {
                    // Allocate array for file flags and load it
                    pBetTable->pFileFlags = ALLOCMEM(DWORD, BetHeader.dwFlagCount);
                    if(pBetTable->pFileFlags != NULL)
                    {
                        LengthInBytes = BetHeader.dwFlagCount * sizeof(DWORD);
                        memcpy(pBetTable->pFileFlags, pbSrcData, LengthInBytes);
                        BSWAP_ARRAY32_UNSIGNED(pBetTable->pFileFlags, LengthInBytes);
                        pbSrcData += LengthInBytes;
                    }

                    // Save the number of flags
                    pBetTable->dwFlagCount = BetHeader.dwFlagCount;
                }

                // Load the bit-based file table
                pBetTable->pFileTable = CreateBitArray(pBetTable->dwTableEntrySize * BetHeader.dwFileCount, 0);
                LengthInBytes = (pBetTable->pFileTable->NumberOfBits + 7) / 8;
                if(pBetTable->pFileTable != NULL)
                    memcpy(pBetTable->pFileTable->Elements, pbSrcData, LengthInBytes);
                pbSrcData += LengthInBytes;

                // Fill the sizes of BET hash
                pBetTable->dwBetHashSizeTotal = BetHeader.dwBetHashSizeTotal;
                pBetTable->dwBetHashSizeExtra = BetHeader.dwBetHashSizeExtra;
                pBetTable->dwBetHashSize      = BetHeader.dwBetHashSize;
                
                // Create and load the array of BET hashes
                pBetTable->pBetHashes = CreateBitArray(pBetTable->dwBetHashSizeTotal * BetHeader.dwFileCount, 0);
                LengthInBytes = (pBetTable->pBetHashes->NumberOfBits + 7) / 8;
                if(pBetTable->pBetHashes != NULL)
                    memcpy(pBetTable->pBetHashes->Elements, pbSrcData, LengthInBytes);
                pbSrcData += BetHeader.dwBetHashArraySize;

                // Dump both tables
//              DumpHetAndBetTable(ha->pHetTable, pBetTable);
            }
        }
    }

    return pBetTable;
}

TMPQExtTable * TranslateBetTable(
    TMPQArchive * ha,
    ULONGLONG * pcbBetTable)
{
    TMPQExtTable * pExtTable = NULL;
    BET_TABLE_HEADER BetHeader;
    TBitArray * pBitArray = NULL;
    LPBYTE pbLinearTable = NULL;
    LPBYTE pbTrgData;
    size_t BetTableSize;
    DWORD LengthInBytes;
    DWORD FlagArray[MAX_FLAG_INDEX];
    DWORD i;

    // Calculate the bit sizes of various entries
    CreateBetHeader(ha, &BetHeader);
    memset(FlagArray, 0, sizeof(FlagArray));

    // Calculate the size of the BET table
    BetTableSize = sizeof(BET_TABLE_HEADER) +
                   BetHeader.dwFlagCount * sizeof(DWORD) +
                 ((BetHeader.dwTableEntrySize * BetHeader.dwFileCount) + 7) / 8 +
                   BetHeader.dwBetHashArraySize;

    // Allocate space
    pbLinearTable = ALLOCMEM(BYTE, sizeof(TMPQExtTable) + BetTableSize);
    if(pbLinearTable != NULL)
    {
        // Create the common ext table header
        pExtTable = (TMPQExtTable *)pbLinearTable;
        pExtTable->dwSignature = BET_TABLE_SIGNATURE;
        pExtTable->dwVersion   = 1;
        pExtTable->dwDataSize  = (DWORD)BetTableSize;
        pbTrgData = (LPBYTE)(pExtTable + 1);

        // Copy the BET table header
        memcpy(pbTrgData, &BetHeader, sizeof(BET_TABLE_HEADER));
        BSWAP_ARRAY32_UNSIGNED(pbTrgData, sizeof(BET_TABLE_HEADER));
        pbTrgData += sizeof(BET_TABLE_HEADER);

        // Save the bit-based block table
        pBitArray = CreateBitArray(BetHeader.dwFileCount * BetHeader.dwTableEntrySize, 0);
        if(pBitArray != NULL)
        {
            TFileEntry * pFileEntry = ha->pFileTable;
            DWORD dwFlagIndex = 0;
            DWORD nBitOffset = 0;

            // Construct the array of flag values and bit-based file table
            for(i = 0; i < BetHeader.dwFileCount; i++, pFileEntry++)
            {
                // Only count files that exist
                if(pFileEntry->dwFlags & MPQ_FILE_EXISTS)
                {
                    // Save the byte offset
                    pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_FilePos,
                                       BetHeader.dwBitCount_FilePos,
                                      &pFileEntry->ByteOffset,
                                       8);
                    pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_FileSize,
                                       BetHeader.dwBitCount_FileSize,
                                      &pFileEntry->dwFileSize,
                                       4);
                    pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_CmpSize,
                                       BetHeader.dwBitCount_CmpSize,
                                      &pFileEntry->dwCmpSize,
                                       4);

                    // Get the flag array for the file
                    dwFlagIndex = GetFileFlagIndex(FlagArray, pFileEntry->dwFlags);
                    pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_FlagIndex,
                                       BetHeader.dwBitCount_FlagIndex,
                                      &dwFlagIndex,
                                       4);
                }

                // Move the bit offset
                nBitOffset += BetHeader.dwTableEntrySize;
            }

            // Write the array of flags
            LengthInBytes = BetHeader.dwFlagCount * sizeof(DWORD);
            memcpy(pbTrgData, FlagArray, LengthInBytes);
            BSWAP_ARRAY32_UNSIGNED(pbTrgData, LengthInBytes);
            pbTrgData += LengthInBytes;

            // Write the bit-based block table
            LengthInBytes = (pBitArray->NumberOfBits + 7) / 8;
            memcpy(pbTrgData, pBitArray->Elements, LengthInBytes);
            pbTrgData += LengthInBytes;

            // Free the bit array
            FREEMEM(pBitArray);
        }

        // Create bit array for BET hashes
        pBitArray = CreateBitArray(BetHeader.dwBetHashSizeTotal * BetHeader.dwFileCount, 0);
        if(pBitArray != NULL)
        {
            TFileEntry * pFileEntry = ha->pFileTable;
            ULONGLONG AndMask64 = ha->pHetTable->AndMask64;
            ULONGLONG OrMask64 = ha->pHetTable->OrMask64;

            for(i = 0; i < BetHeader.dwFileCount; i++)
            {
                ULONGLONG FileNameHash = 0;

                // Calculate 64-bit hash of the file name
                if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) && pFileEntry->szFileName != NULL)
                {
                    FileNameHash = (HashStringJenkins(pFileEntry->szFileName) & AndMask64) | OrMask64;
                    FileNameHash = FileNameHash & (AndMask64 >> 0x08);
                }

                // Insert the name hash to the bit array
                pBitArray->SetBits(BetHeader.dwBetHashSizeTotal * i,
                                   BetHeader.dwBetHashSize,
                                  &FileNameHash,
                                   8);

                // Move to the next file entry
                pFileEntry++;
            }

            // Write the array of BET hashes
            LengthInBytes = (pBitArray->NumberOfBits + 7) / 8;
            memcpy(pbTrgData, pBitArray->Elements, LengthInBytes);
            pbTrgData += LengthInBytes;

            // Free the bit array
            FREEMEM(pBitArray);
        }

        // Write the size of the BET table in the MPQ
        if(pcbBetTable != NULL)
        {
            *pcbBetTable = (ULONGLONG)(sizeof(TMPQExtTable) + BetTableSize);
        }
    }

    return pExtTable;
}

void FreeBetTable(TMPQBetTable * pBetTable)
{
    if(pBetTable != NULL)
    {
        if(pBetTable->pFileTable != NULL)
            FREEMEM(pBetTable->pFileTable);
        if(pBetTable->pFileFlags != NULL)
            FREEMEM(pBetTable->pFileFlags);
        if(pBetTable->pBetHashes != NULL)
            FREEMEM(pBetTable->pBetHashes);

        FREEMEM(pBetTable);
    }
}

//-----------------------------------------------------------------------------
// Support for file table

TFileEntry * GetFileEntryAny(TMPQArchive * ha, const char * szFileName)
{
    TMPQHash * pHash;
    DWORD dwFileIndex;

    // If we have HET table in the MPQ, try to find the file in HET table
    if(ha->pHetTable != NULL)
    {
        dwFileIndex = GetFileIndex_Het(ha, szFileName);
        if(dwFileIndex != HASH_ENTRY_FREE)
            return ha->pFileTable + dwFileIndex;
    }

    // Otherwise, perform the file search in the classic hash table
    if(ha->pHashTable != NULL)
    {
        pHash = GetHashEntryAny(ha, szFileName);
        if(pHash != NULL && pHash->dwBlockIndex < ha->dwFileTableSize)
            return ha->pFileTable + pHash->dwBlockIndex;
    }
    
    // Not found
    return NULL;
}

TFileEntry * GetFileEntryLocale(TMPQArchive * ha, const char * szFileName, LCID lcLocale)
{
    TMPQHash * pHash;
    DWORD dwFileIndex;

    // If we have HET table in the MPQ, try to find the file in HET table
    if(ha->pHetTable != NULL)
    {
        dwFileIndex = GetFileIndex_Het(ha, szFileName);
        if(dwFileIndex != HASH_ENTRY_FREE)
            return ha->pFileTable + dwFileIndex;
    }

    // Otherwise, perform the file search in the classic hash table
    if(ha->pHashTable != NULL)
    {
        pHash = GetHashEntryLocale(ha, szFileName, lcLocale);
        if(pHash != NULL && pHash->dwBlockIndex < ha->dwFileTableSize)
            return ha->pFileTable + pHash->dwBlockIndex;
    }
    
    // Not found
    return NULL;
}

TFileEntry * GetFileEntryExact(TMPQArchive * ha, const char * szFileName, LCID lcLocale)
{
    TMPQHash * pHash;
    DWORD dwFileIndex;

    // If we have HET table in the MPQ, try to find the file in HET table
    if(ha->pHetTable != NULL)
    {
        dwFileIndex = GetFileIndex_Het(ha, szFileName);
        if(dwFileIndex != HASH_ENTRY_FREE)
            return ha->pFileTable + dwFileIndex;
    }

    // Otherwise, perform the file search in the classic hash table
    if(ha->pHashTable != NULL)
    {
        pHash = GetHashEntryExact(ha, szFileName, lcLocale);
        if(pHash != NULL && pHash->dwBlockIndex < ha->dwFileTableSize)
            return ha->pFileTable + pHash->dwBlockIndex;
    }
    
    // Not found
    return NULL;
}

TFileEntry * GetFileEntryByIndex(TMPQArchive * ha, DWORD dwIndex)
{
    // For MPQs with classic hash table
    if(dwIndex < ha->dwFileTableSize)
        return ha->pFileTable + dwIndex;
    return NULL;
}

void AllocateFileName(TFileEntry * pFileEntry, const char * szFileName)
{
    // Sanity check
    assert(pFileEntry != NULL);

    // If the file name is pseudo file name, free it at this point
    if(IsPseudoFileName(pFileEntry->szFileName, NULL))
    {
        if(pFileEntry->szFileName != NULL)
            FREEMEM(pFileEntry->szFileName);
        pFileEntry->szFileName = NULL;
    }

    // Only allocate new file name if it's not there yet
    if(pFileEntry->szFileName == NULL)
    {
        pFileEntry->szFileName = ALLOCMEM(char, strlen(szFileName) + 1);
        if(pFileEntry->szFileName != NULL)
            strcpy(pFileEntry->szFileName, szFileName);
    }
}


// Finds a free file entry. Does NOT increment table size.
TFileEntry * FindFreeFileEntry(TMPQArchive * ha)
{
    TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
    TFileEntry * pDeletedEntry = NULL;
    TFileEntry * pFileEntry;

    // Try to find a free entry
    for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
    {
        // If that entry is free, we reuse it
        if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) == 0)
            return pFileEntry;

        // If that entry is deleted, remember it
        if(pFileEntry->dwFlags & MPQ_FILE_DELETE_MARKER)
            pDeletedEntry = pFileEntry;
    }

    // Do we have a deleted entry?
    if(pDeletedEntry != NULL)
    {
        ClearFileEntry(ha, pDeletedEntry);
        return pDeletedEntry;
    }

    // If no file entry within the existing file table is free,
    // we try the reserve space after current file table
    if(ha->dwFileTableSize < ha->dwMaxFileCount)
        return ha->pFileTable + ha->dwFileTableSize;

    // If we reached maximum file count, we cannot add more files to the MPQ
    assert(ha->dwFileTableSize == ha->dwMaxFileCount);
    return NULL;
}


TFileEntry * AllocateFileEntry(TMPQArchive * ha, const char * szFileName, LCID lcLocale)
{
    TFileEntry * pFileEntry = NULL;
    TMPQHash * pHash;
    DWORD dwHashIndex;
    DWORD dwFileIndex;
    bool bHashEntryExists = false;
    bool bHetEntryExists = false;

    // If the archive has classic hash table, we try to
    // find the file in the hash table
    if(ha->pHashTable != NULL)
    {
        // If the hash entry is already there, we reuse the file entry
        pHash = GetHashEntryExact(ha, szFileName, lcLocale);
        if(pHash != NULL)
        {
            pFileEntry = ha->pFileTable + pHash->dwBlockIndex;
            bHashEntryExists = true;
        }
    }

    // If the archive has HET table, try to use it for
    // finding the file
    if(ha->pHetTable != NULL)
    {
        dwFileIndex = GetFileIndex_Het(ha, szFileName);
        if(dwFileIndex != HASH_ENTRY_FREE)
        {
            pFileEntry = ha->pFileTable + dwFileIndex;
            bHetEntryExists = true;
        }
    }

    // If still haven't found the file entry, we allocate new one
    if(pFileEntry == NULL)
    {
        pFileEntry = FindFreeFileEntry(ha);
        if(pFileEntry == NULL)
            return NULL;
    }

    // Fill the rest of the file entry
    pFileEntry->ByteOffset = 0;
    pFileEntry->FileTime   = 0;
    pFileEntry->dwFileSize = 0;
    pFileEntry->dwCmpSize  = 0;
    pFileEntry->dwFlags    = 0;
    pFileEntry->lcLocale   = 0;
    pFileEntry->wPlatform  = 0;
    pFileEntry->dwCrc32    = 0;
    memset(pFileEntry->md5, 0, MD5_DIGEST_SIZE);

    // Allocate space for file name, if it's not there yet
    AllocateFileName(pFileEntry, szFileName);

    // If the free file entry is at the end of the file table,
    // we have to increment file table size
    if(pFileEntry == ha->pFileTable + ha->dwFileTableSize)
    {
        assert(ha->dwFileTableSize < ha->dwMaxFileCount);
        ha->pHeader->dwBlockTableSize++;
        ha->dwFileTableSize++;
    }

    // If the MPQ has hash table, we have to insert the new entry into the hash table
    if(ha->pHashTable != NULL && bHashEntryExists == false)
    {
        dwHashIndex = AllocateHashEntry(ha, pFileEntry);
        assert(dwHashIndex != HASH_ENTRY_FREE);
    }

    // If the MPQ has HET table, we have to insert it to the HET table as well
    if(ha->pHetTable != NULL && bHetEntryExists == false)
    {
        // TODO: Does HET table even support locales?
        // Most probably, Blizzard gave up that silly idea long ago.
        dwHashIndex = AllocateHetEntry(ha, pFileEntry);
        assert(dwHashIndex != HASH_ENTRY_FREE);
    }

    // Return the file entry
    return pFileEntry;
}

int RenameFileEntry(
    TMPQArchive * ha,
    TFileEntry * pFileEntry,
    const char * szNewFileName)
{
    TMPQHash * pHash;
    DWORD dwFileIndex;
    int nError = ERROR_SUCCESS;

    // If the MPQ has classic hash table, clear the entry there
    if(ha->pHashTable != NULL)
    {
        assert(pFileEntry->dwHashIndex < ha->pHeader->dwHashTableSize);

        pHash = ha->pHashTable + pFileEntry->dwHashIndex;
        memset(pHash, 0xFF, sizeof(TMPQHash));
        pHash->dwBlockIndex = HASH_ENTRY_DELETED;
    }

    // If the MPQ has HET table, clear the entry there as well
    if(ha->pHetTable != NULL)
    {
        TMPQHetTable * pHetTable = ha->pHetTable;
        DWORD dwInvalidFileIndex = (1 << pHetTable->dwIndexSizeTotal) - 1;

        assert(pFileEntry->dwHetIndex < pHetTable->dwHashTableSize);

        // Clear the entry in the HET hash array
        pHetTable->pHetHashes[pFileEntry->dwHetIndex] = HET_ENTRY_DELETED;

        // Set the BET index to invalid index
        pHetTable->pBetIndexes->SetBits(pHetTable->dwIndexSizeTotal * pFileEntry->dwHetIndex,
                                        pHetTable->dwIndexSize,
                                       &dwInvalidFileIndex,
                                        4);
    }

    // Free the old file name
    if(pFileEntry->szFileName != NULL)
        FREEMEM(pFileEntry->szFileName);
    pFileEntry->szFileName = NULL;

    // Allocate new file name
    AllocateFileName(pFileEntry, szNewFileName);

    // Now find a hash entry for the new file name
    if(ha->pHashTable != NULL)
    {
        // Try to find the hash table entry for the new file name
        // Note: If this fails, we leave the MPQ in a corrupt state
        dwFileIndex = AllocateHashEntry(ha, pFileEntry);
        if(dwFileIndex == HASH_ENTRY_FREE)
            nError = ERROR_FILE_CORRUPT;
    }

    // If the archive has HET table, we have to allocate HET table for the file as well
    // finding the file
    if(ha->pHetTable != NULL)
    {
        dwFileIndex = AllocateHetEntry(ha, pFileEntry);
        if(dwFileIndex == HASH_ENTRY_FREE)
            nError = ERROR_FILE_CORRUPT;
    }

    // Invalidate the entries for (listfile) and (attributes)
    // After we are done with MPQ changes, we need to re-create them
    InvalidateInternalFiles(ha);
    return nError;
}

void ClearFileEntry(
    TMPQArchive * ha,
    TFileEntry * pFileEntry)
{
    TMPQHash * pHash = NULL;

    // If the MPQ has classic hash table, clear the entry there
    if(ha->pHashTable != NULL)
    {
        assert(pFileEntry->dwHashIndex < ha->pHeader->dwHashTableSize);

        pHash = ha->pHashTable + pFileEntry->dwHashIndex;
        memset(pHash, 0xFF, sizeof(TMPQHash));
        pHash->dwBlockIndex = HASH_ENTRY_DELETED;
    }

    // If the MPQ has HET table, clear the entry there as well
    if(ha->pHetTable != NULL)
    {
        TMPQHetTable * pHetTable = ha->pHetTable;
        DWORD dwInvalidFileIndex = (1 << pHetTable->dwIndexSizeTotal) - 1;

        assert(pFileEntry->dwHetIndex < pHetTable->dwHashTableSize);

        // Clear the entry in the HET hash array
        pHetTable->pHetHashes[pFileEntry->dwHetIndex] = HET_ENTRY_DELETED;

        // Set the BET index to invalid index
        pHetTable->pBetIndexes->SetBits(pHetTable->dwIndexSizeTotal * pFileEntry->dwHetIndex,
                                        pHetTable->dwIndexSize,
                                       &dwInvalidFileIndex,
                                        4);
    }

    // Free the file name, and zero the entire entry
    if(pFileEntry->szFileName != NULL)
        FREEMEM(pFileEntry->szFileName);
    memset(pFileEntry, 0, sizeof(TFileEntry));
}

int FreeFileEntry(
    TMPQArchive * ha,
    TFileEntry * pFileEntry)
{
    TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
    TFileEntry * pTempEntry;
    int nError = ERROR_SUCCESS;

    //
    // If we have HET table, we cannot just get rid of the file
    // Doing so would lead to empty gaps in the HET and BET tables
    // We have to keep BET hash, hash index, HET index, locale, platform and file name
    //

    if(ha->pHetTable == NULL)
    {
        TFileEntry * pLastFileEntry = ha->pFileTable + ha->dwFileTableSize - 1;
        TFileEntry * pLastUsedEntry = pLastFileEntry;

        // Zero the file entry
        ClearFileEntry(ha, pFileEntry);

        // Now there is a chance that we created a chunk of free
        // file entries at the end of the file table. We check this
        // and eventually free all deleted file entries at the end
        for(pTempEntry = ha->pFileTable; pTempEntry < pFileTableEnd; pTempEntry++)
        {
            // Is that an occupied file entry?
            if(pTempEntry->dwFlags & MPQ_FILE_EXISTS)
                pLastUsedEntry = pTempEntry;
        }

        // Can we free some entries at the end?                 
        if(pLastUsedEntry < pLastFileEntry)
        {
            // Fix the size of the file table entry
            ha->dwFileTableSize = (DWORD)(pLastUsedEntry - ha->pFileTable) + 1;
            ha->pHeader->dwBlockTableSize = ha->dwFileTableSize;
        }
    }
    else
    {
        memset(pFileEntry->md5, 0, MD5_DIGEST_SIZE);
        pFileEntry->ByteOffset = 0;
        pFileEntry->FileTime   = 0;
        pFileEntry->dwFileSize = 0;
        pFileEntry->dwCmpSize  = 0;
        pFileEntry->dwFlags    = MPQ_FILE_EXISTS | MPQ_FILE_DELETE_MARKER;
        pFileEntry->dwCrc32    = 0;
        nError = ERROR_MARKED_FOR_DELETE;
    }

    return nError;
}

void InvalidateInternalFiles(TMPQArchive * ha)
{
    TFileEntry * pFileEntry;

    // Invalidate the (listfile), if not done yet
    if(!(ha->dwFlags & MPQ_FLAG_INV_LISTFILE))
    {
        pFileEntry = GetFileEntryExact(ha, LISTFILE_NAME, LANG_NEUTRAL);
        if(pFileEntry != NULL)
            FreeFileEntry(ha, pFileEntry);
        ha->dwFlags |= MPQ_FLAG_INV_LISTFILE;
    }

    // Invalidate the (attributes), if not done yet
    if(!(ha->dwFlags & MPQ_FLAG_INV_ATTRIBUTES))
    {
        pFileEntry = GetFileEntryExact(ha, ATTRIBUTES_NAME, LANG_NEUTRAL);
        if(pFileEntry != NULL)
            FreeFileEntry(ha, pFileEntry);
        ha->dwFlags |= MPQ_FLAG_INV_ATTRIBUTES;
    }

    // Remember that the MPQ has been changed and it will ne necessary
    // to update the tables
    ha->dwFlags |= MPQ_FLAG_CHANGED;
}

//-----------------------------------------------------------------------------
// Support for file tables - hash table, block table, hi-block table,
// (attributes) and (listfile)

static void FixBlockTableSize(
    TMPQArchive * ha,
    TMPQBlock * pBlockTable,
    DWORD dwClaimedSize)
{
    TMPQHeader * pHeader = ha->pHeader;
    ULONGLONG BlockTableStart;
    ULONGLONG BlockTableEnd;
    ULONGLONG FileDataStart;

    // Only perform this check on MPQs version 1.0
    if(pHeader->dwHeaderSize == MPQ_HEADER_SIZE_V1)
    {
        // Calculate claimed block table begin and end
        BlockTableStart = ha->MpqPos + MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos);
        BlockTableEnd = BlockTableStart + (pHeader->dwBlockTableSize * sizeof(TMPQBlock));

        for(DWORD i = 0; i < dwClaimedSize; i++)
        {
            // If the block table end goes into that file, fix the block table end
            FileDataStart = ha->MpqPos + pBlockTable[i].dwFilePos;
            if(BlockTableStart < FileDataStart && BlockTableEnd > FileDataStart)
            {
                dwClaimedSize = (DWORD)((FileDataStart - BlockTableStart) / sizeof(TMPQBlock));
                BlockTableEnd = FileDataStart;
            }
        }
    }

    // Fix the block table size
    pHeader->BlockTableSize64 = dwClaimedSize * sizeof(TMPQBlock);
    pHeader->dwBlockTableSize = dwClaimedSize;
}

int CreateHashTable(TMPQArchive * ha, DWORD dwHashTableSize)
{
    TMPQHash * pHashTable;

    // Sanity checks
    assert((dwHashTableSize & (dwHashTableSize - 1)) == 0);
    assert(ha->pHashTable == NULL);

    // Create the hash table
    pHashTable = ALLOCMEM(TMPQHash, dwHashTableSize);
    if(pHashTable == NULL)
        return ERROR_NOT_ENOUGH_MEMORY;

    // Fill it
    memset(pHashTable, 0xFF, dwHashTableSize * sizeof(TMPQHash));
    ha->pHashTable = pHashTable;

    // Set the max file count, if needed
    if(ha->pHetTable == NULL)
        ha->dwMaxFileCount = dwHashTableSize;
    return ERROR_SUCCESS;
}

int LoadHashTable(TMPQArchive * ha)
{
    TMPQHeader * pHeader = ha->pHeader;
    ULONGLONG ByteOffset;
    TMPQHash * pHashTable;
    DWORD dwTableSize;
    DWORD dwCmpSize;
    int nError;

    // If the MPQ has no hash table, do nothing
    if(pHeader->dwHashTablePos == 0 && pHeader->wHashTablePosHi == 0)
        return ERROR_SUCCESS;

    // If the hash table size is zero, do nothing
    if(pHeader->dwHashTableSize == 0)
        return ERROR_SUCCESS;

    // Allocate buffer for the hash table
    dwTableSize = pHeader->dwHashTableSize * sizeof(TMPQHash);
    pHashTable = ALLOCMEM(TMPQHash, pHeader->dwHashTableSize);
    if(pHashTable == NULL)
        return ERROR_NOT_ENOUGH_MEMORY;

    // Compressed size of the hash table
    dwCmpSize = (DWORD)pHeader->HashTableSize64;

    // 
    // Load the table from the MPQ, with decompression
    //
    // Note: We will NOT check if the hash table is properly decrypted.
    // Some MPQ protectors corrupt the hash table by rewriting part of it.
    // Hash table, the way how it works, allows arbitrary values for unused entries.
    // 

    ByteOffset = ha->MpqPos + MAKE_OFFSET64(pHeader->wHashTablePosHi, pHeader->dwHashTablePos);
    nError = LoadMpqTable(ha, ByteOffset, pHashTable, dwCmpSize, dwTableSize, MPQ_KEY_HASH_TABLE);
    if(nError != ERROR_SUCCESS)
    {
        FREEMEM(pHashTable);
        pHashTable = NULL;
        return nError;
    }

    // Set the maximum file count to the size of the hash table
    // In case there is HET table, we have to keep the file limit
    if(ha->pHetTable == NULL)
        ha->dwMaxFileCount = pHeader->dwHashTableSize;

    // Store the hash table to the MPQ
    ha->pHashTable = pHashTable;
    return ERROR_SUCCESS;
}

int LoadHetTable(TMPQArchive * ha)
{
    TMPQExtTable * pExtTable;
    TMPQHeader * pHeader = ha->pHeader;
    int nError = ERROR_SUCCESS;

    // If the HET table position is not NULL, we expect
    // both HET and BET tables to be present.
    if(pHeader->HetTablePos64 != 0)
    {
        // Attempt to load the HET table (Hash Extended Table)
        pExtTable = LoadExtTable(ha, pHeader->HetTablePos64, (size_t)pHeader->HetTableSize64, HET_TABLE_SIGNATURE, MPQ_KEY_HASH_TABLE);
        if(pExtTable != NULL)
        {
            // If succeeded, we have to limit the maximum file count
            // to the values saved in the HET table
            // If loading HET table fails, we ignore the result.
            ha->pHetTable = TranslateHetTable(pExtTable);
            if(ha->pHetTable != NULL)
                ha->dwMaxFileCount = ha->pHetTable->dwMaxFileCount;

            FREEMEM(pExtTable);
        }

        // If the HET hable failed to load, it's corrupt.
        if(ha->pHetTable == NULL)
            nError = ERROR_FILE_CORRUPT;
    }

    return nError;
}

TMPQBetTable * LoadBetTable(TMPQArchive * ha)
{
    TMPQExtTable * pExtTable;
    TMPQBetTable * pBetTable = NULL;
    TMPQHeader * pHeader = ha->pHeader;

    // If the HET table position is not NULL, we expect
    // both HET and BET tables to be present.
    if(pHeader->BetTablePos64 != 0)
    {
        // Attempt to load the HET table (Hash Extended Table)
        pExtTable = LoadExtTable(ha, pHeader->BetTablePos64, (size_t)pHeader->BetTableSize64, BET_TABLE_SIGNATURE, MPQ_KEY_BLOCK_TABLE);
        if(pExtTable != NULL)
        {
            // If succeeded, we translate the BET table
            // to more readable form
            pBetTable = TranslateBetTable(ha, pExtTable);
            FREEMEM(pExtTable);
        }
    }

    return pBetTable;
}

int LoadAnyHashTable(TMPQArchive * ha)
{
    TMPQHeader * pHeader = ha->pHeader;
    bool bHashTableLoaded = false;

    // If the MPQ archive is empty, don't bother trying to load anything
    if(pHeader->dwHashTableSize == 0 && pHeader->HetTableSize64 == 0)
        return CreateHashTable(ha, HASH_TABLE_SIZE_DEFAULT);

    // Try to load HET table
    if(LoadHetTable(ha) == ERROR_SUCCESS)
        bHashTableLoaded = true;

    // Try to load the classic hash table
    if(LoadHashTable(ha) == ERROR_SUCCESS)
        bHashTableLoaded = true;

    // In theory, a MPQ could have bigger block table than hash table
    if(ha->pHeader->dwBlockTableSize > ha->dwMaxFileCount)
    {
        ha->dwMaxFileCount = ha->pHeader->dwBlockTableSize;
        ha->dwFlags |= MPQ_FLAG_READ_ONLY;
    }
        
    return bHashTableLoaded ? ERROR_SUCCESS : ERROR_FILE_CORRUPT;
}

int BuildFileTable_Classic(
    TMPQArchive * ha,
    TFileEntry * pFileTable,
    ULONGLONG FileSize)
{
    TFileEntry * pFileEntry;
    TMPQHeader * pHeader = ha->pHeader;
    TMPQBlock * pBlockTable;
    TMPQBlock * pBlock;
    int nError = ERROR_SUCCESS;

    // Sanity checks
    assert(ha->pHashTable != NULL);

    // Do nothing if the size of the block table is zero
    if(pHeader->dwBlockTablePos != 0 && pHeader->dwBlockTableSize != 0)
    {
        // Sanity check, enforced by LoadAnyHashTable
        assert(ha->dwMaxFileCount >= pHeader->dwBlockTableSize);

        // Allocate space for the block table
        // Note: pHeader->dwBlockTableSize can be zero !!!
        pBlockTable = ALLOCMEM(TMPQBlock, ha->dwMaxFileCount);
        if(pBlockTable != NULL)
        {
            ULONGLONG ByteOffset = ha->MpqPos + MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos);
            TMPQHash * pHashEnd = ha->pHashTable + pHeader->dwHashTableSize;
            TMPQHash * pHash;
            DWORD dwTableSize = pHeader->dwBlockTableSize * sizeof(TMPQBlock);
            DWORD dwCmpSize = (DWORD)pHeader->BlockTableSize64;

            // Fill the block table with zeros
            memset(pBlockTable, 0, dwTableSize);

            // I have found a MPQ which claimed 0x200 entries in the block table,
            // but the file was cut and there was only 0x1A0 entries.
            // We will handle this case properly.
            if(dwTableSize == dwCmpSize && (ByteOffset + dwTableSize) > FileSize)
            {
                pHeader->dwBlockTableSize = (DWORD)((FileSize - ByteOffset) / sizeof(TMPQBlock));
                pHeader->BlockTableSize64 = pHeader->dwBlockTableSize * sizeof(TMPQBlock);
                dwTableSize = dwCmpSize = pHeader->dwBlockTableSize * sizeof(TMPQBlock);
            }

            //
            // One of the first cracked versions of Diablo I had block table unencrypted 
            // StormLib does NOT support such MPQs anymore, as they are incompatible
            // with compressed block table feature
            //

            // Load the block table
            nError = LoadMpqTable(ha, ByteOffset, pBlockTable, dwCmpSize, dwTableSize, MPQ_KEY_BLOCK_TABLE);
            if(nError == ERROR_SUCCESS)
            {
                // Defense against MPQs that claim block table to be bigger than it really is
                FixBlockTableSize(ha, pBlockTable, pHeader->dwBlockTableSize);

                // Merge the block table to the file table
                for(pHash = ha->pHashTable; pHash < pHashEnd; pHash++)
                {
                    if(pHash->dwBlockIndex < pHeader->dwBlockTableSize)
                    {
                        pBlock = pBlockTable + pHash->dwBlockIndex;

                        //
                        // Yet another silly map protector: For each valid file,
                        // there are 4 items in the hash table, that appears to be valid:
                        //
                        //   a6d79af0 e61a0932 001e0000 0000770b <== Fake valid
                        //   a6d79af0 e61a0932 0000d761 0000dacb <== Fake valid
                        //   a6d79af0 e61a0932 00000000 0000002f <== Real file entry
                        //   a6d79af0 e61a0932 00005a4f 000093bc <== Fake valid
                        // 

                        if(!(pBlock->dwFlags & ~MPQ_FILE_VALID_FLAGS) && (pBlock->dwFlags & MPQ_FILE_EXISTS))
                        {
                            // Get the entry
                            pFileEntry = pFileTable + pHash->dwBlockIndex;

                            // Fill the entry
                            pFileEntry->ByteOffset  = pBlock->dwFilePos;
                            pFileEntry->dwHashIndex = (DWORD)(pHash - ha->pHashTable);
                            pFileEntry->dwFileSize  = pBlock->dwFSize;
                            pFileEntry->dwCmpSize   = pBlock->dwCSize;
                            pFileEntry->dwFlags     = pBlock->dwFlags;
                            pFileEntry->lcLocale    = pHash->lcLocale;
                            pFileEntry->wPlatform   = pHash->wPlatform;
                        }
                        else
                        {
                            // If the hash table entry doesn't point to the valid file item,
                            // we invalidate the entire hash table entry
                            pHash->dwName1      = 0xFFFFFFFF;
                            pHash->dwName2      = 0xFFFFFFFF;
                            pHash->lcLocale     = 0xFFFF;
                            pHash->wPlatform    = 0xFFFF;
                            pHash->dwBlockIndex = HASH_ENTRY_DELETED;
                        }
                    }
                }
            }

            // Free the block table
            FREEMEM(pBlockTable);
        }
        else
        {
            nError = ERROR_NOT_ENOUGH_MEMORY;
        }
    }

    // Load the hi-block table
    if(nError == ERROR_SUCCESS && pHeader->HiBlockTablePos64 != 0)
    {
        ULONGLONG ByteOffset;
        USHORT * pHiBlockTable = NULL;
        DWORD dwTableSize = pHeader->dwBlockTableSize * sizeof(USHORT);

        // Allocate space for the hi-block table
        // Note: pHeader->dwBlockTableSize can be zero !!!
        pHiBlockTable = ALLOCMEM(USHORT, pHeader->dwBlockTableSize + 1);
        if(pHiBlockTable != NULL)
        {
            // Load the hi-block table. It is not encrypted, nor compressed
            ByteOffset = ha->MpqPos + pHeader->HiBlockTablePos64;
            if(!FileStream_Read(ha->pStream, &ByteOffset, pHiBlockTable, dwTableSize))
                nError = GetLastError();

            // Now merge the hi-block table to the file table
            if(nError == ERROR_SUCCESS)
            {
                pFileEntry = pFileTable;

                // Add the high file offset to the base file offset.
                // We also need to swap it during the process.
                for(DWORD i = 0; i < pHeader->dwBlockTableSize; i++)
                {
                    pFileEntry->ByteOffset |= ((ULONGLONG)BSWAP_INT16_UNSIGNED(pHiBlockTable[i]) << 32);
                    pFileEntry++;
                }
            }

            // Free the hi-block table
            FREEMEM(pHiBlockTable);
        }
        else
        {
            nError = ERROR_NOT_ENOUGH_MEMORY;
        }
    }

    // Set the current size of the file table
    ha->dwFileTableSize = pHeader->dwBlockTableSize;
    return nError;
}

int BuildFileTable_HetBet(
    TMPQArchive * ha,
    TFileEntry * pFileTable)
{
    TMPQHetTable * pHetTable = ha->pHetTable;
    TMPQBetTable * pBetTable;
    TFileEntry * pFileEntry = pFileTable;
    TBitArray * pBitArray;
    DWORD dwBitPosition = 0;
    DWORD i;
    int nError = ERROR_FILE_CORRUPT;

    // Load the BET table from the MPQ
    pBetTable = LoadBetTable(ha);
    if(pBetTable != NULL)
    {
        // Step one: Fill the indexes to the HET table
        for(i = 0; i < pHetTable->dwHashTableSize; i++)
        {
            DWORD dwFileIndex = 0;

            // Is the entry in the HET table occupied?
            if(pHetTable->pHetHashes[i] != 0)
            {
                // Load the index to the BET table
                pHetTable->pBetIndexes->GetBits(pHetTable->dwIndexSizeTotal * i,
                                                pHetTable->dwIndexSize,
                                               &dwFileIndex,
                                                4);
                // Overflow test
                if(dwFileIndex < pBetTable->dwFileCount)
                {
                    // Get the file entry and save HET index
                    pFileEntry = pFileTable + dwFileIndex;
                    pFileEntry->dwHetIndex = i;

                    // Load the BET hash
                    pBetTable->pBetHashes->GetBits(pBetTable->dwBetHashSizeTotal * dwFileIndex,
                                                   pBetTable->dwBetHashSize,
                                                  &pFileEntry->BetHash,
                                                   8);
                }
            }
        }

        // Go through the entire BET table and convert it to the file table.
        pFileEntry = pFileTable;
        pBitArray = pBetTable->pFileTable; 
        for(i = 0; i < pBetTable->dwFileCount; i++)
        {
            DWORD dwFlagIndex = 0;

            // Read the file position
            pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_FilePos,
                               pBetTable->dwBitCount_FilePos,
                              &pFileEntry->ByteOffset,
                               8);

            // Read the file size
            pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_FileSize,
                               pBetTable->dwBitCount_FileSize,
                              &pFileEntry->dwFileSize,
                               4);

            // Read the compressed size
            pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_CmpSize,
                               pBetTable->dwBitCount_CmpSize,
                              &pFileEntry->dwCmpSize,
                               4);


            // Read the flag index
            if(pBetTable->dwFlagCount != 0)
            {
                pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_FlagIndex,
                                   pBetTable->dwBitCount_FlagIndex,
                                  &dwFlagIndex,
                                   4);

                pFileEntry->dwFlags = pBetTable->pFileFlags[dwFlagIndex];
            }

            //
            // TODO: Locale (?)
            //

            // Move the current bit position
            dwBitPosition += pBetTable->dwTableEntrySize;
            pFileEntry++;
        }

        // Set the current size of the file table
        ha->dwFileTableSize = pBetTable->dwFileCount;
        FreeBetTable(pBetTable);
        nError = ERROR_SUCCESS;
    }
    else
    {
        nError = ERROR_FILE_CORRUPT;
    }

    return nError;
}

int BuildFileTable(TMPQArchive * ha, ULONGLONG FileSize)
{
    TFileEntry * pFileTable;
    bool bFileTableCreated = false;

    // Sanity checks
    assert(ha->dwFileTableSize == 0);
    assert(ha->dwMaxFileCount != 0);

    // Allocate the file table with size determined before
    pFileTable = ALLOCMEM(TFileEntry, ha->dwMaxFileCount);
    if(pFileTable == NULL)
        return ERROR_NOT_ENOUGH_MEMORY;

    // Fill the table with zeros
    memset(pFileTable, 0, ha->dwMaxFileCount * sizeof(TFileEntry));

    // If we have HET table, we load file table from the BET table
    // Note: If BET table is corrupt or missing, we set the archive as read only
    if(ha->pHetTable != NULL)
    {
        if(BuildFileTable_HetBet(ha, pFileTable) != ERROR_SUCCESS)
            ha->dwFlags |= MPQ_FLAG_READ_ONLY;
        else
            bFileTableCreated = true;
    }

    // If we have hash table, we load the file table from the block table
    // Note: If block table is corrupt or missing, we set the archive as read only
    if(ha->pHashTable != NULL)
    {
        if(BuildFileTable_Classic(ha, pFileTable, FileSize) != ERROR_SUCCESS)
            ha->dwFlags |= MPQ_FLAG_READ_ONLY;
        else
            bFileTableCreated = true;
    }
    
    // If something failed, we free the file table entry
    if(bFileTableCreated == false)
    {
        FREEMEM(pFileTable);
        return ERROR_FILE_CORRUPT;
    }

    // Assign it to the archive structure
    ha->pFileTable = pFileTable;
    return ERROR_SUCCESS;
}

// Saves MPQ header, hash table, block table and hi-block table.
int SaveMPQTables(TMPQArchive * ha)
{
    TMPQExtTable * pHetTable = NULL;
    TMPQExtTable * pBetTable = NULL;
    TMPQHeader * pHeader = ha->pHeader;
    TMPQBlock * pBlockTable = NULL;
    TMPQHash * pHashTable = NULL;
    ULONGLONG HetTableSize64 = 0;
    ULONGLONG BetTableSize64 = 0;
    ULONGLONG HashTableSize64 = 0;
    ULONGLONG BlockTableSize64 = 0;
    ULONGLONG HiBlockTableSize64 = 0;
    ULONGLONG TablePos = 0;             // A table position, relative to the begin of the MPQ
    USHORT * pHiBlockTable = NULL;
    DWORD cbTotalSize;
    bool bNeedHiBlockTable = false;
    int nError = ERROR_SUCCESS;

    // We expect this function to be called only when tables have been changed
    assert(ha->dwFlags & MPQ_FLAG_CHANGED);

    // Find the space where the MPQ tables will be saved 
    FindFreeMpqSpace(ha, &TablePos);

    // If the MPQ has HET table, we prepare a ready-to-save version
    if(nError == ERROR_SUCCESS && ha->pHetTable != NULL)
    {
        pHetTable = TranslateHetTable(ha->pHetTable, &HetTableSize64);
        if(pHetTable == NULL)
            nError = ERROR_NOT_ENOUGH_MEMORY;
    }

    // If the MPQ has HET table, we also must create BET table to be saved
    if(nError == ERROR_SUCCESS && ha->pHetTable != NULL)
    {
        pBetTable = TranslateBetTable(ha, &BetTableSize64);
        if(pBetTable == NULL)
            nError = ERROR_NOT_ENOUGH_MEMORY;
    }

    // Now create hash table
    if(nError == ERROR_SUCCESS && ha->pHashTable != NULL)
    {
        pHashTable = TranslateHashTable(ha, &HashTableSize64);
        if(pHashTable == NULL)
            nError = ERROR_NOT_ENOUGH_MEMORY;
    }

    // Create block table
    if(nError == ERROR_SUCCESS && ha->pHashTable != NULL)
    {
        pBlockTable = TranslateBlockTable(ha, &BlockTableSize64, &bNeedHiBlockTable);
        if(pBlockTable == NULL)
            nError = ERROR_NOT_ENOUGH_MEMORY;
    }

    // Create hi-block table, if needed
    if(nError == ERROR_SUCCESS && bNeedHiBlockTable)
    {
        pHiBlockTable = TranslateHiBlockTable(ha, &HiBlockTableSize64);
        if(pHiBlockTable == NULL)
            nError = ERROR_NOT_ENOUGH_MEMORY;
    }

    // Write the HET table, if any
    if(nError == ERROR_SUCCESS && pHetTable != NULL)
    {
        pHeader->HetTableSize64 = HetTableSize64;
        pHeader->HetTablePos64  = TablePos;
        nError = SaveExtTable(ha, pHetTable, TablePos, (DWORD)HetTableSize64, pHeader->MD5_HetTable, MPQ_KEY_HASH_TABLE, false, &cbTotalSize);
        TablePos += cbTotalSize;
    }

    // Write the BET table, if any
    if(nError == ERROR_SUCCESS && pBetTable != NULL)
    {
        pHeader->BetTableSize64 = BetTableSize64;
        pHeader->BetTablePos64  = TablePos;
        nError = SaveExtTable(ha, pBetTable, TablePos, (DWORD)BetTableSize64, pHeader->MD5_BetTable, MPQ_KEY_BLOCK_TABLE, false, &cbTotalSize);
        TablePos += cbTotalSize;
    }

    // Write the hash table, if we have any
    if(nError == ERROR_SUCCESS && pHashTable != NULL)
    {
        pHeader->HashTableSize64 = HashTableSize64;
        pHeader->wHashTablePosHi = (USHORT)(TablePos >> 32);
        pHeader->dwHashTableSize = (DWORD)(HashTableSize64 / sizeof(TMPQHash));
        pHeader->dwHashTablePos = (DWORD)TablePos;
        nError = SaveMpqTable(ha, pHashTable, TablePos, (size_t)HashTableSize64, pHeader->MD5_HashTable, MPQ_KEY_HASH_TABLE, false);
        TablePos += HashTableSize64;
    }

    // Write the block table, if we have any
    if(nError == ERROR_SUCCESS && pBlockTable != NULL)
    {
        pHeader->BlockTableSize64 = BlockTableSize64;
        pHeader->wBlockTablePosHi = (USHORT)(TablePos >> 32);
        pHeader->dwBlockTableSize = (DWORD)(BlockTableSize64 / sizeof(TMPQBlock));
        pHeader->dwBlockTablePos = (DWORD)TablePos;
        nError = SaveMpqTable(ha, pBlockTable, TablePos, (size_t)BlockTableSize64, pHeader->MD5_BlockTable, MPQ_KEY_BLOCK_TABLE, false);
        TablePos += BlockTableSize64;
    }

    // Write the hi-block table, if we have any
    if(nError == ERROR_SUCCESS && pHiBlockTable != NULL)
    {
        ULONGLONG ByteOffset = ha->MpqPos + TablePos;

        pHeader->HiBlockTableSize64 = HiBlockTableSize64;
        pHeader->HiBlockTablePos64 = TablePos;
        BSWAP_ARRAY16_UNSIGNED(pHiBlockTable, HiBlockTableSize64);
        
        if(!FileStream_Write(ha->pStream, &ByteOffset, pHiBlockTable, (DWORD)HiBlockTableSize64))
            nError = GetLastError();
        TablePos += HiBlockTableSize64;
    }

    // Cut the MPQ
    if(nError == ERROR_SUCCESS)
    {
        ULONGLONG FileSize = ha->MpqPos + TablePos;

        if(!FileStream_SetSize(ha->pStream, FileSize))
            nError = GetLastError();
    }

    // Write the MPQ header
    if(nError == ERROR_SUCCESS)
    {
        hash_state md5_state;

        // Update the size of the archive
        pHeader->ArchiveSize64 = TablePos;
        pHeader->dwArchiveSize = (DWORD)TablePos;
        
        // Update the MD5 of the archive header
        md5_init(&md5_state);
        md5_process(&md5_state, (unsigned char *)pHeader, MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE);
        md5_done(&md5_state, pHeader->MD5_MpqHeader);

        // Write the MPQ header to the file
        BSWAP_TMPQHEADER(pHeader);
        if(!FileStream_Write(ha->pStream, &ha->MpqPos, pHeader, pHeader->dwHeaderSize))
            nError = GetLastError();
        BSWAP_TMPQHEADER(pHeader);
    }

    // Clear the changed flag
    if(nError == ERROR_SUCCESS)
        ha->dwFlags &= ~MPQ_FLAG_CHANGED;

    // Cleanup and exit
    if(pHetTable != NULL)
        FREEMEM(pHetTable);
    if(pBetTable != NULL)
        FREEMEM(pBetTable);
    if(pHashTable != NULL)
        FREEMEM(pHashTable);
    if(pBlockTable != NULL)
        FREEMEM(pBlockTable);
    if(pHiBlockTable != NULL)
        FREEMEM(pHiBlockTable);
    return nError;
}
