// Oleg Petruny proprietary.

#include "ContentLoader.h"

#include "IPlatformFilePak.h"
#include "Misc/FileHelper.h"

#include "CommonFunctions.h"

#include <regex>
#include <string>
#include <vector>

namespace
{
    constexpr auto httpGet = TEXT("GET");

    constexpr int32 httpResponseOK = 200;

    constexpr auto httpContentTypeHtml = TEXT("text/html");
    constexpr auto httpContentTypeJson = TEXT("application/json");

    constexpr auto pakIndexUrl = TEXT("https://pixelyfier.com/lost_edge/content/");
    const auto pakFileSavePath = FString::Printf(TEXT("%sDownloaded/"), *FPaths::ProjectDir());
}

void UContentLoader::BeginDestroy()
{
    //if(mountedFiles.Num() > 0)
    //{
    //    while(mountedFiles.Num() > 0)
    //        UnmountPak(*mountedFiles.begin());
    //
    //    FPlatformFileManager::Get().RemovePlatformFile(GetPakPlatformFile());
    //}

    UObject::BeginDestroy();
}

void UContentLoader::HttpGet(const FString& url, FHttpRequestCompleteDelegate requestCompleteCallback)
{
    TSharedRef<IHttpRequest> request = FHttpModule::Get().CreateRequest();
    request->SetURL(url);
    request->SetVerb(httpGet);
    request->OnProcessRequestComplete() = std::move(requestCompleteCallback);
    request->ProcessRequest();
}

TArray<FString> UContentLoader::ParseDirectoryIndex(const TArray<uint8>& content, const FString& contentType)
{
    TArray<FString> result;

    if(contentType == httpContentTypeHtml)
    {
        std::string contentHtml{ reinterpret_cast<const char*>(content.GetData()), static_cast<size_t>(content.Num()) };
        std::regex hrefRegex(R"(<a href="((?!\.)[^"]+))");
        std::smatch match;
        auto it = contentHtml.cbegin();
        auto end = contentHtml.cend();
        while(std::regex_search(it, end, match, hrefRegex))
        {
            if(match.size() > 1)
            {
                result.Add(UTF8_TO_TCHAR(match[1].str().c_str()));
            }
            it = match.suffix().first;
        }
    }
    else if(contentType == httpContentTypeJson)
    {
        // TODO
    }

    return result;
}

FString UContentLoader::SelectContentByMethod(const TArray<FString>& content, const EContentDownloadMethod method)
{
    switch(method)
    {
        case EContentDownloadMethod::NonRepeatRandom:
        {
            auto filteredContent = UCommonFunctions::ArrayDiff(content, downloadedContent);
            if(filteredContent.Num() == 0 && content.Num() != 0)
            {
                downloadedContent.Reset();
                filteredContent = UCommonFunctions::ArrayDiff(content, downloadedContent);
            }
            return filteredContent[FMath::RandRange(0, filteredContent.Num() - 1)];
        }
        case EContentDownloadMethod::RatingOftenRandom:
            // TODO
        case EContentDownloadMethod::ClearRandom:
        default:
            return content[FMath::RandRange(0, content.Num() - 1)];
    }
}

void UContentLoader::DownloadPak(FContentDownloadedCallback downloadedCallback, const EContentDownloadMethod method)
{
    UE_LOG(LogTemp, Log, TEXT("ContentLoader: Requesting pak http index at %s."), pakIndexUrl);

    FHttpRequestCompleteDelegate indexRequestCallback;
    indexRequestCallback.BindLambda([=, this](FHttpRequestPtr request, FHttpResponsePtr response, bool successful)
        {
            if(!successful || response->GetResponseCode() != httpResponseOK)
            {
                int32 responseCode = response.IsValid() ? response->GetResponseCode() : -1;
                UE_LOG(LogTemp, Log, TEXT("ContentLoader: Requesting pak http index failed with code: %d (-1 if response is null)."), responseCode);
                return;
            }

            TArray<FString> availablePaks = ParseDirectoryIndex(response->GetContent(), response->GetContentType());
            if(availablePaks.Num() == 0)
            {
                UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak http index is empty."));
                return;
            }

            FString pakToDownload = SelectContentByMethod(availablePaks, method);
            downloadedContent.Add(pakToDownload);
            UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak selected for loading: %s."), *pakToDownload);

            auto savePath = FString::Printf(TEXT("%s%s"), *pakFileSavePath, *pakToDownload);
            if(FPlatformFileManager::Get().GetPlatformFile().FileExists(*savePath))
            {
                UE_LOG(LogTemp, Log, TEXT("ContentLoader: Selected pak already exists... skipping download."));
                downloadedCallback.ExecuteIfBound(savePath);
                return;
            }

            FHttpRequestCompleteDelegate pakDownloadCallback;
            pakDownloadCallback.BindLambda([=, this](FHttpRequestPtr request, FHttpResponsePtr response, bool successful)
                {
                    if(!successful || response->GetResponseCode() != httpResponseOK)
                    {
                        UCommonFunctions::ArrayRemoveFirstFromEnd(downloadedContent, pakToDownload);
                        int32 responseCode = response.IsValid() ? response->GetResponseCode() : -1;
                        UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak downloading failed with code: %d (-1 if response is null)."), responseCode);
                        return;
                    }

                    if(!FFileHelper::SaveArrayToFile(response->GetContent(), *savePath))
                    {
                        UE_LOG(LogTemp, Log, TEXT("ContentLoader: Saving pak file %s failed."), *savePath);
                        return;
                    }
                    UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak file saved as %s."), *savePath);

                    downloadedCallback.ExecuteIfBound(savePath);
                });

            UE_LOG(LogTemp, Log, TEXT("ContentLoader: Starting to download pak %s from index."), *pakToDownload);
            HttpGet(FString::Printf(TEXT("%s%s"), pakIndexUrl, *pakToDownload), pakDownloadCallback);
        });

    HttpGet(pakIndexUrl, indexRequestCallback);
}

UClass* UContentLoader::LoadPak(const FString& pakFilePath)
{
    UE_LOG(LogTemp, Log, TEXT("ContentLoader: Starting to mount pak %s."), *pakFilePath);

    FPakPlatformFile* pakFileMgr = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile")));
    if(!pakFileMgr)
    {
        UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Unable to get PakPlatformFile."));
        return nullptr;
    }

    if(!pakFileMgr->Mount(*pakFilePath, 0))
    {
        UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Unable to mount pak %s."), *pakFilePath);
        return nullptr;
    }

    UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Pak %s mounted successfully."), *pakFilePath);

    TArray<FString> content;
    const FString mountPoint = GetPakMountContent(pakFilePath, &content);
    UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak '%s' contains %d items."), *FPaths::GetCleanFilename(pakFilePath), content.Num());

    FPackageName::RegisterMountPoint("/Game/", mountPoint);
    for(auto& i : content)
    {
        if(FPaths::GetCleanFilename(i).StartsWith(TEXT("BP_")))
        {
            UE_LOG(LogTemp, Log, TEXT("ContentLoader: Found BP class '%s' of pak %s."), *i, *pakFilePath);
            return GetPakClass(i);
        }
    }

    return nullptr;
}

FString UContentLoader::GetPakMountContent(const FString& pakFilePath, TArray<FString>* content)
{
    TArray<FString> contentProxy;
    if(!content)
        content = &contentProxy;

    FString mountPoint, contentPath, contentPathTail;
    TRefCountPtr<FPakFile> pakFile = new FPakFile(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile")), *pakFilePath, false);
    auto pak = pakFile.GetReference();
    if(pakFile.GetReference()->IsValid())
    {
        mountPoint = pakFile.GetReference()->GetMountPoint();
        mountPoint.Split("/Content/", &contentPath, &contentPathTail);

        for(FPakFile::FFilenameIterator It(*pakFile.GetReference(), false); It; ++It)
            if(FPaths::GetExtension(It.Filename()) == TEXT("uasset"))
                content->Add(FString::Printf(TEXT("%s%s"), *contentPathTail, *It.Filename()));
    }

    if(!contentPath.IsEmpty())
    {
        return FString::Printf(TEXT("%s/Content/"), *contentPath);
    }
    else if(content->Num())
    {
        const FString fullPath = FString::Printf(TEXT("%s%s"), *mountPoint, *((*content)[0]));
        if(fullPath.Split("/Content/", &contentPath, &contentPathTail))
        {
            return FString::Printf(TEXT("%s/Content/"), *contentPath);
        }
    }

    return FString("");
}

UClass* UContentLoader::GetPakClass(const FString& pakContentPath)
{
    const FString assetName = FString::Printf(TEXT("/Game/%s.%s"), *FPaths::GetBaseFilename(pakContentPath, false), *FPaths::GetBaseFilename(pakContentPath, true));
    const FString className = assetName + TEXT(".") + FPackageName::GetShortName(assetName) + TEXT("_C");
    auto result = StaticLoadClass(UObject::StaticClass(), nullptr, *className);
    if(!result)
        UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Unable to load pak class: %s."), *className);
    return result;
}

bool UContentLoader::UnloadPak(const FString& pakFilePath)
{
    FPakPlatformFile* pakFileMgr = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile")));
    if(!pakFileMgr)
    {
        UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Unable to get PakPlatformFile for pak file (Unmount): %s."), *pakFilePath);
        return false;
    }

    if(!pakFileMgr->Unmount(*pakFilePath))
    {
        UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Unable to unmount pak: %s."), *pakFilePath);
        return false;
    }

    const FString mountPoint = GetPakMountContent(pakFilePath);
    FPackageName::UnRegisterMountPoint("/Game/", mountPoint);

    return true;
}