/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.

#include "stdafx.h"
#include <smartptr.h>
#include "Util.h"
#include "ZipFileFormat.h"
#include "zipdirstructures.h"
#include "ZipDirTree.h"
#include "ZipDirList.h"
#include "ZipDirCache.h"
#include "ZipDirCacheRW.h"
#include "ZipDirCacheFactory.h"
#include "ZipDirFindRW.h"

#include "ThreadUtils.h"

#include <zlib.h>  // declaration of Z_OK for ZipRawDecompress
#include <AzCore/std/parallel/mutex.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include "Codec.h"
#include <zstd.h>
#include <lz4.h>
#include <chrono>
#include <ratio>

enum PackFileStatus
{
    PACKFILE_COMPRESSED,

    PACKFILE_ADDED,
    PACKFILE_UPTODATE,
    PACKFILE_SKIPPED,
    PACKFILE_MISSING,
    PACKFILE_FAILED
};

enum PackFileCompressionPolicy
{
    PACKFILE_USE_REQUESTED_COMPRESSOR,
    PACKFILE_USE_FASTEST_DECOMPRESSING_CODEC
};

class PackFilePool;
struct PackFileBatch
{
    PackFilePool* pool;

    int zipMaxSize;
    int sourceMinSize;
    int sourceMaxSize;
    int compressionMethod;
    int compressionLevel;

    PackFileBatch()
        : pool(0)
        , sourceMinSize(0)
        , sourceMaxSize(0)
        , zipMaxSize(0)
        , compressionMethod(0)
        , compressionLevel(0)
    {
    }
};

class PackFilePool;
struct PackFileJob
{
    int index;
    int key;
    PackFileBatch* batch;
    const char* relativePathSrc;
    const char* realFilename;

    unsigned int existingCRC;

    void* compressedData;
    unsigned long compressedSize;
    unsigned long compressedSizePreviously;

    void* uncompressedData;
    unsigned long uncompressedSize;
    unsigned long uncompressedSizePreviously;

    int64 modTime;
    ZipDir::ErrorEnum zdError;
    PackFileStatus status;
    PackFileCompressionPolicy compressionPolicy;

    PackFileJob()
        : index(0)
        , key(0)
        , batch(0)
        , realFilename(0)
        , relativePathSrc(0)
        , existingCRC(0)
        , compressedData(0)
        , compressedSize(0)
        , compressedSizePreviously(0)
        , uncompressedData(0)
        , uncompressedSize(0)
        , uncompressedSizePreviously(0)
        , modTime(0)
        , zdError(ZipDir::ZD_ERROR_NOT_IMPLEMENTED)
        , status(PACKFILE_FAILED)
        , compressionPolicy(PACKFILE_USE_REQUESTED_COMPRESSOR)
    {
    }

    void DetachUncompressedData()
    {
        if (uncompressedData && uncompressedData == compressedData)
        {
            compressedData = 0;
            compressedSize = 0;
        }

        uncompressedData = 0;
        uncompressedSize = 0;
    }

    ~PackFileJob()
    {
        if (compressedData && compressedData != uncompressedData)
        {
            azfree(compressedData);
            compressedData = 0;
        }

        if (uncompressedData)
        {
            azfree(uncompressedData);
            uncompressedData = 0;
        }
    }
};


// ---------------------------------------------------------------------------
static void PackFileFromDisc(PackFileJob* job);
class PackFilePool
{
public:
    PackFilePool(int numFiles, size_t memoryLimit)
        : m_pool(false)
        , m_skip(false)
        , m_awaitedFile(0)
        , m_memoryLimit(memoryLimit)
        , m_allocatedMemory(0)
    {
        m_files.reserve(numFiles);
    }

    ~PackFilePool()
    {
    }

    void Submit(int key, const PackFileJob& job)
    {
        PackFileJob* newJob = new PackFileJob(job);

        // index in queue, and custom key for identification
        newJob->index = int(m_files.size());
        newJob->key = key;

        m_files.push_back(newJob);
    }

    PackFileJob* WaitForFile(int index)
    {
        while (true)
        {
            {
                AZStd::lock_guard<AZStd::mutex> lock(m_filesLock);
                m_awaitedFile = index;
                if (size_t(index) >= m_files.size())
                {
                    return 0;
                }
                if (m_files[index])
                {
                    return m_files[index];
                }
            }
            Sleep(0);
        }

        assert(0);
        return 0;
    }

    void Start(unsigned numExtraThreads)
    {
        if (numExtraThreads == 0)
        {
            for (PackFileJob* job : m_files)
            {
                PackFileFromDisc(job);
            }
        }
        else
        {
            for (size_t i = 0; i < m_files.size(); ++i)
            {
                PackFileJob* job = m_files[i];
                m_files[i] = 0;
                m_pool.Submit(&ProcessFile, job);
            }

            m_pool.Start(numExtraThreads);
        }
    }

    size_t GetJobCount() const
    {
        return m_files.size();
    }

    void SkipPendingFiles()
    {
        m_skip = true;
    }

    void ReleaseFile(int index)
    {
        assert(m_files[index] != 0);
        if (m_files[index])
        {
            if (m_memoryLimit != 0)
            {
                AZStd::lock_guard<AZStd::mutex> lock(m_filesLock);

                m_allocatedMemory -= m_files[index]->uncompressedSize;
                m_allocatedMemory -= m_files[index]->compressedSize;
            }

            delete m_files[index];
            m_files[index] = 0;
        }
    }

private:

    // called from non-main thread
    static void ProcessFile(PackFileJob* job)
    {
        PackFilePool* self = job->batch->pool;

        if (!self->m_skip)
        {
            if (self->m_memoryLimit != 0)
            {
                while (true)
                {
                    size_t allocatedMemory = 0;
                    int awaitedFile = 0;
                    {
                        AZStd::lock_guard<AZStd::mutex> lock(self->m_filesLock);
                        allocatedMemory = self->m_allocatedMemory;
                        awaitedFile = self->m_awaitedFile;
                    }

                    if (allocatedMemory > self->m_memoryLimit && job->index > awaitedFile + 1)
                    {
                        Sleep(10); // give time to main thread to write data to file
                    }
                    else
                    {
                        break;
                    }
                }
            }

            PackFileFromDisc(job);
        }

        self->FileCompleted(job);
    }

    // called from non-main thread
    void FileCompleted(PackFileJob* job)
    {
        AZStd::lock_guard<AZStd::mutex> lock(m_filesLock);

        assert(job);
        assert(job->index < m_files.size());
        assert(m_files[job->index] == 0);
        m_files[job->index] = job;

        if (m_memoryLimit != 0)
        {
            m_allocatedMemory += job->uncompressedSize;
            m_allocatedMemory += job->compressedSize;
        }
    }

    size_t m_memoryLimit;

    AZStd::mutex m_filesLock;
    std::vector<PackFileJob*> m_files;
    int m_awaitedFile;
    size_t m_allocatedMemory;
    bool m_skip;

    ThreadUtils::SimpleThreadPool m_pool;
};

//////////////////////////////////////////////////////////////////////////
static size_t AlignTo(size_t offset, size_t alignment)
{
    const size_t remainder = offset % alignment;
    return remainder ? offset + alignment - remainder : offset;
}
//////////////////////////////////////////////////////////////////////////
// Calculates new offset of the header to make sure that following data are
// aligned properly
static size_t CalculateAlignedHeaderOffset(const char* fileName, size_t currentOffset, size_t alignment)
{
    // Since file should start from header
    if (currentOffset == 0)
    {
        return 0;
    }

    // Local header is followed by filename
    const size_t totalHeaderSize = sizeof(ZipFile::LocalFileHeader) + strlen(fileName);

    // Align end of the header
    const size_t dataOffset = AlignTo(currentOffset + totalHeaderSize, alignment);

    return dataOffset - totalHeaderSize;
}

//////////////////////////////////////////////////////////////////////////
ZipDir::CacheRW::CacheRW(bool encryptHeaders, const EncryptionKey& encryptionKey)
    : m_pFile (NULL)
    , m_nFlags (0)
    , m_lCDROffset (0)
    , m_fileAlignment (1)
    , m_bEncryptedHeaders(encryptHeaders)
    , m_bHeadersEncryptedOnClose(encryptHeaders)
    , m_encryptionKey(encryptionKey)
{
    m_nRefCount = 0;
}
//////////////////////////////////////////////////////////////////////////
ZipDir::CacheRW::~CacheRW()
{
    Close();
}
//////////////////////////////////////////////////////////////////////////
void ZipDir::CacheRW::AddRef()
{
    ++m_nRefCount;
}

//////////////////////////////////////////////////////////////////////////
void ZipDir::CacheRW::Release()
{
    if (--m_nRefCount <= 0)
    {
        delete this;
    }
}

void ZipDir::CacheRW::Close()
{
    if (m_pFile)
    {
        if (!(m_nFlags & FLAGS_READ_ONLY))
        {
            if ((m_nFlags & FLAGS_UNCOMPACTED) && !(m_nFlags & FLAGS_DONT_COMPACT))
            {
                if (!RelinkZip())
                {
                    WriteCDR();
                }
            }
            else
            if (m_nFlags & FLAGS_CDR_DIRTY)
            {
                WriteCDR();
            }
        }

        if (m_pFile)                     // RelinkZip() might have closed the file
        {
            fclose (m_pFile);
        }

        m_pFile = NULL;
    }
    m_treeDir.Clear();
}

//////////////////////////////////////////////////////////////////////////
char* ZipDir::CacheRW::UnifyPath(char* const str, const char* pPath)
{
    assert(str);
    const char* src = pPath;
    char* trg = str;
    while (*src)
    {
        if (*src != '/')
        {
            *trg++ = ::tolower(*src++);
        }
        else
        {
            *trg++ = '\\';
            src++;
        }
    }
    *trg = 0;
    return str;
}

//////////////////////////////////////////////////////////////////////////
char* ZipDir::CacheRW::ToUnixPath(char* const str, const char* pPath)
{
    assert(str);
    const char* src = pPath;
    char* trg = str;
    while (*src)
    {
        if (*src != '/')
        {
            *trg++ = *src++;
        }
        else
        {
            *trg++ = '\\';
            src++;
        }
    }
    *trg = 0;
    return str;
}

//////////////////////////////////////////////////////////////////////////
char* ZipDir::CacheRW::AllocPath(const char* pPath)
{
    char str[_MAX_PATH];
    char* temp = ToUnixPath(str, pPath);
    temp = m_tempStringPool.Append(temp, strlen(temp));
    return temp;
}

static bool UseZlibForFileType(const char* filename)
{
    AZStd::string f(filename);

    //some files types are forced to use zlib
    bool found = AzFramework::StringFunc::Path::IsExtension(filename, ".dds") || f.find("cover.ctc") != string::npos || AzFramework::StringFunc::Path::IsExtension(filename, ".uicanvas");

    return found;
}

static const char* CodecAsString(CompressionCodec::Codec codec)
{
    switch (codec)
    {
    case CompressionCodec::Codec::ZLIB:
        return "ZLIB";
    case CompressionCodec::Codec::ZSTD:
        return "ZSTD";
    case CompressionCodec::Codec::LZ4:
        return "LZ4";
    }
    return "ERROR";
}

static bool CompressData(PackFileJob *job)
{
    bool bUseZlib = UseZlibForFileType(job->relativePathSrc) || (job->compressionPolicy == PACKFILE_USE_REQUESTED_COMPRESSOR);

    bool compressionSuccessful = true;

    if (bUseZlib)
    {
        job->compressedSize = ZipDir::GetCompressedSizeEstimate(job->uncompressedSize,CompressionCodec::Codec::ZLIB);
        job->compressedData = azmalloc(job->compressedSize);
        int error = ZipDir::ZipRawCompress(job->uncompressedData, &job->compressedSize, job->compressedData, job->uncompressedSize, job->batch->compressionLevel);
        if (error == Z_OK)
        {
            job->status = PACKFILE_COMPRESSED;
            job->zdError = ZipDir::ZD_ERROR_SUCCESS;
        }
        else
        {
            compressionSuccessful = false;
        }
    }
    else
    {
        unsigned long   compressedSize[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)];
        void*           compressedData[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)];
        std::chrono::milliseconds decompressionTime[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)];
        bool            compressionCodecWasSuccessful[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)];

        std::chrono::time_point<std::chrono::steady_clock> start;

        //do compression
        for (CompressionCodec::Codec codec : CompressionCodec::s_AllCodecs)
        {
            unsigned int index = static_cast<int>(codec);
            compressedSize[index] = ZipDir::GetCompressedSizeEstimate(job->uncompressedSize, codec);
            compressedData[index] = azmalloc(compressedSize[index]);
            AZStd::unique_ptr<char[]> tempBuffer;
            unsigned long tempSize = 0;

            //some files decompress so fast they are beyond our ability to measure so we need to do it a few times to get a reading
            int numTimesToDecompress = 1 + ZipDir::TARGET_MIN_TEST_COMPRESS_BYTES / job->uncompressedSize;

            auto testDecompressionTime = [&tempSize, job, &tempBuffer, &start, &compressionCodecWasSuccessful, index, numTimesToDecompress, &compressedData, &compressedSize, &decompressionTime]() {
                tempSize = job->uncompressedSize;
                tempBuffer = AZStd::make_unique<char[]>(tempSize);
                start = std::chrono::high_resolution_clock::now();

                //start by assuming the decompression test is never going to result in an error
                compressionCodecWasSuccessful[index] = true;

                for (int i = 0; i < numTimesToDecompress; i++)
                {
                    int zerror = ZipDir::ZipRawUncompress(tempBuffer.get(), &tempSize, compressedData[index], compressedSize[index]);
                    if (zerror != Z_OK)
                    {
                        compressionCodecWasSuccessful[index] = false;
                        break;
                    }
                }
                decompressionTime[index] = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start);
            };

            switch (codec)
            {
            case CompressionCodec::Codec::ZLIB:
                if (ZipDir::ZipRawCompress(job->uncompressedData, &compressedSize[index], compressedData[index], job->uncompressedSize, job->batch->compressionLevel) == Z_OK)
                {
                    testDecompressionTime();
                }
                else
                {
                    compressionCodecWasSuccessful[index] = false;
                }
                break;

            case CompressionCodec::Codec::ZSTD:
                if (ZipDir::ZipRawCompressZSTD(job->uncompressedData, &compressedSize[index], compressedData[index], job->uncompressedSize, 1) == Z_OK)
                {
                    testDecompressionTime();
                }
                else
                {
                    compressionCodecWasSuccessful[index] = false;
                }
                break;

            case CompressionCodec::Codec::LZ4:
                if (ZipDir::ZipRawCompressLZ4(job->uncompressedData, &compressedSize[index], compressedData[index], job->uncompressedSize, job->batch->compressionLevel) == Z_OK)
                {
                    testDecompressionTime();
                }
                else
                {
                    compressionCodecWasSuccessful[index] = false;
                }

                break;

            default:
                break;
            }
        }

        //check decompression speed
        int bestTimeIndex = -1;
        int numberOfSuccessfulCodecs = 0;
        for (CompressionCodec::Codec codec : CompressionCodec::s_AllCodecs)
        {
            int index = static_cast<int>(codec);
            if (compressionCodecWasSuccessful[index])
            {
                numberOfSuccessfulCodecs++;
                if (bestTimeIndex == -1)
                {
                    bestTimeIndex = index;
                    continue;
                }
                if ((decompressionTime[index] < decompressionTime[bestTimeIndex]))
                {
                    bestTimeIndex = index;
                }
            }
        }

        if (!numberOfSuccessfulCodecs)
        {
            AZ_Error("ZipDirCacheRW", false, "None of the available codecs were able to compress the file: %s", job->relativePathSrc);
            compressionSuccessful = false;
        }
        else
        {
#ifdef AZ_DEBUG_BUILD        
            AZ_Printf("ZipDirCacheRW", "Winner for %s is %s with: %d ms ", job->realFilename, CodecAsString(static_cast<CompressionCodec::Codec>(bestTimeIndex)), decompressionTime[bestTimeIndex]);
#endif
        }

        //get rid of losing data
        for (CompressionCodec::Codec codec : CompressionCodec::s_AllCodecs)
        {
            int index = static_cast<int>(codec);
            if (index != bestTimeIndex)
            {
                azfree(compressedData[index]);
                compressedData[index] = nullptr;
            }
        }

        if (compressionSuccessful)
        {
            job->compressedSize = compressedSize[bestTimeIndex];
            job->compressedData = compressedData[bestTimeIndex];
        }
    }

    //if there was a problem with the compression so just store the file
    if (!compressionSuccessful)
    {
        azfree(job->compressedData);
        job->compressedData = job->uncompressedData;
        job->compressedSize = job->uncompressedSize;
    }
    job->status = PACKFILE_COMPRESSED;
    job->zdError = ZipDir::ZD_ERROR_SUCCESS;
    return true;
}

static void PackFileFromMemory(PackFileJob* job)
{
    if (job->existingCRC != 0)
    {
        unsigned int crcCode = (unsigned int)crc32(0, (unsigned char*)job->uncompressedData, job->uncompressedSize);
        if (crcCode == job->existingCRC)
        {
            job->compressedData = 0;
            job->compressedSize = 0;
            job->status = PACKFILE_UPTODATE;
            job->zdError = ZipDir::ZD_ERROR_SUCCESS;
            // This file with same data already in pak, skip it.
            return;
        }
    }

    switch (job->batch->compressionMethod)
    {
    case ZipFile::METHOD_DEFLATE_AND_ENCRYPT:
    case ZipFile::METHOD_DEFLATE:
    {
        // allocate memory for compression. Min is nSize * 1.001 + 12
        if (job->uncompressedSize > 0)
        {
            CompressData(job);
        }
        else
        {
            job->status = PACKFILE_COMPRESSED;
            job->zdError = ZipDir::ZD_ERROR_SUCCESS;

            job->compressedSize = 0;
            job->compressedData = 0;
        }
        break;
    }
    case ZipFile::METHOD_STORE:
        job->compressedData = job->uncompressedData;
        job->compressedSize = job->uncompressedSize;
        job->status = PACKFILE_COMPRESSED;
        job->zdError = ZipDir::ZD_ERROR_SUCCESS;
        break;

    default:
        job->status = PACKFILE_FAILED;
        job->zdError = ZipDir::ZD_ERROR_UNSUPPORTED;
        break;
    }
}

bool ZipDir::CacheRW::WriteCompressedData(const char* data, size_t size, bool encrypt, FILE* file)
{
    if (size <= 0)
    {
        return true;
    }

    std::vector<char> buffer;
    if (encrypt)
    {
        buffer.resize(size);
        memcpy(&buffer[0], data, size);
        ZipDir::Encrypt(&buffer[0], size, m_encryptionKey);
        data = &buffer[0];
    }

    // Danny - writing a single large chunk (more than 6MB?) causes
    // Windows fwrite to (silently?!) fail. So we're writing data
    // in small chunks.
    while (size > 0)
    {
        const size_t sizeToWrite = Util::getMin(size, size_t(1024 * 1024));
        if (fwrite(data, sizeToWrite, 1, file) != 1)
        {
            return false;
        }
        data += sizeToWrite;
        size -= sizeToWrite;
    }

    return true;
}

static bool WriteRandomData(FILE* file, size_t size)
{
    if (size <= 0)
    {
        return true;
    }

    const size_t bufferSize = Util::getMin(size, size_t(1024 * 1024));
    std::vector<char> buffer(bufferSize);

    while (size > 0)
    {
        const size_t sizeToWrite = Util::getMin(size, bufferSize);

        for (size_t i = 0; i < sizeToWrite; ++i)
        {
            buffer[i] = rand() & 0xff;
        }

        if (fwrite(&buffer[0], sizeToWrite, 1, file) != 1)
        {
            return false;
        }

        size -= sizeToWrite;
    }

    return true;
}

bool ZipDir::CacheRW::WriteNullData(size_t size)
{
    if (size <= 0)
    {
        return true;
    }

    const size_t bufferSize = Util::getMin(size, size_t(1024 * 1024));
    std::vector<char> buffer(bufferSize, 0);

    while (size > 0)
    {
        const size_t sizeToWrite = Util::getMin(size, bufferSize);

        if (fwrite(&buffer[0], sizeToWrite, 1, m_pFile) != 1)
        {
            return false;
        }

        size -= sizeToWrite;
    }

    return true;
}

void ZipDir::CacheRW::StorePackedFile(PackFileJob* job)
{
    if (job->batch->zipMaxSize > 0 && GetTotalFileSize() > job->batch->zipMaxSize)
    {
        job->status = PACKFILE_SKIPPED;
        job->zdError = ZipDir::ZD_ERROR_SUCCESS;
        return;
    }

    job->status = PACKFILE_FAILED;

    char str[_MAX_PATH];
    char* relativePath = UnifyPath(str, job->relativePathSrc);

    // create or find the file entry.. this object will rollback (delete the object
    // if the operation fails) if needed.
    FileEntryTransactionAdd pFileEntry(this, AllocPath(job->relativePathSrc), AllocPath(relativePath));

    if (!pFileEntry)
    {
        job->zdError = ZipDir::ZD_ERROR_INVALID_PATH;
        return;
    }

    pFileEntry->OnNewFileData(job->uncompressedData, job->uncompressedSize,
        job->compressedSize, job->batch->compressionMethod, false);
    pFileEntry->SetFromFileTimeNTFS(job->modTime);

    // since we changed the time, we'll have to update CDR
    m_nFlags |= FLAGS_CDR_DIRTY;

    // the new CDR position, if the operation completes successfully
    unsigned lNewCDROffset = m_lCDROffset;

    if (pFileEntry->IsInitialized())
    {
        // this file entry is already allocated in CDR

        // check if the new compressed data fits into the old place
        unsigned nFreeSpace = pFileEntry->nEOFOffset - pFileEntry->nFileHeaderOffset - (unsigned)sizeof(ZipFile::LocalFileHeader) - (unsigned)strlen(relativePath);

        if (nFreeSpace != job->compressedSize)
        {
            m_nFlags |= FLAGS_UNCOMPACTED;
        }

        if (nFreeSpace >= job->compressedSize)
        {
            // and we can just override the compressed data in the file
            ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, job->relativePathSrc, m_bEncryptedHeaders);
            if (e != ZipDir::ZD_ERROR_SUCCESS)
            {
                job->zdError = e;
                return;
            }
        }
        else
        {
            // we need to write the file anew - in place of current CDR
            pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(job->relativePathSrc, m_lCDROffset, m_fileAlignment);
            ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, job->relativePathSrc, m_bEncryptedHeaders);
            lNewCDROffset = pFileEntry->nEOFOffset;
            if (e != ZipDir::ZD_ERROR_SUCCESS)
            {
                job->zdError = e;
                return;
            }
        }
    }
    else
    {
        pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(job->relativePathSrc, m_lCDROffset, m_fileAlignment);
        ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, job->relativePathSrc, m_bEncryptedHeaders);
        if (e != ZipDir::ZD_ERROR_SUCCESS)
        {
            job->zdError = e;
            return;
        }

        lNewCDROffset = pFileEntry->nFileDataOffset + job->compressedSize;

        m_nFlags |= FLAGS_CDR_DIRTY;
    }

    // now we have the fresh local header and data offset

#ifdef WIN32
    if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileDataOffset, SEEK_SET) != 0)
#else
    if (fseek (m_pFile, pFileEntry->nFileDataOffset, SEEK_SET) != 0)
#endif
    {
        job->zdError = ZD_ERROR_IO_FAILED;
        return;
    }

    const bool encrypt = pFileEntry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT;

    if (!WriteCompressedData((char*)job->compressedData, job->compressedSize, encrypt, m_pFile))
    {
        job->zdError = ZD_ERROR_IO_FAILED;
        return;
    }

    // since we wrote the file successfully, update the new CDR position
    m_lCDROffset = lNewCDROffset;
    pFileEntry.Commit();

    job->status = PACKFILE_ADDED;
    job->zdError = ZD_ERROR_SUCCESS;
}

// Adds a new file to the zip or update an existing one
// adds a directory (creates several nested directories if needed)
ZipDir::ErrorEnum ZipDir::CacheRW::UpdateFile (const char* szRelativePathSrc, void* pUncompressed, unsigned nSize,
    unsigned nCompressionMethod, int nCompressionLevel, int64 modTime)
{
    char str[_MAX_PATH];
    char* szRelativePath = UnifyPath(str, szRelativePathSrc);


    PackFileBatch batch;
    batch.compressionMethod = nCompressionMethod;
    batch.compressionLevel = nCompressionLevel;

    PackFileJob job;
    job.relativePathSrc = szRelativePathSrc;
    job.modTime = modTime;
    job.uncompressedData = pUncompressed;
    job.uncompressedSize = nSize;
    job.batch = &batch;

    // crc will be used to check if this file need to be updated at all
    ZipDir::FileEntry* entry = FindFile(szRelativePath);
    if (entry)
    {
        job.existingCRC = entry->desc.lCRC32;
    }

    PackFileFromMemory(&job);

    switch (job.status)
    {
    case PACKFILE_SKIPPED:
    case PACKFILE_MISSING:
    case PACKFILE_FAILED:
        return ZD_ERROR_IO_FAILED;
    }

    StorePackedFile(&job);
    job.DetachUncompressedData();
    return job.zdError;
}

static FILETIME GetFileWriteTimeAndSize(uint64* fileSize, const char* filename)
{
    // Warning: FindFirstFile on NTFS may report file size that
    // is not up-to-date with the actual file content.
    // http://blogs.msdn.com/b/oldnewthing/archive/2011/12/26/10251026.aspx

    FILETIME fileTime;

#if defined(AZ_PLATFORM_WINDOWS)
    WIN32_FIND_DATAA FindFileData;
    HANDLE hFind = FindFirstFileA(filename, &FindFileData);

    if (hFind == INVALID_HANDLE_VALUE)
    {
        fileTime.dwLowDateTime = 0;
        fileTime.dwHighDateTime = 0;
        if (fileSize)
        {
            *fileSize = 0;
        }
    }
    else
    {
        fileTime.dwLowDateTime = FindFileData.ftLastWriteTime.dwLowDateTime;
        fileTime.dwHighDateTime = FindFileData.ftLastWriteTime.dwHighDateTime;
        if (fileSize)
        {
            *fileSize = (uint64(FindFileData.nFileSizeHigh) << 32) + FindFileData.nFileSizeLow;
        }
        FindClose(hFind);
    }
#elif AZ_TRAIT_OS_PLATFORM_APPLE || defined(AZ_PLATFORM_LINUX)
    //We cant use this implmentation for the windows version because ModificationTime
    //returns the time filename was changed(ChangeTime) not last written into(LastWriteTime).
    //If LocalFileIO ever adds support for LastWriteTime we can have a common implementation.
    AZ::IO::LocalFileIO localFileIO;
    AZ::u64 modTime = 0;
    modTime = localFileIO.ModificationTime(filename);
    if(modTime != 0)
    {
        fileTime.dwHighDateTime = modTime >> 32;
        fileTime.dwLowDateTime = modTime & 0xFFFFFFFF;
        if (fileSize)
        {
            localFileIO.Size(filename, *fileSize);
        }
    }
#else
#error Needs implmentation!
#endif
    return fileTime;
}
static void PackFileFromDisc(PackFileJob* job)
{
    const FILETIME ft = GetFileWriteTimeAndSize(0, job->realFilename);
    LARGE_INTEGER lt;
    lt.HighPart = ft.dwHighDateTime;
    lt.LowPart = ft.dwLowDateTime;
    job->modTime = lt.QuadPart;

    FILE* f = nullptr; 
    azfopen(&f, job->realFilename, "rb");
    if (!f)
    {
        job->status = PACKFILE_FAILED;
        job->zdError = ZipDir::ZD_ERROR_FILE_NOT_FOUND;
        return;
    }

    fseek(f, 0, SEEK_END);
    size_t fileSize = (size_t)ftell(f);

    if ((fileSize < job->batch->sourceMinSize) || (job->batch->sourceMaxSize > 0 && fileSize > job->batch->sourceMaxSize))
    {
        fclose(f);

        job->status = PACKFILE_SKIPPED;
        job->zdError = ZipDir::ZD_ERROR_SUCCESS;
        return;
    }

    if (!fileSize)
    {
        //Allow 0-Bytes long files.
        job->uncompressedData = nullptr;
    }
    else
    {
        job->uncompressedData = azmalloc(fileSize);

        fseek(f, 0, SEEK_SET);
        if (fread(job->uncompressedData, 1, fileSize, f) != fileSize)
        {
            azfree(job->uncompressedData);
            job->uncompressedData = 0;
            fclose(f);

            job->status = PACKFILE_FAILED;
            job->zdError = ZipDir::ZD_ERROR_IO_FAILED;
            return;
        }
    }
    fclose(f);
    job->uncompressedSize = fileSize;

    PackFileFromMemory(job);
}

bool ZipDir::CacheRW::UpdateMultipleFiles(const char** realFilenames, const char** filenamesInZip, size_t fileCount,
    int compressionLevel, bool encryptContent, size_t zipMaxSize, int sourceMinSize, int sourceMaxSize,
    unsigned numExtraThreads, ZipDir::IReporter* reporter, ZipDir::ISplitter* splitter, bool useFastestDecompressionCodec)
{
    int compressionMethod = ZipFile::METHOD_DEFLATE;
    if (encryptContent)
    {
        compressionMethod = ZipFile::METHOD_DEFLATE_AND_ENCRYPT;
    }
    else if (compressionLevel == 0)
    {
        compressionMethod = ZipFile::METHOD_STORE;
    }

    uint64 totalSize = 0;

    clock_t startTime = clock();

    PackFileBatch batch;
    batch.compressionLevel = compressionLevel;
    batch.compressionMethod = compressionMethod;
    batch.sourceMinSize = sourceMinSize;
    batch.sourceMaxSize = sourceMaxSize;
    batch.zipMaxSize = zipMaxSize;

    const size_t memoryLimit = 1024 * 1024 * 1024; // prevents threads from generating more than 1GB of data
    PackFilePool pool(fileCount, memoryLimit);
    batch.pool = &pool;

    for (int i = 0; i < fileCount; ++i)
    {
        const char* realFilename = realFilenames[i];
        const char* filenameInZip = filenamesInZip[i];

        PackFileJob job;

        job.relativePathSrc = filenameInZip;
        job.realFilename = realFilename;
        job.batch = &batch;
        job.compressionPolicy = useFastestDecompressionCodec ? PACKFILE_USE_FASTEST_DECOMPRESSING_CODEC : PACKFILE_USE_REQUESTED_COMPRESSOR;

        {
            // crc will be used to check if this file need to be updated at all
            ZipDir::FileEntry* entry = FindFile(filenameInZip);
            if (entry)
            {
                uint64 fileSize = 0;

                const FILETIME ft = GetFileWriteTimeAndSize(&fileSize, realFilename);
                LARGE_INTEGER lt;

                lt.HighPart = ft.dwHighDateTime;
                lt.LowPart = ft.dwLowDateTime;
                job.modTime = lt.QuadPart;
                job.existingCRC = entry->desc.lCRC32;
                job.compressedSizePreviously = entry->desc.lSizeCompressed;
                job.uncompressedSizePreviously = entry->desc.lSizeUncompressed;

                // Check if file with the same name, timestamp and size already exists in pak.
                if (entry->CompareFileTimeNTFS(job.modTime) && fileSize == entry->desc.lSizeUncompressed)
                {
                    if (reporter)
                    {
                        reporter->ReportUpToDate(filenameInZip);
                    }
                    continue;
                }
            }
        }

        pool.Submit(i, job);
    }

    // Get the number of submitted jobs, which is at most
    // as large as the largest successfully submitted file-index.
    // Any number of files can be skipped for submission.
    const int jobCount = pool.GetJobCount();
    if (jobCount == 0)
    {
        return true;
    }

    pool.Start(numExtraThreads);

    for (int i = 0; i < jobCount; ++i)
    {
        PackFileJob* job = pool.WaitForFile(i);
        if (!job)
        {
            assert(job);
            continue;
        }

        if (job->status == PACKFILE_COMPRESSED)
        {
            if (splitter)
            {
                size_t dsk = GetTotalFileSizeOnDiskSoFar();
                size_t bse = 0;
                size_t add = 0;
                size_t sub = 0;

                bse += sizeof(ZipFile::CDRFileHeader)   + strlen(job->relativePathSrc);
                bse += sizeof(ZipFile::LocalFileHeader) + strlen(job->relativePathSrc);

                if (job->compressedSize)
                {
                    add += bse + job->compressedSize;
                }
                if (job->compressedSizePreviously)
                {
                    sub += bse + job->compressedSizePreviously;
                }

                if (splitter->CheckWriteLimit(dsk, add, sub))
                {
                    splitter->SetLastFile(dsk, add, sub, job->key - 1);

                    // deplete the pool before leaving the loop
                    pool.SkipPendingFiles();
                    for (; i < jobCount; ++i)
                    {
                        pool.WaitForFile(i);
                        pool.ReleaseFile(i);
                    }

                    break;
                }
            }

            StorePackedFile(job);
        }

        switch (job->status)
        {
        case PACKFILE_ADDED:
            if (reporter)
            {
                reporter->ReportAdded(job->relativePathSrc);
            }

            totalSize += job->uncompressedSize;
            break;
        case PACKFILE_MISSING:
            if (reporter)
            {
                reporter->ReportMissing(job->realFilename);
            }
            break;
        case PACKFILE_UPTODATE:
            if (reporter)
            {
                reporter->ReportUpToDate(job->realFilename);
            }
            break;
        case PACKFILE_SKIPPED:
            if (reporter)
            {
                reporter->ReportSkipped(job->realFilename);
            }
            break;
        default:
            if (reporter)
            {
                reporter->ReportFailed(job->realFilename, ""); // TODO reason
            }
            break;
        }

        pool.ReleaseFile(i);
    }

    clock_t endTime = clock();
    double timeSeconds = double(endTime - startTime) / CLOCKS_PER_SEC;
    double speed = (endTime - startTime) == 0 ? 0.0 : double(totalSize) / timeSeconds;

    if (reporter)
    {
        reporter->ReportSpeed(speed);
    }

    return true;
}


//   Adds a new file to the zip or update an existing one if it is not compressed - just stored  - start a big file
ZipDir::ErrorEnum ZipDir::CacheRW::StartContinuousFileUpdate(const char* szRelativePathSrc, unsigned nSize)
{
    char str[_MAX_PATH];
    char* szRelativePath = UnifyPath(str, szRelativePathSrc);

    SmartPtr pBufferDestroyer;

    // create or find the file entry.. this object will rollback (delete the object
    // if the operation fails) if needed.
    FileEntryTransactionAdd pFileEntry(this, AllocPath(szRelativePathSrc), AllocPath(szRelativePath));

    if (!pFileEntry)
    {
        return ZD_ERROR_INVALID_PATH;
    }

    pFileEntry->OnNewFileData (NULL, nSize, nSize, ZipFile::METHOD_STORE, false);
    // since we changed the time, we'll have to update CDR
    m_nFlags |= FLAGS_CDR_DIRTY;

    // the new CDR position, if the operation completes successfully
    unsigned lNewCDROffset = m_lCDROffset;
    if (pFileEntry->IsInitialized())
    {
        // check if the new compressed data fits into the old place
        unsigned nFreeSpace = pFileEntry->nEOFOffset - pFileEntry->nFileHeaderOffset - (unsigned)sizeof(ZipFile::LocalFileHeader) - (unsigned)strlen(szRelativePath);

        if (nFreeSpace != nSize)
        {
            m_nFlags |= FLAGS_UNCOMPACTED;
        }

        if (nFreeSpace >= nSize)
        {
            // and we can just override the compressed data in the file
            ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePathSrc, m_bEncryptedHeaders);
            if (e != ZD_ERROR_SUCCESS)
            {
                return e;
            }
        }
        else
        {
            // we need to write the file anew - in place of current CDR
            pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(szRelativePathSrc, m_lCDROffset, m_fileAlignment);
            ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePathSrc, m_bEncryptedHeaders);
            lNewCDROffset = pFileEntry->nEOFOffset;
            if (e != ZD_ERROR_SUCCESS)
            {
                return e;
            }
        }
    }
    else
    {
        pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(szRelativePathSrc, m_lCDROffset, m_fileAlignment);
        ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePathSrc, m_bEncryptedHeaders);
        if (e != ZD_ERROR_SUCCESS)
        {
            return e;
        }

        lNewCDROffset = pFileEntry->nFileDataOffset + nSize;

        m_nFlags |= FLAGS_CDR_DIRTY;
    }

#ifdef WIN32
    if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileDataOffset, SEEK_SET) != 0)
#else
    if (fseek (m_pFile, pFileEntry->nFileDataOffset, SEEK_SET) != 0)
#endif
    {
        return ZD_ERROR_IO_FAILED;
    }

    if (!WriteNullData(nSize))
    {
        return ZD_ERROR_IO_FAILED;
    }

    pFileEntry->nEOFOffset = pFileEntry->nFileDataOffset;

    // since we wrote the file successfully, update the new CDR position
    m_lCDROffset = lNewCDROffset;
    pFileEntry.Commit();

    return ZD_ERROR_SUCCESS;
}

// Adds a new file to the zip or update an existing's segment if it is not compressed - just stored
// adds a directory (creates several nested directories if needed)
ZipDir::ErrorEnum ZipDir::CacheRW::UpdateFileContinuousSegment (const char* szRelativePathSrc, unsigned nSize, void* pUncompressed, unsigned nSegmentSize, unsigned nOverwriteSeekPos)
{
    char str[_MAX_PATH];
    char* szRelativePath = UnifyPath(str, szRelativePathSrc);

    SmartPtr pBufferDestroyer;

    // create or find the file entry.. this object will rollback (delete the object
    // if the operation fails) if needed.
    FileEntryTransactionAdd pFileEntry(this, AllocPath(szRelativePathSrc), AllocPath(szRelativePath));

    if (!pFileEntry)
    {
        return ZD_ERROR_INVALID_PATH;
    }

    pFileEntry->OnNewFileData (pUncompressed, nSegmentSize, nSegmentSize, ZipFile::METHOD_STORE, true);
    // since we changed the time, we'll have to update CDR
    m_nFlags |= FLAGS_CDR_DIRTY;

    // this file entry is already allocated in CDR
    unsigned lSegmentOffset = pFileEntry->nEOFOffset;

#ifdef WIN32
    if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileHeaderOffset, SEEK_SET) != 0)
#else
    if (fseek (m_pFile, pFileEntry->nFileHeaderOffset, SEEK_SET) != 0)
#endif
    {
        return ZD_ERROR_IO_FAILED;
    }

    // and we can just override the compressed data in the file
    ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePath, m_bEncryptedHeaders);
    if (e != ZD_ERROR_SUCCESS)
    {
        return e;
    }

    if (nOverwriteSeekPos != 0xffffffff)
    {
        lSegmentOffset = pFileEntry->nFileDataOffset + nOverwriteSeekPos;
    }

    // now we have the fresh local header and data offset
#ifdef WIN32
    if (_fseeki64 (m_pFile, (__int64)lSegmentOffset, SEEK_SET) != 0)
#else
    if (fseek (m_pFile, lSegmentOffset, SEEK_SET) != 0)
#endif
    {
        return ZD_ERROR_IO_FAILED;
    }

    const bool encrypt = false; // encryption is not supported for continous updates
    if (!WriteCompressedData((char*)pUncompressed, nSegmentSize, encrypt, m_pFile))
    {
        return ZD_ERROR_IO_FAILED;
    }

    if (nOverwriteSeekPos == 0xffffffff)
    {
        pFileEntry->nEOFOffset = lSegmentOffset + nSegmentSize;
    }

    // since we wrote the file successfully, update CDR
    pFileEntry.Commit();
    return ZD_ERROR_SUCCESS;
}


ZipDir::ErrorEnum ZipDir::CacheRW::UpdateFileCRC (const char* szRelativePathSrc, unsigned dwCRC32)
{
    char str[_MAX_PATH];
    char* szRelativePath = UnifyPath(str, szRelativePathSrc);

    SmartPtr pBufferDestroyer;

    // create or find the file entry.. this object will rollback (delete the object
    // if the operation fails) if needed.
    FileEntryTransactionAdd pFileEntry(this, AllocPath(szRelativePathSrc), AllocPath(szRelativePath));

    if (!pFileEntry)
    {
        return ZD_ERROR_INVALID_PATH;
    }

    // since we changed the time, we'll have to update CDR
    m_nFlags |= FLAGS_CDR_DIRTY;

    pFileEntry->desc.lCRC32 = dwCRC32;

#ifdef WIN32
    if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileHeaderOffset, SEEK_SET) != 0)
#else
    if (fseek (m_pFile, pFileEntry->nFileHeaderOffset, SEEK_SET) != 0)
#endif
    {
        return ZD_ERROR_IO_FAILED;
    }

    // and we can just override the compressed data in the file
    ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePath, m_bEncryptedHeaders);
    if (e != ZD_ERROR_SUCCESS)
    {
        return e;
    }

    // since we wrote the file successfully, update
    pFileEntry.Commit();
    return ZD_ERROR_SUCCESS;
}


// deletes the file from the archive
ZipDir::ErrorEnum ZipDir::CacheRW::RemoveFile (const char* szRelativePathSrc)
{
    char str[_MAX_PATH];
    char* szRelativePath = UnifyPath(str, szRelativePathSrc);

    // find the last slash in the path
    const char* pSlash = (std::max)(strrchr(szRelativePath, '/'), strrchr(szRelativePath, '\\'));

    const char* pFileName; // the name of the file to delete

    FileEntryTree* pDir; // the dir from which the subdir will be deleted

    if (pSlash)
    {
        FindDirRW fd (GetRoot());
        // the directory to remove
        pDir = fd.FindExact(string (szRelativePath, pSlash - szRelativePath).c_str());
        if (!pDir)
        {
            return ZD_ERROR_DIR_NOT_FOUND;// there is no such directory
        }
        pFileName = pSlash + 1;
    }
    else
    {
        pDir = GetRoot();
        pFileName = szRelativePath;
    }

    ErrorEnum e = pDir->RemoveFile (pFileName);
    if (e == ZD_ERROR_SUCCESS)
    {
        m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY;
    }
    return e;
}


// deletes the directory, with all its descendants (files and subdirs)
ZipDir::ErrorEnum ZipDir::CacheRW::RemoveDir (const char* szRelativePathSrc)
{
    char str[_MAX_PATH];
    char* szRelativePath = UnifyPath(str, szRelativePathSrc);

    // find the last slash in the path
    const char* pSlash = (std::max)(strrchr(szRelativePath, '/'), strrchr(szRelativePath, '\\'));

    const char* pDirName; // the name of the dir to delete

    FileEntryTree* pDir; // the dir from which the subdir will be deleted

    if (pSlash)
    {
        FindDirRW fd (GetRoot());
        // the directory to remove
        pDir = fd.FindExact(string (szRelativePath, pSlash - szRelativePath).c_str());
        if (!pDir)
        {
            return ZD_ERROR_DIR_NOT_FOUND;// there is no such directory
        }
        pDirName = pSlash + 1;
    }
    else
    {
        pDir = GetRoot();
        pDirName = szRelativePath;
    }

    ErrorEnum e = pDir->RemoveDir (pDirName);
    if (e == ZD_ERROR_SUCCESS)
    {
        m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY;
    }
    return e;
}

// deletes all files and directories in this archive
ZipDir::ErrorEnum ZipDir::CacheRW::RemoveAll()
{
    ErrorEnum e = m_treeDir.RemoveAll();
    if (e == ZD_ERROR_SUCCESS)
    {
        m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY;
    }
    return e;
}

ZipDir::ErrorEnum ZipDir::CacheRW::ReadFile (FileEntry* pFileEntry, void* pCompressed, void* pUncompressed)
{
    if (!pFileEntry)
    {
        return ZD_ERROR_INVALID_CALL;
    }

    if (pFileEntry->desc.lSizeUncompressed == 0)
    {
        assert (pFileEntry->desc.lSizeCompressed == 0);
        return ZD_ERROR_SUCCESS;
    }

    assert (pFileEntry->desc.lSizeCompressed > 0);

    ErrorEnum nError = Refresh(pFileEntry);
    if (nError != ZD_ERROR_SUCCESS)
    {
        return nError;
    }

#ifdef WIN32
    if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileDataOffset, SEEK_SET))
#else
    if (fseek (m_pFile, pFileEntry->nFileDataOffset, SEEK_SET))
#endif
    {
        return ZD_ERROR_IO_FAILED;
    }

    SmartPtr pBufferDestroyer;

    void* pBuffer = pCompressed; // the buffer where the compressed data will go

    if (pFileEntry->nMethod == 0 && pUncompressed)
    {
        // we can directly read into the uncompress buffer
        pBuffer = pUncompressed;
    }

    if (!pBuffer)
    {
        if (!pUncompressed)
        {
            // what's the sense of it - no buffers at all?
            return ZD_ERROR_INVALID_CALL;
        }

        pBuffer = azmalloc(pFileEntry->desc.lSizeCompressed);
        pBufferDestroyer.Attach(pBuffer); // we want it auto-freed once we return
    }

    if (fread((char*)pBuffer, pFileEntry->desc.lSizeCompressed, 1, m_pFile) != 1)
    {
        return ZD_ERROR_IO_FAILED;
    }

    if (pFileEntry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT)
    {
        ZipDir::Decrypt((char*)pBuffer, pFileEntry->desc.lSizeCompressed, m_encryptionKey);
    }

    // if there's a buffer for uncompressed data, uncompress it to that buffer
    if (pUncompressed)
    {
        if (pFileEntry->nMethod == 0)
        {
            assert (pBuffer == pUncompressed);
            //assert (pFileEntry->nSizeCompressed == pFileEntry->nSizeUncompressed);
            //memcpy (pUncompressed, pBuffer, pFileEntry->nSizeCompressed);
        }
        else
        {
            unsigned long nSizeUncompressed = pFileEntry->desc.lSizeUncompressed;
            if (nSizeUncompressed > 0)
            {
                if (Z_OK != ZipRawUncompress(pUncompressed, &nSizeUncompressed, pBuffer, pFileEntry->desc.lSizeCompressed))
                {
                    return ZD_ERROR_CORRUPTED_DATA;
                }
            }
        }
    }

    return ZD_ERROR_SUCCESS;
}


//////////////////////////////////////////////////////////////////////////
// finds the file by exact path
ZipDir::FileEntry* ZipDir::CacheRW::FindFile (const char* szPathSrc, bool bFullInfo)
{
    char str[_MAX_PATH];
    char* szPath = UnifyPath(str, szPathSrc);

    ZipDir::FindFileRW fd (GetRoot());
    if (!fd.FindExact(szPath))
    {
        assert (!fd.GetFileEntry());
        return NULL;
    }
    assert (fd.GetFileEntry());
    return fd.GetFileEntry();
}

// returns the size of memory occupied by the instance referred to by this cache
size_t ZipDir::CacheRW::GetSize() const
{
    return sizeof(*this) + m_strFilePath.capacity() + m_treeDir.GetSize() - sizeof(m_treeDir);
}

// returns the compressed size of all the entries
size_t ZipDir::CacheRW::GetCompressedSize() const
{
    return m_treeDir.GetCompressedFileSize();
}

// returns the total size of memory occupied by the instance of this cache and all the compressed files
size_t ZipDir::CacheRW::GetTotalFileSize() const
{
    return GetSize() + GetCompressedSize();
}

// returns the total size of space occupied on disk by the instance of this cache and all the compressed files
size_t ZipDir::CacheRW::GetTotalFileSizeOnDiskSoFar()
{
    FileRecordList arrFiles(GetRoot());
    FileRecordList::ZipStats statFiles = arrFiles.GetStats();

    return m_lCDROffset + statFiles.nSizeCDR;
}

// refreshes information about the given file entry into this file entry
ZipDir::ErrorEnum ZipDir::CacheRW::Refresh (FileEntry* pFileEntry)
{
    if (!pFileEntry)
    {
        return ZD_ERROR_INVALID_CALL;
    }

    if (pFileEntry->nFileDataOffset != pFileEntry->INVALID_DATA_OFFSET)
    {
        return ZD_ERROR_SUCCESS; // the data offset has been successfully read..
    }

    return ZipDir::Refresh(m_pFile, pFileEntry, m_bEncryptedHeaders);
}


// writes the CDR to the disk
bool ZipDir::CacheRW::WriteCDR(FILE* fTarget, bool encryptCDR)
{
    if (!fTarget)
    {
        return false;
    }

#ifdef WIN32
    if (_fseeki64(fTarget, (__int64)m_lCDROffset, SEEK_SET))
#else
    if (fseek(fTarget, m_lCDROffset, SEEK_SET))
#endif
    {
        return false;
    }

    FileRecordList arrFiles(GetRoot());
    //arrFiles.SortByFileOffset();
    size_t nSizeCDR = arrFiles.GetStats().nSizeCDR;
    void* pCDR = malloc(nSizeCDR);
    size_t nSizeCDRSerialized = arrFiles.MakeZipCDR(m_lCDROffset, pCDR, encryptCDR);
    assert (nSizeCDRSerialized == nSizeCDR);

    if (encryptCDR)
    {
        // We do not encrypt CDREnd, so we could find it by signature
        ZipDir::Encrypt((char*)pCDR, nSizeCDR - sizeof(ZipFile::CDREnd), m_encryptionKey);
    }

    size_t nWriteRes = fwrite (pCDR, nSizeCDR, 1, fTarget);
    free(pCDR);
    return nWriteRes == 1;
}

// generates random file name
string ZipDir::CacheRW::GetRandomName(int nAttempt)
{
    if (nAttempt)
    {
        char szBuf[8];
        int i;
        for (i = 0; i < sizeof(szBuf) - 1; ++i)
        {
            int r = rand() % (10 + 'z' - 'a' + 1);
            szBuf[i] = r > 9 ? (r - 10) + 'a' : '0' + r;
        }
        szBuf[i] = '\0';
        return szBuf;
    }
    else
    {
        return string();
    }
}

bool ZipDir::CacheRW::RelinkZip()
{
    AZ::IO::LocalFileIO localFileIO;
    for (int nAttempt = 0; nAttempt < 32; ++nAttempt)
    {
        string strNewFilePath = m_strFilePath + "$" + GetRandomName(nAttempt);

        FILE* f = nullptr; 
        azfopen(&f, strNewFilePath.c_str(), "wb");
        if (f)
        {
            bool bOk = RelinkZip(f);
            fclose (f); // we don't need the temporary file handle anyway

            if (!bOk)
            {
                // we don't need the temporary file
                localFileIO.Remove(strNewFilePath.c_str());
                return false;
            }

            // we successfully relinked, now copy the temporary file to the original file
            fclose (m_pFile);
            m_pFile = NULL;

            localFileIO.Remove(m_strFilePath.c_str());
            if (localFileIO.Rename(strNewFilePath.c_str(), m_strFilePath.c_str()) == 0)
            {
                // successfully renamed - reopen
                m_pFile = nullptr; 
                azfopen(&m_pFile, m_strFilePath.c_str(), "r+b");
                return m_pFile == NULL;
            }
            else
            {
                // could not rename

                //m_pFile = fopen (strNewFilePath.c_str(), "r+b");
                return false;
            }
        }
    }

    // couldn't open temp file
    return false;
}

bool ZipDir::CacheRW::RelinkZip(FILE* fTmp)
{
    FileRecordList arrFiles(GetRoot());
    arrFiles.SortByFileOffset();
    FileRecordList::ZipStats Stats = arrFiles.GetStats();

    // we back up our file entries, because we'll need to restore them
    // in case the operation fails
    std::vector<FileEntry> arrFileEntryBackup;
    arrFiles.Backup (arrFileEntryBackup);

    // this is the set of files that are to be written out - compressed data and the file record iterator
    std::vector<FileDataRecordPtr> queFiles;
    queFiles.reserve (g_nMaxItemsRelinkBuffer);

    // the total size of data in the queue
    unsigned nQueueSize = 0;

    for (FileRecordList::iterator it = arrFiles.begin(); it != arrFiles.end(); ++it)
    {
        FileEntry* entry = it->pFileEntry;
        // find the file data offset
        if (ZD_ERROR_SUCCESS != Refresh(entry))
        {
            return false;
        }

        // go to the file data
#ifdef WIN32
        if (_fseeki64 (m_pFile, (__int64)entry->nFileDataOffset, SEEK_SET) != 0)
#else
        if (fseek (m_pFile, entry->nFileDataOffset, SEEK_SET) != 0)
#endif
        {
            return false;
        }

        // allocate memory for the file compressed data
        FileDataRecordPtr pFile = FileDataRecord::New (*it);

        if (!pFile)
        {
            return false;
        }

        // read the compressed data
        if (entry->desc.lSizeCompressed && fread (pFile->GetData(), entry->desc.lSizeCompressed, 1, m_pFile) != 1)
        {
            return false;
        }

        if (entry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT)
        {
            ZipDir::Decrypt((char*)pFile->GetData(), entry->desc.lSizeCompressed, m_encryptionKey);
        }

        // put the file into the queue for copying (writing)
        queFiles.push_back(pFile);
        nQueueSize += entry->desc.lSizeCompressed;

        // if the queue is big enough, write it out
        if (nQueueSize > g_nSizeRelinkBuffer || queFiles.size() >= g_nMaxItemsRelinkBuffer)
        {
            nQueueSize = 0;
            if (!WriteZipFiles(queFiles, fTmp))
            {
                return false;
            }
        }
    }

    if (!WriteZipFiles(queFiles, fTmp))
    {
        return false;
    }

    ZipFile::ulong lOldCDROffset = m_lCDROffset;
    // the file data has now been written out. Now write the CDR
#ifdef WIN32
    m_lCDROffset = (ZipFile::ulong)_ftelli64(fTmp);
#else
    m_lCDROffset = ftell(fTmp);
#endif
    if (m_lCDROffset >= 0 && WriteCDR(fTmp, m_bHeadersEncryptedOnClose) && 0 == fflush (fTmp))
    {
        // the new file positions are already there - just discard the backup and return
        return true;
    }
    // recover from backup
    arrFiles.Restore (arrFileEntryBackup);
    m_lCDROffset = lOldCDROffset;
    m_bEncryptedHeaders = m_bHeadersEncryptedOnClose;
    return false;
}

// writes out the file data in the queue into the given file. Empties the queue
bool ZipDir::CacheRW::WriteZipFiles(std::vector<FileDataRecordPtr>& queFiles, FILE* fTmp)
{
    for (std::vector<FileDataRecordPtr>::iterator it = queFiles.begin(); it != queFiles.end(); ++it)
    {
        // set the new header offset to the file entry - we won't need it
#ifdef WIN32
        const unsigned long currentPos = (unsigned long)_ftelli64 (fTmp);
#else
        const unsigned long currentPos = ftell (fTmp);
#endif
        (*it)->pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset((*it)->strPath.c_str(), currentPos, m_fileAlignment);

        // while writing the local header, the data offset will also be calculated
        if (ZD_ERROR_SUCCESS != WriteLocalHeader(fTmp, (*it)->pFileEntry, (*it)->strPath.c_str(), m_bHeadersEncryptedOnClose))
        {
            return false;
        }
        ;

        // write the compressed file data
        const bool encrypt = (*it)->pFileEntry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT;
        if (!WriteCompressedData((char*)(*it)->GetData(), (*it)->pFileEntry->desc.lSizeCompressed, encrypt, fTmp))
        {
            return false;
        }

#ifdef WIN32
        assert ((*it)->pFileEntry->nEOFOffset == (unsigned long)_ftelli64 (fTmp));
#else
        assert ((*it)->pFileEntry->nEOFOffset == ftell (fTmp));
#endif
    }
    queFiles.clear();
    queFiles.reserve (g_nMaxItemsRelinkBuffer);
    return true;
}

void TruncateFile(FILE* file, size_t newLength)
{
#if defined(AZ_PLATFORM_WINDOWS)
    int filedes = _fileno(file);
    _chsize_s(filedes, newLength);
#elif AZ_TRAIT_OS_PLATFORM_APPLE || defined(AZ_PLATFORM_LINUX)
    ftruncate(fileno(file), newLength);
#else
#error Not implemented!
#endif
}

bool ZipDir::CacheRW::EncryptArchive(EncryptionChange change, IEncryptPredicate* encryptContentPredicate, int* numChanged, int* numSkipped)
{
    FileRecordList arrFiles(GetRoot());
    arrFiles.SortByFileOffset();

    // the total size of data in the queue
    unsigned nQueueSize = 0;

    size_t unusedSpace = 0;
    size_t lastDataEnd = 0;

    for (FileRecordList::iterator it = arrFiles.begin(); it != arrFiles.end(); ++it)
    {
        FileEntry* entry = it->pFileEntry;

        if (entry->nFileHeaderOffset > lastDataEnd)
        {
            fseek(m_pFile, lastDataEnd, SEEK_SET);
            size_t gapLength = entry->nFileHeaderOffset - lastDataEnd;
            unusedSpace += gapLength;
            if (change == ENCRYPT)
            {
                if (!WriteRandomData(m_pFile, gapLength))
                {
                    return false;
                }
            }
            else
            {
                if (!WriteNullData(gapLength))
                {
                    return false;
                }
            }
        }
        lastDataEnd = entry->nEOFOffset;

        if (numSkipped)
        {
            ++(*numSkipped);
        }

        // find the file data offset
        if (ZD_ERROR_SUCCESS != Refresh (entry))
        {
            return false;
        }

        bool methodChanged = false;

        ZipFile::ushort oldMethod = entry->nMethod;
        ZipFile::ushort newMethod = oldMethod;
        if (change == ENCRYPT)
        {
            if (entry->nMethod == ZipFile::METHOD_DEFLATE)
            {
                newMethod = ZipFile::METHOD_DEFLATE_AND_ENCRYPT;
            }
        }
        else
        {
            if (entry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT)
            {
                newMethod = ZipFile::METHOD_DEFLATE;
            }
        }

        // allow encryption only for matching files
        if (newMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT &&
            (!encryptContentPredicate || !encryptContentPredicate->Match(it->strPath.c_str())))
        {
            newMethod = ZipFile::METHOD_DEFLATE;
        }

        entry->nMethod = newMethod;

        const bool encryptHeaders = change == ENCRYPT;
        // encryption is toggled or compression method changed...
        if (newMethod != oldMethod || encryptHeaders != m_bEncryptedHeaders)
        {
            // ... update header
            if (ZipDir::WriteLocalHeader(m_pFile, entry, it->strPath.c_str(), encryptHeaders) != ZD_ERROR_SUCCESS)
            {
                return false;
            }
        }

        if (newMethod == oldMethod)
        {
            // no need to update file content
            continue;
        }

        // go to the file data
#ifdef WIN32
        if (_fseeki64 (m_pFile, (__int64)entry->nFileDataOffset, SEEK_SET) != 0)
#else
        if (fseek (m_pFile, entry->nFileDataOffset, SEEK_SET) != 0)
#endif
        {
            return false;
        }

        // allocate memory for the file compressed data
        FileDataRecordPtr pFile = FileDataRecord::New(*it);
        if (!pFile)
        {
            return false;
        }

        // read the compressed data
        if (entry->desc.lSizeCompressed && fread (pFile->GetData(), entry->desc.lSizeCompressed, 1, m_pFile) != 1)
        {
            return false;
        }

        if (oldMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT)
        {
            ZipDir::Decrypt((char*)pFile->GetData(), entry->desc.lSizeCompressed, m_encryptionKey);
        }

#ifdef WIN32
        if (_fseeki64 (m_pFile, (__int64)entry->nFileDataOffset, SEEK_SET) != 0)
#else
        if (fseek (m_pFile, entry->nFileDataOffset, SEEK_SET) != 0)
#endif
        {
            return false;
        }

        const bool encryptContent = newMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT;
        if (!WriteCompressedData((const char*)pFile->GetData(), entry->desc.lSizeCompressed, encryptContent, m_pFile))
        {
            return false;
        }

        if (numSkipped)
        {
            --(*numSkipped);
        }
        if (numChanged)
        {
            ++(*numChanged);
        }
    }

    m_bEncryptedHeaders = change == ENCRYPT;
    m_bHeadersEncryptedOnClose = m_bEncryptedHeaders;

    if (!WriteCDR(m_pFile, m_bEncryptedHeaders))
    {
        return false;
    }

    if (fflush (m_pFile) != 0)
    {
        return false;
    }

    size_t endOfCDR = (size_t)ftell(m_pFile);

    fseek(m_pFile, 0, SEEK_END);
    size_t fileSize = (size_t)ftell(m_pFile);

    if (fileSize != endOfCDR)
    {
        TruncateFile(m_pFile, endOfCDR);
    }

    fclose(m_pFile);
    m_pFile = 0;
    m_treeDir.Clear();
    return true;
}