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

#include "SpriteRender.h"
#include "Engine/Core/Types/Variant.h"
#include "Engine/Core/Math/OrientedBoundingBox.h"
#include "Engine/Level/Scene/SceneRendering.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Content/Assets/MaterialInstance.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Level/Actors/Camera.h"
#include "Engine/Serialization/Serialization.h"

SpriteRender::SpriteRender(const SpawnParams& params)
    : Actor(params)
    , _color(Color::White)
    , _size(100.0f)
{
    _quadModel = Content::LoadAsyncInternal<Model>(TEXT("Engine/Models/Quad"));
    Material.Loaded.Bind<SpriteRender, &SpriteRender::OnMaterialLoaded>(this);
    Image.Changed.Bind<SpriteRender, &SpriteRender::SetImage>(this);
}

Float2 SpriteRender::GetSize() const
{
    return _size;
}

void SpriteRender::SetSize(const Float2& value)
{
    if (_size == value)
        return;
    _size = value;
    OnTransformChanged();
}

Color SpriteRender::GetColor() const
{
    return _color;
}

void SpriteRender::SetColor(const Color& value)
{
    _color = value;
    if (_paramColor)
        _paramColor->SetValue(value);
}

SpriteHandle SpriteRender::GetSprite() const
{
    return _sprite;
}

void SpriteRender::SetSprite(const SpriteHandle& value)
{
    _sprite = value;
    SetImage();
}

void SpriteRender::OnMaterialLoaded()
{
    // Setup material instance
    if (!_materialInstance)
    {
        _materialInstance = Content::CreateVirtualAsset<MaterialInstance>();
        _materialInstance->AddReference();
    }
    _materialInstance->SetBaseMaterial(Material);

    // Cache parameters
    _paramImageMAD = _materialInstance->GetParameter(TEXT("ImageMAD"));
    if (_paramImageMAD && _paramImageMAD->GetParameterType() != MaterialParameterType::Vector4)
        _paramImageMAD = nullptr;
    _paramImage = _materialInstance->GetParameter(TEXT("Image"));
    if (_paramImage && _paramImage->GetParameterType() != MaterialParameterType::Texture)
        _paramImage = nullptr;
    else if (_paramImage)
        SetImage();
    _paramColor = _materialInstance->GetParameter(TEXT("Color"));
    if (_paramColor && _paramColor->GetParameterType() != MaterialParameterType::Color && _paramColor->GetParameterType() != MaterialParameterType::Vector4 && _paramColor->GetParameterType() != MaterialParameterType::Vector3)
        _paramColor = nullptr;
    else if (_paramColor)
        _paramColor->SetValue(_color);
}

void SpriteRender::SetImage()
{
    TextureBase* image = Image.Get();
    Vector4 imageMAD(Vector2::One, Vector2::Zero);
    if (!image && _sprite.IsValid())
    {
        image = _sprite.Atlas.Get();
        Sprite* sprite = &_sprite.Atlas->Sprites.At(_sprite.Index);
        imageMAD = Vector4(sprite->Area.Size, sprite->Area.Location);
    }
    if (_paramImage)
        _paramImage->SetValue(image);
    if (_paramImageMAD)
        _paramImageMAD->SetValue(imageMAD);
}

bool SpriteRender::HasContentLoaded() const
{
    return (Material == nullptr || Material->IsLoaded()) && (Image == nullptr || Image->IsLoaded());
}

void SpriteRender::Draw(RenderContext& renderContext)
{
    if (renderContext.View.Pass == DrawPass::GlobalSDF || renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas)
        return;
    if (!Material || !Material->IsLoaded() || !_quadModel || !_quadModel->IsLoaded())
        return;
    auto model = _quadModel.As<Model>();
    if (model->GetLoadedLODs() == 0)
        return;
    const auto& view = renderContext.View;
    Matrix m1, m2, m3, world;
    Matrix::Scaling(_size.X, _size.Y, 1.0f, m2);
    Matrix::RotationY(PI, m3);
    Matrix::Multiply(m2, m3, m1);
    if (FaceCamera)
    {
        Matrix::Billboard(_transform.Translation - view.Origin, view.Position, Vector3::Up, view.Direction, m2);
        Matrix::Multiply(m1, m2, m3);
        Matrix::Scaling(_transform.Scale, m1);
        Matrix::Multiply(m1, m3, world);
    }
    else
    {
        view.GetWorldMatrix(_transform, m2);
        Matrix::Multiply(m1, m2, world);
    }
    model->LODs[0].Draw(renderContext, _materialInstance, world, GetStaticFlags(), false, DrawModes, GetPerInstanceRandom(), SortOrder);
}

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

    SERIALIZE_GET_OTHER_OBJ(SpriteRender);

    SERIALIZE_MEMBER(Size, _size);
    SERIALIZE_MEMBER(Color, _color);
    SERIALIZE_MEMBER(Sprite, _sprite);
    SERIALIZE(Image);
    SERIALIZE(Material);
    SERIALIZE(FaceCamera);
    SERIALIZE(DrawModes);
    SERIALIZE(SortOrder);
}

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

    DESERIALIZE_MEMBER(Size, _size);
    DESERIALIZE_MEMBER(Color, _color);
    DESERIALIZE_MEMBER(Sprite, _sprite);
    DESERIALIZE(Image);
    DESERIALIZE(Material);
    DESERIALIZE(FaceCamera);
    DESERIALIZE(DrawModes);
    DESERIALIZE(SortOrder);

    SetImage();
    if (_paramColor)
        _paramColor->SetValue(_color);
}

void SpriteRender::OnLayerChanged()
{
    if (_sceneRenderingKey != -1)
        GetSceneRendering()->UpdateActor(this, _sceneRenderingKey);
}

void SpriteRender::OnEndPlay()
{
    // Base
    Actor::OnEndPlay();

    // Release material instance
    if (_materialInstance)
    {
        _materialInstance->SetBaseMaterial(nullptr);
        _materialInstance->Params.Resize(0);
        _materialInstance->RemoveReference();
        _materialInstance = nullptr;
    }
}

void SpriteRender::OnEnable()
{
    GetSceneRendering()->AddActor(this, _sceneRenderingKey);

    // Base
    Actor::OnEnable();
}

void SpriteRender::OnDisable()
{
    GetSceneRendering()->RemoveActor(this, _sceneRenderingKey);

    // Base
    Actor::OnDisable();
}

void SpriteRender::OnTransformChanged()
{
    // Base
    Actor::OnTransformChanged();

    const BoundingSphere localSphere(Vector3::Zero, _size.Length());
    Matrix world;
    _transform.GetWorld(world);
    BoundingSphere::Transform(localSphere, world, _sphere);
    BoundingBox::FromSphere(_sphere, _box);
    if (_sceneRenderingKey != -1)
        GetSceneRendering()->UpdateActor(this, _sceneRenderingKey);
}
