// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#include "Builder.h"
#include "Engine/Level/Scene/Lightmap.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Content/Content.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/ContentImporters/ImportTexture.h"
#include "Engine/Core/Log.h"
#include "Engine/Graphics/GPUBuffer.h"
#include "Engine/Graphics/GPUBufferDescription.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/PixelFormatExtensions.h"

ShadowsOfMordor::Builder::LightmapBuildCache::~LightmapBuildCache()
{
    SAFE_DELETE_GPU_RESOURCE(LightmapData);
}

bool ShadowsOfMordor::Builder::LightmapBuildCache::Init(const LightmapSettings* settings)
{
    if (LightmapData)
        return false;
    LightmapData = GPUDevice::Instance->CreateBuffer(TEXT("LightmapBuildCache"));
    const auto elementsCount = (int32)settings->AtlasSize * (int32)settings->AtlasSize * NUM_SH_TARGETS;
    if (LightmapData->Init(GPUBufferDescription::Typed(elementsCount, HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT], true)))
        return true;
    return false;
}

ShadowsOfMordor::Builder::SceneBuildCache::SceneBuildCache()
    : Scene(nullptr)
    , TempLightmapData(nullptr)
    , LightmapsCount(0)
    , HemispheresCount(0)
    , MergedHemispheresCount(0)
    , ImportLightmapIndex(0)
    , ImportLightmapTextureIndex(0)
{
}

const LightmapSettings& ShadowsOfMordor::Builder::SceneBuildCache::GetSettings() const
{
    ASSERT(Scene);
    return Scene->Info.LightmapSettings;
}

bool ShadowsOfMordor::Builder::SceneBuildCache::WaitForLightmaps()
{
    Texture* lightmaps[3];
    for (int32 lightmapIndex = 0; lightmapIndex < Lightmaps.Count(); lightmapIndex++)
    {
        const auto lightmap = Scene->LightmapsData.GetLightmap(lightmapIndex);
        lightmap->GetTextures(lightmaps);

        for (int32 textureIndex = 0; textureIndex < NUM_SH_TARGETS; textureIndex++)
        {
            AssetReference<Texture> lightmapTexture = lightmaps[textureIndex];
            if (lightmapTexture == nullptr)
            {
                LOG(Error, "Missing lightmap {0} texture{1}", lightmapIndex, textureIndex);
                return true;
            }

            // Wait for loading end and check result
            if (lightmapTexture->WaitForLoaded())
            {
                LOG(Error, "Failed to load lightmap {0} texture {1}", lightmapIndex, textureIndex);
                return true;
            }

            // TODO: disable streaming for lightmap texture here (enable it later after baking)

            // Wait texture to be streamed to the target quality
            const auto t = lightmapTexture->GetTexture();
            const int32 stepSize = 30;
            const int32 maxWaitTime = 60000;
            int32 stepsCount = static_cast<int32>(maxWaitTime / stepSize);
            while ((t->ResidentMipLevels() < t->MipLevels() || t->ResidentMipLevels() == 0) && stepsCount-- > 0)
                Platform::Sleep(stepSize);
            if (t->ResidentMipLevels() < t->MipLevels() || t->ResidentMipLevels() == 0)
            {
                LOG(Error, "Waiting for lightmap no. {0} texture {1} to be fully resided timed out (loaded mips: {2}, mips count: {3})", lightmapIndex, textureIndex, t->ResidentMipLevels(), t->MipLevels());
                return true;
            }
        }
    }

    return false;
}

void ShadowsOfMordor::Builder::SceneBuildCache::UpdateLightmaps()
{
    Texture* lightmaps[3];
    for (int32 lightmapIndex = 0; lightmapIndex < Lightmaps.Count(); lightmapIndex++)
    {
        // Cache data
        auto& lightmapEntry = Lightmaps[lightmapIndex];
        auto lightmap = Scene->LightmapsData.GetLightmap(lightmapIndex);
        ASSERT(lightmap);
        lightmap->GetTextures(lightmaps);

        // Download buffer data
        if (lightmapEntry.LightmapData->DownloadData(ImportLightmapTextureData))
        {
            LOG(Error, "Cannot download LightmapData.");
            return;
        }

        // Import all textures but don't use file proxy to improve performance
        for (int32 textureIndex = 0; textureIndex < NUM_SH_TARGETS; textureIndex++)
        {
            // Get asset name
            String assetPath;
            if (lightmaps[textureIndex])
                assetPath = lightmaps[textureIndex]->GetPath();
            else
                Scene->LightmapsData.GetCachedLightmapPath(&assetPath, lightmapIndex, textureIndex);

            // Import texture with custom options
#if COMPILE_WITH_ASSETS_IMPORTER
            Guid id = Guid::Empty;
            ImportTexture::Options options;
            options.Type = TextureFormatType::HdrRGBA;
            options.IndependentChannels = true;
#if PLATFORM_WINDOWS
            options.Compress = Scene->GetLightmapSettings().CompressLightmaps;
#else
            options.Compress = false; // TODO: use better BC7 compressor that would handle alpha more precisely (otherwise lightmaps have artifacts, see TextureTool.stb.cpp)
#endif
            options.GenerateMipMaps = true;
            options.IsAtlas = false;
            options.sRGB = false;
            options.NeverStream = false;
            ImportLightmapIndex = lightmapIndex;
            ImportLightmapTextureIndex = textureIndex;
            options.InternalLoad.Bind<SceneBuildCache, &SceneBuildCache::onImportLightmap>(this);
            if (AssetsImportingManager::Create(AssetsImportingManager::CreateTextureTag, assetPath, id, &options))
            {
                LOG(Error, "Cannot create new lightmap {0}:{1}", lightmapIndex, textureIndex);
                return;
            }
            const auto result = Content::LoadAsync<Texture>(id);
            if (result == nullptr)
#else
#error "Cannot import lightmaps. Assets importer module iss missing."
            auto result = nullptr;
#endif
            {
                LOG(Error, "Cannot load new lightmap {0}:{1}", lightmapIndex, textureIndex);
                return;
            }

            // Update lightmap
            lightmap->UpdateTexture(result, textureIndex);
        }

#if DEBUG_EXPORT_LIGHTMAPS_PREVIEW
        // Temporary save lightmaps (after last bounce)
        if (Builder->_giBounceRunningIndex == Builder->_bounceCount - 1)
        {
            exportLightmapPreview(this, lightmapIndex);
        }
#endif

        ImportLightmapTextureData.Release();
    }
}

bool ShadowsOfMordor::Builder::SceneBuildCache::Init(ShadowsOfMordor::Builder* builder, int32 index, ::Scene* scene)
{
    Builder = builder;
    SceneIndex = index;
    Scene = scene;
    const int32 atlasSize = (int32)GetSettings().AtlasSize;
    TempLightmapData = GPUDevice::Instance->CreateBuffer(TEXT("LightmapBuildCache"));
    const auto elementsCount = atlasSize * atlasSize * NUM_SH_TARGETS;
    if (TempLightmapData->Init(GPUBufferDescription::Typed(elementsCount, HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT], true)))
        return true;

    LOG(Info, "Scene \'{0}\' quality: {1}", scene->GetName(), scene->Info.LightmapSettings.Quality);
    return false;
}

void ShadowsOfMordor::Builder::SceneBuildCache::Release()
{
    EntriesLocker.Lock();

    // Cleanup
    Entries.Resize(0);
    Lightmaps.Resize(0);
    Charts.Resize(0);
    Scene = nullptr;

    EntriesLocker.Unlock();

    SAFE_DELETE_GPU_RESOURCE(TempLightmapData);
}

#if COMPILE_WITH_ASSETS_IMPORTER

bool ShadowsOfMordor::Builder::SceneBuildCache::onImportLightmap(TextureData& image)
{
    // Cache data
    const int32 lightmapIndex = ImportLightmapIndex;
    const int32 textureIndex = ImportLightmapTextureIndex;

    // Setup image
    image.Width = image.Height = (int32)GetSettings().AtlasSize;
    image.Depth = 1;
    image.Format = HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT];
    image.Items.Resize(1);
    image.Items[0].Mips.Resize(1);
    auto& mip = image.Items[0].Mips[0];
    mip.RowPitch = PixelFormatExtensions::SizeInBytes(image.Format) * image.Width;
    mip.DepthPitch = mip.RowPitch * image.Height;
    mip.Lines = image.Height;
    mip.Data.Allocate(mip.DepthPitch);

#if HEMISPHERES_IRRADIANCE_FORMAT == HEMISPHERES_FORMAT_R32G32B32A32
    auto pos = (Float4*)mip.Data.Get();
    const auto textureData = ImportLightmapTextureData.Get<Float4>();
    for (int32 y = 0; y < image.Height; y++)
    {
        for (int32 x = 0; x < image.Width; x++)
        {
            const int32 texelAddress = (y * image.Width + x) * NUM_SH_TARGETS;
            *pos = textureData[texelAddress + textureIndex];
            pos++;
        }
    }
#elif HEMISPHERES_IRRADIANCE_FORMAT == HEMISPHERES_FORMAT_R16G16B16A16
    auto pos = (Half4*)mip.Data.Get();
    const auto textureData = ImportLightmapTextureData.Get<Half4>();
    for (int32 y = 0; y < image.Height; y++)
    {
        for (int32 x = 0; x < image.Width; x++)
        {
            const int32 texelAddress = (y * image.Width + x) * NUM_SH_TARGETS;
            *pos = textureData[texelAddress + textureIndex];
            pos++;
        }
    }
#endif

    return false;
}

#endif
