// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

// Shader-based scroll-edge fade effect.
//
// Inserts ImDrawCallback entries into the draw list to switch from ImGui's
// default shader to a custom shader that multiplies output alpha by a
// smoothstep gradient based on screen-Y position.  After the fade region,
// ImDrawCallback_ResetRenderState restores ImGui's default render state.
//
// GL  path: custom GLSL 130 program (VS + FS), uses gl_FragCoord.y
// DX11 path: custom pixel shader only, uses SV_POSITION.y

#pragma once

#include "imgui.h"
#include "imgui_internal.h"

// ---- Platform headers ----
#ifdef DRAGONX_HAS_GLAD
#include <glad/gl.h>
#endif

#ifdef DRAGONX_USE_DX11
#include <d3d11.h>
#include <d3dcompiler.h>
#endif

#include <cstdio>
#include <cstring>
#include <cstddef>
#include "../../util/logger.h"

namespace dragonx {
namespace ui {
namespace effects {

// ============================================================================
// ScrollFadeShader — per-pixel scroll-edge alpha mask via ImDrawCallback
// ============================================================================

struct ScrollFadeShader {
    // ---- Caller-set parameters (logical screen-space, per-frame) ----
    float fadeTopY       = 0.0f;  // top of scrollable region (screen Y)
    float fadeBottomY    = 0.0f;  // bottom of scrollable region (screen Y)
    float fadeZoneTop    = 0.0f;  // top fade height (0 = disabled)
    float fadeZoneBottom = 0.0f;  // bottom fade height (0 = disabled)

    // ---- Internal state ----
    bool ready = false;

#ifdef DRAGONX_HAS_GLAD
    GLuint program_       = 0;
    GLint  locProjMtx_    = -1;
    GLint  locTexture_    = -1;
    GLint  locFadeTopY_   = -1;
    GLint  locFadeBottomY_= -1;
    GLint  locFadeZoneTop_= -1;
    GLint  locFadeZoneBot_= -1;
    GLint  locViewportH_  = -1;
#endif

#ifdef DRAGONX_USE_DX11
    ID3D11PixelShader* pFadePS_ = nullptr;
    ID3D11Buffer*      pFadeCB_ = nullptr;
#endif

    // ----------------------------------------------------------------
    // Init / Destroy
    // ----------------------------------------------------------------

    bool init() {
        if (ready) return true;
#ifdef DRAGONX_HAS_GLAD
        ready = initGL();
#elif defined(DRAGONX_USE_DX11)
        ready = initDX11();
#endif
        return ready;
    }

    void destroy() {
#ifdef DRAGONX_HAS_GLAD
        destroyGL();
#elif defined(DRAGONX_USE_DX11)
        destroyDX11();
#endif
        ready = false;
    }

    // ----------------------------------------------------------------
    // Draw-list integration helpers
    // ----------------------------------------------------------------

    /// Insert a "bind fade shader" callback into the draw list.
    /// Must be followed later by addUnbind() or AddCallback(ImDrawCallback_ResetRenderState).
    void addBind(ImDrawList* dl) {
        dl->AddCallback(BindCB, this);
    }

    /// Insert ImDrawCallback_ResetRenderState to restore ImGui's default shader.
    static void addUnbind(ImDrawList* dl) {
        dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
    }

    // ================================================================
    // GL implementation
    // ================================================================
#ifdef DRAGONX_HAS_GLAD

    static constexpr const char* kFadeVS_130 =
#ifdef __APPLE__
        "#version 150\n"
#else
        "#version 130\n"
#endif
        "uniform mat4 ProjMtx;\n"
        "in vec2 Position;\n"
        "in vec2 UV;\n"
        "in vec4 Color;\n"
        "out vec2 Frag_UV;\n"
        "out vec4 Frag_Color;\n"
        "void main() {\n"
        "    Frag_UV = UV;\n"
        "    Frag_Color = Color;\n"
        "    gl_Position = ProjMtx * vec4(Position.xy, 0, 1);\n"
        "}\n";

    static constexpr const char* kFadeFS_130 =
#ifdef __APPLE__
        "#version 150\n"
#else
        "#version 130\n"
#endif
        "uniform sampler2D Texture;\n"
        "uniform float u_fadeTopY;\n"
        "uniform float u_fadeBottomY;\n"
        "uniform float u_fadeZoneTop;\n"
        "uniform float u_fadeZoneBot;\n"
        "uniform float u_viewportH;\n"
        "in vec2 Frag_UV;\n"
        "in vec4 Frag_Color;\n"
        "out vec4 Out_Color;\n"
        "void main() {\n"
        "    Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n"
        "    float y = u_viewportH - gl_FragCoord.y;\n"  // bottom-up → top-down
        "    float topA  = (u_fadeZoneTop > 0.0)\n"
        "        ? smoothstep(u_fadeTopY, u_fadeTopY + u_fadeZoneTop, y)\n"
        "        : 1.0;\n"
        "    float botA  = (u_fadeZoneBot > 0.0)\n"
        "        ? (1.0 - smoothstep(u_fadeBottomY - u_fadeZoneBot, u_fadeBottomY, y))\n"
        "        : 1.0;\n"
        "    Out_Color.a *= topA * botA;\n"
        "}\n";

    bool initGL() {
        GLuint vs = glCreateShader(GL_VERTEX_SHADER);
        GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(vs, 1, &kFadeVS_130, nullptr);
        glCompileShader(vs);
        if (!checkShader(vs, "fade VS")) { glDeleteShader(vs); glDeleteShader(fs); return false; }

        glShaderSource(fs, 1, &kFadeFS_130, nullptr);
        glCompileShader(fs);
        if (!checkShader(fs, "fade FS")) { glDeleteShader(vs); glDeleteShader(fs); return false; }

        program_ = glCreateProgram();
        glAttachShader(program_, vs);
        glAttachShader(program_, fs);

        // Force attribute locations to match ImGui's typical assignment
        // (Position=0, UV=1, Color=2) so that the VAO set up by ImGui's
        // backend is compatible with our program.
        glBindAttribLocation(program_, 0, "Position");
        glBindAttribLocation(program_, 1, "UV");
        glBindAttribLocation(program_, 2, "Color");

        glLinkProgram(program_);
        glDetachShader(program_, vs);
        glDetachShader(program_, fs);
        glDeleteShader(vs);
        glDeleteShader(fs);

        if (!checkProgram(program_, "fade program")) {
            glDeleteProgram(program_);
            program_ = 0;
            return false;
        }

        locProjMtx_     = glGetUniformLocation(program_, "ProjMtx");
        locTexture_     = glGetUniformLocation(program_, "Texture");
        locFadeTopY_    = glGetUniformLocation(program_, "u_fadeTopY");
        locFadeBottomY_ = glGetUniformLocation(program_, "u_fadeBottomY");
        locFadeZoneTop_ = glGetUniformLocation(program_, "u_fadeZoneTop");
        locFadeZoneBot_ = glGetUniformLocation(program_, "u_fadeZoneBot");
        locViewportH_   = glGetUniformLocation(program_, "u_viewportH");

        DEBUG_LOGF("ScrollFadeShader: GL program %u  (ProjMtx=%d Tex=%d top=%d bot=%d zt=%d zb=%d vh=%d)\n",
               program_, locProjMtx_, locTexture_,
               locFadeTopY_, locFadeBottomY_, locFadeZoneTop_, locFadeZoneBot_, locViewportH_);
        return true;
    }

    void destroyGL() {
        if (program_) { glDeleteProgram(program_); program_ = 0; }
    }

    static bool checkShader(GLuint s, const char* label) {
        GLint ok = 0;
        glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
        if (!ok) {
            char log[512];
            glGetShaderInfoLog(s, sizeof(log), nullptr, log);
            DEBUG_LOGF("ScrollFadeShader: %s compile error:\n%s\n", label, log);
        }
        return ok != 0;
    }

    static bool checkProgram(GLuint p, const char* label) {
        GLint ok = 0;
        glGetProgramiv(p, GL_LINK_STATUS, &ok);
        if (!ok) {
            char log[512];
            glGetProgramInfoLog(p, sizeof(log), nullptr, log);
            DEBUG_LOGF("ScrollFadeShader: %s link error:\n%s\n", label, log);
        }
        return ok != 0;
    }

#endif // DRAGONX_HAS_GLAD

    // ================================================================
    // DX11 implementation
    // ================================================================
#ifdef DRAGONX_USE_DX11

    // Pixel-shader constant buffer layout (must be 16-byte aligned)
    struct alignas(16) FadeCBData {
        float fadeTopY;
        float fadeBottomY;
        float fadeZoneTop;
        float fadeZoneBot;
    };

    static constexpr const char* kFadePS_HLSL =
        "cbuffer FadeParams : register(b1) {\n"
        "    float u_fadeTopY;\n"
        "    float u_fadeBottomY;\n"
        "    float u_fadeZoneTop;\n"
        "    float u_fadeZoneBot;\n"
        "};\n"
        "struct PS_INPUT {\n"
        "    float4 pos : SV_POSITION;\n"
        "    float4 col : COLOR0;\n"
        "    float2 uv  : TEXCOORD0;\n"
        "};\n"
        "sampler sampler0;\n"
        "Texture2D texture0;\n"
        "float4 main(PS_INPUT input) : SV_Target {\n"
        "    float4 out_col = input.col * texture0.Sample(sampler0, input.uv);\n"
        "    float y = input.pos.y;\n"  // SV_POSITION.y is top-down in FB pixels
        "    float topA = (u_fadeZoneTop > 0.0)\n"
        "        ? smoothstep(u_fadeTopY, u_fadeTopY + u_fadeZoneTop, y)\n"
        "        : 1.0;\n"
        "    float botA = (u_fadeZoneBot > 0.0)\n"
        "        ? (1.0 - smoothstep(u_fadeBottomY - u_fadeZoneBot, u_fadeBottomY, y))\n"
        "        : 1.0;\n"
        "    out_col.a *= topA * botA;\n"
        "    return out_col;\n"
        "}\n";

    bool initDX11() {
        // Get DX11 device from ImGui backend
        auto* bd = ImGui::GetIO().BackendRendererUserData;
        if (!bd) { DEBUG_LOGF("ScrollFadeShader: no DX11 backend data\n"); return false; }

        // ImGui_ImplDX11_Data layout: first field is ID3D11Device*
        ID3D11Device* device = *(ID3D11Device**)bd;
        if (!device) { DEBUG_LOGF("ScrollFadeShader: null DX11 device\n"); return false; }

        // Compile pixel shader
        ID3DBlob* psBlob = nullptr;
        ID3DBlob* errBlob = nullptr;
        HRESULT hr = D3DCompile(kFadePS_HLSL, strlen(kFadePS_HLSL),
                                nullptr, nullptr, nullptr,
                                "main", "ps_4_0", 0, 0,
                                &psBlob, &errBlob);
        if (FAILED(hr)) {
            if (errBlob) {
                DEBUG_LOGF("ScrollFadeShader: PS compile error:\n%s\n",
                       (const char*)errBlob->GetBufferPointer());
                errBlob->Release();
            }
            return false;
        }
        if (errBlob) errBlob->Release();

        hr = device->CreatePixelShader(psBlob->GetBufferPointer(),
                                       psBlob->GetBufferSize(),
                                       nullptr, &pFadePS_);
        psBlob->Release();
        if (FAILED(hr)) {
            DEBUG_LOGF("ScrollFadeShader: CreatePixelShader failed 0x%08lx\n", hr);
            return false;
        }

        // Create constant buffer for fade params
        D3D11_BUFFER_DESC cbDesc = {};
        cbDesc.ByteWidth      = sizeof(FadeCBData);
        cbDesc.Usage           = D3D11_USAGE_DYNAMIC;
        cbDesc.BindFlags       = D3D11_BIND_CONSTANT_BUFFER;
        cbDesc.CPUAccessFlags  = D3D11_CPU_ACCESS_WRITE;
        hr = device->CreateBuffer(&cbDesc, nullptr, &pFadeCB_);
        if (FAILED(hr)) {
            DEBUG_LOGF("ScrollFadeShader: CreateBuffer (CB) failed 0x%08lx\n", hr);
            pFadePS_->Release(); pFadePS_ = nullptr;
            return false;
        }

        VERBOSE_LOGF("ScrollFadeShader: DX11 pixel shader + CB created\n");
        return true;
    }

    void destroyDX11() {
        if (pFadeCB_) { pFadeCB_->Release(); pFadeCB_ = nullptr; }
        if (pFadePS_) { pFadePS_->Release(); pFadePS_ = nullptr; }
    }

#endif // DRAGONX_USE_DX11

    // ================================================================
    // Callback — called by ImGui's render backend during RenderDrawData
    // ================================================================

    static void BindCB(const ImDrawList* /*dl*/, const ImDrawCmd* cmd) {
        auto* self = static_cast<ScrollFadeShader*>(cmd->UserCallbackData);
        if (!self || !self->ready) return;

        ImGuiIO& io = ImGui::GetIO();
        float fbScaleY = io.DisplayFramebufferScale.y;
        float fb_height = io.DisplaySize.y * fbScaleY;

        // In multi-viewport mode ImGui coordinates are OS-absolute, but
        // SV_POSITION / gl_FragCoord are render-target-relative.  Subtract
        // the main viewport origin to convert screen-space → RT-local.
        float vpY = ImGui::GetMainViewport()->Pos.y;

        // Convert logical screen-space params to framebuffer pixels
        float topY    = (self->fadeTopY    - vpY) * fbScaleY;
        float botY    = (self->fadeBottomY - vpY) * fbScaleY;
        float zoneTop = self->fadeZoneTop * fbScaleY;
        float zoneBot = self->fadeZoneBottom * fbScaleY;

#ifdef DRAGONX_HAS_GLAD
        glUseProgram(self->program_);

        // Rebind vertex attribute pointers so our forced locations (0=Position,
        // 1=UV, 2=Color) match the already-bound VBO data.  ImGui's linker may
        // assign different locations (e.g. Mesa uses alphabetical order:
        // Color=0, Position=1, UV=2), but the VBO layout is fixed, so we
        // must explicitly point our attributes at the correct offsets.
        glVertexAttribPointer(0, 2, GL_FLOAT,         GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, pos));
        glVertexAttribPointer(1, 2, GL_FLOAT,         GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, uv));
        glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE,  sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, col));
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glEnableVertexAttribArray(2);

        // Ortho projection — must match ImGui's SetupRenderState which uses
        // draw_data->DisplayPos as the origin.  With ViewportsEnable, vertex
        // positions are in OS screen-space; the projection matrix maps them
        // to window-local framebuffer coordinates.
        float vpX = ImGui::GetMainViewport()->Pos.x;
        float L = vpX;
        float R = vpX + io.DisplaySize.x;
        float T = vpY;
        float B = vpY + io.DisplaySize.y;
        const float ortho[4][4] = {
            { 2.0f/(R-L),   0.0f,         0.0f,  0.0f },
            { 0.0f,         2.0f/(T-B),   0.0f,  0.0f },
            { 0.0f,         0.0f,        -1.0f,  0.0f },
            { (R+L)/(L-R),  (T+B)/(B-T),  0.0f,  1.0f },
        };
        glUniformMatrix4fv(self->locProjMtx_, 1, GL_FALSE, &ortho[0][0]);
        glUniform1i(self->locTexture_, 0);
        glUniform1f(self->locFadeTopY_,    topY);
        glUniform1f(self->locFadeBottomY_, botY);
        glUniform1f(self->locFadeZoneTop_, zoneTop);
        glUniform1f(self->locFadeZoneBot_, zoneBot);
        glUniform1f(self->locViewportH_,   fb_height);
#endif

#ifdef DRAGONX_USE_DX11
        (void)fb_height;
        // Get device context from ImGui backend (first two fields of ImGui_ImplDX11_Data)
        auto* bd = io.BackendRendererUserData;
        if (!bd) return;
        struct DX11BD { ID3D11Device* dev; ID3D11DeviceContext* ctx; };
        auto* dx = reinterpret_cast<DX11BD*>(bd);
        ID3D11DeviceContext* ctx = dx->ctx;
        if (!ctx) return;

        // Update constant buffer
        D3D11_MAPPED_SUBRESOURCE mapped;
        if (SUCCEEDED(ctx->Map(self->pFadeCB_, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) {
            auto* cb = static_cast<FadeCBData*>(mapped.pData);
            cb->fadeTopY    = topY;
            cb->fadeBottomY = botY;
            cb->fadeZoneTop = zoneTop;
            cb->fadeZoneBot = zoneBot;
            ctx->Unmap(self->pFadeCB_, 0);
        }

        // Bind our pixel shader + constant buffer (slot 1 = register(b1))
        ctx->PSSetShader(self->pFadePS_, nullptr, 0);
        ctx->PSSetConstantBuffers(1, 1, &self->pFadeCB_);
#endif
    }
};

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