// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Offscreen render-target scroll fade — the ImGui equivalent of CSS mask-image.
// Renders scrollable content to an offscreen surface, then composites it back
// as a textured mesh strip with vertex alpha for edge fading.
// This produces a true per-pixel fade that works with any background
// (including acrylic/backdrop transparency).
//
// Supports both OpenGL (DRAGONX_HAS_GLAD) and DX11 (DRAGONX_USE_DX11).

#pragma once

#include "imgui.h"
#include "imgui_internal.h"
#include <cstdio>

// ============================================================================
// Platform detection
// ============================================================================
#if defined(DRAGONX_USE_DX11)
  #include <d3d11.h>
  #define SCROLL_FADE_HAS_OFFSCREEN 1
  #define SCROLL_FADE_DX11          1
#elif defined(DRAGONX_HAS_GLAD)
  #include <glad/gl.h>
#include "../../util/logger.h"
  #ifndef GL_FRAMEBUFFER_BINDING
  #define GL_FRAMEBUFFER_BINDING 0x8CA6
  #endif
  #ifndef GL_VIEWPORT
  #define GL_VIEWPORT 0x0BA2
  #endif
  #ifndef GL_SCISSOR_TEST
  #define GL_SCISSOR_TEST 0x0C11
  #endif
  #define SCROLL_FADE_HAS_OFFSCREEN 1
  #define SCROLL_FADE_GL             1
#endif

#ifdef SCROLL_FADE_HAS_OFFSCREEN

namespace dragonx {
namespace ui {
namespace effects {

// ============================================================================
// ScrollFadeRT — manages an offscreen render target for scroll-fade rendering
// ============================================================================

class ScrollFadeRT {
public:
    ScrollFadeRT() = default;
    ~ScrollFadeRT() { destroy(); }

    // Non-copyable
    ScrollFadeRT(const ScrollFadeRT&) = delete;
    ScrollFadeRT& operator=(const ScrollFadeRT&) = delete;

    /// Ensure RT matches the required dimensions.  Returns true if ready.
    bool ensure(int w, int h) {
        if (w <= 0 || h <= 0) return false;
        if (isValid() && w == width_ && h == height_) return true;
        return init(w, h);
    }

    void destroy();
    bool isValid() const;

    /// Get the texture as an ImTextureID for compositing.
    ImTextureID textureID() const;

    int width()  const { return width_; }
    int height() const { return height_; }

#ifdef SCROLL_FADE_DX11
    ID3D11RenderTargetView* rtv() const { return rtv_; }
#endif
#ifdef SCROLL_FADE_GL
    unsigned int fbo() const { return fbo_; }
#endif

private:
    bool init(int w, int h);

    int width_  = 0;
    int height_ = 0;

#ifdef SCROLL_FADE_DX11
    ID3D11Texture2D*          tex_  = nullptr;
    ID3D11RenderTargetView*   rtv_  = nullptr;
    ID3D11ShaderResourceView* srv_  = nullptr;
#endif
#ifdef SCROLL_FADE_GL
    unsigned int fbo_      = 0;
    unsigned int colorTex_ = 0;
#endif
};

// ============================================================================
// Implementations
// ============================================================================

#ifdef SCROLL_FADE_DX11

// --- DX11 helpers to get device/context from ImGui backend ---
inline ID3D11Device* GetDX11Device() {
    ImGuiIO& io = ImGui::GetIO();
    if (!io.BackendRendererUserData) return nullptr;
    return *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
}
inline ID3D11DeviceContext* GetDX11Context() {
    ID3D11Device* dev = GetDX11Device();
    if (!dev) return nullptr;
    ID3D11DeviceContext* ctx = nullptr;
    dev->GetImmediateContext(&ctx);
    return ctx; // caller must Release()
}

inline bool ScrollFadeRT::init(int w, int h) {
    destroy();
    ID3D11Device* dev = GetDX11Device();
    if (!dev) return false;

    width_  = w;
    height_ = h;

    // Create texture
    D3D11_TEXTURE2D_DESC td = {};
    td.Width      = (UINT)w;
    td.Height     = (UINT)h;
    td.MipLevels  = 1;
    td.ArraySize  = 1;
    td.Format     = DXGI_FORMAT_R8G8B8A8_UNORM;
    td.SampleDesc.Count = 1;
    td.Usage      = D3D11_USAGE_DEFAULT;
    td.BindFlags  = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;

    if (FAILED(dev->CreateTexture2D(&td, nullptr, &tex_))) {
        DEBUG_LOGF("ScrollFadeRT: CreateTexture2D failed\n");
        destroy();
        return false;
    }

    // Render target view
    if (FAILED(dev->CreateRenderTargetView(tex_, nullptr, &rtv_))) {
        DEBUG_LOGF("ScrollFadeRT: CreateRenderTargetView failed\n");
        destroy();
        return false;
    }

    // Shader resource view (for sampling as texture)
    if (FAILED(dev->CreateShaderResourceView(tex_, nullptr, &srv_))) {
        DEBUG_LOGF("ScrollFadeRT: CreateShaderResourceView failed\n");
        destroy();
        return false;
    }

    return true;
}

inline void ScrollFadeRT::destroy() {
    if (srv_) { srv_->Release(); srv_ = nullptr; }
    if (rtv_) { rtv_->Release(); rtv_ = nullptr; }
    if (tex_) { tex_->Release(); tex_ = nullptr; }
    width_ = height_ = 0;
}

inline bool ScrollFadeRT::isValid() const { return rtv_ != nullptr; }

inline ImTextureID ScrollFadeRT::textureID() const {
    return (ImTextureID)srv_;
}

#endif // SCROLL_FADE_DX11

#ifdef SCROLL_FADE_GL

inline bool ScrollFadeRT::init(int w, int h) {
    destroy();
    width_  = w;
    height_ = h;

    glGenFramebuffers(1, &fbo_);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo_);

    glGenTextures(1, &colorTex_);
    glBindTexture(GL_TEXTURE_2D, colorTex_);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                           GL_TEXTURE_2D, colorTex_, 0);

    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindTexture(GL_TEXTURE_2D, 0);

    if (status != GL_FRAMEBUFFER_COMPLETE) {
        DEBUG_LOGF("ScrollFadeRT: FBO incomplete (0x%X)\n", status);
        destroy();
        return false;
    }
    return true;
}

inline void ScrollFadeRT::destroy() {
    if (colorTex_) { glDeleteTextures(1, &colorTex_); colorTex_ = 0; }
    if (fbo_)      { glDeleteFramebuffers(1, &fbo_);  fbo_ = 0; }
    width_ = height_ = 0;
}

inline bool ScrollFadeRT::isValid() const { return fbo_ != 0; }

inline ImTextureID ScrollFadeRT::textureID() const {
    return (ImTextureID)(intptr_t)colorTex_;
}

#endif // SCROLL_FADE_GL

// ============================================================================
// Callback state — singleton storage for bind/unbind data
// ============================================================================

struct ScrollFadeState {
#ifdef SCROLL_FADE_DX11
    ID3D11RenderTargetView* offscreenRTV = nullptr;
    ID3D11RenderTargetView* savedRTV     = nullptr;
    ID3D11DepthStencilView* savedDSV     = nullptr;
    D3D11_VIEWPORT          savedVP      = {};
#endif
#ifdef SCROLL_FADE_GL
    unsigned int fbo = 0;
    int savedFBO     = 0;
    int savedVP[4]   = {};
    bool savedScissorEnabled = true;  // ImGui always has scissor enabled
#endif
    int vpW = 0, vpH = 0;  // framebuffer pixel dimensions for viewport
};

inline ScrollFadeState& GetScrollFadeState() {
    static ScrollFadeState s;
    return s;
}

// ============================================================================
// Callbacks — inserted into the draw list via AddCallback
// ============================================================================

#ifdef SCROLL_FADE_DX11

inline void BindRTCallback(const ImDrawList*, const ImDrawCmd*) {
    auto& st = GetScrollFadeState();
    ID3D11DeviceContext* ctx = GetDX11Context();
    if (!ctx) return;

    // Save current RT and viewport
    UINT numVP = 1;
    ctx->OMGetRenderTargets(1, &st.savedRTV, &st.savedDSV);
    ctx->RSGetViewports(&numVP, &st.savedVP);

    // Bind offscreen RT
    ctx->OMSetRenderTargets(1, &st.offscreenRTV, nullptr);

    // Set viewport to match RT size
    D3D11_VIEWPORT vp = {};
    vp.Width    = (FLOAT)st.vpW;
    vp.Height   = (FLOAT)st.vpH;
    vp.MaxDepth = 1.0f;
    ctx->RSSetViewports(1, &vp);

    // Clear to transparent
    float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
    ctx->ClearRenderTargetView(st.offscreenRTV, clearColor);

    ctx->Release();
}

inline void UnbindRTCallback(const ImDrawList*, const ImDrawCmd*) {
    auto& st = GetScrollFadeState();
    ID3D11DeviceContext* ctx = GetDX11Context();
    if (!ctx) return;

    // Restore previous RT and viewport
    ctx->OMSetRenderTargets(1, &st.savedRTV, st.savedDSV);
    ctx->RSSetViewports(1, &st.savedVP);

    // Release the refs from OMGetRenderTargets
    if (st.savedRTV) { st.savedRTV->Release(); st.savedRTV = nullptr; }
    if (st.savedDSV) { st.savedDSV->Release(); st.savedDSV = nullptr; }

    ctx->Release();
}

#endif // SCROLL_FADE_DX11

#ifdef SCROLL_FADE_GL

inline void BindRTCallback(const ImDrawList*, const ImDrawCmd*) {
    auto& st = GetScrollFadeState();

    // Save current FBO and viewport
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &st.savedFBO);
    glGetIntegerv(GL_VIEWPORT, st.savedVP);

    glBindFramebuffer(GL_FRAMEBUFFER, st.fbo);
    glViewport(0, 0, st.vpW, st.vpH);

    // Disable scissor test inside the FBO.  ImGui's renderer computes
    // scissor rects relative to the main framebuffer dimensions — those
    // coordinates would be wrong for our offscreen surface.  The child
    // window's content is already bounded by ImGui's layout, and the
    // composite step applies its own clip rect, so skipping scissor
    // in the FBO is safe.
    glDisable(GL_SCISSOR_TEST);

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
}

inline void UnbindRTCallback(const ImDrawList*, const ImDrawCmd*) {
    auto& st = GetScrollFadeState();
    glBindFramebuffer(GL_FRAMEBUFFER, (unsigned int)st.savedFBO);
    glViewport(st.savedVP[0], st.savedVP[1], st.savedVP[2], st.savedVP[3]);
    if (st.savedScissorEnabled)
        glEnable(GL_SCISSOR_TEST);
}

#endif // SCROLL_FADE_GL

// ============================================================================
// Composite helper — draw the RT texture as a mesh strip with alpha fade
// ============================================================================

/// Draw the offscreen texture onto `dl` as a vertical strip with alpha=0 at
/// the faded edges and alpha=1 in the middle.  Produces a true CSS-like
/// mask-image: linear-gradient() result.
///
/// @param logicalW, logicalH  Logical display dimensions (DisplaySize) for
///        UV calculation — NOT the RT pixel dimensions.  ImGui screen coords
///        are in logical units, and the FBO projection maps them 1:1 to the
///        logical coordinate space, so UVs must divide by logical size.
inline void CompositeWithFade(ImDrawList* dl,
                              ImTextureID texID,
                              const ImVec2& screenMin,
                              const ImVec2& screenMax,
                              int logicalW, int logicalH,
                              float fadeTop, float fadeBot,
                              bool needTop, bool needBot)
{
    float left  = screenMin.x;
    float right = screenMax.x;
    float y0 = screenMin.y;
    float y1 = screenMin.y + (needTop ? fadeTop : 0.0f);
    float y2 = screenMax.y - (needBot ? fadeBot : 0.0f);
    float y3 = screenMax.y;

    // Clamp in case fade zones overlap
    if (y1 > y2) { float mid = (y0 + y3) * 0.5f; y1 = y2 = mid; }

    // UV coordinates — map screen position (logical) to render target texture.
    // Screen coords are in logical (DisplaySize) space.  The FBO projection
    // maps these 1:1, so divide by logical dimensions to get [0,1] UVs.
    float uL = screenMin.x / (float)logicalW;
    float uR = screenMax.x / (float)logicalW;

#ifdef SCROLL_FADE_GL
    // OpenGL: FBO Y is flipped (ImGui top=0 → GL bottom=0)
    auto uvY = [&](float y) -> float { return 1.0f - y / (float)logicalH; };
#else
    // DX11: no Y flip (both ImGui and DX11 have (0,0) at top-left)
    auto uvY = [&](float y) -> float { return y / (float)logicalH; };
#endif

    ImU32 colOpaque = IM_COL32(255, 255, 255, 255);
    ImU32 colClear  = IM_COL32(255, 255, 255, 0);
    ImU32 colTop = needTop ? colClear  : colOpaque;
    ImU32 colBot = needBot ? colClear  : colOpaque;

    dl->PushTextureID(texID);
    dl->PrimReserve(18, 8);

    ImDrawVert* vtx = dl->_VtxWritePtr;
    ImDrawIdx*  idx = dl->_IdxWritePtr;
    ImDrawIdx   base = (ImDrawIdx)dl->_VtxCurrentIdx;

    vtx[0] = { ImVec2(left,  y0), ImVec2(uL, uvY(y0)), colTop };
    vtx[1] = { ImVec2(right, y0), ImVec2(uR, uvY(y0)), colTop };
    vtx[2] = { ImVec2(left,  y1), ImVec2(uL, uvY(y1)), colOpaque };
    vtx[3] = { ImVec2(right, y1), ImVec2(uR, uvY(y1)), colOpaque };
    vtx[4] = { ImVec2(left,  y2), ImVec2(uL, uvY(y2)), colOpaque };
    vtx[5] = { ImVec2(right, y2), ImVec2(uR, uvY(y2)), colOpaque };
    vtx[6] = { ImVec2(left,  y3), ImVec2(uL, uvY(y3)), colBot };
    vtx[7] = { ImVec2(right, y3), ImVec2(uR, uvY(y3)), colBot };

    idx[0]  = base+0; idx[1]  = base+1; idx[2]  = base+3;
    idx[3]  = base+0; idx[4]  = base+3; idx[5]  = base+2;
    idx[6]  = base+2; idx[7]  = base+3; idx[8]  = base+5;
    idx[9]  = base+2; idx[10] = base+5; idx[11] = base+4;
    idx[12] = base+4; idx[13] = base+5; idx[14] = base+7;
    idx[15] = base+4; idx[16] = base+7; idx[17] = base+6;

    dl->_VtxWritePtr  += 8;
    dl->_IdxWritePtr  += 18;
    dl->_VtxCurrentIdx += 8;

    dl->PopTextureID();
}

} // namespace effects
} // namespace ui
} // namespace dragonx

#endif // SCROLL_FADE_HAS_OFFSCREEN
