/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
#include "plBackgroundDownloader.h"

#include <process.h>

#include "pnUtils/pnUtils.h"
#include "pnNetBase/pnNetBase.h"
#include "plEncryption/plChecksum.h"

#include "hsResMgr.h"

#include "plAgeDescription/plAgeManifest.h"
#include "plResMgr/plResManager.h"
#include "plFile/plFileUtils.h"
#include "plFile/plEncryptedStream.h"
#include "plCompression/plZlibStream.h"
#include "plAudioCore/plAudioFileReader.h"
#include "plProgressMgr/plProgressMgr.h"

#include "pnAsyncCore/pnAsyncCore.h"
#include "pnNetCli/pnNetCli.h"
#include "plNetGameLib/plNetGameLib.h"

#include "pnDispatch/plDispatch.h"
#include "plStatusLog/plStatusLog.h"

static const unsigned kMaxDownloadTries = 10;
static const wchar s_manifest[] = L"AllAges";

plBackgroundDownloader* plBackgroundDownloader::fInstance = NULL;

hsBool gUseBackgroundDownloader = false;

//============================================================================
enum DownloaderLogType
{
    kHeader,
    kInfo,
    kMajorStatus,
    kStatus,
    kError,
};
void BackgroundDownloaderLog(DownloaderLogType type, const char* format, ...)
{
    UInt32 color = 0;
    switch (type)
    {
    case kHeader:       color = plStatusLog::kWhite;    break;
    case kInfo:         color = plStatusLog::kBlue;     break;
    case kMajorStatus:  color = plStatusLog::kYellow;   break;
    case kStatus:       color = plStatusLog::kGreen;    break;
    case kError:        color = plStatusLog::kRed;      break;
    }

    static plStatusLog* gStatusLog = nil;
    if (!gStatusLog)
    {
        gStatusLog = plStatusLogMgr::GetInstance().CreateStatusLog(
            20,
            "bgdownload.log",
            plStatusLog::kFilledBackground | plStatusLog::kAlignToTop | plStatusLog::kDeleteForMe);
    }

    va_list args;
    va_start(args, format);

    gStatusLog->AddLineV(color, format, args);

    va_end(args);
}

//============================================================================
class plBGDownloadStream : public plZlibStream
{
public:
    plBGDownloadStream() : plZlibStream() {}
    virtual ~plBGDownloadStream() {}

    virtual UInt32 Write(UInt32 byteCount, const void* buffer);
};

UInt32 plBGDownloadStream::Write(UInt32 byteCount, const void* buffer)
{
    return plZlibStream::Write(byteCount, buffer);
}

//============================================================================
static void DownloadFileCallback(ENetError result, void* param, const wchar filename[], hsStream* writer)
{
    plBackgroundDownloader* bgdownloader = (plBackgroundDownloader*)param;

    // Retry download unless shutting down or file not found
    switch (result) {
        case kNetSuccess:
            writer->Close();
            bgdownloader->DoneWithFile(true);
        break;
        
        case kNetErrFileNotFound:
        case kNetErrRemoteShutdown:
            writer->Close();
            bgdownloader->DoneWithFile(false);
        break;
        
        default:
            writer->Rewind();
            NetCliFileDownloadRequest(
                filename,
                writer,
                DownloadFileCallback,
                param
            );
        break;
    }
}

static void ManifestCallback(ENetError result, void* param, const wchar group[], const NetCliFileManifestEntry manifest[], unsigned entryCount)
{
    plBackgroundDownloader* bgdownloader = (plBackgroundDownloader*)param;
    bgdownloader->DoneWithManifest(result == kNetSuccess, manifest, entryCount);
}

//============================================================================
plBackgroundDownloader* plBackgroundDownloader::GetInstance()
{
    return fInstance;
}

void plBackgroundDownloader::ThreadMain(void * param)
{
    Init();

    plBackgroundDownloader::GetInstance()->Run();
    plBackgroundDownloader::GetInstance()->CleanUp();

    Shutdown();
}

void plBackgroundDownloader::StartThread()
{
    _beginthread(plBackgroundDownloader::ThreadMain, 0, NULL);
}

void plBackgroundDownloader::Init()
{
    fInstance = TRACKED_NEW plBackgroundDownloader();
}

void plBackgroundDownloader::Shutdown()
{
    delete fInstance;
    fInstance = NULL;
}

plBackgroundDownloader::plBackgroundDownloader()
{
    BackgroundDownloaderLog(kHeader, "--- Starting background download ---");

    fBGDownloaderRun = CreateEvent(
        NULL,               // default security attributes
        TRUE,               // manual-reset event
        FALSE,              // initial state is signaled
        NULL                // unnamed
    );

    fBGDownloaderIsPaused = CreateEvent(
        NULL,               // default security attributes
        FALSE,              // manual-reset event
        TRUE,               // initial state is signaled
        NULL                // unnamed
    );
}

plBackgroundDownloader::~plBackgroundDownloader()
{
    HANDLE runHandle = fBGDownloaderRun;
    fBGDownloaderRun = NULL;
    CloseHandle(runHandle);
    
    HANDLE pausedHandle = fBGDownloaderIsPaused;
    fBGDownloaderIsPaused = NULL;
    CloseHandle(pausedHandle);

    BackgroundDownloaderLog(kHeader, "--- Background download done ---");
}

UInt32 plBackgroundDownloader::IGetDownloadSize()
{
    if (!IGetDataManifest())
        return 0;

    UInt32 downloadSize = 0;
    UInt32 downloadFiles = 0;
    for (MfsFileVec::iterator i = fMfsVec.begin(); i != fMfsVec.end(); ++i)
    {
        plManifestFile* mfsFile = (*i);
        
        if (!mfsFile->IsLocalUpToDate())
        {
            downloadFiles++;
            downloadSize += mfsFile->GetDownloadSize();
        }
    }

    BackgroundDownloaderLog(kInfo, "Got download stats, %d files, %d bytes", downloadFiles, downloadSize);

    return downloadSize;
}

bool plBackgroundDownloader::CheckFreeSpace(UInt32 bytesNeeded)
{
#ifdef HS_BUILD_FOR_WIN32
    ULARGE_INTEGER freeBytesAvailable, totalNumberOfBytes, neededBytes;
    if (GetDiskFreeSpaceEx(NULL, &freeBytesAvailable, &totalNumberOfBytes, NULL))
    {
        neededBytes.HighPart = 0;
        neededBytes.LowPart = bytesNeeded;

        if (neededBytes.QuadPart > freeBytesAvailable.QuadPart)
        {
            BackgroundDownloaderLog(kInfo, "Not enough disk space (asked for %d bytes)", bytesNeeded);
            return false;
        }
    }
#endif // HS_BUILD_FOR_WIN32

    return true;
}

bool plBackgroundDownloader::IDecompressSound(plManifestFile* mfsFile, bool noOverwrite)
{
    UInt32 flags = mfsFile->GetFlags();

    if ( (hsCheckBits(flags, plManifestFile::kSndFlagCacheSplit) || hsCheckBits(flags, plManifestFile::kSndFlagCacheStereo)) && stricmp(plFileUtils::GetFileExt(mfsFile->GetName()), "ogg") == 0)
    {
        plAudioFileReader* reader = plAudioFileReader::CreateReader(mfsFile->GetName(), plAudioCore::kAll, plAudioFileReader::kStreamNative);
        if (!reader)
        {
            BackgroundDownloaderLog(kInfo, "Unable to create audio file reader for %s", mfsFile->GetName());
            return false;
        }

        UInt32 size = reader->GetDataSize();
        delete reader;

        // Make sure we have enough free space
        if (!CheckFreeSpace(size))
            return false;

        if (hsCheckBits(flags, plManifestFile::kSndFlagCacheSplit))
            plAudioFileReader::CacheFile(mfsFile->GetName(), true, noOverwrite);
        if (hsCheckBits(flags, plManifestFile::kSndFlagCacheStereo))
            plAudioFileReader::CacheFile(mfsFile->GetName(), false, noOverwrite);
    }

    return true;
}

bool plBackgroundDownloader::Run()
{
    // Wait to be signaled that we've gotten at least as far as the startup age
    WaitForSingleObject(fBGDownloaderRun, INFINITE);

    IGetDataManifest();

    plFileUtils::CreateDir("dat");
    plFileUtils::CreateDir("sfx");

    bool result = true;
    plResManager* resMgr = ((plResManager*)hsgResMgr::ResMgr());

    for (MfsFileVec::iterator i = fMfsVec.begin(); i != fMfsVec.end(); ++i)
    {
        plManifestFile* mfsFile = (*i);

        if (!mfsFile->IsLocalUpToDate())
        {
            if (!CheckFreeSpace(mfsFile->GetDiskSize()))
                return false;

            FileType type = IGetFile(mfsFile);
            if (type == kPrp)
            {
                // Checks for existence before attempting to remove
                resMgr->RemoveSinglePage(mfsFile->GetName());
                if (!resMgr->FindSinglePage(mfsFile->GetName()))
                {
                    resMgr->AddSinglePage(mfsFile->GetName());
                }
            }
            else if (type == kOther)
            {
                if (!IDecompressSound(mfsFile, false))
                {
                    char text[MAX_PATH];
                    StrPrintf(text, arrsize(text), "%s could not be decompressed", mfsFile->GetName());
                    BackgroundDownloaderLog(kInfo, text );
                    hsAssert(false, text);
                    result = false;
                }
            }
            else
            {
                char text[MAX_PATH];
                StrPrintf(text, arrsize(text), "Failed downloading file: %s", mfsFile->GetName());
                BackgroundDownloaderLog(kInfo, text );
                hsAssert(false, text);
                result = false;
            }
        }
    }

    return result;
}

void plBackgroundDownloader::CleanUp()
{
    BackgroundDownloaderLog(kMajorStatus, "Cleaning up background downloader..." );

    for (MfsFileVec::iterator i = fMfsVec.begin(); i != fMfsVec.end(); ++i)
    {
        plManifestFile* file = (*i);
        delete file;
    }
    fMfsVec.clear();
}

void plBackgroundDownloader::Pause()
{
    if (fBGDownloaderRun != NULL && fBGDownloaderIsPaused != NULL)
    {
        ResetEvent(fBGDownloaderRun);
        WaitForSingleObject(fBGDownloaderIsPaused, INFINITE);

        BackgroundDownloaderLog(kStatus, "--- Background download paused ---");
    }
}

void plBackgroundDownloader::UnPause()
{
    if (fBGDownloaderRun != NULL && fBGDownloaderIsPaused != NULL)
    {
        SetEvent(fBGDownloaderRun);

        BackgroundDownloaderLog(kStatus, "--- Background download resumed ---");
    }
}

plBackgroundDownloader::FileType plBackgroundDownloader::IGetFile(const plManifestFile* mfsFile)
{
    BackgroundDownloaderLog(kInfo, "    Setting up to download file %s", mfsFile->GetName());

    bool downloadDone = false;
    wchar* wServerPath = hsStringToWString(mfsFile->GetServerPath());
    int numTries = 0;

    while (!downloadDone)
    {
        if (WaitForSingleObject(fBGDownloaderRun, 0) == WAIT_TIMEOUT)
            SignalObjectAndWait(fBGDownloaderIsPaused, fBGDownloaderRun, INFINITE, FALSE);

        if (numTries >= kMaxDownloadTries)
        {
            BackgroundDownloaderLog(kInfo, "    Max download tries exceeded (%d). Aborting download...", kMaxDownloadTries);
            return kFail;
        }

        plBGDownloadStream* downloadStream = TRACKED_NEW plBGDownloadStream();
        if (!downloadStream->Open(mfsFile->GetName(), "wb"))
        {
            BackgroundDownloaderLog(kInfo, "    Unable to create file. Aborting download...");
            return kFail;
        }

        BackgroundDownloaderLog(kInfo, "    Downloading file %s...", mfsFile->GetName());
        
        fSuccess = false;   
        fDoneWithFile = false;
        NetCliFileDownloadRequest(
            wServerPath,
            downloadStream,
            DownloadFileCallback,
            this
        );

        while (!fDoneWithFile) {
            AsyncSleep(100);
        }

        if (!fSuccess) {
            // remove partial file and die (server didn't have the file or server is shutting down)
            plFileUtils::RemoveFile(mfsFile->GetName(), true);
            BackgroundDownloaderLog(kError, "      File %s failed to download.", mfsFile->GetName());
        }
        else {
            AsyncSleep(100);
            if (downloadStream->DecompressedOk()) {
                BackgroundDownloaderLog(kInfo, "      Decompress successful." );
                // download and decompress successful, do a md5 check on the resulting file
                plMD5Checksum localMD5(mfsFile->GetName());
                if (localMD5 != mfsFile->GetChecksum()) {
                    plFileUtils::RemoveFile(mfsFile->GetName(), true);
                    BackgroundDownloaderLog(kError, "      File %s MD5 check FAILED.", mfsFile->GetName());
                    // don't set downloadDone so we attempt to re-download from the server
                }
                else {
                    BackgroundDownloaderLog(kInfo, "      MD5 check succeeded.");
                    downloadDone = true;
                }
            }
            else {
                plFileUtils::RemoveFile(mfsFile->GetName(), true);
                BackgroundDownloaderLog(kError, "      File %s failed to decompress.", mfsFile->GetName());
                // don't set downloadDone so we attempt to re-download from the server
            }
        }

        delete downloadStream;

        ++numTries;
    }
    delete [] wServerPath;

    if (!fSuccess)  
        return kFail;
    
    if (stricmp(plFileUtils::GetFileExt(mfsFile->GetName()), "prp") == 0)
        return kPrp;

    return kOther;
}

bool plBackgroundDownloader::IGetDataManifest()
{
    if (fMfsVec.size() > 0)
        return true;

    BackgroundDownloaderLog(kMajorStatus, "Downloading new manifest from data server..." );

    fSuccess = false;
    unsigned numTries = 0;
    while (!fSuccess)
    {
        numTries++;
        fDoneWithFile = false;
        NetCliFileManifestRequest(ManifestCallback, this, s_manifest);
        while (!fDoneWithFile)
        {
            NetClientUpdate();
            plgDispatch::Dispatch()->MsgQueueProcess();
            AsyncSleep(10);
        }

        if (!fSuccess)
        {
            fMfsVec.clear(); // clear out any bad data
            if (numTries > kMaxDownloadTries)
                break; // abort
        }
    }

    if (fSuccess)
        BackgroundDownloaderLog(kStatus, "New manifest read; number of files: %d", fMfsVec.size() );
    else
        BackgroundDownloaderLog(kStatus, "Failed to download manifest after trying %d times", kMaxDownloadTries);

    return fSuccess;
}

void plBackgroundDownloader::DoneWithFile(bool success)
{
    fDoneWithFile = true;
    fSuccess = success;
}

void plBackgroundDownloader::DoneWithManifest(bool success, const NetCliFileManifestEntry manifestEntires[], unsigned entryCount)
{
    BackgroundDownloaderLog(kStatus, "New age manifest received. Reading...");
    
    if (success)
    {
        for (unsigned i = 0; i < entryCount; i++)
        {
            char* name = hsWStringToString(manifestEntires[i].clientName);
            char* serverPath = hsWStringToString(manifestEntires[i].downloadName);
            char* md5Str = hsWStringToString(manifestEntires[i].md5);
            int size = manifestEntires[i].fileSize;
            int zipsize = manifestEntires[i].zipSize;
            int flags = manifestEntires[i].flags;
            if (stricmp(plFileUtils::GetFileExt(name), "gz"))
                flags |= plManifestFile::kFlagZipped; // add zipped flag if necessary

            plMD5Checksum sum;
            sum.SetFromHexString(md5Str);
            fMfsVec.push_back(TRACKED_NEW plManifestFile(name, serverPath, sum, size, zipsize, flags, false));

            delete [] name;
            delete [] serverPath;
            delete [] md5Str;
        }
    }

    fDoneWithFile = true;
    fSuccess = success;
}