/*****************************************************************************/
/* CascRootFile_Text.cpp                  Copyright (c) Ladislav Zezula 2017 */
/*---------------------------------------------------------------------------*/
/* Support for loading ROOT files in plain text                              */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 28.10.15  1.00  Lad  The first version of CascRootFile_Text.cpp           */
/*****************************************************************************/

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

// Implemented in "overwatch/apm.cpp"
DWORD LoadApplicationPackageManifestFile(TCascStorage * hs, CASC_FILE_TREE & FileTree, PCASC_CKEY_ENTRY pCKeyEntry, const char * szApmFileName);

// Implemented in "overwatch/cmf.cpp"
DWORD LoadContentManifestFile(TCascStorage * hs, CASC_FILE_TREE & FileTree, PCASC_CKEY_ENTRY pCKeyEntry, const char * szFileName);

//-----------------------------------------------------------------------------
// Structure definitions for APM files

typedef struct _APM_HEADER_V3
{
    ULONGLONG BuildNumber;              // Build number of the game
    ULONGLONG ZeroValue1;
    DWORD ZeroValue2;
    DWORD PackageCount;
    DWORD ZeroValue3;
    DWORD EntryCount;
    DWORD Checksum;

    // Followed by the array of APM_ENTRY (count is in "EntryCount")
    // Followed by the array of APM_PACKAGE (count is in "PackageCount")

} APM_HEADER_V3, *PAPM_HEADER_V3;

typedef struct _APM_HEADER_V2
{
    ULONGLONG BuildNumber;              // Build number of the game
    ULONGLONG ZeroValue1;
    DWORD PackageCount;
    DWORD ZeroValue2;
    DWORD EntryCount;
    DWORD Checksum;

    // Followed by the array of APM_ENTRY (count is in "EntryCount")
    // Followed by the array of APM_PACKAGE (count is in "PackageCount")

} APM_HEADER_V2, *PAPM_HEADER_V2;

typedef struct _APM_HEADER_V1
{
    ULONGLONG BuildNumber;              // Build number of the game
    DWORD BuildVersion;
    DWORD PackageCount;
    DWORD EntryCount;
    DWORD Checksum;

    // Followed by the array of APM_ENTRY (count is in "EntryCount")
    // Followed by the array of APM_PACKAGE (count is in "PackageCount")

} APM_HEADER_V1, *PAPM_HEADER_V1;

// On-disk format, size = 0x0C
typedef struct _APM_ENTRY_V1
{
    DWORD     Index;
    DWORD     HashA_Lo;                     // Must split the hashes in order to make this structure properly aligned
    DWORD     HashA_Hi;
} APM_ENTRY_V1, *PAPM_ENTRY_V1;

// On-disk format, size = 0x14
typedef struct _APM_ENTRY_V2
{
    DWORD     Index;
    DWORD     HashA_Lo;                     // Must split the hashes in order to make this structure properly aligned
    DWORD     HashA_Hi;
    DWORD     HashB_Lo;
    DWORD     HashB_Hi;
} APM_ENTRY_V2, *PAPM_ENTRY_V2;

// On-disk format
typedef struct _APM_PACKAGE_ENTRY_V1
{
    ULONGLONG EntryPointGUID;               // virtual most likely
    ULONGLONG PrimaryGUID;                  // real
    ULONGLONG SecondaryGUID;                // real
    ULONGLONG Key;                          // encryption
    ULONGLONG PackageGUID;                  // 077 file
    ULONGLONG Unknown1;
    DWORD Unknown2;
} APM_PACKAGE_ENTRY_V1, *PAPM_PACKAGE_ENTRY_V1;

// On-disk format
typedef struct _APM_PACKAGE_ENTRY_V2
{
    ULONGLONG PackageGUID;                  // 077 file
    ULONGLONG Unknown1;
    DWORD Unknown2;
    DWORD Unknown3;
    ULONGLONG Unknown4;
} APM_PACKAGE_ENTRY_V2, *PAPM_PACKAGE_ENTRY_V2;

//-----------------------------------------------------------------------------
// Local functions (non-class)

static bool IsManifestFolderName(const char * szFileName, const char * szManifestFolder, size_t nLength)
{
    if(!_strnicmp(szFileName, szManifestFolder, nLength))
    {
        return (szFileName[nLength] == '\\' || szFileName[nLength] == '/');
    }
    return false;
}

//-----------------------------------------------------------------------------
// Public functions (non-class)

static void BinaryReverse64(LPBYTE GuidReversed, LPBYTE pbGuid)
{
    GuidReversed[0] = pbGuid[7];
    GuidReversed[1] = pbGuid[6];
    GuidReversed[2] = pbGuid[5];
    GuidReversed[3] = pbGuid[4];
    GuidReversed[4] = pbGuid[3];
    GuidReversed[5] = pbGuid[2];
    GuidReversed[6] = pbGuid[1];
    GuidReversed[7] = pbGuid[0];
}

static const char * ExtractAssetSubString(char * szBuffer, size_t ccBuffer, const char * szPlainName)
{
    char * szBufferEnd = szBuffer + ccBuffer - 1;

    while(szBuffer < szBufferEnd && szPlainName[0] != 0 && szPlainName[0] != '.' && szPlainName[0] != '_')
        *szBuffer++ = *szPlainName++;

    if(szBuffer <= szBufferEnd)
        szBuffer[0] = 0;
    return szPlainName;
}

static const char * AppendAssetSubString(char * szBuffer, size_t ccBuffer, const char * szPlainName)
{
    char * szBufferPtr = szBuffer + strlen(szBuffer);
    char * szBufferEnd = szBuffer + ccBuffer - 1;

    if(szBufferPtr < szBufferEnd)
        *szBufferPtr++ = '-';

    while(szBufferPtr < szBufferEnd && szPlainName[0] != '_')
        *szBufferPtr++ = *szPlainName++;

    szBufferPtr[0] = 0;
    return szPlainName;
}

size_t BuildAssetFileNameTemplate(
    char * szNameTemplate,
    size_t ccNameTemplate,
    const char * szPrefix,
    const char * szAssetName)
{
    const char * szFileName = "0000000000000000";           // Base name for 64-bit GUID
    const char * szFileExt = NULL;
    char * szBufferEnd = szNameTemplate + ccNameTemplate;
    char * szBufferPtr = szNameTemplate;
    char * szPlainName;
    char szPlatform[64] = {0};
    char szLocale[64] = {0};
    char szAsset[64] = {0};

    // Parse the plain name
    while(szAssetName[0] != '.')
    {
        // Watch start of the new field
        if(szAssetName[0] == '_')
        {
            // Extract platform from "_SP"
            if(szAssetName[1] == 'S' && szAssetName[2] == 'P' && !_strnicmp(szAssetName, "_SPWin_", 7))
            {
                CascStrCopy(szPlatform, _countof(szPlatform), "Windows");
                szAssetName += 6;
                continue;
            }

            // Extract "RDEV" or "RCN"
            if(szAssetName[1] == 'R')
            {
                szAssetName = AppendAssetSubString(szPlatform, _countof(szPlatform), szAssetName + 1);
                continue;
            }

            // Extract locale
            if(szAssetName[1] == 'L')
            {
                szAssetName = ExtractAssetSubString(szLocale, _countof(szLocale), szAssetName + 2);
                continue;
            }

            // Ignore "_EExt"
            if(szAssetName[1] == 'E' && szAssetName[2] == 'E')
            {
                szAssetName += 5;
                continue;
            }

            // Extract the asset name
            szAssetName = ExtractAssetSubString(szAsset, _countof(szAsset), szAssetName + 1);

            // Extract a possible extension
            //if(!_stricmp(szAsset, "speech"))
            //    szFileExt = ".wav";
            //if(!_stricmp(szAsset, "text"))
            //    szFileExt = ".text";
            continue;
        }
        szAssetName++;
    }

    // Combine the path like "%PREFIX%\\%PLATFORM%-%DEV%\\%LOCALE%\\%ASSET%\\%PLAIN_NAME%.%EXTENSSION%"
    if(szPrefix && szPrefix[0])
        szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szPrefix);
    if(szPlatform[0])
        szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szPlatform);
    if(szLocale[0])
        szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szLocale);
    if(szAsset[0])
        szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szAsset);
    szPlainName = szBufferPtr;

    // Append file name and extension
    if(szFileName && szFileName[0])
        szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s", szFileName);
    if(szFileExt && szFileExt[0])
        CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s", szFileExt);

    // Return the length of the path
    return (szPlainName - szNameTemplate);
}

DWORD InsertAssetFile(
    TCascStorage * hs,
    CASC_FILE_TREE & FileTree,
    char * szFileName,
    size_t nPlainName,              // Offset of the plain name in the name template
    LPBYTE pbCKey,
    LPBYTE pbGuid)
{
    PCASC_CKEY_ENTRY pCKeyEntry;
    DWORD dwErrCode = ERROR_SUCCESS;
    BYTE GuidReversed[8];

    // Try to find the CKey
    if((pCKeyEntry = FindCKeyEntry_CKey(hs, pbCKey)) != NULL)
    {
        // Save the character at the end of the name (dot or EOS)
        char chSaveChar = szFileName[nPlainName + 16];

        // Imprint the GUID as binary value
        BinaryReverse64(GuidReversed, pbGuid);
        StringFromBinary(GuidReversed, sizeof(GuidReversed), szFileName + nPlainName);
        szFileName[nPlainName + 16] = chSaveChar;

        // Insert the asset to the file tree
        if(FileTree.InsertByName(pCKeyEntry, szFileName) == NULL)
            dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
    }
    return dwErrCode;
}

//-----------------------------------------------------------------------------
// Handler definition for OVERWATCH root file
//
// -------------------------------------
// Overwatch ROOT file (build 24919):
// -------------------------------------
// #MD5|CHUNK_ID|FILENAME|INSTALLPATH
// FE3AD8A77EEF77B383DF4929AED816FD|0|RetailClient/GameClientApp.exe|GameClientApp.exe
// 5EDDEFECA544B6472C5CD52BE63BC02F|0|RetailClient/Overwatch Launcher.exe|Overwatch Launcher.exe
// 6DE09F0A67F33F874F2DD8E2AA3B7AAC|0|RetailClient/ca-bundle.crt|ca-bundle.crt
// 99FE9EB6A4BB20209202F8C7884859D9|0|RetailClient/ortp_x64.dll|ortp_x64.dll
//
// -------------------------------------
// Overwatch ROOT file (build 47161):
// -------------------------------------
// #FILEID|MD5|CHUNK_ID|PRIORITY|MPRIORITY|FILENAME|INSTALLPATH
// RetailClient/Overwatch.exe|807F96661280C07E762A8C129FEBDA6F|0|0|255|RetailClient/Overwatch.exe|Overwatch.exe
// RetailClient/Overwatch Launcher.exe|5EDDEFECA544B6472C5CD52BE63BC02F|0|0|255|RetailClient/Overwatch Launcher.exe|Overwatch Launcher.exe
// RetailClient/ortp_x64.dll|7D1B5DEC267480F3E8DAD6B95143A59C|0|0|255|RetailClient/ortp_x64.dll|ortp_x64.dll
//

struct TRootHandler_OW : public TFileTreeRoot
{
    TRootHandler_OW() : TFileTreeRoot(0)
    {
        // We have file names and return CKey as result of search
        dwFeatures |= (CASC_FEATURE_FILE_NAMES | CASC_FEATURE_ROOT_CKEY);
    }

    DWORD Load(TCascStorage * hs, CASC_CSV & Csv, size_t nFileNameIndex, size_t nCKeyIndex)
    {
        PCASC_CKEY_ENTRY pCKeyEntry;
        size_t nFileCount;
        DWORD dwErrCode = ERROR_SUCCESS;
        BYTE CKey[MD5_HASH_SIZE];

        CASCLIB_UNUSED(hs);

        // Keep loading every line until there is something
        while(Csv.LoadNextLine())
        {
            const CASC_CSV_COLUMN & FileName = Csv[CSV_ZERO][nFileNameIndex];
            const CASC_CSV_COLUMN & CKeyStr = Csv[CSV_ZERO][nCKeyIndex];

            // Retrieve the file name and the content key
            if(FileName.szValue && CKeyStr.szValue && CKeyStr.nLength == MD5_STRING_SIZE)
            {
                // Convert the string CKey to binary
                if(BinaryFromString(CKeyStr.szValue, MD5_STRING_SIZE, CKey) == ERROR_SUCCESS)
                {
                    // Find the item in the tree
                    if((pCKeyEntry = FindCKeyEntry_CKey(hs, CKey)) != NULL)
                    {
                        // Insert the file name and the CKey into the tree
                        FileTree.InsertByName(pCKeyEntry, FileName.szValue);
                    }
                }
            }
        }

        // Get the total file count that we loaded so far
        nFileCount = FileTree.GetCount();

        // Parse Content Manifest Files (.cmf)
        for(size_t i = 0; i < nFileCount && dwErrCode == ERROR_SUCCESS; i++)
        {
            PCASC_FILE_NODE pFileNode;
            const char * szExtension;
            char szFileName[MAX_PATH];

            // Get the n-th file
            pFileNode = (PCASC_FILE_NODE)FileTree.PathAt(szFileName, _countof(szFileName), i);
            if(pFileNode != NULL)
            {
                if(IsManifestFolderName(szFileName, "Manifest", 8) || IsManifestFolderName(szFileName, "TactManifest", 12))
                {
                    // Retrieve the file extension
                    szExtension = GetFileExtension(szFileName);

                    // Check for content manifest files
                    if(!_stricmp(szExtension, ".cmf"))
                    {
                        dwErrCode = LoadContentManifestFile(hs, FileTree, pFileNode->pCKeyEntry, szFileName);
                    }
                    else if(!_stricmp(szExtension, ".apm"))
                    {
                        dwErrCode = LoadApplicationPackageManifestFile(hs, FileTree, pFileNode->pCKeyEntry, szFileName);
                    }
                }
            }
        }
        return dwErrCode;
    }
};

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

// TODO: There is way more files in the Overwatch CASC storage than present in the ROOT file.
DWORD RootHandler_CreateOverwatch(TCascStorage * hs, CASC_BLOB & RootFile)
{
    TRootHandler_OW * pRootHandler = NULL;
    CASC_CSV Csv(0, true);
    size_t Indices[2];
    DWORD dwErrCode;

    // Load the ROOT file
    dwErrCode = Csv.Load(RootFile.pbData, RootFile.cbData);
    if(dwErrCode == ERROR_SUCCESS)
    {
        // Retrieve the indices of the file name and MD5 columns
        Indices[0] = Csv.GetColumnIndex("FILENAME");
        Indices[1] = Csv.GetColumnIndex("MD5");

        // If both indices were found OK, then load the root file
        if(Indices[0] != CSV_INVALID_INDEX && Indices[1] != CSV_INVALID_INDEX)
        {
            pRootHandler = new TRootHandler_OW();
            if(pRootHandler != NULL)
            {
                // Load the root directory. If load failed, we free the object
                dwErrCode = pRootHandler->Load(hs, Csv, Indices[0], Indices[1]);
                if(dwErrCode != ERROR_SUCCESS)
                {
                    delete pRootHandler;
                    pRootHandler = NULL;
                }
            }
        }
        else
        {
            dwErrCode = ERROR_BAD_FORMAT;
        }
    }

    // Assign the root directory (or NULL) and return error
    hs->pRootHandler = pRootHandler;
    return dwErrCode;
}
