// ------------------------------------------------
// Copyright Joe Marshall 2024- All Rights Reserved
// ------------------------------------------------
//
// Renders mesh directly to framebuffer from video
// stream (without texture between)
// ------------------------------------------------
#include "DirectVideoMeshRenderer.h"

#include "AndroidVulkanTextureSample.h"

#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "EngineModule.h"
#include "IMediaPlayer.h"
#include "RHIResources.h"
#include "RawIndexBuffer.h"
#include "RenderGraphBuilder.h"
#include "Rendering/ColorVertexBuffer.h"
#include "Rendering/PositionVertexBuffer.h"
#include "Rendering/StaticMeshVertexBuffer.h"
#include "SceneView.h"
#include "SceneViewExtension.h"
#include "StaticMeshResources.h"
#include "StereoRendering.h"

#include "Math/Matrix.h"

#include "MeshDescription.h"

DEFINE_LOG_CATEGORY(LogDirectVideoMeshRenderer);

class FRendererSceneViewExt : public FSceneViewExtensionBase
{
  public:
    FRendererSceneViewExt(const FAutoRegister &AutoRegister,
                          UDirectVideoMeshRendererComponent &InOwner)
        : FSceneViewExtensionBase(AutoRegister), Owner(InOwner)
    {
    }

    virtual void PostRenderView_RenderThread(FRDGBuilder &GraphBuilder, FSceneView &InView) override
    {
    }

    virtual void PostRenderBasePassMobile_RenderThread(FRHICommandList &RHICmdList,
                                                       FSceneView &InView) override

    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "PostRenderBasePassMobile");

#if PLATFORM_ANDROID
        Owner.DrawToCommandList(RHICmdList, InView);
#endif
    }

    virtual void SetupViewFamily(FSceneViewFamily &InViewFamily) override
    {
    }

    virtual void SetupView(FSceneViewFamily &InViewFamily, FSceneView &InView) override
    {
    }

    virtual void PreRenderViewFamily_RenderThread(FRDGBuilder &GraphBuilder,
                                                  FSceneViewFamily &InViewFamily)
    {
#if PLATFORM_ANDROID

        // we are outside of a command list, so we can update the mesh if needed
        Owner.DoWorkOutsideRenderPass(GraphBuilder.RHICmdList, InViewFamily);

#endif
    }

    virtual void PreRenderView_RenderThread(FRDGBuilder &GraphBuilder, FSceneView &InView)
    {
    }

    virtual void BeginRenderViewFamily(FSceneViewFamily &InViewFamily) override
    {
        Owner.UpdateVisibility();
    }

    UDirectVideoMeshRendererComponent &Owner;
};

UDirectVideoMeshRendererComponent::UDirectVideoMeshRendererComponent()
{
    HideMeshWhileWeHaveVideo = true;
    PrimaryComponentTick.bCanEverTick = true;
    Initialized = false;
}

UDirectVideoMeshRendererComponent::~UDirectVideoMeshRendererComponent()
{
    DeInit();
}

void UDirectVideoMeshRendererComponent::DeInit()
{
    Initialized = false;
#if PLATFORM_ANDROID
    UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Deinit");
    // Extensions clean up on destroy
    ExtensionHolder.Reset();

    if (PreparedSample != NULL)
    {
        AndroidVulkanTextureSample *VulkanSample =
            static_cast<AndroidVulkanTextureSample *>(PreparedSample.Get());
        VulkanSample->ImplDeleted();
    }
    if (CurrentSample != NULL)
    {
        AndroidVulkanTextureSample *VulkanSample =
            static_cast<AndroidVulkanTextureSample *>(CurrentSample.Get());
        VulkanSample->ImplDeleted();
    }
    CurrentSample = NULL;
    PreparedSample = NULL;
    if (Facade != NULL)
    {
        auto PFacade = Facade.Pin();
    }
#endif

    Facade.Reset();
    // the media facade sample sink keeps a weak pointer to
    // the texture sink and then clears it if destroyed,
    // so we don't need to remove the texture sink, we can just
    // kill it here.
    TextureSink.Reset();
}

void UDirectVideoMeshRendererComponent::BeginDestroy()
{
    DeInit();
    UActorComponent::BeginDestroy();
}

void UDirectVideoMeshRendererComponent::Initialize()
{
#if PLATFORM_ANDROID
    TextureSink = MakeShared<_TextureSink>(this);

    if (Facade == NULL)
    {
        if (LinkedPlayer != NULL)
        {
            Facade = LinkedPlayer->GetPlayerFacade();
        }
    }

    if (Facade != NULL)
    {
        if (!HasAnyFlags(RF_ClassDefaultObject))
        {
            auto PFacade = Facade.Pin();
            TSharedRef<FMediaTextureSampleSink> sinkPtr = TextureSink.ToSharedRef();
            PFacade.Get()->AddVideoSampleSink(sinkPtr);

            UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Registered for frames");
            Initialized = true;
        }
    }
    else
    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "No media player");
    }

    GetMeshObject();

    if (ExtensionHolder == NULL)
    {
        ExtensionHolder = FSceneViewExtensions::NewExtension<FRendererSceneViewExt>(*this);
    }

#endif
}

void UDirectVideoMeshRendererComponent::HandlePlayerMediaEvent(EMediaSampleSinkEvent Event)
{
    int IEvent = (int)Event;
    UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Handle event {0}", IEvent);

    switch (Event)
    {
    case EMediaSampleSinkEvent::Detached:
    case EMediaSampleSinkEvent::MediaClosed:
        CurrentSample = NULL;
        PreparedSample = NULL;
        break;
    }
}

void UDirectVideoMeshRendererComponent::TickComponent(float DeltaTime, ELevelTick TickType,
                                                      FActorComponentTickFunction *ThisTickFunction)
{
    if (!Initialized)
    {
        Initialize();
    }
}

void UDirectVideoMeshRendererComponent::UpdateVisibility()
{
    if (HideMeshWhileWeHaveVideo)
    {
        MeshActor = GetOwner<AStaticMeshActor>();
        if (MeshActor == NULL)
        {
            UE_LOGFMT(LogDirectVideoMeshRenderer, Warning,
                      "Mesh renderer has to be on StaticMeshActor");
            return;
        }
        // need to unhide or hide the underlying staticmesh here
        // depending on whether we have video
        UStaticMeshComponent *MeshComponent = MeshActor->GetStaticMeshComponent();
        if (MeshComponent == NULL)
        {
            UE_LOGFMT(LogDirectVideoMeshRenderer, Warning,
                      "Mesh renderer needs a static mesh component");
            return;
        }

        MeshComponent->bRenderInDepthPass = 0;
        MeshComponent->SetRenderInMainPass(CurrentSample == NULL);
    }
}

void UDirectVideoMeshRendererComponent::GetMeshObject()
{
    MeshActor = GetOwner<AStaticMeshActor>();
    if (MeshActor == NULL)
    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, Warning,
                  "Mesh renderer has to be on StaticMeshActor");
        return;
    }
    UStaticMeshComponent *MeshComponent = MeshActor->GetStaticMeshComponent();
    TObjectPtr<UStaticMesh> Mesh = MeshComponent->GetStaticMesh();
#if WITH_EDITOR
    if (Mesh != NULL)
    {
        FStaticMeshRenderData *RenderData = Mesh->GetRenderData();
        if (RenderData != NULL)
        {
            const FStaticMeshLODResources &LODRenderData = RenderData->LODResources[0];
            const auto &VertexBuffers = LODRenderData.VertexBuffers;
            const auto &IndexBuffer = LODRenderData.IndexBuffer; // index buffer for rendering this

            if (ConstructMeshFromRenderData(VertexBuffers, IndexBuffer))
            {
                HasMesh = true;
            }
            else
            {
                UE_LOGFMT(LogDirectVideoMeshRenderer, Warning, "Mesh making failed");
            }
        }
        else
        {
            UE_LOGFMT(LogDirectVideoMeshRenderer, Warning, "No render data");
        }
    }
    else
    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, Warning, "Mesh is null");
    }
#endif
}

void DumpMatrix(const char *Name, FMatrix44f Matrix)
{
    char MatrixDesc[256];
    int ofs = snprintf(MatrixDesc, 250, "%s:\n", Name);
    for (int c = 0; c < 4; c++)
    {
        for (int d = 0; d < 4; d++)
        {
            int len = snprintf(&MatrixDesc[ofs], 250 - ofs, "%4.4f,", Matrix.M[c][d]);
            ofs += len;
        }
        MatrixDesc[ofs] = '\n';
        ofs++;
    }
    MatrixDesc[ofs] = 0;
    UE_LOGFMT(LogDirectVideoMeshRenderer, Verbose, "{0}", MatrixDesc);
}

void UDirectVideoMeshRendererComponent::DrawToCommandList(FRHICommandList &RHICmdList,
                                                          FSceneView &InView)
{
    bool hasSample = (PreparedSample != NULL);
#if PLATFORM_ANDROID
    UE_LOGFMT(LogDirectVideoMeshRenderer, Verbose, "DrawToCOmmandList_Init:{0} HasSample:{1}",
              Initialized, hasSample);

    if (!Initialized)
    {
        return;
    }
    if (PreparedSample != NULL)
    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, Verbose, "DrawToCOmmandList:{0}",
                  RHICmdList.IsOutsideRenderPass());
        AndroidVulkanTextureSample *VulkanSample =
            static_cast<AndroidVulkanTextureSample *>(PreparedSample.Get());
        const FTextureRHIRef &ColourTex = InView.Family->RenderTarget->GetRenderTargetTexture();
        VulkanSample->RenderToMesh(ColourTex, RHICmdList, (void *)(MeshActor.Get()));
    }
#endif
}

void UDirectVideoMeshRendererComponent::GetShaderInfo(const FSceneView *InView, float *m44,
                                                      float *vp44, float *pre_view_translation)
{
    FTransform Transform = MeshActor->ActorToWorld();
    FMatrix44f LocalToRelativeWorld = FMatrix44f(Transform.ToMatrixWithScale());
    //    DumpMatrix("Model", LocalToRelativeWorld);

    auto TranslatedWorldToClip =
        FMatrix44f(InView->ViewMatrices.GetTranslatedViewProjectionMatrix());
    //            DumpMatrix("VP", TranslatedWorldToClip);
    auto MVPMatrix = LocalToRelativeWorld * TranslatedWorldToClip;
    //            DumpMatrix("MVP", MVPMatrix);

    auto Translation = InView->ViewMatrices.GetPreViewTranslation();
    FVector4f Translation4 = FVector4f(Translation.X, Translation.Y, Translation.Z, 0);
    UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Pre view translation 2:{0},{1},{2},{3}",
              Translation4.X, Translation4.Y, Translation4.Z, Translation4.W);
    memcpy(vp44, TranslatedWorldToClip.M, 16 * sizeof(float));
    // auto TransposedVP=TranslatedWorldToClip.GetTransposed();
    //            memcpy(vp44,TransposedVP.M,16*sizeof(float));
    //            auto TransposedM = LocalToRelativeWorld.GetTransposed();
    //            memcpy(m44,TransposedM.M,16*sizeof(float));
    memcpy(m44, LocalToRelativeWorld.M, 16 * sizeof(float));
    pre_view_translation[0] = Translation4.X;
    pre_view_translation[1] = Translation4.Y;
    pre_view_translation[2] = Translation4.Z;
    pre_view_translation[3] = Translation4.W;
}

void UDirectVideoMeshRendererComponent::DoWorkOutsideRenderPass(FRHICommandList &RHICmdList,
                                                                FSceneViewFamily &InViewFamily)
{

    // render both views if we are stereo multiview (with single pass rendering)
#if PLATFORM_ANDROID
    if (!Initialized)
    {
        return;
    }
    if (CurrentSample != NULL)
    {
        ShaderViewMatrices ViewMatrices;
        ShaderModelMatrix ModelMatrix;
        PreparedSample = CurrentSample;
        AndroidVulkanTextureSample *VulkanSample =
            static_cast<AndroidVulkanTextureSample *>(PreparedSample.Get());

        UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Work outside render pass:{0}",
                  RHICmdList.IsOutsideRenderPass());
        for (auto &InView : InViewFamily.Views)
        {

            if (IStereoRendering::IsASecondaryView(*InView))
            {
                GetShaderInfo(InView, ModelMatrix.m44, ViewMatrices.vp44_2,
                              ViewMatrices.pre_view_translation_2);
            }
            else
            {
                GetShaderInfo(InView, ModelMatrix.m44, ViewMatrices.vp44,
                              ViewMatrices.pre_view_translation);
            }
        }
        const FTextureRHIRef &ColourTex = InViewFamily.RenderTarget->GetRenderTargetTexture();
        VulkanSample->UpdateViewMatrices(ColourTex, RHICmdList, ViewMatrices,
                                         (void *)(MeshActor.Get()));
        VulkanSample->UpdateMesh(ColourTex, RHICmdList, Positions, Indices, ModelMatrix,
                                 (void *)(MeshActor.Get()));
        VulkanSample->InitFrameForMeshRendering(ColourTex, RHICmdList, (void *)(MeshActor.Get()));
    }
    else
    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Emptying prepared sample");
        PreparedSample = NULL;
    }
#endif
}

bool UDirectVideoMeshRendererComponent::Enqueue(
    const TSharedRef<IMediaTextureSample, ESPMode::ThreadSafe> &Sample)
{
    if (!Initialized)
    {
        return false;
    }
    static FGuid OurGUID(0x9bf2d7c6, 0xb2b84d26, 0xb6ae5a3a, 0xc9883569);
    auto PFacade = Facade.Pin();
    if (PFacade->GetPlayer()->GetPlayerPluginGUID() == OurGUID)
    {
        CurrentSample = Sample;
        UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Mesh component has sample");
    }
    else
    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, Warning, "Mesh component has wrong player {0}",
                  PFacade.Get()->GetGuid());
    }
    return true;
}

void UDirectVideoMeshRendererComponent::Serialize(FArchive &Ar)
{
    /*    if(!Ar.IsSaving()){
            char bob[119];
            Ar.Serialize(bob,119);
            return;
        }*/
#if WITH_EDITOR
    if (Ar.IsSaving())
    {
        if (!HasMesh)
        {
            UE_LOGFMT(LogDirectVideoMeshRenderer, Warning, "Needs mesh object");

            GetMeshObject();
        }
        if (!HasMesh)
        {
            UE_LOGFMT(LogDirectVideoMeshRenderer, Warning,
                      "Couldn't get mesh for DirectVideoMeshRenderComponent");
        }
        else
        {
            UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose, "Saving mesh details");
        }
    }
#endif
    UActorComponent::Serialize(Ar);

    if (Ar.IsLoading())
    {
        if (HasMesh)
        {
            int NumVertices = PositionsAsFloats.Num() / 5;
            Positions.Init({}, NumVertices);
            for (int c = 0; c < NumVertices; c++)
            {
                Positions[c].x = PositionsAsFloats[c * 5];
                Positions[c].y = PositionsAsFloats[c * 5 + 1];
                Positions[c].z = PositionsAsFloats[c * 5 + 2];
                Positions[c].u = PositionsAsFloats[c * 5 + 3];
                Positions[c].v = PositionsAsFloats[c * 5 + 4];
            }
            UE_LOGFMT(LogDirectVideoMeshRenderer, VeryVerbose,
                      "DirectVideoMeshRenderComponent - Vertices {0} Indices {1}", Indices.Num(),
                      Positions.Num());
        }
        else
        {
            UE_LOGFMT(LogDirectVideoMeshRenderer, Warning,
                      "No saved mesh for DirectVideoMeshRenderComponent");
        }
    }
}

bool UDirectVideoMeshRendererComponent::ConstructMeshFromRenderData(
    const FStaticMeshVertexBuffers &VertexBuffers, const FRawStaticIndexBuffer &IndexBuffer)
{
    const FPositionVertexBuffer &Pos =
        VertexBuffers.PositionVertexBuffer; // contains vertex positions
    const FStaticMeshVertexBuffer &TangentsAndTextureCoords =
        VertexBuffers
            .StaticMeshVertexBuffer; // contains tangents, texture coords and lightmap coords
    const FColorVertexBuffer &Color = VertexBuffers.ColorVertexBuffer; // vertex colours
    if (IndexBuffer.GetIndexDataSize() == 0)
    {
        UE_LOGFMT(LogDirectVideoMeshRenderer, Warning, "No indices for mesh");
        return false;
    }

    // make 1 buffer with:
    //   position (float * 3, packed as 4 byte)
    //   texture coordinates (float*2), packed as 4 byte
    int NumVertices = Pos.GetNumVertices();
    Positions.Init({}, NumVertices);
    int NumIndices = IndexBuffer.GetNumIndices();
    Indices.Init({}, NumIndices);

    for (int c = 0; c < NumVertices; c++)
    {
        const FVector3f &VPos = Pos.VertexPosition(c);
        Positions[c].x = VPos.X;
        Positions[c].y = VPos.Y;
        Positions[c].z = VPos.Z;
        FVector2f TexPos = TangentsAndTextureCoords.GetVertexUV(c, 0);
        Positions[c].u = TexPos.X;
        Positions[c].v = TexPos.Y;
    }

    // and 1 index buffer (int32)
    IndexBuffer.GetCopy(Indices);
    PositionsAsFloats.Init({}, NumVertices * 5);
    for (int c = 0; c < NumVertices; c++)
    {
        PositionsAsFloats[c * 5] = Positions[c].x;
        PositionsAsFloats[c * 5 + 1] = Positions[c].y;
        PositionsAsFloats[c * 5 + 2] = Positions[c].z;
        PositionsAsFloats[c * 5 + 3] = Positions[c].u;
        PositionsAsFloats[c * 5 + 4] = Positions[c].v;
    }
    return true;
}
