/*****************************************************************************/
/* FileStream.cpp                         Copyright (c) Ladislav Zezula 2010 */
/*---------------------------------------------------------------------------*/
/* File stream support for StormLib                                          */
/*                                                                           */
/* Windows support: Written by Ladislav Zezula                               */
/* Mac support:     Written by Sam Wilkins                                   */
/* Linux support:   Written by Sam Wilkins and Ivan Komissarov               */
/* Big-endian:      Written & debugged by Sam Wilkins                        */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 11.06.10  1.00  Lad  Derived from StormPortMac.cpp and StormPortLinux.cpp */
/*****************************************************************************/

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

#ifdef _MSC_VER
#pragma comment(lib, "wininet.lib")             // Internet functions for HTTP stream
#pragma warning(disable: 4800)                  // 'BOOL' : forcing value to bool 'true' or 'false' (performance warning)
#endif

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

#ifndef INVALID_HANDLE_VALUE
#define INVALID_HANDLE_VALUE ((HANDLE)-1)
#endif

//-----------------------------------------------------------------------------
// Local functions - platform-specific functions

static DWORD StringToInt(const char * szString)
{
    DWORD dwValue = 0;

    while('0' <= szString[0] && szString[0] <= '9')
    {
        dwValue = (dwValue * 10) + (szString[0] - '0');
        szString++;
    }

    return dwValue;
}

static void CreateNameWithSuffix(LPTSTR szBuffer, size_t cchMaxChars, LPCTSTR szName, unsigned int nValue)
{
    LPTSTR szBufferEnd = szBuffer + cchMaxChars - 1;

    // Copy the name
    while(szBuffer < szBufferEnd && szName[0] != 0)
        *szBuffer++ = *szName++;

    // Append "."
    if(szBuffer < szBufferEnd)
        *szBuffer++ = '.';

    // Append the number
    SMemIntToStr(szBuffer, szBufferEnd - szBuffer + 1, nValue);
}

//-----------------------------------------------------------------------------
// Dummy init function

static void BaseNone_Init(TFileStream *)
{
    // Nothing here
}

//-----------------------------------------------------------------------------
// Local functions - base file support

static bool BaseFile_Create(TFileStream * pStream)
{
#ifdef STORMLIB_WINDOWS
    {
        DWORD dwWriteShare = (pStream->dwFlags & STREAM_FLAG_WRITE_SHARE) ? FILE_SHARE_WRITE : 0;

        pStream->Base.File.hFile = CreateFile(pStream->szFileName,
                                              GENERIC_READ | GENERIC_WRITE,
                                              dwWriteShare | FILE_SHARE_READ,
                                              NULL,
                                              CREATE_ALWAYS,
                                              0,
                                              NULL);
        if(pStream->Base.File.hFile == INVALID_HANDLE_VALUE)
            return false;
    }
#endif

#if defined(STORMLIB_MAC) || defined(STORMLIB_LINUX)
    {
        intptr_t handle;

        handle = open(pStream->szFileName, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
        if(handle == -1)
        {
            pStream->Base.File.hFile = INVALID_HANDLE_VALUE;
            SErrSetLastError(errno);
            return false;
        }

        pStream->Base.File.hFile = (HANDLE)handle;
    }
#endif

    // Reset the file size and position
    pStream->Base.File.FileSize = 0;
    pStream->Base.File.FilePos = 0;
    return true;
}

static bool BaseFile_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags)
{
#ifdef STORMLIB_WINDOWS
    {
        ULARGE_INTEGER FileSize;
        DWORD dwWriteAccess = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? 0 : FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES;
        DWORD dwWriteShare = (dwStreamFlags & STREAM_FLAG_WRITE_SHARE) ? FILE_SHARE_WRITE : 0;

        // Open the file
        pStream->Base.File.hFile = CreateFile(szFileName,
                                              FILE_READ_DATA | FILE_READ_ATTRIBUTES | dwWriteAccess,
                                              FILE_SHARE_READ | dwWriteShare,
                                              NULL,
                                              OPEN_EXISTING,
                                              0,
                                              NULL);
        if(pStream->Base.File.hFile == INVALID_HANDLE_VALUE)
            return false;

        // Query the file size
        FileSize.LowPart = GetFileSize(pStream->Base.File.hFile, &FileSize.HighPart);
        pStream->Base.File.FileSize = FileSize.QuadPart;

        // Query last write time
        GetFileTime(pStream->Base.File.hFile, NULL, NULL, (LPFILETIME)&pStream->Base.File.FileTime);
    }
#endif

#if defined(STORMLIB_MAC) || defined(STORMLIB_LINUX)
    {
        struct stat64 fileinfo;
        int oflag = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? O_RDONLY : O_RDWR;
        intptr_t handle;

        // Open the file
        handle = open(szFileName, oflag | O_LARGEFILE);
        if(handle == -1)
        {
            pStream->Base.File.hFile = INVALID_HANDLE_VALUE;
            SErrSetLastError(errno);
            return false;
        }

        // Get the file size
        if(fstat64(handle, &fileinfo) == -1)
        {
            pStream->Base.File.hFile = INVALID_HANDLE_VALUE;
            SErrSetLastError(errno);
            close(handle);
            return false;
        }

        // time_t is number of seconds since 1.1.1970, UTC.
        // 1 second = 10000000 (decimal) in FILETIME
        // Set the start to 1.1.1970 00:00:00
        pStream->Base.File.FileTime = 0x019DB1DED53E8000ULL + (10000000 * fileinfo.st_mtime);
        pStream->Base.File.FileSize = (ULONGLONG)fileinfo.st_size;
        pStream->Base.File.hFile = (HANDLE)handle;
    }
#endif

    // Reset the file position
    pStream->Base.File.FilePos = 0;
    return true;
}

static bool BaseFile_Read(
    TFileStream * pStream,                  // Pointer to an open stream
    ULONGLONG * pByteOffset,                // Pointer to file byte offset. If NULL, it reads from the current position
    void * pvBuffer,                        // Pointer to data to be read
    DWORD dwBytesToRead)                    // Number of bytes to read from the file
{
    ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.File.FilePos;
    DWORD dwBytesRead = 0;                  // Must be set by platform-specific code

#ifdef STORMLIB_WINDOWS
    {
        // Note: StormLib no longer supports Windows 9x.
        // Thus, we can use the OVERLAPPED structure to specify
        // file offset to read from file. This allows us to skip
        // one system call to SetFilePointer

        // Update the byte offset
        pStream->Base.File.FilePos = ByteOffset;

        // Read the data
        if(dwBytesToRead != 0)
        {
            OVERLAPPED Overlapped;

            Overlapped.OffsetHigh = (DWORD)(ByteOffset >> 32);
            Overlapped.Offset = (DWORD)ByteOffset;
            Overlapped.hEvent = NULL;
            if(!ReadFile(pStream->Base.File.hFile, pvBuffer, dwBytesToRead, &dwBytesRead, &Overlapped))
                return false;
        }
    }
#endif

#if defined(STORMLIB_MAC) || defined(STORMLIB_LINUX)
    {
        ssize_t bytes_read;

        // If the byte offset is different from the current file position,
        // we have to update the file position   xxx
        if(ByteOffset != pStream->Base.File.FilePos)
        {
            lseek64((intptr_t)pStream->Base.File.hFile, (off64_t)(ByteOffset), SEEK_SET);
            pStream->Base.File.FilePos = ByteOffset;
        }

        // Perform the read operation
        if(dwBytesToRead != 0)
        {
            bytes_read = read((intptr_t)pStream->Base.File.hFile, pvBuffer, (size_t)dwBytesToRead);
            if(bytes_read == -1)
            {
                SErrSetLastError(errno);
                return false;
            }

            dwBytesRead = (DWORD)(size_t)bytes_read;
        }
    }
#endif

    // Increment the current file position by number of bytes read
    // If the number of bytes read doesn't match to required amount, return false
    pStream->Base.File.FilePos = ByteOffset + dwBytesRead;
    if(dwBytesRead != dwBytesToRead)
        SErrSetLastError(ERROR_HANDLE_EOF);
    return (dwBytesRead == dwBytesToRead);
}

/**
 * \a pStream Pointer to an open stream
 * \a pByteOffset Pointer to file byte offset. If NULL, writes to current position
 * \a pvBuffer Pointer to data to be written
 * \a dwBytesToWrite Number of bytes to write to the file
 */

static bool BaseFile_Write(TFileStream * pStream, ULONGLONG * pByteOffset, const void * pvBuffer, DWORD dwBytesToWrite)
{
    ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.File.FilePos;
    DWORD dwBytesWritten = 0;               // Must be set by platform-specific code

#ifdef STORMLIB_WINDOWS
    {
        // Note: StormLib no longer supports Windows 9x.
        // Thus, we can use the OVERLAPPED structure to specify
        // file offset to read from file. This allows us to skip
        // one system call to SetFilePointer

        // Update the byte offset
        pStream->Base.File.FilePos = ByteOffset;

        // Read the data
        if(dwBytesToWrite != 0)
        {
            OVERLAPPED Overlapped;

            Overlapped.OffsetHigh = (DWORD)(ByteOffset >> 32);
            Overlapped.Offset = (DWORD)ByteOffset;
            Overlapped.hEvent = NULL;
            if(!WriteFile(pStream->Base.File.hFile, pvBuffer, dwBytesToWrite, &dwBytesWritten, &Overlapped))
                return false;
        }
    }
#endif

#if defined(STORMLIB_MAC) || defined(STORMLIB_LINUX)
    {
        ssize_t bytes_written;

        // If the byte offset is different from the current file position,
        // we have to update the file position
        if(ByteOffset != pStream->Base.File.FilePos)
        {
            lseek64((intptr_t)pStream->Base.File.hFile, (off64_t)(ByteOffset), SEEK_SET);
            pStream->Base.File.FilePos = ByteOffset;
        }

        // Perform the read operation
        bytes_written = write((intptr_t)pStream->Base.File.hFile, pvBuffer, (size_t)dwBytesToWrite);
        if(bytes_written == -1)
        {
            SErrSetLastError(errno);
            return false;
        }

        dwBytesWritten = (DWORD)(size_t)bytes_written;
    }
#endif

    // Increment the current file position by number of bytes read
    pStream->Base.File.FilePos = ByteOffset + dwBytesWritten;

    // Also modify the file size, if needed
    if(pStream->Base.File.FilePos > pStream->Base.File.FileSize)
        pStream->Base.File.FileSize = pStream->Base.File.FilePos;

    if(dwBytesWritten != dwBytesToWrite)
        SErrSetLastError(ERROR_DISK_FULL);
    return (dwBytesWritten == dwBytesToWrite);
}

/**
 * \a pStream Pointer to an open stream
 * \a NewFileSize New size of the file
 */
static bool BaseFile_Resize(TFileStream * pStream, ULONGLONG NewFileSize)
{
#ifdef STORMLIB_WINDOWS
    {
        LONG FileSizeHi = (LONG)(NewFileSize >> 32);
        LONG FileSizeLo;
        DWORD dwNewPos;
        bool bResult;

        // Set the position at the new file size
        dwNewPos = SetFilePointer(pStream->Base.File.hFile, (LONG)NewFileSize, &FileSizeHi, FILE_BEGIN);
        if(dwNewPos == INVALID_SET_FILE_POINTER && SErrGetLastError() != ERROR_SUCCESS)
            return false;

        // Set the current file pointer as the end of the file
        bResult = (bool)SetEndOfFile(pStream->Base.File.hFile);
        if(bResult)
            pStream->Base.File.FileSize = NewFileSize;

        // Restore the file position
        FileSizeHi = (LONG)(pStream->Base.File.FilePos >> 32);
        FileSizeLo = (LONG)(pStream->Base.File.FilePos);
        SetFilePointer(pStream->Base.File.hFile, FileSizeLo, &FileSizeHi, FILE_BEGIN);
        return bResult;
    }
#endif

#if defined(STORMLIB_MAC) || defined(STORMLIB_LINUX)
    {
        if(ftruncate64((intptr_t)pStream->Base.File.hFile, (off64_t)NewFileSize) == -1)
        {
            SErrSetLastError(errno);
            return false;
        }

        pStream->Base.File.FileSize = NewFileSize;
        return true;
    }
#endif
}

// Gives the current file size
static bool BaseFile_GetSize(TFileStream * pStream, ULONGLONG * pFileSize)
{
    // Note: Used by all thre base providers.
    // Requires the TBaseData union to have the same layout for all three base providers
    *pFileSize = pStream->Base.File.FileSize;
    return true;
}

// Gives the current file position
static bool BaseFile_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset)
{
    // Note: Used by all thre base providers.
    // Requires the TBaseData union to have the same layout for all three base providers
    *pByteOffset = pStream->Base.File.FilePos;
    return true;
}

// Renames the file pointed by pStream so that it contains data from pNewStream
static bool BaseFile_Replace(TFileStream * pStream, TFileStream * pNewStream)
{
#ifdef STORMLIB_WINDOWS
    // Delete the original stream file. Don't check the result value,
    // because if the file doesn't exist, it would fail
    DeleteFile(pStream->szFileName);

    // Rename the new file to the old stream's file
    return (bool)MoveFile(pNewStream->szFileName, pStream->szFileName);
#endif

#if defined(STORMLIB_MAC) || defined(STORMLIB_LINUX)
    // "rename" on Linux also works if the target file exists
    if(rename(pNewStream->szFileName, pStream->szFileName) == -1)
    {
        SErrSetLastError(errno);
        return false;
    }

    return true;
#endif
}

static void BaseFile_Close(TFileStream * pStream)
{
    if(pStream->Base.File.hFile != INVALID_HANDLE_VALUE)
    {
#ifdef STORMLIB_WINDOWS
        CloseHandle(pStream->Base.File.hFile);
#endif

#if defined(STORMLIB_MAC) || defined(STORMLIB_LINUX)
        close((intptr_t)pStream->Base.File.hFile);
#endif
    }

    // Also invalidate the handle
    pStream->Base.File.hFile = INVALID_HANDLE_VALUE;
}

// Initializes base functions for the disk file
static void BaseFile_Init(TFileStream * pStream)
{
    pStream->BaseCreate  = BaseFile_Create;
    pStream->BaseOpen    = BaseFile_Open;
    pStream->BaseRead    = BaseFile_Read;
    pStream->BaseWrite   = BaseFile_Write;
    pStream->BaseResize  = BaseFile_Resize;
    pStream->BaseGetSize = BaseFile_GetSize;
    pStream->BaseGetPos  = BaseFile_GetPos;
    pStream->BaseClose   = BaseFile_Close;
}

//-----------------------------------------------------------------------------
// Local functions - base memory-mapped file support

#ifdef STORMLIB_WINDOWS

typedef struct _SECTION_BASIC_INFORMATION
{
    PVOID BaseAddress;
    ULONG Attributes;
    LARGE_INTEGER Size;
} SECTION_BASIC_INFORMATION, *PSECTION_BASIC_INFORMATION;

typedef ULONG (WINAPI * NTQUERYSECTION)(
    IN  HANDLE SectionHandle,
    IN  ULONG SectionInformationClass,
    OUT PVOID SectionInformation,
    IN  SIZE_T Length,
    OUT PSIZE_T ResultLength);

static bool RetrieveFileMappingSize(HANDLE hSection, ULARGE_INTEGER & RefFileSize)
{
    SECTION_BASIC_INFORMATION BasicInfo = {0};
    NTQUERYSECTION PfnQuerySection;
    HMODULE hNtdll;
    SIZE_T ReturnLength = 0;

    if((hNtdll = GetModuleHandle(_T("ntdll.dll"))) != NULL)
    {
        PfnQuerySection = (NTQUERYSECTION)GetProcAddress(hNtdll, "NtQuerySection");
        if(PfnQuerySection != NULL)
        {
            if(PfnQuerySection(hSection, 0, &BasicInfo, sizeof(SECTION_BASIC_INFORMATION), &ReturnLength) == 0)
            {
                RefFileSize.HighPart = BasicInfo.Size.HighPart;
                RefFileSize.LowPart = BasicInfo.Size.LowPart;
                return true;
            }
        }
    }

    return false;
}
#endif

static bool BaseMap_Open(TFileStream * pStream, LPCTSTR szFileName, DWORD dwStreamFlags)
{
#ifdef STORMLIB_WINDOWS

    ULARGE_INTEGER FileSize = {0};
    HANDLE hFile = INVALID_HANDLE_VALUE;
    HANDLE hMap = NULL;
    bool bResult = false;

    // Keep compilers happy
    STORMLIB_UNUSED(dwStreamFlags);

    // 1) Try to treat "szFileName" as a section name
    hMap = OpenFileMapping(SECTION_QUERY | FILE_MAP_READ, FALSE, szFileName);
    if(hMap != NULL)
    {
        // Try to retrieve the size of the mapping
        if(!RetrieveFileMappingSize(hMap, FileSize))
        {
            CloseHandle(hMap);
            hMap = NULL;
        }
    }

    // 2) Treat the name as file name
    else
    {
        hFile = CreateFile(szFileName, FILE_READ_DATA, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
        if(hFile != INVALID_HANDLE_VALUE)
        {
            // Retrieve file size. Don't allow mapping file of a zero size.
            FileSize.LowPart = GetFileSize(hFile, &FileSize.HighPart);
            if(FileSize.QuadPart != 0)
            {
                // Now create file mapping over the file
                hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
            }
        }
    }

    // Did it succeed?
    if(hMap != NULL)
    {
        // Map the entire view into memory
        // Note that this operation will fail if the file can't fit
        // into usermode address space
        pStream->Base.Map.pbFile = (LPBYTE)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
        if(pStream->Base.Map.pbFile != NULL)
        {
            // Retrieve file time. If it's named section, put 0
            if(hFile != INVALID_HANDLE_VALUE)
                GetFileTime(hFile, NULL, NULL, (LPFILETIME)&pStream->Base.Map.FileTime);

            // Retrieve file size and position
            pStream->Base.Map.FileSize = FileSize.QuadPart;
            pStream->Base.Map.FilePos = 0;
            bResult = true;
        }

        // Close the map handle
        CloseHandle(hMap);
    }

    // Close the file handle
    if(hFile != INVALID_HANDLE_VALUE)
        CloseHandle(hFile);

    // Return the result of the operation
    return bResult;

#elif defined(STORMLIB_HAS_MMAP)

    struct stat64 fileinfo;
    intptr_t handle;
    bool bResult = false;

    // Open the file
    handle = open(szFileName, O_RDONLY);
    if(handle != -1)
    {
        // Get the file size
        if(fstat64(handle, &fileinfo) != -1)
        {
            pStream->Base.Map.pbFile = (LPBYTE)mmap(NULL, (size_t)fileinfo.st_size, PROT_READ, MAP_PRIVATE, handle, 0);
            if(pStream->Base.Map.pbFile != NULL)
            {
                // time_t is number of seconds since 1.1.1970, UTC.
                // 1 second = 10000000 (decimal) in FILETIME
                // Set the start to 1.1.1970 00:00:00
                pStream->Base.Map.FileTime = 0x019DB1DED53E8000ULL + (10000000 * fileinfo.st_mtime);
                pStream->Base.Map.FileSize = (ULONGLONG)fileinfo.st_size;
                pStream->Base.Map.FilePos = 0;
                bResult = true;
            }
        }
        close(handle);
    }

    // Did the mapping fail?
    if(bResult == false)
        SErrSetLastError(errno);
    return bResult;

#else

    // File mapping is not supported
    return false;

#endif
}

static bool BaseMap_Read(
    TFileStream * pStream,                  // Pointer to an open stream
    ULONGLONG * pByteOffset,                // Pointer to file byte offset. If NULL, it reads from the current position
    void * pvBuffer,                        // Pointer to data to be read
    DWORD dwBytesToRead)                    // Number of bytes to read from the file
{
    ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.Map.FilePos;

    // Do we have to read anything at all?
    if(dwBytesToRead != 0)
    {
        // Don't allow reading past file size
        if((ByteOffset + dwBytesToRead) > pStream->Base.Map.FileSize)
            return false;

        // Copy the required data
        memcpy(pvBuffer, pStream->Base.Map.pbFile + (size_t)ByteOffset, dwBytesToRead);
    }

    // Move the current file position
    pStream->Base.Map.FilePos += dwBytesToRead;
    return true;
}

static void BaseMap_Close(TFileStream * pStream)
{

#ifdef STORMLIB_WINDOWS

    if(pStream->Base.Map.pbFile != NULL)
        UnmapViewOfFile(pStream->Base.Map.pbFile);

#elif defined(STORMLIB_HAS_MMAP)

    if(pStream->Base.Map.pbFile != NULL)
        munmap(pStream->Base.Map.pbFile, (size_t )pStream->Base.Map.FileSize);

#endif

    pStream->Base.Map.pbFile = NULL;
}

// Initializes base functions for the mapped file
static void BaseMap_Init(TFileStream * pStream)
{
    // Supply the file stream functions
    pStream->BaseOpen    = BaseMap_Open;
    pStream->BaseRead    = BaseMap_Read;
    pStream->BaseGetSize = BaseFile_GetSize;    // Reuse BaseFile function
    pStream->BaseGetPos  = BaseFile_GetPos;     // Reuse BaseFile function
    pStream->BaseClose   = BaseMap_Close;

    // Mapped files are read-only
    pStream->dwFlags |= STREAM_FLAG_READ_ONLY;
}

//-----------------------------------------------------------------------------
// Local functions - base HTTP file support

static const TCHAR * BaseHttp_ExtractServerName(const TCHAR * szFileName, TCHAR * szServerName)
{
    // Check for HTTP
    if(!_tcsnicmp(szFileName, _T("http://"), 7))
        szFileName += 7;

    // Cut off the server name
    if(szServerName != NULL)
    {
        while(szFileName[0] != 0 && szFileName[0] != _T('/'))
            *szServerName++ = *szFileName++;
        *szServerName = 0;
    }
    else
    {
        while(szFileName[0] != 0 && szFileName[0] != _T('/'))
            szFileName++;
    }

    // Return the remainder
    return szFileName;
}

static bool BaseHttp_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags)
{
#ifdef STORMLIB_WINDOWS

    HINTERNET hRequest;
    DWORD dwTemp = 0;

    // Keep compilers happy
    STORMLIB_UNUSED(dwStreamFlags);

    // Don't connect to the internet
    if(!InternetGetConnectedState(&dwTemp, 0))
        return false;

    // Initiate the connection to the internet
    pStream->Base.Http.hInternet = InternetOpen(_T("StormLib HTTP MPQ reader"),
                                                INTERNET_OPEN_TYPE_PRECONFIG,
                                                NULL,
                                                NULL,
                                                0);
    if(pStream->Base.Http.hInternet != NULL)
    {
        TCHAR szServerName[MAX_PATH];
        DWORD dwFlags = INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_CACHE_WRITE;

        // Initiate connection with the server
        szFileName = BaseHttp_ExtractServerName(szFileName, szServerName);
        pStream->Base.Http.hConnect = InternetConnect(pStream->Base.Http.hInternet,
                                                      szServerName,
                                                      INTERNET_DEFAULT_HTTP_PORT,
                                                      NULL,
                                                      NULL,
                                                      INTERNET_SERVICE_HTTP,
                                                      dwFlags,
                                                      0);
        if(pStream->Base.Http.hConnect != NULL)
        {
            // Open HTTP request to the file
            hRequest = HttpOpenRequest(pStream->Base.Http.hConnect, _T("GET"), szFileName, NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0);
            if(hRequest != NULL)
            {
                if(HttpSendRequest(hRequest, NULL, 0, NULL, 0))
                {
                    ULONGLONG FileTime = 0;
                    DWORD dwFileSize = 0;
                    DWORD dwDataSize;
                    DWORD dwIndex = 0;
                    TCHAR StatusCode[0x08];

                    // Check if the file succeeded to open
                    dwDataSize = sizeof(StatusCode);
                    if(HttpQueryInfo(hRequest, HTTP_QUERY_STATUS_CODE, StatusCode, &dwDataSize, &dwIndex))
                    {
                        if(_tcscmp(StatusCode, _T("200")))
                        {
                            InternetCloseHandle(hRequest);
                            SErrSetLastError(ERROR_FILE_NOT_FOUND);
                            return false;
                        }
                    }

                    // Check if the MPQ has Last Modified field
                    dwDataSize = sizeof(ULONGLONG);
                    if(HttpQueryInfo(hRequest, HTTP_QUERY_LAST_MODIFIED | HTTP_QUERY_FLAG_SYSTEMTIME, &FileTime, &dwDataSize, &dwIndex))
                        pStream->Base.Http.FileTime = FileTime;

                    // Verify if the server supports random access
                    dwDataSize = sizeof(DWORD);
                    if(HttpQueryInfo(hRequest, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &dwFileSize, &dwDataSize, &dwIndex))
                    {
                        if(dwFileSize != 0)
                        {
                            InternetCloseHandle(hRequest);
                            pStream->Base.Http.FileSize = dwFileSize;
                            pStream->Base.Http.FilePos = 0;
                            return true;
                        }
                    }
                }

                // Close the request
                InternetCloseHandle(hRequest);
            }

            // Close the connection handle
            InternetCloseHandle(pStream->Base.Http.hConnect);
            pStream->Base.Http.hConnect = NULL;
        }

        // Close the internet handle
        InternetCloseHandle(pStream->Base.Http.hInternet);
        pStream->Base.Http.hInternet = NULL;
    }

    // If the file is not there or is not available for random access, report error
    pStream->BaseClose(pStream);
    return false;

#else

    // Not supported
    SErrSetLastError(ERROR_NOT_SUPPORTED);
    pStream = pStream;
    return false;

#endif
}

static bool BaseHttp_Read(
    TFileStream * pStream,                  // Pointer to an open stream
    ULONGLONG * pByteOffset,                // Pointer to file byte offset. If NULL, it reads from the current position
    void * pvBuffer,                        // Pointer to data to be read
    DWORD dwBytesToRead)                    // Number of bytes to read from the file
{
#ifdef STORMLIB_WINDOWS
    ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.Http.FilePos;
    DWORD dwTotalBytesRead = 0;

    // Do we have to read anything at all?
    if(dwBytesToRead != 0)
    {
        HINTERNET hRequest;
        LPCTSTR szFileName;
        LPBYTE pbBuffer = (LPBYTE)pvBuffer;
        TCHAR szRangeRequest[0x80];
        DWORD dwStartOffset = (DWORD)ByteOffset;
        DWORD dwEndOffset = dwStartOffset + dwBytesToRead;

        // Open HTTP request to the file
        szFileName = BaseHttp_ExtractServerName(pStream->szFileName, NULL);
        hRequest = HttpOpenRequest(pStream->Base.Http.hConnect, _T("GET"), szFileName, NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0);
        if(hRequest != NULL)
        {
            // Add range request to the HTTP headers
            // http://www.clevercomponents.com/articles/article015/resuming.asp
            wsprintf(szRangeRequest, _T("Range: bytes=%u-%u"), (unsigned int)dwStartOffset, (unsigned int)dwEndOffset);
            HttpAddRequestHeaders(hRequest, szRangeRequest, 0xFFFFFFFF, HTTP_ADDREQ_FLAG_ADD_IF_NEW);

            // Send the request to the server
            if(HttpSendRequest(hRequest, NULL, 0, NULL, 0))
            {
                while(dwTotalBytesRead < dwBytesToRead)
                {
                    DWORD dwBlockBytesToRead = dwBytesToRead - dwTotalBytesRead;
                    DWORD dwBlockBytesRead = 0;

                    // Read the block from the file
                    if(dwBlockBytesToRead > 0x200)
                        dwBlockBytesToRead = 0x200;
                    InternetReadFile(hRequest, pbBuffer, dwBlockBytesToRead, &dwBlockBytesRead);

                    // Check for end
                    if(dwBlockBytesRead == 0)
                        break;

                    // Move buffers
                    dwTotalBytesRead += dwBlockBytesRead;
                    pbBuffer += dwBlockBytesRead;
                }
            }
            InternetCloseHandle(hRequest);
        }
    }

    // Increment the current file position by number of bytes read
    pStream->Base.Http.FilePos = ByteOffset + dwTotalBytesRead;

    // If the number of bytes read doesn't match the required amount, return false
    if(dwTotalBytesRead != dwBytesToRead)
        SErrSetLastError(ERROR_HANDLE_EOF);
    return (dwTotalBytesRead == dwBytesToRead);

#else

    // Not supported
    pStream = pStream;
    pByteOffset = pByteOffset;
    pvBuffer = pvBuffer;
    dwBytesToRead = dwBytesToRead;
    SErrSetLastError(ERROR_NOT_SUPPORTED);
    return false;

#endif
}

static void BaseHttp_Close(TFileStream * pStream)
{
#ifdef STORMLIB_WINDOWS
    if(pStream->Base.Http.hConnect != NULL)
        InternetCloseHandle(pStream->Base.Http.hConnect);
    pStream->Base.Http.hConnect = NULL;

    if(pStream->Base.Http.hInternet != NULL)
        InternetCloseHandle(pStream->Base.Http.hInternet);
    pStream->Base.Http.hInternet = NULL;
#else
    pStream = pStream;
#endif
}

// Initializes base functions for the mapped file
static void BaseHttp_Init(TFileStream * pStream)
{
    // Supply the stream functions
    pStream->BaseOpen    = BaseHttp_Open;
    pStream->BaseRead    = BaseHttp_Read;
    pStream->BaseGetSize = BaseFile_GetSize;    // Reuse BaseFile function
    pStream->BaseGetPos  = BaseFile_GetPos;     // Reuse BaseFile function
    pStream->BaseClose   = BaseHttp_Close;

    // HTTP files are read-only
    pStream->dwFlags |= STREAM_FLAG_READ_ONLY;
}

//-----------------------------------------------------------------------------
// Local functions - base block-based support

// Generic function that loads blocks from the file
// The function groups the block with the same availability,
// so the called BlockRead can finish the request in a single system call
static bool BlockStream_Read(
    TBlockStream * pStream,                 // Pointer to an open stream
    ULONGLONG * pByteOffset,                // Pointer to file byte offset. If NULL, it reads from the current position
    void * pvBuffer,                        // Pointer to data to be read
    DWORD dwBytesToRead)                    // Number of bytes to read from the file
{
    ULONGLONG BlockOffset0;
    ULONGLONG BlockOffset;
    ULONGLONG ByteOffset;
    ULONGLONG EndOffset;
    LPBYTE TransferBuffer;
    LPBYTE BlockBuffer;
    DWORD BlockBufferOffset;                // Offset of the desired data in the block buffer
    DWORD BytesNeeded;                      // Number of bytes that really need to be read
    DWORD BlockSize = pStream->BlockSize;
    DWORD BlockCount;
    bool bPrevBlockAvailable;
    bool bCallbackCalled = false;
    bool bBlockAvailable;
    bool bResult = true;

    // The base block read function must be present
    assert(pStream->BlockRead != NULL);

    // NOP reading of zero bytes
    if(dwBytesToRead == 0)
        return true;

    // Get the current position in the stream
    ByteOffset = (pByteOffset != NULL) ? pByteOffset[0] : pStream->StreamPos;
    EndOffset = ByteOffset + dwBytesToRead;
    if(EndOffset > pStream->StreamSize)
    {
        SErrSetLastError(ERROR_HANDLE_EOF);
        return false;
    }

    // Calculate the block parameters
    BlockOffset0 = BlockOffset = ByteOffset & ~((ULONGLONG)BlockSize - 1);
    BlockCount  = (DWORD)(((EndOffset - BlockOffset) + (BlockSize - 1)) / BlockSize);
    BytesNeeded = (DWORD)(EndOffset - BlockOffset);

    // Remember where we have our data
    assert((BlockSize & (BlockSize - 1)) == 0);
    BlockBufferOffset = (DWORD)(ByteOffset & (BlockSize - 1));

    // Allocate buffer for reading blocks
    TransferBuffer = BlockBuffer = STORM_ALLOC(BYTE, (BlockCount * BlockSize));
    if(TransferBuffer == NULL)
    {
        SErrSetLastError(ERROR_NOT_ENOUGH_MEMORY);
        return false;
    }

    // If all blocks are available, just read all blocks at once
    if(pStream->IsComplete == 0)
    {
        // Now parse the blocks and send the block read request
        // to all blocks with the same availability
        assert(pStream->BlockCheck != NULL);
        bPrevBlockAvailable = pStream->BlockCheck(pStream, BlockOffset);

        // Loop as long as we have something to read
        while(BlockOffset < EndOffset)
        {
            // Determine availability of the next block
            bBlockAvailable = pStream->BlockCheck(pStream, BlockOffset);

            // If the availability has changed, read all blocks up to this one
            if(bBlockAvailable != bPrevBlockAvailable)
            {
                // Call the file stream callback, if the block is not available
                if(pStream->pMaster && pStream->pfnCallback && bPrevBlockAvailable == false)
                {
                    pStream->pfnCallback(pStream->UserData, BlockOffset0, (DWORD)(BlockOffset - BlockOffset0));
                    bCallbackCalled = true;
                }

                // Load the continuous blocks with the same availability
                assert(BlockOffset > BlockOffset0);
                bResult = pStream->BlockRead(pStream, BlockOffset0, BlockOffset, BlockBuffer, BytesNeeded, bPrevBlockAvailable);
                if(!bResult)
                    break;

                // Move the block offset
                BlockBuffer += (DWORD)(BlockOffset - BlockOffset0);
                BytesNeeded -= (DWORD)(BlockOffset - BlockOffset0);
                bPrevBlockAvailable = bBlockAvailable;
                BlockOffset0 = BlockOffset;
            }

            // Move to the block offset in the stream
            BlockOffset += BlockSize;
        }

        // If there is a block(s) remaining to be read, do it
        if(BlockOffset > BlockOffset0)
        {
            // Call the file stream callback, if the block is not available
            if(pStream->pMaster && pStream->pfnCallback && bPrevBlockAvailable == false)
            {
                pStream->pfnCallback(pStream->UserData, BlockOffset0, (DWORD)(BlockOffset - BlockOffset0));
                bCallbackCalled = true;
            }

            // Read the complete blocks from the file
            if(BlockOffset > pStream->StreamSize)
                BlockOffset = pStream->StreamSize;
            bResult = pStream->BlockRead(pStream, BlockOffset0, BlockOffset, BlockBuffer, BytesNeeded, bPrevBlockAvailable);
        }
    }
    else
    {
        // Read the complete blocks from the file
        if(EndOffset > pStream->StreamSize)
            EndOffset = pStream->StreamSize;
        bResult = pStream->BlockRead(pStream, BlockOffset, EndOffset, BlockBuffer, BytesNeeded, true);
    }

    // Now copy the data to the user buffer
    if(bResult)
    {
        memcpy(pvBuffer, TransferBuffer + BlockBufferOffset, dwBytesToRead);
        pStream->StreamPos = ByteOffset + dwBytesToRead;
    }
    else
    {
        // If the block read failed, set the last error
        SErrSetLastError(ERROR_FILE_INCOMPLETE);
    }

    // Call the callback to indicate we are done
    if(bCallbackCalled)
        pStream->pfnCallback(pStream->UserData, 0, 0);

    // Free the block buffer and return
    STORM_FREE(TransferBuffer);
    return bResult;
}

static bool BlockStream_GetSize(TFileStream * pStream, ULONGLONG * pFileSize)
{
    *pFileSize = pStream->StreamSize;
    return true;
}

static bool BlockStream_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset)
{
    *pByteOffset = pStream->StreamPos;
    return true;
}

static void BlockStream_Close(TBlockStream * pStream)
{
    // Free the data map, if any
    if(pStream->FileBitmap != NULL)
        STORM_FREE(pStream->FileBitmap);
    pStream->FileBitmap = NULL;

    // Call the base class for closing the stream
    pStream->BaseClose(pStream);
}

//-----------------------------------------------------------------------------
// File stream allocation function

static STREAM_INIT StreamBaseInit[4] =
{
    BaseFile_Init,
    BaseMap_Init,
    BaseHttp_Init,
    BaseNone_Init
};

// This function allocates an empty structure for the file stream
// The stream structure is created as flat block, variable length
// The file name is placed after the end of the stream structure data
static TFileStream * AllocateFileStream(
    const TCHAR * szFileName,
    size_t StreamSize,
    DWORD dwStreamFlags)
{
    TFileStream * pMaster = NULL;
    TFileStream * pStream;
    const TCHAR * szNextFile = szFileName;
    size_t FileNameSize;

    // Sanity check
    assert(StreamSize != 0);

    // The caller can specify chain of files in the following form:
    // C:\archive.MPQ*http://www.server.com/MPQs/archive-server.MPQ
    // In that case, we use the part after "*" as master file name
    while(szNextFile[0] != 0 && szNextFile[0] != _T('*'))
        szNextFile++;
    FileNameSize = (size_t)((szNextFile - szFileName) * sizeof(TCHAR));

    // If we have a next file, we need to open it as master stream
    // Note that we don't care if the master stream exists or not,
    // If it doesn't, later attempts to read missing file block will fail
    if(szNextFile[0] == _T('*'))
    {
        // Don't allow another master file in the string
        if(_tcschr(szNextFile + 1, _T('*')) != NULL)
        {
            SErrSetLastError(ERROR_INVALID_PARAMETER);
            return NULL;
        }

        // Open the master file
        pMaster = FileStream_OpenFile(szNextFile + 1, STREAM_FLAG_READ_ONLY);
    }

    // Allocate the stream structure for the given stream type
    pStream = (TFileStream *)STORM_ALLOC(BYTE, StreamSize + FileNameSize + sizeof(TCHAR));
    if(pStream != NULL)
    {
        // Zero the entire structure
        memset(pStream, 0, StreamSize);
        pStream->pMaster = pMaster;
        pStream->dwFlags = dwStreamFlags;

        // Initialize the file name
        pStream->szFileName = (TCHAR *)((BYTE *)pStream + StreamSize);
        memcpy(pStream->szFileName, szFileName, FileNameSize);
        pStream->szFileName[FileNameSize / sizeof(TCHAR)] = 0;

        // Initialize the stream functions
        StreamBaseInit[dwStreamFlags & 0x03](pStream);
    }

    return pStream;
}

//-----------------------------------------------------------------------------
// Local functions - flat stream support

static DWORD FlatStream_CheckFile(TBlockStream * pStream)
{
    LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap;
    DWORD WholeByteCount = (pStream->BlockCount / 8);
    DWORD ExtraBitsCount = (pStream->BlockCount & 7);
    BYTE ExpectedValue;

    // Verify the whole bytes - their value must be 0xFF
    for(DWORD i = 0; i < WholeByteCount; i++)
    {
        if(FileBitmap[i] != 0xFF)
            return 0;
    }

    // If there are extra bits, calculate the mask
    if(ExtraBitsCount != 0)
    {
        ExpectedValue = (BYTE)((1 << ExtraBitsCount) - 1);
        if(FileBitmap[WholeByteCount] != ExpectedValue)
            return 0;
    }

    // Yes, the file is complete
    return 1;
}

static bool FlatStream_LoadBitmap(TBlockStream * pStream)
{
    FILE_BITMAP_FOOTER Footer;
    ULONGLONG ByteOffset;
    LPBYTE FileBitmap;
    DWORD BlockCount;
    DWORD BitmapSize;

    // Do not load the bitmap if we should not have to
    if(!(pStream->dwFlags & STREAM_FLAG_USE_BITMAP))
        return false;

    // Only if the size is greater than size of bitmap footer
    if(pStream->Base.File.FileSize > sizeof(FILE_BITMAP_FOOTER))
    {
        // Load the bitmap footer
        ByteOffset = pStream->Base.File.FileSize - sizeof(FILE_BITMAP_FOOTER);
        if(pStream->BaseRead(pStream, &ByteOffset, &Footer, sizeof(FILE_BITMAP_FOOTER)))
        {
            // Make sure that the array is properly BSWAP-ed
            BSWAP_ARRAY32_UNSIGNED((LPDWORD)(&Footer), sizeof(FILE_BITMAP_FOOTER));

            // Verify if there is actually a footer
            if(Footer.Signature == ID_FILE_BITMAP_FOOTER && Footer.Version == 0x03 && Footer.BlockSize != 0)
            {
                // Get the offset of the bitmap, number of blocks and size of the bitmap
                ByteOffset = MAKE_OFFSET64(Footer.MapOffsetHi, Footer.MapOffsetLo);
                BlockCount = (DWORD)(((ByteOffset - 1) / Footer.BlockSize) + 1);
                BitmapSize = ((BlockCount + 7) / 8);

                // Check if the sizes match
                if(ByteOffset + BitmapSize + sizeof(FILE_BITMAP_FOOTER) == pStream->Base.File.FileSize)
                {
                    // Allocate space for the bitmap
                    FileBitmap = STORM_ALLOC(BYTE, BitmapSize);
                    if(FileBitmap != NULL)
                    {
                        // Load the bitmap bits
                        if(!pStream->BaseRead(pStream, &ByteOffset, FileBitmap, BitmapSize))
                        {
                            STORM_FREE(FileBitmap);
                            return false;
                        }

                        // Update the stream size
                        pStream->BuildNumber = Footer.BuildNumber;
                        pStream->StreamSize = ByteOffset;

                        // Fill the bitmap information
                        pStream->FileBitmap = FileBitmap;
                        pStream->BitmapSize = BitmapSize;
                        pStream->BlockSize  = Footer.BlockSize;
                        pStream->BlockCount = BlockCount;
                        pStream->IsComplete = FlatStream_CheckFile(pStream);
                        return true;
                    }
                }
            }
        }
    }

    return false;
}

static void FlatStream_UpdateBitmap(
    TBlockStream * pStream,                // Pointer to an open stream
    ULONGLONG StartOffset,
    ULONGLONG EndOffset)
{
    LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap;
    DWORD BlockIndex;
    DWORD BlockSize = pStream->BlockSize;
    DWORD ByteIndex;
    BYTE BitMask;

    // Sanity checks
    assert((StartOffset & (BlockSize - 1)) == 0);
    assert(FileBitmap != NULL);

    // Calculate the index of the block
    BlockIndex = (DWORD)(StartOffset / BlockSize);
    ByteIndex = (BlockIndex / 0x08);
    BitMask = (BYTE)(1 << (BlockIndex & 0x07));

    // Set all bits for the specified range
    while(StartOffset < EndOffset)
    {
        // Set the bit
        FileBitmap[ByteIndex] |= BitMask;

        // Move all
        StartOffset += BlockSize;
        ByteIndex += (BitMask >> 0x07);
        BitMask = (BitMask >> 0x07) | (BitMask << 0x01);
    }

    // Increment the bitmap update count
    pStream->IsModified = 1;
}

static bool FlatStream_BlockCheck(
    TBlockStream * pStream,                // Pointer to an open stream
    ULONGLONG BlockOffset)
{
    LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap;
    DWORD BlockIndex;
    BYTE BitMask;

    // Sanity checks
    assert((BlockOffset & (pStream->BlockSize - 1)) == 0);
    assert(FileBitmap != NULL);

    // Calculate the index of the block
    BlockIndex = (DWORD)(BlockOffset / pStream->BlockSize);
    BitMask = (BYTE)(1 << (BlockIndex & 0x07));

    // Check if the bit is present
    return (FileBitmap[BlockIndex / 0x08] & BitMask) ? true : false;
}

static bool FlatStream_BlockRead(
    TBlockStream * pStream,                // Pointer to an open stream
    ULONGLONG StartOffset,
    ULONGLONG EndOffset,
    LPBYTE BlockBuffer,
    DWORD BytesNeeded,
    bool bAvailable)
{
    DWORD BytesToRead = (DWORD)(EndOffset - StartOffset);

    // The starting offset must be aligned to size of the block
    assert(pStream->FileBitmap != NULL);
    assert((StartOffset & (pStream->BlockSize - 1)) == 0);
    assert(StartOffset < EndOffset);

    // If the blocks are not available, we need to load them from the master
    // and then save to the mirror
    if(bAvailable == false)
    {
        // If we have no master, we cannot satisfy read request
        if(pStream->pMaster == NULL)
            return false;

        // Load the blocks from the master stream
        // Note that we always have to read complete blocks
        // so they get properly stored to the mirror stream
        if(!FileStream_Read(pStream->pMaster, &StartOffset, BlockBuffer, BytesToRead))
            return false;

        // Store the loaded blocks to the mirror file.
        // Note that this operation is not required to succeed
        if(pStream->BaseWrite(pStream, &StartOffset, BlockBuffer, BytesToRead))
            FlatStream_UpdateBitmap(pStream, StartOffset, EndOffset);

        return true;
    }
    else
    {
        if(BytesToRead > BytesNeeded)
            BytesToRead = BytesNeeded;
        return pStream->BaseRead(pStream, &StartOffset, BlockBuffer, BytesToRead);
    }
}

static void FlatStream_Close(TBlockStream * pStream)
{
    FILE_BITMAP_FOOTER Footer;

    if(pStream->FileBitmap && pStream->IsModified)
    {
        // Write the file bitmap
        pStream->BaseWrite(pStream, &pStream->StreamSize, pStream->FileBitmap, pStream->BitmapSize);

        // Prepare and write the file footer
        Footer.Signature   = ID_FILE_BITMAP_FOOTER;
        Footer.Version     = 3;
        Footer.BuildNumber = pStream->BuildNumber;
        Footer.MapOffsetLo = (DWORD)(pStream->StreamSize & 0xFFFFFFFF);
        Footer.MapOffsetHi = (DWORD)(pStream->StreamSize >> 0x20);
        Footer.BlockSize   = pStream->BlockSize;
        BSWAP_ARRAY32_UNSIGNED(&Footer, sizeof(FILE_BITMAP_FOOTER));
        pStream->BaseWrite(pStream, NULL, &Footer, sizeof(FILE_BITMAP_FOOTER));
    }

    // Close the base class
    BlockStream_Close(pStream);
}

static bool FlatStream_CreateMirror(TBlockStream * pStream)
{
    ULONGLONG MasterSize = 0;
    ULONGLONG MirrorSize = 0;
    LPBYTE FileBitmap = NULL;
    DWORD dwBitmapSize;
    DWORD dwBlockCount;
    bool bNeedCreateMirrorStream = true;
    bool bNeedResizeMirrorStream = true;

    // Do we have master function and base creation function?
    if(pStream->pMaster == NULL || pStream->BaseCreate == NULL)
        return false;

    // Retrieve the master file size, block count and bitmap size
    FileStream_GetSize(pStream->pMaster, &MasterSize);
    dwBlockCount = (DWORD)((MasterSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE);
    dwBitmapSize = (DWORD)((dwBlockCount + 7) / 8);

    // Setup stream size and position
    pStream->BuildNumber = DEFAULT_BUILD_NUMBER;        // BUGBUG: Really???
    pStream->StreamSize = MasterSize;
    pStream->StreamPos = 0;

    // Open the base stream for write access
    if(pStream->BaseOpen(pStream, pStream->szFileName, 0))
    {
        // If the file open succeeded, check if the file size matches required size
        pStream->BaseGetSize(pStream, &MirrorSize);
        if(MirrorSize == MasterSize + dwBitmapSize + sizeof(FILE_BITMAP_FOOTER))
        {
            // Attempt to load an existing file bitmap
            if(FlatStream_LoadBitmap(pStream))
                return true;

            // We need to create new file bitmap
            bNeedResizeMirrorStream = false;
        }

        // We need to create mirror stream
        bNeedCreateMirrorStream = false;
    }

    // Create a new stream, if needed
    if(bNeedCreateMirrorStream)
    {
        if(!pStream->BaseCreate(pStream))
            return false;
    }

    // If we need to, then resize the mirror stream
    if(bNeedResizeMirrorStream)
    {
        if(!pStream->BaseResize(pStream, MasterSize + dwBitmapSize + sizeof(FILE_BITMAP_FOOTER)))
            return false;
    }

    // Allocate the bitmap array
    FileBitmap = STORM_ALLOC(BYTE, dwBitmapSize);
    if(FileBitmap == NULL)
        return false;

    // Initialize the bitmap
    memset(FileBitmap, 0, dwBitmapSize);
    pStream->FileBitmap = FileBitmap;
    pStream->BitmapSize = dwBitmapSize;
    pStream->BlockSize  = DEFAULT_BLOCK_SIZE;
    pStream->BlockCount = dwBlockCount;
    pStream->IsComplete = 0;
    pStream->IsModified = 1;

    // Note: Don't write the stream bitmap right away.
    // Doing so would cause sparse file resize on NTFS,
    // which would take long time on larger files.
    return true;
}

static TFileStream * FlatStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags)
{
    TBlockStream * pStream;
    ULONGLONG ByteOffset = 0;

    // Create new empty stream
    pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags);
    if(pStream == NULL)
        return NULL;

    // Do we have a master stream?
    if(pStream->pMaster != NULL)
    {
        if(!FlatStream_CreateMirror(pStream))
        {
            FileStream_Close(pStream);
            SErrSetLastError(ERROR_FILE_NOT_FOUND);
            return NULL;
        }
    }
    else
    {
        // Attempt to open the base stream
        if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags))
        {
            FileStream_Close(pStream);
            return NULL;
        }

        // Load the bitmap, if required to
        if(dwStreamFlags & STREAM_FLAG_USE_BITMAP)
            FlatStream_LoadBitmap(pStream);
    }

    // If we have a stream bitmap, set the reading functions
    // which check presence of each file block
    if(pStream->FileBitmap != NULL)
    {
        // Set the stream position to zero. Stream size is already set
        assert(pStream->StreamSize != 0);
        pStream->StreamPos = 0;
        pStream->dwFlags |= STREAM_FLAG_READ_ONLY;

        // Supply the stream functions
        pStream->StreamRead    = (STREAM_READ)BlockStream_Read;
        pStream->StreamGetSize = BlockStream_GetSize;
        pStream->StreamGetPos  = BlockStream_GetPos;
        pStream->StreamClose   = (STREAM_CLOSE)FlatStream_Close;

        // Supply the block functions
        pStream->BlockCheck    = (BLOCK_CHECK)FlatStream_BlockCheck;
        pStream->BlockRead     = (BLOCK_READ)FlatStream_BlockRead;
    }
    else
    {
        // Reset the base position to zero
        pStream->BaseRead(pStream, &ByteOffset, NULL, 0);

        // Setup stream size and position
        pStream->StreamSize = pStream->Base.File.FileSize;
        pStream->StreamPos = 0;

        // Set the base functions
        pStream->StreamRead    = pStream->BaseRead;
        pStream->StreamWrite   = pStream->BaseWrite;
        pStream->StreamResize  = pStream->BaseResize;
        pStream->StreamGetSize = pStream->BaseGetSize;
        pStream->StreamGetPos  = pStream->BaseGetPos;
        pStream->StreamClose   = pStream->BaseClose;
    }

    return pStream;
}

//-----------------------------------------------------------------------------
// Local functions - partial stream support

static bool IsPartHeader(PPART_FILE_HEADER pPartHdr)
{
    // Version number must be 2
    if(pPartHdr->PartialVersion == 2)
    {
        // GameBuildNumber must be an ASCII number
        if(isdigit(pPartHdr->GameBuildNumber[0]) && isdigit(pPartHdr->GameBuildNumber[1]) && isdigit(pPartHdr->GameBuildNumber[2]))
        {
            // Block size must be power of 2
            if((pPartHdr->BlockSize & (pPartHdr->BlockSize - 1)) == 0)
                return true;
        }
    }

    return false;
}

static DWORD PartStream_CheckFile(TBlockStream * pStream)
{
    PPART_FILE_MAP_ENTRY FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap;
    DWORD dwBlockCount;

    // Get the number of blocks
    dwBlockCount = (DWORD)((pStream->StreamSize + pStream->BlockSize - 1) / pStream->BlockSize);

    // Check all blocks
    for(DWORD i = 0; i < dwBlockCount; i++, FileBitmap++)
    {
        // Few sanity checks
        assert(FileBitmap->LargeValueHi == 0);
        assert(FileBitmap->LargeValueLo == 0);
        assert(FileBitmap->Flags == 0 || FileBitmap->Flags == 3);

        // Check if this block is present
        if(FileBitmap->Flags != 3)
            return 0;
    }

    // Yes, the file is complete
    return 1;
}

static bool PartStream_LoadBitmap(TBlockStream * pStream)
{
    PPART_FILE_MAP_ENTRY FileBitmap;
    PART_FILE_HEADER PartHdr;
    ULONGLONG ByteOffset = 0;
    ULONGLONG StreamSize = 0;
    DWORD BlockCount;
    DWORD BitmapSize;

    // Only if the size is greater than size of the bitmap header
    if(pStream->Base.File.FileSize > sizeof(PART_FILE_HEADER))
    {
        // Attempt to read PART file header
        if(pStream->BaseRead(pStream, &ByteOffset, &PartHdr, sizeof(PART_FILE_HEADER)))
        {
            // We need to swap PART file header on big-endian platforms
            BSWAP_ARRAY32_UNSIGNED(&PartHdr, sizeof(PART_FILE_HEADER));

            // Verify the PART file header
            if(IsPartHeader(&PartHdr))
            {
                // Get the number of blocks and size of one block
                StreamSize = MAKE_OFFSET64(PartHdr.FileSizeHi, PartHdr.FileSizeLo);
                ByteOffset = sizeof(PART_FILE_HEADER);
                BlockCount = (DWORD)((StreamSize + PartHdr.BlockSize - 1) / PartHdr.BlockSize);
                BitmapSize = BlockCount * sizeof(PART_FILE_MAP_ENTRY);

                // Check if sizes match
                if((ByteOffset + BitmapSize) < pStream->Base.File.FileSize)
                {
                    // Allocate space for the array of PART_FILE_MAP_ENTRY
                    FileBitmap = STORM_ALLOC(PART_FILE_MAP_ENTRY, BlockCount);
                    if(FileBitmap != NULL)
                    {
                        // Load the block map
                        if(!pStream->BaseRead(pStream, &ByteOffset, FileBitmap, BitmapSize))
                        {
                            STORM_FREE(FileBitmap);
                            return false;
                        }

                        // Make sure that the byte order is correct
                        BSWAP_ARRAY32_UNSIGNED(FileBitmap, BitmapSize);

                        // Update the stream size
                        pStream->BuildNumber = StringToInt(PartHdr.GameBuildNumber);
                        pStream->StreamSize = StreamSize;

                        // Fill the bitmap information
                        pStream->FileBitmap = FileBitmap;
                        pStream->BitmapSize = BitmapSize;
                        pStream->BlockSize  = PartHdr.BlockSize;
                        pStream->BlockCount = BlockCount;
                        pStream->IsComplete = PartStream_CheckFile(pStream);
                        return true;
                    }
                }
            }
        }
    }

    return false;
}

static void PartStream_UpdateBitmap(
    TBlockStream * pStream,                // Pointer to an open stream
    ULONGLONG StartOffset,
    ULONGLONG EndOffset,
    ULONGLONG RealOffset)
{
    PPART_FILE_MAP_ENTRY FileBitmap;
    DWORD BlockSize = pStream->BlockSize;

    // Sanity checks
    assert((StartOffset & (BlockSize - 1)) == 0);
    assert(pStream->FileBitmap != NULL);

    // Calculate the first entry in the block map
    FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + (StartOffset / BlockSize);

    // Set all bits for the specified range
    while(StartOffset < EndOffset)
    {
        // Set the bit
        FileBitmap->BlockOffsHi = (DWORD)(RealOffset >> 0x20);
        FileBitmap->BlockOffsLo = (DWORD)(RealOffset & 0xFFFFFFFF);
        FileBitmap->Flags = 3;

        // Move all
        StartOffset += BlockSize;
        RealOffset += BlockSize;
        FileBitmap++;
    }

    // Increment the bitmap update count
    pStream->IsModified = 1;
}

static bool PartStream_BlockCheck(
    TBlockStream * pStream,                // Pointer to an open stream
    ULONGLONG BlockOffset)
{
    PPART_FILE_MAP_ENTRY FileBitmap;

    // Sanity checks
    assert((BlockOffset & (pStream->BlockSize - 1)) == 0);
    assert(pStream->FileBitmap != NULL);

    // Calculate the block map entry
    FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + (BlockOffset / pStream->BlockSize);

    // Check if the flags are present
    return (FileBitmap->Flags & 0x03) ? true : false;
}

static bool PartStream_BlockRead(
    TBlockStream * pStream,
    ULONGLONG StartOffset,
    ULONGLONG EndOffset,
    LPBYTE BlockBuffer,
    DWORD BytesNeeded,
    bool bAvailable)
{
    PPART_FILE_MAP_ENTRY FileBitmap;
    ULONGLONG ByteOffset;
    DWORD BytesToRead;
    DWORD BlockIndex = (DWORD)(StartOffset / pStream->BlockSize);

    // The starting offset must be aligned to size of the block
    assert(pStream->FileBitmap != NULL);
    assert((StartOffset & (pStream->BlockSize - 1)) == 0);
    assert(StartOffset < EndOffset);

    // If the blocks are not available, we need to load them from the master
    // and then save to the mirror
    if(bAvailable == false)
    {
        // If we have no master, we cannot satisfy read request
        if(pStream->pMaster == NULL)
            return false;

        // Load the blocks from the master stream
        // Note that we always have to read complete blocks
        // so they get properly stored to the mirror stream
        BytesToRead = (DWORD)(EndOffset - StartOffset);
        if(!FileStream_Read(pStream->pMaster, &StartOffset, BlockBuffer, BytesToRead))
            return false;

        // The loaded blocks are going to be stored to the end of the file
        // Note that this operation is not required to succeed
        if(pStream->BaseGetSize(pStream, &ByteOffset))
        {
            // Store the loaded blocks to the mirror file.
            if(pStream->BaseWrite(pStream, &ByteOffset, BlockBuffer, BytesToRead))
            {
                PartStream_UpdateBitmap(pStream, StartOffset, EndOffset, ByteOffset);
            }
        }
    }
    else
    {
        // Get the file map entry
        FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + BlockIndex;

        // Read all blocks
        while(StartOffset < EndOffset)
        {
            // Get the number of bytes to be read
            BytesToRead = (DWORD)(EndOffset - StartOffset);
            if(BytesToRead > pStream->BlockSize)
                BytesToRead = pStream->BlockSize;
            if(BytesToRead > BytesNeeded)
                BytesToRead = BytesNeeded;

            // Read the block
            ByteOffset = MAKE_OFFSET64(FileBitmap->BlockOffsHi, FileBitmap->BlockOffsLo);
            if(!pStream->BaseRead(pStream, &ByteOffset, BlockBuffer, BytesToRead))
                return false;

            // Move the pointers
            StartOffset += pStream->BlockSize;
            BlockBuffer += pStream->BlockSize;
            BytesNeeded -= pStream->BlockSize;
            FileBitmap++;
        }
    }

    return true;
}

static void PartStream_Close(TBlockStream * pStream)
{
    PART_FILE_HEADER PartHeader;
    ULONGLONG ByteOffset = 0;

    if(pStream->FileBitmap && pStream->IsModified)
    {
        // Prepare the part file header
        memset(&PartHeader, 0, sizeof(PART_FILE_HEADER));
        PartHeader.PartialVersion = 2;
        PartHeader.FileSizeHi     = (DWORD)(pStream->StreamSize >> 0x20);
        PartHeader.FileSizeLo     = (DWORD)(pStream->StreamSize & 0xFFFFFFFF);
        PartHeader.BlockSize      = pStream->BlockSize;

        // Make sure that the header is properly BSWAPed
        BSWAP_ARRAY32_UNSIGNED(&PartHeader, sizeof(PART_FILE_HEADER));
        SMemIntToStr(PartHeader.GameBuildNumber, _countof(PartHeader.GameBuildNumber), pStream->BuildNumber);

        // Write the part header
        pStream->BaseWrite(pStream, &ByteOffset, &PartHeader, sizeof(PART_FILE_HEADER));

        // Write the block bitmap
        BSWAP_ARRAY32_UNSIGNED(pStream->FileBitmap, pStream->BitmapSize);
        pStream->BaseWrite(pStream, NULL, pStream->FileBitmap, pStream->BitmapSize);
    }

    // Close the base class
    BlockStream_Close(pStream);
}

static bool PartStream_CreateMirror(TBlockStream * pStream)
{
    ULONGLONG RemainingSize;
    ULONGLONG MasterSize = 0;
    ULONGLONG MirrorSize = 0;
    LPBYTE FileBitmap = NULL;
    DWORD dwBitmapSize;
    DWORD dwBlockCount;
    bool bNeedCreateMirrorStream = true;
    bool bNeedResizeMirrorStream = true;

    // Do we have master function and base creation function?
    if(pStream->pMaster == NULL || pStream->BaseCreate == NULL)
        return false;

    // Retrieve the master file size, block count and bitmap size
    FileStream_GetSize(pStream->pMaster, &MasterSize);
    dwBlockCount = (DWORD)((MasterSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE);
    dwBitmapSize = (DWORD)(dwBlockCount * sizeof(PART_FILE_MAP_ENTRY));

    // Setup stream size and position
    pStream->BuildNumber = DEFAULT_BUILD_NUMBER;        // BUGBUG: Really???
    pStream->StreamSize = MasterSize;
    pStream->StreamPos = 0;

    // Open the base stream for write access
    if(pStream->BaseOpen(pStream, pStream->szFileName, 0))
    {
        // If the file open succeeded, check if the file size matches required size
        pStream->BaseGetSize(pStream, &MirrorSize);
        if(MirrorSize >= sizeof(PART_FILE_HEADER) + dwBitmapSize)
        {
            // Check if the remaining size is aligned to block
            RemainingSize = MirrorSize - sizeof(PART_FILE_HEADER) - dwBitmapSize;
            if((RemainingSize & (DEFAULT_BLOCK_SIZE - 1)) == 0 || RemainingSize == MasterSize)
            {
                // Attempt to load an existing file bitmap
                if(PartStream_LoadBitmap(pStream))
                    return true;
            }
        }

        // We need to create mirror stream
        bNeedCreateMirrorStream = false;
    }

    // Create a new stream, if needed
    if(bNeedCreateMirrorStream)
    {
        if(!pStream->BaseCreate(pStream))
            return false;
    }

    // If we need to, then resize the mirror stream
    if(bNeedResizeMirrorStream)
    {
        if(!pStream->BaseResize(pStream, sizeof(PART_FILE_HEADER) + dwBitmapSize))
            return false;
    }

    // Allocate the bitmap array
    FileBitmap = STORM_ALLOC(BYTE, dwBitmapSize);
    if(FileBitmap == NULL)
        return false;

    // Initialize the bitmap
    memset(FileBitmap, 0, dwBitmapSize);
    pStream->FileBitmap = FileBitmap;
    pStream->BitmapSize = dwBitmapSize;
    pStream->BlockSize  = DEFAULT_BLOCK_SIZE;
    pStream->BlockCount = dwBlockCount;
    pStream->IsComplete = 0;
    pStream->IsModified = 1;

    // Note: Don't write the stream bitmap right away.
    // Doing so would cause sparse file resize on NTFS,
    // which would take long time on larger files.
    return true;
}


static TFileStream * PartStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags)
{
    TBlockStream * pStream;

    // Create new empty stream
    pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags);
    if(pStream == NULL)
        return NULL;

    // Do we have a master stream?
    if(pStream->pMaster != NULL)
    {
        if(!PartStream_CreateMirror(pStream))
        {
            FileStream_Close(pStream);
            SErrSetLastError(ERROR_FILE_NOT_FOUND);
            return NULL;
        }
    }
    else
    {
        // Attempt to open the base stream
        if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags))
        {
            FileStream_Close(pStream);
            return NULL;
        }

        // Load the part stream block map
        if(!PartStream_LoadBitmap(pStream))
        {
            FileStream_Close(pStream);
            SErrSetLastError(ERROR_BAD_FORMAT);
            return NULL;
        }
    }

    // Set the stream position to zero. Stream size is already set
    assert(pStream->StreamSize != 0);
    pStream->StreamPos = 0;
    pStream->dwFlags |= STREAM_FLAG_READ_ONLY;

    // Set new function pointers
    pStream->StreamRead    = (STREAM_READ)BlockStream_Read;
    pStream->StreamGetPos  = BlockStream_GetPos;
    pStream->StreamGetSize = BlockStream_GetSize;
    pStream->StreamClose   = (STREAM_CLOSE)PartStream_Close;

    // Supply the block functions
    pStream->BlockCheck    = (BLOCK_CHECK)PartStream_BlockCheck;
    pStream->BlockRead     = (BLOCK_READ)PartStream_BlockRead;
    return pStream;
}

//-----------------------------------------------------------------------------
// Local functions - MPQE stream support

static const char * szKeyTemplate = "expand 32-byte k000000000000000000000000000000000000000000000000";

static const char * AuthCodeArray[] =
{
    // Starcraft II (Heart of the Swarm)
    // Authentication code URL: http://dist.blizzard.com/mediakey/hots-authenticationcode-bgdl.txt
    //                                                                                          -0C-    -1C--08-    -18--04-    -14--00-    -10-
    "S48B6CDTN5XEQAKQDJNDLJBJ73FDFM3U",         // SC2 Heart of the Swarm-all : "expand 32-byte kQAKQ0000FM3UN5XE000073FD6CDT0000LJBJS48B0000DJND"

    // Diablo III: Agent.exe (1.0.0.954)
    // Address of decryption routine: 00502b00
    // Pointer to decryptor object: ECX
    // Pointer to key: ECX+0x5C
    // Authentication code URL: http://dist.blizzard.com/mediakey/d3-authenticationcode-enGB.txt
    //                                                                                           -0C-    -1C--08-    -18--04-    -14--00-    -10-
    "UCMXF6EJY352EFH4XFRXCFH2XC9MQRZK",         // Diablo III Installer (deDE): "expand 32-byte kEFH40000QRZKY3520000XC9MF6EJ0000CFH2UCMX0000XFRX"
    "MMKVHY48RP7WXP4GHYBQ7SL9J9UNPHBP",         // Diablo III Installer (enGB): "expand 32-byte kXP4G0000PHBPRP7W0000J9UNHY4800007SL9MMKV0000HYBQ"
    "8MXLWHQ7VGGLTZ9MQZQSFDCLJYET3CPP",         // Diablo III Installer (enSG): "expand 32-byte kTZ9M00003CPPVGGL0000JYETWHQ70000FDCL8MXL0000QZQS"
    "EJ2R5TM6XFE2GUNG5QDGHKQ9UAKPWZSZ",         // Diablo III Installer (enUS): "expand 32-byte kGUNG0000WZSZXFE20000UAKP5TM60000HKQ9EJ2R00005QDG"
    "PBGFBE42Z6LNK65UGJQ3WZVMCLP4HQQT",         // Diablo III Installer (esES): "expand 32-byte kK65U0000HQQTZ6LN0000CLP4BE420000WZVMPBGF0000GJQ3"
    "X7SEJJS9TSGCW5P28EBSC47AJPEY8VU2",         // Diablo III Installer (esMX): "expand 32-byte kW5P200008VU2TSGC0000JPEYJJS90000C47AX7SE00008EBS"
    "5KVBQA8VYE6XRY3DLGC5ZDE4XS4P7YA2",         // Diablo III Installer (frFR): "expand 32-byte kRY3D00007YA2YE6X0000XS4PQA8V0000ZDE45KVB0000LGC5"
    "478JD2K56EVNVVY4XX8TDWYT5B8KB254",         // Diablo III Installer (itIT): "expand 32-byte kVVY40000B2546EVN00005B8KD2K50000DWYT478J0000XX8T"
    "8TS4VNFQRZTN6YWHE9CHVDH9NVWD474A",         // Diablo III Installer (koKR): "expand 32-byte k6YWH0000474ARZTN0000NVWDVNFQ0000VDH98TS40000E9CH"
    "LJ52Z32DF4LZ4ZJJXVKK3AZQA6GABLJB",         // Diablo III Installer (plPL): "expand 32-byte k4ZJJ0000BLJBF4LZ0000A6GAZ32D00003AZQLJ520000XVKK"
    "K6BDHY2ECUE2545YKNLBJPVYWHE7XYAG",         // Diablo III Installer (ptBR): "expand 32-byte k545Y0000XYAGCUE20000WHE7HY2E0000JPVYK6BD0000KNLB"
    "NDVW8GWLAYCRPGRNY8RT7ZZUQU63VLPR",         // Diablo III Installer (ruRU): "expand 32-byte kXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    "6VWCQTN8V3ZZMRUCZXV8A8CGUX2TAA8H",         // Diablo III Installer (zhTW): "expand 32-byte kMRUC0000AA8HV3ZZ0000UX2TQTN80000A8CG6VWC0000ZXV8"
//  "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",         // Diablo III Installer (zhCN): "expand 32-byte kXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

    // Starcraft II (Wings of Liberty): Installer.exe (4.1.1.4219)
    // Address of decryption routine: 0053A3D0
    // Pointer to decryptor object: ECX
    // Pointer to key: ECX+0x5C
    // Authentication code URL: http://dist.blizzard.com/mediakey/sc2-authenticationcode-enUS.txt
    //                                                                                          -0C-    -1C--08-    -18--04-    -14--00-    -10-
    "Y45MD3CAK4KXSSXHYD9VY64Z8EKJ4XFX",         // SC2 Wings of Liberty (deDE): "expand 32-byte kSSXH00004XFXK4KX00008EKJD3CA0000Y64ZY45M0000YD9V"
    "G8MN8UDG6NA2ANGY6A3DNY82HRGF29ZH",         // SC2 Wings of Liberty (enGB): "expand 32-byte kANGY000029ZH6NA20000HRGF8UDG0000NY82G8MN00006A3D"
    "W9RRHLB2FDU9WW5B3ECEBLRSFWZSF7HW",         // SC2 Wings of Liberty (enSG): "expand 32-byte kWW5B0000F7HWFDU90000FWZSHLB20000BLRSW9RR00003ECE"
    "3DH5RE5NVM5GTFD85LXGWT6FK859ETR5",         // SC2 Wings of Liberty (enUS): "expand 32-byte kTFD80000ETR5VM5G0000K859RE5N0000WT6F3DH500005LXG"
    "8WLKUAXE94PFQU4Y249PAZ24N4R4XKTQ",         // SC2 Wings of Liberty (esES): "expand 32-byte kQU4Y0000XKTQ94PF0000N4R4UAXE0000AZ248WLK0000249P"
    "A34DXX3VHGGXSQBRFE5UFFDXMF9G4G54",         // SC2 Wings of Liberty (esMX): "expand 32-byte kSQBR00004G54HGGX0000MF9GXX3V0000FFDXA34D0000FE5U"
    "ZG7J9K938HJEFWPQUA768MA2PFER6EAJ",         // SC2 Wings of Liberty (frFR): "expand 32-byte kFWPQ00006EAJ8HJE0000PFER9K9300008MA2ZG7J0000UA76"
    "NE7CUNNNTVAPXV7E3G2BSVBWGVMW8BL2",         // SC2 Wings of Liberty (itIT): "expand 32-byte kXV7E00008BL2TVAP0000GVMWUNNN0000SVBWNE7C00003G2B"
    "3V9E2FTMBM9QQWK7U6MAMWAZWQDB838F",         // SC2 Wings of Liberty (koKR): "expand 32-byte kQWK70000838FBM9Q0000WQDB2FTM0000MWAZ3V9E0000U6MA"
    "2NSFB8MELULJ83U6YHA3UP6K4MQD48L6",         // SC2 Wings of Liberty (plPL): "expand 32-byte k83U6000048L6LULJ00004MQDB8ME0000UP6K2NSF0000YHA3"
    "QA2TZ9EWZ4CUU8BMB5WXCTY65F9CSW4E",         // SC2 Wings of Liberty (ptBR): "expand 32-byte kU8BM0000SW4EZ4CU00005F9CZ9EW0000CTY6QA2T0000B5WX"
    "VHB378W64BAT9SH7D68VV9NLQDK9YEGT",         // SC2 Wings of Liberty (ruRU): "expand 32-byte k9SH70000YEGT4BAT0000QDK978W60000V9NLVHB30000D68V"
    "U3NFQJV4M6GC7KBN9XQJ3BRDN3PLD9NE",         // SC2 Wings of Liberty (zhTW): "expand 32-byte k7KBN0000D9NEM6GC0000N3PLQJV400003BRDU3NF00009XQJ"

    NULL
};

static DWORD Rol32(DWORD dwValue, DWORD dwRolCount)
{
    DWORD dwShiftRight = 32 - dwRolCount;

    return (dwValue << dwRolCount) | (dwValue >> dwShiftRight);
}

static void CreateKeyFromAuthCode(
    LPBYTE pbKeyBuffer,
    const char * szAuthCode)
{
    LPDWORD KeyPosition = (LPDWORD)(pbKeyBuffer + 0x10);
    LPDWORD AuthCode32 = (LPDWORD)szAuthCode;

    memcpy(pbKeyBuffer, szKeyTemplate, MPQE_CHUNK_SIZE);
    KeyPosition[0x00] = AuthCode32[0x03];
    KeyPosition[0x02] = AuthCode32[0x07];
    KeyPosition[0x03] = AuthCode32[0x02];
    KeyPosition[0x05] = AuthCode32[0x06];
    KeyPosition[0x06] = AuthCode32[0x01];
    KeyPosition[0x08] = AuthCode32[0x05];
    KeyPosition[0x09] = AuthCode32[0x00];
    KeyPosition[0x0B] = AuthCode32[0x04];
    BSWAP_ARRAY32_UNSIGNED(pbKeyBuffer, MPQE_CHUNK_SIZE);
}

static void DecryptFileChunk(
    DWORD * MpqData,
    LPBYTE pbKey,
    ULONGLONG ByteOffset,
    DWORD dwLength)
{
    ULONGLONG ChunkOffset;
    DWORD KeyShuffled[0x10];
    DWORD KeyMirror[0x10];
    DWORD RoundCount = 0x14;

    // Prepare the key
    ChunkOffset = ByteOffset / MPQE_CHUNK_SIZE;
    memcpy(KeyMirror, pbKey, MPQE_CHUNK_SIZE);
    BSWAP_ARRAY32_UNSIGNED(KeyMirror, MPQE_CHUNK_SIZE);
    KeyMirror[0x05] = (DWORD)(ChunkOffset >> 32);
    KeyMirror[0x08] = (DWORD)(ChunkOffset);

    while(dwLength >= MPQE_CHUNK_SIZE)
    {
        // Shuffle the key - part 1
        KeyShuffled[0x0E] = KeyMirror[0x00];
        KeyShuffled[0x0C] = KeyMirror[0x01];
        KeyShuffled[0x05] = KeyMirror[0x02];
        KeyShuffled[0x0F] = KeyMirror[0x03];
        KeyShuffled[0x0A] = KeyMirror[0x04];
        KeyShuffled[0x07] = KeyMirror[0x05];
        KeyShuffled[0x0B] = KeyMirror[0x06];
        KeyShuffled[0x09] = KeyMirror[0x07];
        KeyShuffled[0x03] = KeyMirror[0x08];
        KeyShuffled[0x06] = KeyMirror[0x09];
        KeyShuffled[0x08] = KeyMirror[0x0A];
        KeyShuffled[0x0D] = KeyMirror[0x0B];
        KeyShuffled[0x02] = KeyMirror[0x0C];
        KeyShuffled[0x04] = KeyMirror[0x0D];
        KeyShuffled[0x01] = KeyMirror[0x0E];
        KeyShuffled[0x00] = KeyMirror[0x0F];

        // Shuffle the key - part 2
        for(DWORD i = 0; i < RoundCount; i += 2)
        {
            KeyShuffled[0x0A] = KeyShuffled[0x0A] ^ Rol32((KeyShuffled[0x0E] + KeyShuffled[0x02]), 0x07);
            KeyShuffled[0x03] = KeyShuffled[0x03] ^ Rol32((KeyShuffled[0x0A] + KeyShuffled[0x0E]), 0x09);
            KeyShuffled[0x02] = KeyShuffled[0x02] ^ Rol32((KeyShuffled[0x03] + KeyShuffled[0x0A]), 0x0D);
            KeyShuffled[0x0E] = KeyShuffled[0x0E] ^ Rol32((KeyShuffled[0x02] + KeyShuffled[0x03]), 0x12);

            KeyShuffled[0x07] = KeyShuffled[0x07] ^ Rol32((KeyShuffled[0x0C] + KeyShuffled[0x04]), 0x07);
            KeyShuffled[0x06] = KeyShuffled[0x06] ^ Rol32((KeyShuffled[0x07] + KeyShuffled[0x0C]), 0x09);
            KeyShuffled[0x04] = KeyShuffled[0x04] ^ Rol32((KeyShuffled[0x06] + KeyShuffled[0x07]), 0x0D);
            KeyShuffled[0x0C] = KeyShuffled[0x0C] ^ Rol32((KeyShuffled[0x04] + KeyShuffled[0x06]), 0x12);

            KeyShuffled[0x0B] = KeyShuffled[0x0B] ^ Rol32((KeyShuffled[0x05] + KeyShuffled[0x01]), 0x07);
            KeyShuffled[0x08] = KeyShuffled[0x08] ^ Rol32((KeyShuffled[0x0B] + KeyShuffled[0x05]), 0x09);
            KeyShuffled[0x01] = KeyShuffled[0x01] ^ Rol32((KeyShuffled[0x08] + KeyShuffled[0x0B]), 0x0D);
            KeyShuffled[0x05] = KeyShuffled[0x05] ^ Rol32((KeyShuffled[0x01] + KeyShuffled[0x08]), 0x12);

            KeyShuffled[0x09] = KeyShuffled[0x09] ^ Rol32((KeyShuffled[0x0F] + KeyShuffled[0x00]), 0x07);
            KeyShuffled[0x0D] = KeyShuffled[0x0D] ^ Rol32((KeyShuffled[0x09] + KeyShuffled[0x0F]), 0x09);
            KeyShuffled[0x00] = KeyShuffled[0x00] ^ Rol32((KeyShuffled[0x0D] + KeyShuffled[0x09]), 0x0D);
            KeyShuffled[0x0F] = KeyShuffled[0x0F] ^ Rol32((KeyShuffled[0x00] + KeyShuffled[0x0D]), 0x12);

            KeyShuffled[0x04] = KeyShuffled[0x04] ^ Rol32((KeyShuffled[0x0E] + KeyShuffled[0x09]), 0x07);
            KeyShuffled[0x08] = KeyShuffled[0x08] ^ Rol32((KeyShuffled[0x04] + KeyShuffled[0x0E]), 0x09);
            KeyShuffled[0x09] = KeyShuffled[0x09] ^ Rol32((KeyShuffled[0x08] + KeyShuffled[0x04]), 0x0D);
            KeyShuffled[0x0E] = KeyShuffled[0x0E] ^ Rol32((KeyShuffled[0x09] + KeyShuffled[0x08]), 0x12);

            KeyShuffled[0x01] = KeyShuffled[0x01] ^ Rol32((KeyShuffled[0x0C] + KeyShuffled[0x0A]), 0x07);
            KeyShuffled[0x0D] = KeyShuffled[0x0D] ^ Rol32((KeyShuffled[0x01] + KeyShuffled[0x0C]), 0x09);
            KeyShuffled[0x0A] = KeyShuffled[0x0A] ^ Rol32((KeyShuffled[0x0D] + KeyShuffled[0x01]), 0x0D);
            KeyShuffled[0x0C] = KeyShuffled[0x0C] ^ Rol32((KeyShuffled[0x0A] + KeyShuffled[0x0D]), 0x12);

            KeyShuffled[0x00] = KeyShuffled[0x00] ^ Rol32((KeyShuffled[0x05] + KeyShuffled[0x07]), 0x07);
            KeyShuffled[0x03] = KeyShuffled[0x03] ^ Rol32((KeyShuffled[0x00] + KeyShuffled[0x05]), 0x09);
            KeyShuffled[0x07] = KeyShuffled[0x07] ^ Rol32((KeyShuffled[0x03] + KeyShuffled[0x00]), 0x0D);
            KeyShuffled[0x05] = KeyShuffled[0x05] ^ Rol32((KeyShuffled[0x07] + KeyShuffled[0x03]), 0x12);

            KeyShuffled[0x02] = KeyShuffled[0x02] ^ Rol32((KeyShuffled[0x0F] + KeyShuffled[0x0B]), 0x07);
            KeyShuffled[0x06] = KeyShuffled[0x06] ^ Rol32((KeyShuffled[0x02] + KeyShuffled[0x0F]), 0x09);
            KeyShuffled[0x0B] = KeyShuffled[0x0B] ^ Rol32((KeyShuffled[0x06] + KeyShuffled[0x02]), 0x0D);
            KeyShuffled[0x0F] = KeyShuffled[0x0F] ^ Rol32((KeyShuffled[0x0B] + KeyShuffled[0x06]), 0x12);
        }

        // Decrypt one data chunk
        BSWAP_ARRAY32_UNSIGNED(MpqData, MPQE_CHUNK_SIZE);
        MpqData[0x00] = MpqData[0x00] ^ (KeyShuffled[0x0E] + KeyMirror[0x00]);
        MpqData[0x01] = MpqData[0x01] ^ (KeyShuffled[0x04] + KeyMirror[0x0D]);
        MpqData[0x02] = MpqData[0x02] ^ (KeyShuffled[0x08] + KeyMirror[0x0A]);
        MpqData[0x03] = MpqData[0x03] ^ (KeyShuffled[0x09] + KeyMirror[0x07]);
        MpqData[0x04] = MpqData[0x04] ^ (KeyShuffled[0x0A] + KeyMirror[0x04]);
        MpqData[0x05] = MpqData[0x05] ^ (KeyShuffled[0x0C] + KeyMirror[0x01]);
        MpqData[0x06] = MpqData[0x06] ^ (KeyShuffled[0x01] + KeyMirror[0x0E]);
        MpqData[0x07] = MpqData[0x07] ^ (KeyShuffled[0x0D] + KeyMirror[0x0B]);
        MpqData[0x08] = MpqData[0x08] ^ (KeyShuffled[0x03] + KeyMirror[0x08]);
        MpqData[0x09] = MpqData[0x09] ^ (KeyShuffled[0x07] + KeyMirror[0x05]);
        MpqData[0x0A] = MpqData[0x0A] ^ (KeyShuffled[0x05] + KeyMirror[0x02]);
        MpqData[0x0B] = MpqData[0x0B] ^ (KeyShuffled[0x00] + KeyMirror[0x0F]);
        MpqData[0x0C] = MpqData[0x0C] ^ (KeyShuffled[0x02] + KeyMirror[0x0C]);
        MpqData[0x0D] = MpqData[0x0D] ^ (KeyShuffled[0x06] + KeyMirror[0x09]);
        MpqData[0x0E] = MpqData[0x0E] ^ (KeyShuffled[0x0B] + KeyMirror[0x06]);
        MpqData[0x0F] = MpqData[0x0F] ^ (KeyShuffled[0x0F] + KeyMirror[0x03]);
        BSWAP_ARRAY32_UNSIGNED(MpqData, MPQE_CHUNK_SIZE);

        // Update byte offset in the key
        KeyMirror[0x08]++;
        if(KeyMirror[0x08] == 0)
            KeyMirror[0x05]++;

        // Move pointers and decrease number of bytes to decrypt
        MpqData  += (MPQE_CHUNK_SIZE / sizeof(DWORD));
        dwLength -= MPQE_CHUNK_SIZE;
    }
}

static bool MpqeStream_DetectFileKey(TEncryptedStream * pStream)
{
    ULONGLONG ByteOffset = 0;
    BYTE EncryptedHeader[MPQE_CHUNK_SIZE];
    BYTE FileHeader[MPQE_CHUNK_SIZE];

    // Read the first file chunk
    if(pStream->BaseRead(pStream, &ByteOffset, EncryptedHeader, sizeof(EncryptedHeader)))
    {
        // We just try all known keys one by one
        for(int i = 0; AuthCodeArray[i] != NULL; i++)
        {
            // Prepare they decryption key from game serial number
            CreateKeyFromAuthCode(pStream->Key, AuthCodeArray[i]);

            // Try to decrypt with the given key
            memcpy(FileHeader, EncryptedHeader, MPQE_CHUNK_SIZE);
            DecryptFileChunk((LPDWORD)FileHeader, pStream->Key, ByteOffset, MPQE_CHUNK_SIZE);

            // We check the decrypted data
            // All known encrypted MPQs have header at the begin of the file,
            // so we check for MPQ signature there.
            if(FileHeader[0] == 'M' && FileHeader[1] == 'P' && FileHeader[2] == 'Q')
            {
                // Update the stream size
                pStream->StreamSize = pStream->Base.File.FileSize;

                // Fill the block information
                pStream->BlockSize  = MPQE_CHUNK_SIZE;
                pStream->BlockCount = (DWORD)(pStream->Base.File.FileSize + MPQE_CHUNK_SIZE - 1) / MPQE_CHUNK_SIZE;
                pStream->IsComplete = 1;
                return true;
            }
        }
    }

    // Key not found, sorry
    return false;
}

static bool MpqeStream_BlockRead(
    TEncryptedStream * pStream,
    ULONGLONG StartOffset,
    ULONGLONG EndOffset,
    LPBYTE BlockBuffer,
    DWORD BytesNeeded,
    bool bAvailable)
{
    DWORD dwBytesToRead;

    assert((StartOffset & (pStream->BlockSize - 1)) == 0);
    assert(StartOffset < EndOffset);
    assert(bAvailable != false);
    BytesNeeded = BytesNeeded;
    bAvailable = bAvailable;

    // Read the file from the stream as-is
    // Limit the reading to number of blocks really needed
    dwBytesToRead = (DWORD)(EndOffset - StartOffset);
    if(!pStream->BaseRead(pStream, &StartOffset, BlockBuffer, dwBytesToRead))
        return false;

    // Decrypt the data
    dwBytesToRead = (dwBytesToRead + MPQE_CHUNK_SIZE - 1) & ~(MPQE_CHUNK_SIZE - 1);
    DecryptFileChunk((LPDWORD)BlockBuffer, pStream->Key, StartOffset, dwBytesToRead);
    return true;
}

static TFileStream * MpqeStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags)
{
    TEncryptedStream * pStream;

    // Create new empty stream
    pStream = (TEncryptedStream *)AllocateFileStream(szFileName, sizeof(TEncryptedStream), dwStreamFlags);
    if(pStream == NULL)
        return NULL;

    // Attempt to open the base stream
    assert(pStream->BaseOpen != NULL);
    if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags))
        return NULL;

    // Determine the encryption key for the MPQ
    if(MpqeStream_DetectFileKey(pStream))
    {
        // Set the stream position and size
        assert(pStream->StreamSize != 0);
        pStream->StreamPos = 0;
        pStream->dwFlags |= STREAM_FLAG_READ_ONLY;

        // Set new function pointers
        pStream->StreamRead    = (STREAM_READ)BlockStream_Read;
        pStream->StreamGetPos  = BlockStream_GetPos;
        pStream->StreamGetSize = BlockStream_GetSize;
        pStream->StreamClose   = pStream->BaseClose;

        // Supply the block functions
        pStream->BlockRead     = (BLOCK_READ)MpqeStream_BlockRead;
        return pStream;
    }

    // Cleanup the stream and return
    FileStream_Close(pStream);
    SErrSetLastError(ERROR_UNKNOWN_FILE_KEY);
    return NULL;
}

//-----------------------------------------------------------------------------
// Local functions - Block4 stream support

#define BLOCK4_BLOCK_SIZE   0x4000          // Size of one block
#define BLOCK4_HASH_SIZE    0x20            // Size of MD5 hash that is after each block
#define BLOCK4_MAX_BLOCKS   0x00002000      // Maximum amount of blocks per file
#define BLOCK4_MAX_FSIZE    0x08040000      // Max size of one file

static bool Block4Stream_BlockRead(
    TBlockStream * pStream,                // Pointer to an open stream
    ULONGLONG StartOffset,
    ULONGLONG EndOffset,
    LPBYTE BlockBuffer,
    DWORD BytesNeeded,
    bool bAvailable)
{
    TBaseProviderData * BaseArray = (TBaseProviderData *)pStream->FileBitmap;
    ULONGLONG ByteOffset;
    DWORD BytesToRead;
    DWORD StreamIndex;
    DWORD BlockIndex;
    bool bResult;

    // The starting offset must be aligned to size of the block
    assert(pStream->FileBitmap != NULL);
    assert((StartOffset & (pStream->BlockSize - 1)) == 0);
    assert(StartOffset < EndOffset);
    assert(bAvailable == true);

    // Keep compilers happy
    STORMLIB_UNUSED(bAvailable);
    STORMLIB_UNUSED(EndOffset);

    while(BytesNeeded != 0)
    {
        // Calculate the block index and the file index
        StreamIndex = (DWORD)((StartOffset / pStream->BlockSize) / BLOCK4_MAX_BLOCKS);
        BlockIndex  = (DWORD)((StartOffset / pStream->BlockSize) % BLOCK4_MAX_BLOCKS);
        if(StreamIndex > pStream->BitmapSize)
            return false;

        // Calculate the block offset
        ByteOffset = ((ULONGLONG)BlockIndex * (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE));
        BytesToRead = STORMLIB_MIN(BytesNeeded, BLOCK4_BLOCK_SIZE);

        // Read from the base stream
        pStream->Base = BaseArray[StreamIndex];
        bResult = pStream->BaseRead(pStream, &ByteOffset, BlockBuffer, BytesToRead);
        BaseArray[StreamIndex] = pStream->Base;

        // Did the result succeed?
        if(bResult == false)
            return false;

        // Move pointers
        StartOffset += BytesToRead;
        BlockBuffer += BytesToRead;
        BytesNeeded -= BytesToRead;
    }

    return true;
}


static void Block4Stream_Close(TBlockStream * pStream)
{
    TBaseProviderData * BaseArray = (TBaseProviderData *)pStream->FileBitmap;

    // If we have a non-zero count of base streams,
    // we have to close them all
    if(BaseArray != NULL)
    {
        // Close all base streams
        for(DWORD i = 0; i < pStream->BitmapSize; i++)
        {
            memcpy(&pStream->Base, BaseArray + i, sizeof(TBaseProviderData));
            pStream->BaseClose(pStream);
        }
    }

    // Free the data map, if any
    if(pStream->FileBitmap != NULL)
        STORM_FREE(pStream->FileBitmap);
    pStream->FileBitmap = NULL;

    // Do not call the BaseClose function,
    // we closed all handles already
    return;
}

static TFileStream * Block4Stream_Open(const TCHAR * szFileName, DWORD dwStreamFlags)
{
    TBaseProviderData * NewBaseArray = NULL;
    ULONGLONG RemainderBlock;
    ULONGLONG BlockCount;
    ULONGLONG FileSize;
    TBlockStream * pStream;
    TCHAR * szNameBuff;
    size_t nNameLength;
    DWORD dwBaseFiles = 0;
    DWORD dwBaseFlags;

    // Create new empty stream
    pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags);
    if(pStream == NULL)
        return NULL;

    // Sanity check
    assert(pStream->BaseOpen != NULL);

    // Get the length of the file name without numeric suffix
    nNameLength = _tcslen(pStream->szFileName);
    if(pStream->szFileName[nNameLength - 2] == '.' && pStream->szFileName[nNameLength - 1] == '0')
        nNameLength -= 2;
    pStream->szFileName[nNameLength] = 0;

    // Supply the stream functions
    pStream->StreamRead    = (STREAM_READ)BlockStream_Read;
    pStream->StreamGetSize = BlockStream_GetSize;
    pStream->StreamGetPos  = BlockStream_GetPos;
    pStream->StreamClose   = (STREAM_CLOSE)Block4Stream_Close;
    pStream->BlockRead     = (BLOCK_READ)Block4Stream_BlockRead;

    // Allocate work space for numeric names
    szNameBuff = STORM_ALLOC(TCHAR, nNameLength + 4);
    if(szNameBuff != NULL)
    {
        // Set the base flags
        dwBaseFlags = (dwStreamFlags & STREAM_PROVIDERS_MASK) | STREAM_FLAG_READ_ONLY;

        // Go all suffixes from 0 to 30
        for(int nSuffix = 0; nSuffix < 30; nSuffix++)
        {
            // Open the n-th file
            CreateNameWithSuffix(szNameBuff, nNameLength + 4, pStream->szFileName, nSuffix);
            if(!pStream->BaseOpen(pStream, szNameBuff, dwBaseFlags))
                break;

            // If the open succeeded, we re-allocate the base provider array
            NewBaseArray = STORM_ALLOC(TBaseProviderData, dwBaseFiles + 1);
            if(NewBaseArray == NULL)
            {
                SErrSetLastError(ERROR_NOT_ENOUGH_MEMORY);
                return NULL;
            }

            // Copy the old base data array to the new base data array
            if(pStream->FileBitmap != NULL)
            {
                memcpy(NewBaseArray, pStream->FileBitmap, sizeof(TBaseProviderData) * dwBaseFiles);
                STORM_FREE(pStream->FileBitmap);
            }

            // Also copy the opened base array
            memcpy(NewBaseArray + dwBaseFiles, &pStream->Base, sizeof(TBaseProviderData));
            pStream->FileBitmap = NewBaseArray;
            dwBaseFiles++;

            // Get the size of the base stream
            pStream->BaseGetSize(pStream, &FileSize);
            assert(FileSize <= BLOCK4_MAX_FSIZE);
            RemainderBlock = FileSize % (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE);
            BlockCount = FileSize / (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE);

            // Increment the stream size and number of blocks
            pStream->StreamSize += (BlockCount * BLOCK4_BLOCK_SIZE);
            pStream->BlockCount += (DWORD)BlockCount;

            // Is this the last file?
            if(FileSize < BLOCK4_MAX_FSIZE)
            {
                if(RemainderBlock)
                {
                    pStream->StreamSize += (RemainderBlock - BLOCK4_HASH_SIZE);
                    pStream->BlockCount++;
                }
                break;
            }
        }

        // Fill the remainining block stream variables
        pStream->BitmapSize = dwBaseFiles;
        pStream->BlockSize  = BLOCK4_BLOCK_SIZE;
        pStream->IsComplete = 1;
        pStream->IsModified = 0;

        // Fill the remaining stream variables
        pStream->StreamPos = 0;
        pStream->dwFlags |= STREAM_FLAG_READ_ONLY;

        STORM_FREE(szNameBuff);
    }

    // If we opened something, return success
    if(dwBaseFiles == 0)
    {
        FileStream_Close(pStream);
        SErrSetLastError(ERROR_FILE_NOT_FOUND);
        pStream = NULL;
    }

    return pStream;
}

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

/**
 * This function creates a new file for read-write access
 *
 * - If the current platform supports file sharing,
 *   the file must be created for read sharing (i.e. another application
 *   can open the file for read, but not for write)
 * - If the file does not exist, the function must create new one
 * - If the file exists, the function must rewrite it and set to zero size
 * - The parameters of the function must be validate by the caller
 * - The function must initialize all stream function pointers in TFileStream
 * - If the function fails from any reason, it must close all handles
 *   and free all memory that has been allocated in the process of stream creation,
 *   including the TFileStream structure itself
 *
 * \a szFileName Name of the file to create
 */

TFileStream * FileStream_CreateFile(
    const TCHAR * szFileName,
    DWORD dwStreamFlags)
{
    TFileStream * pStream;

    // We only support creation of flat, local file
    if((dwStreamFlags & (STREAM_PROVIDERS_MASK)) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE))
    {
        SErrSetLastError(ERROR_NOT_SUPPORTED);
        return NULL;
    }

    // Allocate file stream structure for flat stream
    pStream = AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags);
    if(pStream != NULL)
    {
        // Attempt to create the disk file
        if(BaseFile_Create(pStream))
        {
            // Fill the stream provider functions
            pStream->StreamRead    = pStream->BaseRead;
            pStream->StreamWrite   = pStream->BaseWrite;
            pStream->StreamResize  = pStream->BaseResize;
            pStream->StreamGetSize = pStream->BaseGetSize;
            pStream->StreamGetPos  = pStream->BaseGetPos;
            pStream->StreamClose   = pStream->BaseClose;
            return pStream;
        }

        // File create failed, delete the stream
        STORM_FREE(pStream);
        pStream = NULL;
    }

    // Return the stream
    return pStream;
}

/**
 * This function opens an existing file for read or read-write access
 * - If the current platform supports file sharing,
 *   the file must be open for read sharing (i.e. another application
 *   can open the file for read, but not for write)
 * - If the file does not exist, the function must return NULL
 * - If the file exists but cannot be open, then function must return NULL
 * - The parameters of the function must be validate by the caller
 * - The function must initialize all stream function pointers in TFileStream
 * - If the function fails from any reason, it must close all handles
 *   and free all memory that has been allocated in the process of stream creation,
 *   including the TFileStream structure itself
 *
 * \a szFileName Name of the file to open
 * \a dwStreamFlags specifies the provider and base storage type
 */

TFileStream * FileStream_OpenFile(
    const TCHAR * szFileName,
    DWORD dwStreamFlags)
{
    DWORD dwProvider = dwStreamFlags & STREAM_PROVIDERS_MASK;
    size_t nPrefixLength = FileStream_Prefix(szFileName, &dwProvider);

    // Re-assemble the stream flags
    dwStreamFlags = (dwStreamFlags & STREAM_OPTIONS_MASK) | dwProvider;
    szFileName += nPrefixLength;

    // Perform provider-specific open
    switch(dwStreamFlags & STREAM_PROVIDER_MASK)
    {
        case STREAM_PROVIDER_FLAT:
            return FlatStream_Open(szFileName, dwStreamFlags);

        case STREAM_PROVIDER_PARTIAL:
            return PartStream_Open(szFileName, dwStreamFlags);

        case STREAM_PROVIDER_MPQE:
            return MpqeStream_Open(szFileName, dwStreamFlags);

        case STREAM_PROVIDER_BLOCK4:
            return Block4Stream_Open(szFileName, dwStreamFlags);

        default:
            SErrSetLastError(ERROR_INVALID_PARAMETER);
            return NULL;
    }
}

/**
 * Returns the file name of the stream
 *
 * \a pStream Pointer to an open stream
 */
const TCHAR * FileStream_GetFileName(TFileStream * pStream)
{
    assert(pStream != NULL);
    return pStream->szFileName;
}

/**
 * Returns the length of the provider prefix. Returns zero if no prefix
 *
 * \a szFileName Pointer to a stream name (file, mapped file, URL)
 * \a pdwStreamProvider Pointer to a DWORD variable that receives stream provider (STREAM_PROVIDER_XXX)
 */

size_t FileStream_Prefix(const TCHAR * szFileName, DWORD * pdwProvider)
{
    size_t nPrefixLength1 = 0;
    size_t nPrefixLength2 = 0;
    DWORD dwProvider = 0;

    if(szFileName != NULL)
    {
        //
        // Determine the stream provider
        //

        if(!_tcsnicmp(szFileName, _T("flat-"), 5))
        {
            dwProvider |= STREAM_PROVIDER_FLAT;
            nPrefixLength1 = 5;
        }

        else if(!_tcsnicmp(szFileName, _T("part-"), 5))
        {
            dwProvider |= STREAM_PROVIDER_PARTIAL;
            nPrefixLength1 = 5;
        }

        else if(!_tcsnicmp(szFileName, _T("mpqe-"), 5))
        {
            dwProvider |= STREAM_PROVIDER_MPQE;
            nPrefixLength1 = 5;
        }

        else if(!_tcsnicmp(szFileName, _T("blk4-"), 5))
        {
            dwProvider |= STREAM_PROVIDER_BLOCK4;
            nPrefixLength1 = 5;
        }

        //
        // Determine the base provider
        //

        if(!_tcsnicmp(szFileName+nPrefixLength1, _T("file:"), 5))
        {
            dwProvider |= BASE_PROVIDER_FILE;
            nPrefixLength2 = 5;
        }

        else if(!_tcsnicmp(szFileName+nPrefixLength1, _T("map:"), 4))
        {
            dwProvider |= BASE_PROVIDER_MAP;
            nPrefixLength2 = 4;
        }

        else if(!_tcsnicmp(szFileName+nPrefixLength1, _T("http:"), 5))
        {
            dwProvider |= BASE_PROVIDER_HTTP;
            nPrefixLength2 = 5;
        }

        // Only accept stream provider if we recognized the base provider
        if(nPrefixLength2 != 0)
        {
            // It is also allowed to put "//" after the base provider, e.g. "file://", "http://"
            if(szFileName[nPrefixLength1+nPrefixLength2] == '/' && szFileName[nPrefixLength1+nPrefixLength2+1] == '/')
                nPrefixLength2 += 2;

            if(pdwProvider != NULL)
                *pdwProvider = dwProvider;
            return nPrefixLength1 + nPrefixLength2;
        }
    }

    return 0;
}

/**
 * Sets a download callback. Whenever the stream needs to download one or more blocks
 * from the server, the callback is called
 *
 * \a pStream Pointer to an open stream
 * \a pfnCallback Pointer to callback function
 * \a pvUserData Arbitrary user pointer passed to the download callback
 */

bool FileStream_SetCallback(TFileStream * pStream, SFILE_DOWNLOAD_CALLBACK pfnCallback, void * pvUserData)
{
    TBlockStream * pBlockStream = (TBlockStream *)pStream;

    if(pStream->BlockRead == NULL)
    {
        SErrSetLastError(ERROR_NOT_SUPPORTED);
        return false;
    }

    pBlockStream->pfnCallback = pfnCallback;
    pBlockStream->UserData = pvUserData;
    return true;
}

/**
 * This function gives the block map. The 'pvBitmap' pointer must point to a buffer
 * of at least sizeof(STREAM_BLOCK_MAP) size. It can also have size of the complete
 * block map (i.e. sizeof(STREAM_BLOCK_MAP) + BitmapSize). In that case, the function
 * also copies the bit-based block map.
 *
 * \a pStream Pointer to an open stream
 * \a pvBitmap Pointer to buffer where the block map will be stored
 * \a cbBitmap Length of the buffer, of the block map
 * \a cbLengthNeeded Length of the bitmap, in bytes
 */

bool FileStream_GetBitmap(TFileStream * pStream, void * pvBitmap, DWORD cbBitmap, DWORD * pcbLengthNeeded)
{
    TStreamBitmap * pBitmap = (TStreamBitmap *)pvBitmap;
    TBlockStream * pBlockStream = (TBlockStream *)pStream;
    ULONGLONG BlockOffset;
    LPBYTE Bitmap = (LPBYTE)(pBitmap + 1);
    DWORD BitmapSize;
    DWORD BlockCount;
    DWORD BlockSize;
    bool bResult = false;

    // Retrieve the size of one block
    if(pStream->BlockCheck != NULL)
    {
        BlockCount = pBlockStream->BlockCount;
        BlockSize = pBlockStream->BlockSize;
    }
    else
    {
        BlockCount = (DWORD)((pStream->StreamSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE);
        BlockSize = DEFAULT_BLOCK_SIZE;
    }

    // Fill-in the variables
    BitmapSize = (BlockCount + 7) / 8;

    // Give the number of blocks
    if(pcbLengthNeeded != NULL)
        pcbLengthNeeded[0] = sizeof(TStreamBitmap) + BitmapSize;

    // If the length of the buffer is not enough
    if(pvBitmap != NULL && cbBitmap != 0)
    {
        // Give the STREAM_BLOCK_MAP structure
        if(cbBitmap >= sizeof(TStreamBitmap))
        {
            pBitmap->StreamSize = pStream->StreamSize;
            pBitmap->BitmapSize = BitmapSize;
            pBitmap->BlockCount = BlockCount;
            pBitmap->BlockSize  = BlockSize;
            pBitmap->IsComplete = (pStream->BlockCheck != NULL) ? pBlockStream->IsComplete : 1;
            bResult = true;
        }

        // Give the block bitmap, if enough space
        if(cbBitmap >= sizeof(TStreamBitmap) + BitmapSize)
        {
            // Version with bitmap present
            if(pStream->BlockCheck != NULL)
            {
                DWORD ByteIndex = 0;
                BYTE BitMask = 0x01;

                // Initialize the map with zeros
                memset(Bitmap, 0, BitmapSize);

                // Fill the map
                for(BlockOffset = 0; BlockOffset < pStream->StreamSize; BlockOffset += BlockSize)
                {
                    // Set the bit if the block is present
                    if(pBlockStream->BlockCheck(pStream, BlockOffset))
                        Bitmap[ByteIndex] |= BitMask;

                    // Move bit position
                    ByteIndex += (BitMask >> 0x07);
                    BitMask = (BitMask >> 0x07) | (BitMask << 0x01);
                }
            }
            else
            {
                memset(Bitmap, 0xFF, BitmapSize);
            }
        }
    }

    // Set last error value and return
    if(bResult == false)
        SErrSetLastError(ERROR_INSUFFICIENT_BUFFER);
    return bResult;
}

/**
 * Reads data from the stream
 *
 * - Returns true if the read operation succeeded and all bytes have been read
 * - Returns false if either read failed or not all bytes have been read
 * - If the pByteOffset is NULL, the function must read the data from the current file position
 * - The function can be called with dwBytesToRead = 0. In that case, pvBuffer is ignored
 *   and the function just adjusts file pointer.
 *
 * \a pStream Pointer to an open stream
 * \a pByteOffset Pointer to file byte offset. If NULL, it reads from the current position
 * \a pvBuffer Pointer to data to be read
 * \a dwBytesToRead Number of bytes to read from the file
 *
 * \returns
 * - If the function reads the required amount of bytes, it returns true.
 * - If the function reads less than required bytes, it returns false and SErrGetLastError() returns ERROR_HANDLE_EOF
 * - If the function fails, it reads false and SErrGetLastError() returns an error code different from ERROR_HANDLE_EOF
 */
bool FileStream_Read(TFileStream * pStream, ULONGLONG * pByteOffset, void * pvBuffer, DWORD dwBytesToRead)
{
    assert(pStream->StreamRead != NULL);
    return pStream->StreamRead(pStream, pByteOffset, pvBuffer, dwBytesToRead);
}

/**
 * This function writes data to the stream
 *
 * - Returns true if the write operation succeeded and all bytes have been written
 * - Returns false if either write failed or not all bytes have been written
 * - If the pByteOffset is NULL, the function must write the data to the current file position
 *
 * \a pStream Pointer to an open stream
 * \a pByteOffset Pointer to file byte offset. If NULL, it reads from the current position
 * \a pvBuffer Pointer to data to be written
 * \a dwBytesToWrite Number of bytes to write to the file
 */
bool FileStream_Write(TFileStream * pStream, ULONGLONG * pByteOffset, const void * pvBuffer, DWORD dwBytesToWrite)
{
    if(pStream->dwFlags & STREAM_FLAG_READ_ONLY)
    {
        SErrSetLastError(ERROR_ACCESS_DENIED);
        return false;
    }

    assert(pStream->StreamWrite != NULL);
    return pStream->StreamWrite(pStream, pByteOffset, pvBuffer, dwBytesToWrite);
}

/**
 * Returns the size of a file
 *
 * \a pStream Pointer to an open stream
 * \a FileSize Pointer where to store the file size
 */
bool FileStream_GetSize(TFileStream * pStream, ULONGLONG * pFileSize)
{
    assert(pStream->StreamGetSize != NULL);
    return pStream->StreamGetSize(pStream, pFileSize);
}

/**
 * Sets the size of a file
 *
 * \a pStream Pointer to an open stream
 * \a NewFileSize File size to set
 */
bool FileStream_SetSize(TFileStream * pStream, ULONGLONG NewFileSize)
{
    if(pStream->dwFlags & STREAM_FLAG_READ_ONLY)
    {
        SErrSetLastError(ERROR_ACCESS_DENIED);
        return false;
    }

    assert(pStream->StreamResize != NULL);
    return pStream->StreamResize(pStream, NewFileSize);
}

/**
 * This function returns the current file position
 * \a pStream
 * \a pByteOffset
 */
bool FileStream_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset)
{
    assert(pStream->StreamGetPos != NULL);
    return pStream->StreamGetPos(pStream, pByteOffset);
}

/**
 * Returns the last write time of a file
 *
 * \a pStream Pointer to an open stream
 * \a pFileType Pointer where to store the file last write time
 */
bool FileStream_GetTime(TFileStream * pStream, ULONGLONG * pFileTime)
{
    // Just use the saved filetime value
    *pFileTime = pStream->Base.File.FileTime;
    return true;
}

/**
 * Returns the stream flags
 *
 * \a pStream Pointer to an open stream
 * \a pdwStreamFlags Pointer where to store the stream flags
 */
bool FileStream_GetFlags(TFileStream * pStream, LPDWORD pdwStreamFlags)
{
    *pdwStreamFlags = pStream->dwFlags;
    return true;
}

/**
 * Switches a stream with another. Used for final phase of archive compacting.
 * Performs these steps:
 *
 * 1) Closes the handle to the existing MPQ
 * 2) Renames the temporary MPQ to the original MPQ, overwrites existing one
 * 3) Opens the MPQ stores the handle and stream position to the new stream structure
 *
 * \a pStream Pointer to an open stream
 * \a pNewStream Temporary ("working") stream (created during archive compacting)
 */
bool FileStream_Replace(TFileStream * pStream, TFileStream * pNewStream)
{
    // Only supported on flat files
    if((pStream->dwFlags & STREAM_PROVIDERS_MASK) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE))
    {
        SErrSetLastError(ERROR_NOT_SUPPORTED);
        return false;
    }

    // Not supported on read-only streams
    if(pStream->dwFlags & STREAM_FLAG_READ_ONLY)
    {
        SErrSetLastError(ERROR_ACCESS_DENIED);
        return false;
    }

    // Close both stream's base providers
    pNewStream->BaseClose(pNewStream);
    pStream->BaseClose(pStream);

    // Now we have to delete the (now closed) old file and rename the new file
    if(!BaseFile_Replace(pStream, pNewStream))
        return false;

    // Now open the base file again
    if(!BaseFile_Open(pStream, pStream->szFileName, pStream->dwFlags))
        return false;

    // Cleanup the new stream
    FileStream_Close(pNewStream);
    return true;
}

/**
 * This function closes an archive file and frees any data buffers
 * that have been allocated for stream management. The function must also
 * support partially allocated structure, i.e. one or more buffers
 * can be NULL, if there was an allocation failure during the process
 *
 * \a pStream Pointer to an open stream
 */
void FileStream_Close(TFileStream * pStream)
{
    // Check if the stream structure is allocated at all
    if(pStream != NULL)
    {
        // Free the master stream, if any
        if(pStream->pMaster != NULL)
            FileStream_Close(pStream->pMaster);
        pStream->pMaster = NULL;

        // Close the stream provider
        if(pStream->StreamClose != NULL)
            pStream->StreamClose(pStream);

        // ... or close base stream, if any
        else if(pStream->BaseClose != NULL)
            pStream->BaseClose(pStream);

        // Free the stream itself
        STORM_FREE(pStream);
    }
}
