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

#include "Builder.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Level/Actors/BoxBrush.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Level/Level.h"
#include "Engine/Terrain/Terrain.h"
#include "Engine/Terrain/TerrainPatch.h"
#include "Engine/Foliage/Foliage.h"
#include "Engine/Threading/Threading.h"

bool canUseMaterialWithLightmap(MaterialBase* material, ShadowsOfMordor::Builder::SceneBuildCache* scene)
{
    // Check objects with missing materials can be used
    if (material == nullptr)
        return scene->GetSettings().UseGeometryWithNoMaterials;

    return material->CanUseLightmap();
}

bool cacheStaticGeometryTree(Actor* actor, ShadowsOfMordor::Builder::SceneBuildCache* scene)
{
    ShadowsOfMordor::Builder::GeometryEntry entry;
    const bool useLightmap = actor->GetIsActive() && actor->HasStaticFlag(StaticFlags::Lightmap);
    auto& results = scene->Entries;

    // Switch actor type
    if (auto staticModel = dynamic_cast<StaticModel*>(actor))
    {
        // Check if model has been linked and is loaded
        auto model = staticModel->Model.Get();
        if (model && !model->WaitForLoaded())
        {
            entry.Type = ShadowsOfMordor::Builder::GeometryType::StaticModel;
            entry.UVsBox = Rectangle(Float2::Zero, Float2::One);
            entry.AsStaticModel.Actor = staticModel;
            entry.Scale = Math::Clamp(staticModel->GetScaleInLightmap(), 0.0f, LIGHTMAP_SCALE_MAX);

            // Use the first LOD
            const int32 lodIndex = 0;
            auto& lod = model->LODs[lodIndex];

            bool anyValid = false;
            for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
            {
                const auto& mesh = lod.Meshes[meshIndex];
                auto& bufferEntry = staticModel->Entries[mesh.GetMaterialSlotIndex()];
                if (bufferEntry.Visible && canUseMaterialWithLightmap(staticModel->GetMaterial(meshIndex), scene))
                {
                    if (mesh.HasLightmapUVs())
                    {
                        anyValid = true;
                    }
                    else
                    {
                        LOG(Warning, "Model \'{0}\' mesh index {1} (lod: {2}) has missing lightmap UVs (at actor: {3})", model->GetPath(), meshIndex, lodIndex, staticModel->GetNamePath());
                    }
                }
            }

            if (useLightmap && anyValid && entry.Scale > ZeroTolerance)
            {
                Matrix worldMatrix;
                staticModel->GetLocalToWorldMatrix(worldMatrix);
                entry.Box = model->GetBox(worldMatrix);
                results.Add(entry);
            }
            else
            {
                staticModel->RemoveLightmap();
            }
        }
    }
    else if (auto terrain = dynamic_cast<Terrain*>(actor))
    {
        entry.AsTerrain.Actor = terrain;
        entry.Type = ShadowsOfMordor::Builder::GeometryType::Terrain;
        entry.UVsBox = Rectangle(Float2::Zero, Float2::One);
        entry.Scale = Math::Clamp(terrain->GetScaleInLightmap(), 0.0f, LIGHTMAP_SCALE_MAX);
        for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
        {
            auto patch = terrain->GetPatch(patchIndex);
            entry.AsTerrain.PatchIndex = patchIndex;
            for (int32 chunkIndex = 0; chunkIndex < Terrain::ChunksCount; chunkIndex++)
            {
                auto chunk = patch->Chunks[chunkIndex];
                entry.AsTerrain.ChunkIndex = chunkIndex;
                auto material = chunk.OverrideMaterial ? chunk.OverrideMaterial.Get() : terrain->Material.Get();
                const bool canUseLightmap = useLightmap
                        && canUseMaterialWithLightmap(material, scene)
                        && entry.Scale > ZeroTolerance;
                if (canUseLightmap)
                {
                    entry.Box = chunk.GetBounds();
                    results.Add(entry);
                }
                else
                {
                    chunk.RemoveLightmap();
                }
            }
        }
    }
    else if (auto foliage = dynamic_cast<Foliage*>(actor))
    {
        entry.AsFoliage.Actor = foliage;
        entry.Type = ShadowsOfMordor::Builder::GeometryType::Foliage;
        entry.UVsBox = Rectangle(Float2::Zero, Float2::One);
        for (auto i = foliage->Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            auto& type = foliage->FoliageTypes[instance.Type];
            entry.AsFoliage.InstanceIndex = i.Index();
            entry.AsFoliage.TypeIndex = instance.Type;
            entry.Scale = Math::Clamp(type.ScaleInLightmap, 0.0f, LIGHTMAP_SCALE_MAX);
            const bool canUseLightmap = useLightmap
                    && canUseMaterialWithLightmap(type.Entries[0].Material, scene)
                    && entry.Scale > ZeroTolerance;
            auto model = type.Model.Get();
            if (canUseLightmap && model && !model->WaitForLoaded())
            {
                BoundingBox::FromSphere(instance.Bounds, entry.Box);
                const int32 lodIndex = 0;
                auto& lod = model->LODs[lodIndex];
                for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
                {
                    entry.AsFoliage.MeshIndex = meshIndex;
                    results.Add(entry);
                }
            }
            else
            {
                instance.RemoveLightmap();
            }
        }
    }

    return actor->GetIsActive();
}

void ShadowsOfMordor::Builder::cacheEntries()
{
    auto scene = _scenes[_workerActiveSceneIndex];

    reportProgress(BuildProgressStep::CacheEntries, 0);
    scene->EntriesLocker.Lock();

    // Gather static scene geometry entries
    Function<bool(Actor*, SceneBuildCache*)> cacheGeometry = &cacheStaticGeometryTree;
    scene->Scene->TreeExecute(cacheGeometry, scene);

    scene->EntriesLocker.Unlock();
    reportProgress(BuildProgressStep::CacheEntries, 1.0f);
}

void ShadowsOfMordor::Builder::updateEntries()
{
    auto scene = _scenes[_workerActiveSceneIndex];

    reportProgress(BuildProgressStep::UpdateEntries, 0.0f);
    scene->EntriesLocker.Lock();

    // Update entries (and cache entries list per lightmap as linear array instead of tree structure)
    ScopeLock lock(Level::ScenesLock); // TODO: maybe don;t lock scene?
    const int32 entriesCount = scene->Entries.Count();
    for (int32 i = 0; i < entriesCount; i++)
    {
        auto& e = scene->Entries[i];
        if (e.ChartIndex == INVALID_INDEX)
        {
            // Removed previously baked lightmap info
            LightmapEntry emptyEntry;
            switch (e.Type)
            {
            case GeometryType::StaticModel:
            {
                auto staticModel = e.AsStaticModel.Actor;
                if (staticModel)
                    staticModel->Lightmap = emptyEntry;
            }
            break;
            case GeometryType::Terrain:
            {
                auto terrain = e.AsTerrain.Actor;
                if (terrain)
                    terrain->GetPatch(e.AsTerrain.PatchIndex)->Chunks[e.AsTerrain.ChunkIndex].Lightmap = emptyEntry;
            }
            break;
            case GeometryType::Foliage:
            {
                auto foliage = e.AsFoliage.Actor;
                if (foliage)
                    foliage->Instances[e.AsFoliage.InstanceIndex].Lightmap = emptyEntry;
            }
            break;
            }
            continue;
        }
        auto& chart = scene->Charts[e.ChartIndex];

        // Update result uvs by taking into account lightmap uvs box
        chart.Result.UVsArea.Size /= e.UVsBox.Size;
        chart.Result.UVsArea.Location += e.UVsBox.Location * chart.Result.UVsArea.Size;

        switch (e.Type)
        {
        case GeometryType::StaticModel:
        {
            auto staticModel = e.AsStaticModel.Actor;
            if (staticModel)
            {
                // Update data
                staticModel->Lightmap = chart.Result;
            }
            else
            {
                // Discard chart due to data leaks
                chart.Result.TextureIndex = INVALID_INDEX;
            }
        }
        break;
        case GeometryType::Terrain:
        {
            auto terrain = e.AsTerrain.Actor;
            if (terrain)
            {
                // Update data
                terrain->GetPatch(e.AsTerrain.PatchIndex)->Chunks[e.AsTerrain.ChunkIndex].Lightmap = chart.Result;
            }
            else
            {
                // Discard chart due to data leaks
                chart.Result.TextureIndex = INVALID_INDEX;
            }
        }
        break;
        case GeometryType::Foliage:
        {
            auto foliage = e.AsFoliage.Actor;
            if (foliage)
            {
                // Update data
                foliage->Instances[e.AsFoliage.InstanceIndex].Lightmap = chart.Result;
            }
            else
            {
                // Discard chart due to data leaks
                chart.Result.TextureIndex = INVALID_INDEX;
            }
        }
        break;
        }

        // Cache entry link
        if (chart.Result.TextureIndex != INVALID_INDEX)
            scene->Lightmaps[chart.Result.TextureIndex].Entries.Add(i);

        reportProgress(BuildProgressStep::UpdateEntries, static_cast<float>(i) / entriesCount);
    }

    scene->EntriesLocker.Unlock();
    reportProgress(BuildProgressStep::UpdateEntries, 1.0f);
}
