/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */
#pragma once

#include <osgEarthImGui/ImGuiPanel>
#include <osgEarth/Threading>
#include <osgEarth/MemoryUtils>
#include <osgEarth/GLUtils>
#include <osgEarth/ShaderLoader>
#include <chrono>
#include <list>
#include <unordered_map>

namespace {
    const char* render_view_normals = R"(
#version 330
#pragma vp_function oeui_render_view_normals, fragment_lighting, last
in vec3 vp_Normal;
uniform float oe_render_blend = 1.0;
void oeui_render_view_normals(inout vec4 color) {
    color = mix(color, vec4((vp_Normal+1.0)*0.5, 1), oe_render_blend);
}
)";

    const char* render_model_normals = R"(
#version 330
#pragma vp_function oeui_render_model_normals_vs, vertex_model, last
out vec3 vp_Normal;
out vec3 oeui_model_normal;
void oeui_render_model_normals_vs(inout vec4 vertex) {
    oeui_model_normal = vp_Normal;
}
[break]
#version 330
#pragma vp_function oeui_render_view_normals_fs, fragment_lighting, last
in vec3 oeui_model_normal;
uniform float oe_render_blend = 1.0;
void oeui_render_view_normals_fs(inout vec4 color) {
    color = mix(color, vec4((normalize(oeui_model_normal)+1.0)*0.5, 1), oe_render_blend);
}
)";

    const char* render_fb_normals = R"(
#version 330
#pragma vp_function oeui_render_fb_normals, fragment_lighting, last
in vec3 vp_Normal;
uniform float oe_render_blend = 1.0;
void oeui_render_fb_normals(inout vec4 color) {
    float a = step(0.5, color.a);
    float nz = normalize(vp_Normal).z;
    color = mix(color, vec4(mix(vec3(0,0,0), vec3(1,1,1), (nz+1.0)*0.5), a), oe_render_blend);
}
)";

    const char* render_winding = R"(
#version 450
#extension GL_NV_fragment_shader_barycentric : enable
#pragma vp_function oeui_render_winding_fs, fragment_lighting, last
uniform float oe_render_blend = 1.0;
void oeui_render_winding_fs(inout vec4 color) {
    vec3 c = gl_FrontFacing ? vec3(0,0.75,0) : vec3(1,0,0);
    float b = min(gl_BaryCoordNV.x, min(gl_BaryCoordNV.y, gl_BaryCoordNV.z))*28.0;
    color = mix(color, vec4(mix(vec3(1), c.rgb, clamp(b,0,1)), 1.0), oe_render_blend);
}
)";

    const char* render_outlines = R"(
#version 450
#extension GL_NV_fragment_shader_barycentric : enable
#pragma vp_function oeui_render_outlines, fragment_lighting, last
#define VP_STAGE_FRAGMENT
uniform float oe_render_blend = 1.0;
void oeui_render_outlines(inout vec4 color) {
    float b = min(gl_BaryCoordNV.x, min(gl_BaryCoordNV.y, gl_BaryCoordNV.z))*32.0;
    float mono = dot(color.rgb, vec3(0.299, 0.587, 0.114));
    mono = mod(mono + 0.25, 1.0); 
    color = mix(color, vec4(mix(vec3(mono), color.rgb, clamp(b,0,1)), color.a), oe_render_blend);
}
)";

    const char* render_ao = R"(
#version 330
#pragma vp_function oeui_render_ao, fragment_lighting, last
struct OE_PBR { float displacement, roughness, ao, metal; } oe_pbr;
uniform float oe_render_blend = 1.0;
void oeui_render_ao(inout vec4 color) {
    color = mix(color, vec4(oe_pbr.ao, oe_pbr.ao, oe_pbr.ao, 1), oe_render_blend);
}
)";

    const char* render_roughness = R"(
#version 330
#pragma vp_function oeui_render_roughness, fragment_lighting, last
struct OE_PBR { float displacement, roughness, ao, metal; } oe_pbr;
uniform float oe_render_blend = 1.0;
void oeui_render_roughness(inout vec4 color) {
    color = mix(color, vec4(oe_pbr.roughness, oe_pbr.roughness, oe_pbr.roughness, 1), oe_render_blend);
}
)";

    const char* render_metal = R"(
#version 330
#pragma vp_function oeui_render_metal, fragment_lighting, last
struct OE_PBR { float displacement, roughness, ao, metal; } oe_pbr;
uniform float oe_render_blend = 1.0;
void oeui_render_metal(inout vec4 color) {
    color = mix(color, vec4(oe_pbr.metal, oe_pbr.metal, oe_pbr.metal, 1), oe_render_blend);
}
)";

    const char* render_displacement = R"(
#version 330
#pragma vp_function oeui_render_metal, fragment_lighting, last
struct OE_PBR { float displacement, roughness, ao, metal; } oe_pbr;
uniform float oe_render_blend = 1.0;
void oeui_render_metal(inout vec4 color) {
    color = mix(color, vec4(oe_pbr.displacement, oe_pbr.displacement, oe_pbr.displacement, 1), oe_render_blend);
}
)";

    const char* render_draw_id = R"(
#version 460
#pragma vp_function oeui_render_drawid_vs, vertex_model
flat out int oeui_drawid;
void oeui_render_drawid_vs(inout vec4 vertex) {
    oeui_drawid = gl_DrawID;
}
[break]
#version 460
#pragma vp_function oeui_render_drawid, fragment_lighting, last
flat in int oeui_drawid;
const vec3 colors[32] = { // thanks, GPT
    vec3(1.0, 0.0, 0.0),   // Red
    vec3(0.0, 1.0, 0.0),   // Green
    vec3(0.0, 0.0, 1.0),   // Blue
    vec3(1.0, 1.0, 0.0),   // Yellow
    vec3(1.0, 0.0, 1.0),   // Magenta
    vec3(0.0, 1.0, 1.0),   // Cyan
    vec3(1.0, 0.5, 0.0),   // Orange
    vec3(0.5, 1.0, 0.0),   // Lime
    vec3(0.0, 0.5, 1.0),   // Sky Blue
    vec3(0.5, 0.0, 1.0),   // Purple
    vec3(1.0, 0.5, 0.5),   // Light Red
    vec3(0.5, 1.0, 0.5),   // Light Green
    vec3(0.5, 0.5, 1.0),   // Light Blue
    vec3(1.0, 1.0, 0.5),   // Light Yellow
    vec3(1.0, 0.5, 1.0),   // Light Magenta
    vec3(0.5, 1.0, 1.0),   // Light Cyan
    vec3(0.8, 0.2, 0.2),   // Dark Red
    vec3(0.2, 0.8, 0.2),   // Dark Green
    vec3(0.2, 0.2, 0.8),   // Dark Blue
    vec3(0.8, 0.8, 0.2),   // Dark Yellow
    vec3(0.8, 0.2, 0.8),   // Dark Magenta
    vec3(0.2, 0.8, 0.8),   // Dark Cyan
    vec3(0.8, 0.5, 0.2),   // Brown
    vec3(0.5, 0.8, 0.2),   // Olive Green
    vec3(0.2, 0.5, 0.8),   // Steel Blue
    vec3(0.5, 0.2, 0.8),   // Indigo
    vec3(0.8, 0.5, 0.5),   // Salmon
    vec3(0.5, 0.8, 0.5),   // Light Olive Green
    vec3(0.5, 0.5, 0.8),   // Cornflower Blue
    vec3(0.8, 0.8, 0.5),   // Light Khaki
    vec3(0.8, 0.5, 0.8),   // Orchid
    vec3(0.5, 0.8, 0.8)    // Light Slate Gray
};
uniform float oe_render_blend = 1.0;
void oeui_render_drawid(inout vec4 color) {
    color.rgb = mix(color.rgb, colors[oeui_drawid % 32], oe_render_blend);
}
)";

    const char* render_elevation_marker = R"(
#version 330
#pragma vp_function oeui_render_elevation_marker_vs, vertex_view
out vec4 oeui_color;
flat out int oe_terrain_vertexMarker;
void oeui_render_elevation_marker_vs(inout vec4 vertex) {
    oeui_color = vec4(0);
    int marker = oe_terrain_vertexMarker;
    if ((marker & 4) > 0) // HAS_ELEVATION
        oeui_color = vec4(1,0,0,1);
    else if ((marker & 16) > 0) // CONSTRAINT
        oeui_color = vec4(1,1,0,1);
}
[break]
#pragma vp_function oeui_render_elevation_marker_fs, fragment_lighting, last
in vec4 oeui_color;
uniform float oe_render_blend = 1.0;
void oeui_render_elevation_marker_fs(inout vec4 color) {
    color.rgb = mix(color.rgb, mix(color.rgb, oeui_color.rgb, oeui_color.a), oe_render_blend);
}
)";

    const char* render_lod = R"(
#version 460
#extension GL_NV_fragment_shader_barycentric : enable
#pragma vp_function oeui_render_lod, fragment, last
flat in vec4 oe_tile_key;
const vec3 colors[8] = { // thanks, GPT
    vec3(1.0, 0.0, 0.0),   // Red
    vec3(0.0, 1.0, 0.0),   // Green
    vec3(0.0, 0.0, 1.0),   // Blue
    vec3(1.0, 1.0, 0.0),   // Yellow
    vec3(1.0, 0.0, 1.0),   // Magenta
    vec3(0.0, 1.0, 1.0),   // Cyan
    vec3(1.0, 0.5, 0.0),   // Orange
    vec3(0.5, 1.0, 0.0)    // Lime
};
uniform float oe_render_blend = 1.0;
void oeui_render_lod(inout vec4 color) {
    float b = min(gl_BaryCoordNV.x, min(gl_BaryCoordNV.y, gl_BaryCoordNV.z)) * 32.0;
    vec3 temp = mix(vec3(0), colors[int(oe_tile_key.z) % 8], clamp(b, 0, 1));
    color.rgb = mix(color.rgb, temp, oe_render_blend);
}
)";
}

namespace osgEarth
{
    using namespace osgEarth::Threading;

    class RenderingGUI : public ImGuiPanel
    {
    private:
        osg::observer_ptr<MapNode> _mapNode;
        using time_point = std::chrono::time_point<std::chrono::steady_clock>;
        time_point _lastFrame;
        std::queue<int> _times;
        int _time_accum;
        int _frameCounter;
        int _fps;
        std::string _renderMode;
        bool _renderViewNormals;
        bool _renderModelNormals;
        bool _renderWinding;
        bool _renderOutlines;

    public:
        RenderingGUI() : ImGuiPanel("Rendering"),
            _frameCounter(0), _time_accum(0),
            _renderViewNormals(false), _renderModelNormals(false),
            _renderWinding(false), _renderOutlines(false) { }

        void load(const Config& conf) override
        {
        }

        void save(Config& conf) override
        {
        }

        void setRenderMode(const std::string& mode, osg::RenderInfo& ri)
        {
            auto* vp = VirtualProgram::getOrCreate(stateset(ri));
            if (!_renderMode.empty())
                ShaderLoader::unload(vp, _renderMode);
            _renderMode = mode;
            if (!_renderMode.empty())
                ShaderLoader::load(vp, _renderMode);
        }

        void draw(osg::RenderInfo& ri) override
        {
            if (!isVisible())
                return;

            if (!findNodeOrHide(_mapNode, ri))
                return;

            ImGui::Begin(name(), visible());
            {
                if (ImGuiLTable::Begin("LOD"))
                {
                    float sse = _mapNode->getScreenSpaceError();
                    if (ImGuiLTable::SliderFloat("SSE", &sse, 1.0f, 200.0f))
                        _mapNode->setScreenSpaceError(sse);

                    float lod_scale = camera(ri)->getLODScale();
                    if (ImGuiLTable::SliderFloat("LOD Scale", &lod_scale, 0.1f, 4.0f))
                        camera(ri)->setLODScale(lod_scale);

                    ImGuiLTable::End();
                }

                ImGui::Separator();

                ImGui::TextColored(ImVec4(1, 1, 0, 1), "Overlays");
                static int s_renderMode = 0;
                static float s_renderBlend = 1.0f;
                int m = 0;

                if (ImGui::RadioButton("Off", &s_renderMode, m++)) {
                    setRenderMode("", ri);
                }
                if (ImGui::RadioButton("Wireframe overlay", &s_renderMode, m++)) {
                    setRenderMode(render_outlines, ri);
                }
                if (ImGui::RadioButton("Front/backfacing triangles", &s_renderMode, m++)) {
                    setRenderMode(render_winding, ri);
                }
                if (ImGui::RadioButton("Normals (front/back)", &s_renderMode, m++)) {
                    setRenderMode(render_fb_normals, ri);
                }
                if (ImGui::RadioButton("Normals (view space)", &s_renderMode, m++)) {
                    setRenderMode(render_view_normals, ri);
                }
                if (ImGui::RadioButton("Normals (model space)", &s_renderMode, m++)) {
                    setRenderMode(render_model_normals, ri);
                }
                if (ImGui::RadioButton("Terrain LOD", &s_renderMode, m++)) {
                    setRenderMode(render_lod, ri);
                }
                if (ImGui::RadioButton("Metal (PBR)", &s_renderMode, m++)) {
                    setRenderMode(render_metal, ri);
                }
                if (ImGui::RadioButton("Roughness (PBR)", &s_renderMode, m++)) {
                    setRenderMode(render_roughness, ri);
                }
                if (ImGui::RadioButton("AO (PBR)", &s_renderMode, m++)) {
                    setRenderMode(render_ao, ri);
                }
                if (ImGui::RadioButton("Displacement (PBR)", &s_renderMode, m++)) {
                    setRenderMode(render_displacement, ri);
                }
                if (ImGui::RadioButton("Draw ID", &s_renderMode, m++)) {
                    setRenderMode(render_draw_id, ri);
                }
                if (ImGui::RadioButton("Elevation markers", &s_renderMode, m++)) {
                    setRenderMode(render_elevation_marker, ri);
                }

                if (GLUtils::useNVGL())
                {
                    static bool s_gpuculldebug = false;
                    if (ImGui::Checkbox("GPU cull debug view", &s_gpuculldebug)) {
                        stateset(ri)->removeDefine("OE_GPUCULL_DEBUG");
                        if (s_gpuculldebug)
                            stateset(ri)->setDefine("OE_GPUCULL_DEBUG", "1");
                        else
                            stateset(ri)->setDefine("OE_GPUCULL_DEBUG", "0");
                    }
                }

                ImGui::Separator();

                if (ImGui::SliderFloat("Blend", &s_renderBlend, 0.0f, 1.0f)) {
                    stateset(ri)->getOrCreateUniform("oe_render_blend", osg::Uniform::FLOAT)->set(s_renderBlend);
                }

                ImGui::Separator();

                const osgViewer::ViewerBase::ThreadingModel models[] = {
                    osgViewer::ViewerBase::SingleThreaded,
                    osgViewer::ViewerBase::DrawThreadPerContext,
                    osgViewer::ViewerBase::CullDrawThreadPerContext,
                    osgViewer::ViewerBase::CullThreadPerCameraDrawThreadPerContext
                };
                const std::string modelNames[] = {
                    "SingleThreaded",
                    "DrawThreadPerContext",
                    "CullDrawThreadPerContext",
                    "CullThreadPerCameraDrawThreadPerContext"
                };

                auto vb = view(ri)->getViewerBase();
                int tmi;
                for (tmi = 0; tmi < 4; ++tmi)
                    if (models[tmi] == vb->getThreadingModel())
                        break;

                ImGui::Text("OSG Threading Model: ");
                ImGui::SameLine();
                if (ImGui::Button(modelNames[tmi].c_str())) {
                    auto new_tm = models[(tmi + 1) % 4];
                    vb->addUpdateOperation(new OneTimer([vb, new_tm]() {
                        vb->setThreadingModel(new_tm); }));
                }
            }
            ImGui::End();
        }
    };


    class NVGLInspectorGUI : public ImGuiPanel
    {
        GLint _expanded = -1;

    public:
        NVGLInspectorGUI() : ImGuiPanel("NVGL Inspector")
        {
        }

        void load(const Config& conf) override
        {
        }

        void save(Config& conf) override
        {
        }

        void draw(osg::RenderInfo& ri) override
        {
            if (!isVisible())
                return;

            ImGui::Begin(name(), visible());
            {
                if (!GLUtils::useNVGL())
                {
                    ImGui::TextColored(ImVec4(1, 0, 0, 1), "NVGL not enabled");
                }
                else
                {
                    auto pools = GLObjectPool::getAll();
                    for (auto& iter : pools)
                    {
                        auto cxid = iter.first;
                        auto glpool = iter.second;
                        auto globjects = glpool->objects();

                        if (pools.size() > 1)
                            ImGui::Text("Context %d", cxid);

                        double glmem = (double)glpool->totalBytes() / 1048576.;
                        ImGui::TextColored(ImVec4(1, 1, 0, 1), "NVGL Memory: %.1lf MB", glmem);
                        ImGui::SameLine();
                        static bool sort_by_size = false;
                        ImGui::Checkbox("Sort by size", &sort_by_size);

                        std::map<std::string, std::list<GLObject::Ptr>> categories;
                        for (auto& obj : globjects) {
                            categories[obj->category()].emplace_back(obj);
                        }

                        for (auto& cat : categories)
                        {
                            unsigned total = 0;
                            for (auto& obj : cat.second)
                                total += obj->size();

                            char header[128];
                            snprintf(header, sizeof(header), "%s (%d @ %.1lf MB)###%s", cat.first.c_str(), (int)cat.second.size(), (double)total / 1048576., cat.first.c_str());

                            if (sort_by_size)
                            {
                                cat.second.sort([](const auto& lhs, const auto& rhs) {
                                    return lhs->size() > rhs->size();
                                    });
                            }

                            if (ImGui::TreeNode(header))
                            {
                                ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders;
                                flags &= ~ImGuiTableFlags_BordersOuter;
                                if (ImGui::BeginTable("globj", 2, flags))
                                {
                                    for (auto& obj : cat.second)
                                    {
                                        ImGui::PushID(obj->name());

                                        ImGui::TableNextColumn();
                                        ImGui::Text("%6.1lf KB", (double)obj->size() / (double)1024.);

                                        ImGui::TableNextColumn();
                                        if (auto tex = dynamic_cast<GLTexture*>(obj.get()))
                                        {
                                            bool isExpanded = false;
                                            if (ImGui::Selectable(obj->uid().c_str(), &isExpanded))
                                                _expanded = _expanded == obj->name() ? -1 : obj->name();
                                            if (_expanded == obj->name())
                                            {
                                                ImGui::Text("%d x %d", tex->profile()._width, tex->profile()._height);
                                                ImGui::Text("%s", ImageUtils::getInternalFormatString(tex->profile()._internalFormat).c_str());
                                                ImGuiEx::Texture(tex->name(), tex->profile()._width, tex->profile()._height, tex->profile()._internalFormat);
                                            }
                                        }
                                        else
                                        {
                                            ImGui::Text("%s", obj->uid().c_str());
                                        }

                                        ImGui::PopID();
                                    }
                                    ImGui::EndTable();
                                }

                                ImGui::TreePop();
                            }
                        }
                        if (pools.size() > 1)
                            ImGui::Separator();

                        ImGui::Separator();

                        auto recycle_attempts = glpool->recycleHits() + glpool->recycleMisses();
                        if (recycle_attempts > 0)
                        {
                            float re = (float)glpool->recycleHits() / (float)recycle_attempts;
                            ImGui::Text("Recycle Efficiency: %.0f%%", 100.0f * re);
                        }
                    }

                    ImGui::Separator();
                    int kb_per_frame = GLObjectPool::getBytesToDeletePerFrame() / 1024;
                    if (ImGuiLTable::Begin("Settings"))
                    {
                        if (ImGuiLTable::SliderInt("Rel KB per frame", &kb_per_frame, 128, 1024))
                            GLObjectPool::setBytesToDeletePerFrame(kb_per_frame * 1024);

                        ImGuiLTable::End();
                    }
                }
            }
            ImGui::End();
        }
    };
}
