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

#include "StaticModel.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Utilities/Encryption.h"
#if USE_EDITOR
#include "Editor/Editor.h"
#endif

StaticModel::StaticModel(const SpawnParams& params)
    : ModelInstanceActor(params)
    , _world(Matrix::Identity)
    , _scaleInLightmap(1.0f)
    , _boundsScale(1.0f)
    , _lodBias(0)
    , _forcedLod(-1)
    , _vertexColorsDirty(false)
    , _vertexColorsCount(0)
{
    Model.Changed.Bind<StaticModel, &StaticModel::OnModelChanged>(this);
    Model.Loaded.Bind<StaticModel, &StaticModel::OnModelLoaded>(this);
}

StaticModel::~StaticModel()
{
    for (int32 lodIndex = 0; lodIndex < _vertexColorsCount; lodIndex++)
        SAFE_DELETE_GPU_RESOURCE(_vertexColorsBuffer[lodIndex]);
}

void StaticModel::SetScaleInLightmap(float value)
{
    _scaleInLightmap = value;
}

void StaticModel::SetBoundsScale(float value)
{
    if (Math::NearEqual(_boundsScale, value))
        return;

    _boundsScale = value;
    UpdateBounds();
}

MaterialBase* StaticModel::GetMaterial(int32 meshIndex, int32 lodIndex) const
{
    auto model = Model.Get();
    ASSERT(model &&
        Math::IsInRange(lodIndex, 0, model->GetLODsCount()) &&
        Math::IsInRange(meshIndex, 0, model->LODs[lodIndex].Meshes.Count()));
    const auto& mesh = model->LODs[lodIndex].Meshes[meshIndex];
    const auto materialSlotIndex = mesh.GetMaterialSlotIndex();
    MaterialBase* material = Entries[materialSlotIndex].Material.Get();
    return material ? material : model->MaterialSlots[materialSlotIndex].Material.Get();
}

Color32 StaticModel::GetVertexColor(int32 lodIndex, int32 meshIndex, int32 vertexIndex) const
{
    if (Model && !Model->WaitForLoaded() && _vertexColorsCount == Model->GetLODsCount())
    {
        if (lodIndex < 0 || lodIndex >= Model->GetLODsCount())
        {
            LOG(Warning, "Specified model LOD index {0} was out of range.", lodIndex);
            return Color32::Black;
        }

        int32 index = 0;
        const ModelLOD& lod = Model->LODs[lodIndex];
        auto& vertexColorsData = _vertexColorsData[lodIndex];
        if (vertexColorsData.Count() != lod.GetVertexCount())
            return Color32::Black;
        for (int32 i = 0; i < lod.Meshes.Count(); i++)
        {
            const Mesh& mesh = lod.Meshes[i];
            if (i == meshIndex)
            {
                if (vertexIndex < 0 || vertexIndex >= mesh.GetVertexCount())
                {
                    LOG(Warning, "Specified vertex index {3} was out of range. LOD{0} mesh {1} has {2}.", lodIndex, meshIndex, mesh.GetVertexCount(), vertexIndex);
                    return Color32::Black;
                }
                index += vertexIndex;
                return _vertexColorsData[lodIndex][index];
            }
            index += mesh.GetVertexCount();
        }

        LOG(Warning, "Specified model mesh index was out of range. LOD{0} mesh {1}.", lodIndex, meshIndex);
    }

    return Color32::Black;
}

void StaticModel::SetVertexColor(int32 lodIndex, int32 meshIndex, int32 vertexIndex, const Color32& color)
{
    if (!Model || Model->WaitForLoaded())
    {
        LOG(Warning, "Cannot set vertex color if model is missing or faied to load.");
        return;
    }

    if (lodIndex < 0 || lodIndex >= Model->GetLODsCount())
    {
        LOG(Warning, "Specified model LOD index {0} was out of range.", lodIndex);
        return;
    }

    if (_vertexColorsCount != Model->GetLODsCount())
    {
        // Initialize vertex colors data for all LODs
        RemoveVertexColors();
        _vertexColorsCount = Model->GetLODsCount();
        for (int32 i = 0; i < _vertexColorsCount; i++)
            _vertexColorsBuffer[i] = nullptr;
        _vertexColorsDirty = false;
    }

    int32 index = 0;
    const ModelLOD& lod = Model->LODs[lodIndex];
    auto& vertexColorsData = _vertexColorsData[lodIndex];
    if (vertexColorsData.Count() != lod.GetVertexCount())
    {
        vertexColorsData.Resize(lod.GetVertexCount());
        vertexColorsData.SetAll(Color32::Black);
    }
    for (int32 i = 0; i < lod.Meshes.Count(); i++)
    {
        const Mesh& mesh = lod.Meshes[i];
        if (i == meshIndex)
        {
            if (vertexIndex < 0 || vertexIndex >= mesh.GetVertexCount())
            {
                LOG(Warning, "Specified vertex index {3} was out of range. LOD{0} mesh {1} has {2}.", lodIndex, meshIndex, mesh.GetVertexCount(), vertexIndex);
                return;
            }
            index += vertexIndex;
            vertexColorsData[index] = color;
            _vertexColorsDirty = true;
            return;
        }
        index += mesh.GetVertexCount();
    }

    LOG(Warning, "Specified model mesh index was out of range. LOD{0} mesh {1}.", lodIndex, meshIndex);
}

void StaticModel::RemoveVertexColors()
{
    for (int32 lodIndex = 0; lodIndex < _vertexColorsCount; lodIndex++)
        _vertexColorsData[lodIndex].Resize(0);
    for (int32 lodIndex = 0; lodIndex < _vertexColorsCount; lodIndex++)
        SAFE_DELETE_GPU_RESOURCE(_vertexColorsBuffer[lodIndex]);
    _vertexColorsCount = 0;
    _vertexColorsDirty = false;
}

void StaticModel::OnModelChanged()
{
    RemoveVertexColors();
    Entries.Release();

    if (Model && !Model->IsLoaded())
    {
        UpdateBounds();
    }
}

void StaticModel::OnModelLoaded()
{
    Entries.SetupIfInvalid(Model);

    UpdateBounds();
}

void StaticModel::UpdateBounds()
{
    if (Model && Model->IsLoaded())
    {
        Transform t = _transform;
        t.Scale *= _boundsScale;
        Matrix world;
        t.GetWorld(world);
        _box = Model->GetBox(world);
    }
    else
    {
        _box = BoundingBox(_transform.Translation, _transform.Translation);
    }
    BoundingSphere::FromBox(_box, _sphere);
}

bool StaticModel::HasContentLoaded() const
{
    return (Model == nullptr || Model->IsLoaded()) && Entries.HasContentLoaded();
}

void StaticModel::Draw(RenderContext& renderContext)
{
    GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, _world);

    const DrawPass drawModes = (DrawPass)(DrawModes & renderContext.View.Pass);
    if (Model && Model->IsLoaded() && drawModes != DrawPass::None)
    {
        // Flush vertex colors if need to
        if (_vertexColorsDirty)
        {
            for (int32 lodIndex = 0; lodIndex < _vertexColorsCount; lodIndex++)
            {
                auto& vertexColorsData = _vertexColorsData[lodIndex];
                auto& vertexColorsBuffer = _vertexColorsBuffer[lodIndex];
                if (vertexColorsData.HasItems())
                {
                    const uint32 size = vertexColorsData.Count() * sizeof(Color32);
                    if (!vertexColorsBuffer)
                        vertexColorsBuffer = GPUDevice::Instance->CreateBuffer(TEXT("VertexColors"));
                    if (vertexColorsBuffer->GetSize() != size)
                    {
                        if (vertexColorsBuffer->Init(GPUBufferDescription::Vertex(sizeof(Color32), vertexColorsData.Count())))
                            return;
                    }
                    GPUDevice::Instance->GetMainContext()->UpdateBuffer(vertexColorsBuffer, vertexColorsData.Get(), size);
                }
                else
                {
                    SAFE_DELETE_GPU_RESOURCE(vertexColorsBuffer);
                }
            }
        }

#if USE_EDITOR
        // Disable motion blur effects in editor without play mode enabled to hide minor artifacts on objects moving
        if (!Editor::IsPlayMode)
            _drawState.PrevWorld = _world;
#endif

        Mesh::DrawInfo draw;
        draw.Buffer = &Entries;
        draw.World = &_world;
        draw.DrawState = &_drawState;
        draw.Lightmap = _scene->LightmapsData.GetReadyLightmap(Lightmap.TextureIndex);
        draw.LightmapUVs = &Lightmap.UVsArea;
        draw.Flags = GetStaticFlags();
        draw.DrawModes = drawModes;
        draw.Bounds = _sphere;
        draw.PerInstanceRandom = GetPerInstanceRandom();
        draw.LODBias = _lodBias;
        draw.ForcedLOD = _forcedLod;
        draw.VertexColors = _vertexColorsCount ? _vertexColorsBuffer : nullptr;

        Model->Draw(renderContext, draw);
    }

    GEOMETRY_DRAW_STATE_EVENT_END(_drawState, _world);
}

void StaticModel::DrawGeneric(RenderContext& renderContext)
{
    Draw(renderContext);
}

bool StaticModel::IntersectsItself(const Ray& ray, float& distance, Vector3& normal)
{
    bool result = false;

    if (Model != nullptr && Model->IsLoaded())
    {
        Mesh* mesh;
        result = Model->Intersects(ray, _world, distance, normal, &mesh);
    }

    return result;
}

void StaticModel::Serialize(SerializeStream& stream, const void* otherObj)
{
    // Base
    ModelInstanceActor::Serialize(stream, otherObj);

    SERIALIZE_GET_OTHER_OBJ(StaticModel);

    SERIALIZE_MEMBER(ScaleInLightmap, _scaleInLightmap);
    SERIALIZE_MEMBER(BoundsScale, _boundsScale);
    SERIALIZE(Model);
    SERIALIZE_MEMBER(LODBias, _lodBias);
    SERIALIZE_MEMBER(ForcedLOD, _forcedLod);
    SERIALIZE(DrawModes);

    if (HasLightmap()
#if USE_EDITOR
        && PrefabManager::IsNotCreatingPrefab
#endif
    )
    {
        stream.JKEY("LightmapIndex");
        stream.Int(Lightmap.TextureIndex);

        stream.JKEY("LightmapArea");
        stream.Rectangle(Lightmap.UVsArea);
    }

    stream.JKEY("Buffer");
    stream.Object(&Entries, other ? &other->Entries : nullptr);

    if (_vertexColorsCount)
    {
        stream.JKEY("VertexColors");
        stream.StartArray();
        Array<char> encodedData;
        for (int32 lodIndex = 0; lodIndex < _vertexColorsCount; lodIndex++)
        {
            auto& vertexColorsData = _vertexColorsData[lodIndex];
            if (vertexColorsData.HasItems())
            {
                const int32 size = vertexColorsData.Count() * sizeof(Color32);
                Encryption::Base64Encode((byte*)vertexColorsData.Get(), size, encodedData);
                stream.String(encodedData.Get(), encodedData.Count());
            }
            else
            {
                stream.String("", 0);
            }
        }
        stream.EndArray();
    }
}

void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
    // Base
    ModelInstanceActor::Deserialize(stream, modifier);

    DESERIALIZE_MEMBER(ScaleInLightmap, _scaleInLightmap);
    DESERIALIZE_MEMBER(BoundsScale, _boundsScale);
    DESERIALIZE(Model);

    {
        const auto member = stream.FindMember("LODBias");
        if (member != stream.MemberEnd() && member->value.IsInt())
        {
            SetLODBias(member->value.GetInt());
        }
    }
    {
        const auto member = stream.FindMember("ForcedLOD");
        if (member != stream.MemberEnd() && member->value.IsInt())
        {
            SetForcedLOD(member->value.GetInt());
        }
    }

    DESERIALIZE(DrawModes);
    DESERIALIZE_MEMBER(LightmapIndex, Lightmap.TextureIndex);
    DESERIALIZE_MEMBER(LightmapArea, Lightmap.UVsArea);

    Entries.DeserializeIfExists(stream, "Buffer", modifier);

    {
        const auto member = stream.FindMember("VertexColors");
        if (member != stream.MemberEnd() && member->value.IsArray())
        {
            // TODO: don't stall but just check the length of the loaded vertex colors arrays size later when asset gets loaded
            if (Model && !Model->WaitForLoaded())
            {
                RemoveVertexColors();
                auto& array = member->value;
                _vertexColorsCount = array.Size();
                Array<byte> decodedData;
                if (_vertexColorsCount == Model->GetLODsCount())
                {
                    for (int32 lodIndex = 0; lodIndex < _vertexColorsCount; lodIndex++)
                    {
                        _vertexColorsBuffer[lodIndex] = nullptr;
                        auto& vertexColorsData = _vertexColorsData[lodIndex];
                        vertexColorsData.Clear();
                        auto& v = array[lodIndex];
                        if (v.IsString())
                        {
                            Encryption::Base64Decode(v.GetString(), v.GetStringLength(), decodedData);
                            const int32 length = decodedData.Count() / sizeof(Color32);
                            vertexColorsData.Resize(length);
                            Platform::MemoryCopy(vertexColorsData.Get(), decodedData.Get(), decodedData.Count());
                        }
                    }
                }
                else
                {
                    LOG(Error, "Loaded vertex colors data for {0} has different size than the model {1} LODs count.", ToString(), Model->ToString());
                }
                _vertexColorsDirty = true;
            }
        }
    }

    // [Deprecated on 11.10.2019, expires on 11.10.2020]
    if (modifier->EngineBuild <= 6187)
    {
        const auto member = stream.FindMember("HiddenShadow");
        if (member != stream.MemberEnd() && member->value.IsBool() && member->value.GetBool())
        {
            DrawModes = DrawPass::Depth;
        }
    }

    {
        const auto member = stream.FindMember("RenderPasses");
        if (member != stream.MemberEnd() && member->value.IsInt())
        {
            DrawModes = (DrawPass)member->value.GetInt();
        }
    }
}

bool StaticModel::IntersectsEntry(int32 entryIndex, const Ray& ray, float& distance, Vector3& normal)
{
    auto model = Model.Get();
    if (!model || !model->IsInitialized() || model->GetLoadedLODs() == 0)
        return false;

    // Find mesh in the highest loaded LOD that is using the given material slot index and ray hits it
    auto& meshes = model->LODs[model->HighestResidentLODIndex()].Meshes;
    for (int32 i = 0; i < meshes.Count(); i++)
    {
        const auto& mesh = meshes[i];
        if (mesh.GetMaterialSlotIndex() == entryIndex && mesh.Intersects(ray, _world, distance, normal))
            return true;
    }

    distance = 0;
    normal = Vector3::Up;
    return false;
}

bool StaticModel::IntersectsEntry(const Ray& ray, float& distance, Vector3& normal, int32& entryIndex)
{
    auto model = Model.Get();
    if (!model || !model->IsInitialized() || model->GetLoadedLODs() == 0)
        return false;

    // Find mesh in the highest loaded LOD that is using the given material slot index and ray hits it
    bool result = false;
    float closest = MAX_float;
    Vector3 closestNormal = Vector3::Up;
    int32 closestEntry = -1;
    auto& meshes = model->LODs[model->HighestResidentLODIndex()].Meshes;
    for (int32 i = 0; i < meshes.Count(); i++)
    {
        // Test intersection with mesh and check if is closer than previous
        const auto& mesh = meshes[i];
        float dst;
        Vector3 nrm;
        if (mesh.Intersects(ray, _world, dst, nrm) && dst < closest)
        {
            result = true;
            closest = dst;
            closestNormal = nrm;
            closestEntry = mesh.GetMaterialSlotIndex();
        }
    }

    distance = closest;
    normal = closestNormal;
    entryIndex = closestEntry;
    return result;
}

void StaticModel::OnTransformChanged()
{
    // Base
    ModelInstanceActor::OnTransformChanged();

    _transform.GetWorld(_world);
    UpdateBounds();
}
