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

#include <osgEarthImGui/Common>

#include <osgEarth/BuildConfig>
#include <osgEarth/Config>
#include <osgEarth/GLUtils>
#include <osgEarth/MapNode>
#include <osgEarth/Terrain>
#include <osgEarth/GeoData>
#include <osgEarth/NodeUtils>
#include <osgEarth/StringUtils>
#include <osgEarth/AnnotationUtils>

#include <osgDB/ReadFile>
#include <osg/observer_ptr>
#include <osg/StateSet>
#include <osg/RenderInfo>
#include <osg/ShapeDrawable>
#include <osgViewer/View>
#include <memory>

namespace osgEarth
{
    // base class for GUI elements that allows you to toggle visibility
    // no export. header only.
    class ImGuiPanel // no export
    {
    public:
        void setVisible(bool value) {
            _visible = value;
            dirtySettings();
        }

        //! whether it's current shown
        bool isVisible() const {
            return _visible;
        }

        //! name of the GUI panel
        const char* name() const {
            return _name.c_str();
        }

        //! render this GUI
        virtual void draw(osg::RenderInfo& ri) { }

        //! override to set custom values in the .ini file
        virtual void load(const osgEarth::Config&) { }

        //! override to get custom values from the .ini file
        virtual void save(osgEarth::Config&) { }

    protected:
        ImGuiPanel(const char* name) :
            _name(name), _visible(false), _last_visible(false)
        {
            //nop
        }

        //! convenience function for finding nodes
        template<typename T>
        T* findNode(osg::RenderInfo& ri) const {
            return osgEarth::findTopMostNodeOfType<T>(ri.getCurrentCamera());
        }

        //! convenience function for getting the current view
        inline osgViewer::View* view(osg::RenderInfo& ri) const {
            return dynamic_cast<osgViewer::View*>(ri.getView());
        }

        //! convenience function for getting the current camera
        inline osg::Camera* camera(osg::RenderInfo& ri) const {
            return ri.getCurrentCamera();
        }

        //! convenience function for getting the topmost stateset
        inline osg::StateSet* stateset(osg::RenderInfo& ri) const {
            return ri.getCurrentCamera()->getOrCreateStateSet();
        }

        //! convenience function for finding nodes
        template<typename T>
        bool findNode(osg::observer_ptr<T>& node, osg::RenderInfo& ri) const {
            if (!node.valid())
                node = osgEarth::findTopMostNodeOfType<T>(ri.getCurrentCamera());
            return node.valid();
        }

        //! convenience function for finding nodes
        template<typename T>
        bool findNodeOrHide(osg::observer_ptr<T>& node, osg::RenderInfo& ri) {
            if (!node.valid())
                node = osgEarth::findTopMostNodeOfType<T>(ri.getCurrentCamera());
            if (!node.valid())
                setVisible(false);
            return node.valid();
        }

        template<typename T>
        bool findLayer(osg::observer_ptr<T>& layer, osg::RenderInfo& ri) {
            if (!layer.valid()) {
                MapNode* mapNode = osgEarth::findTopMostNodeOfType<MapNode>(ri.getCurrentCamera());
                if (mapNode)
                    layer = mapNode->getMap()->getLayer<T>();
            }
            return layer.valid();
        }

        template<typename T>
        bool findLayerOrHide(osg::observer_ptr<T>& layer, osg::RenderInfo& ri) {
            if (!layer.valid()) {
                MapNode* mapNode = osgEarth::findTopMostNodeOfType<MapNode>(ri.getCurrentCamera());
                if (mapNode)
                    layer = mapNode->getMap()->getLayer<T>();
            }
            if (!layer.valid())
                setVisible(false);
            return layer.valid();
        }

        //! sets a value and dirties the .ini store
        template<typename A, typename B>
        void set_and_dirty(A& var, const B& value)
        {
            if (var != value) {
                var = value;
                dirtySettings();
            }
        }

        //! Map point under the mouse cursor
        GeoPoint getPointAtMouse(MapNode* mapNode, osg::View* v, float x, float y)
        {
            GeoPoint point;
            osg::Vec3d world;
            if (mapNode->getTerrain()->getWorldCoordsUnderMouse(v, x, y, world))
                point.fromWorld(mapNode->getMapSRS(), world);
            return point;
        }

        const GeoExtent& selectedExtent() const {
            return _selectedExtent;
        }

    private:
        std::string _name;
        bool _visible;
        bool _last_visible;
        bool* _dirtySettings = nullptr;
        GeoExtent _selectedExtent;
        friend class ImGuiAppEngine;

    public:
        bool* visible() {
            return &_visible;
        }

        //! override to set custom values in the .ini file
        void load_base(const osgEarth::Config& conf)
        {
            conf.get("Visible", _visible);
            load(conf);
        }

        //! override to set custom values in the .ini file
        void save_base(osgEarth::Config& conf)
        {
            conf.set("Visible", _visible);
            save(conf);
        }

        //! whether the visibility changed (and resets the flag)
        bool visibilityChanged() {
            bool whether = _visible != _last_visible;
            _last_visible = _visible;
            return whether;
        }

        void dirtySettings()
        {
            if (_dirtySettings)
                *_dirtySettings = true;
        }
    };
}

// various additions to ImGui.
namespace ImGuiLTable
{
    static bool Begin(const char* name, int flags = 0)
    {
        bool ok = ImGui::BeginTable(name, 2, ImGuiTableFlags_SizingFixedFit | flags);
        if (ok) {
            ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoHide);
            ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_WidthStretch);
        }
        return ok;
    }

    static bool SliderFloat(const char* label, float* v, float v_min, float v_max)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::SliderFloat(s.c_str(), v, v_min, v_max);
    }

    static bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::SliderFloat(s.c_str(), v, v_min, v_max, format, flags);
    }

    static bool SliderDouble(const char* label, double* v, double v_min, double v_max, const char* format = nullptr)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        float temp = *v;
        bool ok = ImGui::SliderFloat(s.c_str(), &temp, (float)v_min, (float)v_max, format);
        if (ok) *v = (double)temp;
        return ok;
    }

    static bool SliderDouble(const char* label, double* v, double v_min, double v_max, const char* format, ImGuiSliderFlags flags)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        float temp = *v;
        bool ok = ImGui::SliderFloat(s.c_str(), &temp, (float)v_min, (float)v_max, format, flags);
        if (ok) *v = (double)temp;
        return ok;
    }

    static bool SliderInt(const char* label, int* v, int v_min, int v_max)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::SliderInt(s.c_str(), v, v_min, v_max);
    }

    static bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::SliderInt(s.c_str(), v, v_min, v_max, format, flags);
    }

    static bool Checkbox(const char* label, bool* v)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::Checkbox(s.c_str(), v);
    }

    static bool BeginCombo(const char* label, const char* defaultItem)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::BeginCombo(s.c_str(), defaultItem);
    }

    static void EndCombo()
    {
        return ImGui::EndCombo();
    }

    static bool InputFloat(const char* label, float* v)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::InputFloat(s.c_str(), v);
    }

    template<typename...Args>
    static void Text(const char* label, const char* format, Args...args)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        ImGui::Text(format, args...);
    }

    static void TextUnformatted(const char* label, const char* value)
    {
        ImGui::TableNextColumn();
        ImGui::TextUnformatted(label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        ImGui::TextUnformatted(value);
    }

    static void Section(const char* label)
    {
        ImGui::TableNextColumn();
        ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", label);
        ImGui::TableNextColumn();
    }

    static bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags = 0)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        std::string s("##" + std::string(label));
        return ImGui::InputScalar(s.c_str(), data_type, p_data, p_step, p_step_fast, format, flags);
    }

    static bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        return ImGui::ColorEdit3(label, col, flags);
    }

    static bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        return ImGui::ColorEdit4(label, col, flags);
    }

    static void PlotLines(const char* label, float(*getter)(void*, int), void* data, int values_count, int values_offset, const char* overlay = nullptr,
        float scale_min = FLT_MAX, float scale_max = FLT_MAX)
    {
        ImGui::TableNextColumn();
        ImGui::Text("%s", label);
        ImGui::TableNextColumn();
        ImGui::SetNextItemWidth(-1);
        ImGui::PlotLines("", getter, data, values_count, values_offset, overlay, scale_min, scale_max);
    }

    static void End()
    {
        ImGui::EndTable();
    }
}

// Added functions to the top level ImGui namespace
namespace ImGuiEx
{
    template<typename... Args>
    static void TextCentered(const char* format, Args&&... args)
    {
        char buf[1024];
        sprintf(buf, format, args...);
        ImGui::SetCursorPosX((ImGui::GetWindowSize().x - ImGui::CalcTextSize(buf).x) * 0.5f);
        ImGui::Text("%s", buf);
    }

    static void OSGTexture(osg::Texture2D* texture, osg::RenderInfo& renderInfo, unsigned int width = 0, unsigned int height = 0)
    {
        // Get the context id
        const unsigned int contextID = osgEarth::GLUtils::getSharedContextID(*renderInfo.getState());

        // Apply the texture
        texture->apply(*renderInfo.getState());

        // Default to the textures size
        unsigned int w = texture->getTextureWidth();
        unsigned int h = texture->getTextureHeight();

        // Get the aspect ratio
        double ar = (double)w / (double)h;

        // If both width and height are specified use that.
        if (width != 0 && height != 0)
        {
            w = width;
            h = height;
        }
        // If just the width is specified compute the height using the ar
        else if (width != 0)
        {
            w = width;
            h = (1.0 / ar) * w;
        }
        // If just the height is specified compute the width using the ar
        else if (height != 0)
        {
            h = height;
            w = ar * height;
        }

        // Get the TextureObject.
        osg::Texture::TextureObject* textureObject = texture->getTextureObject(contextID);
        if (textureObject)
        {
            bool flip = (texture->getImage() && texture->getImage()->getOrigin() == osg::Image::TOP_LEFT);
            ImGui::Image((void*)(intptr_t)textureObject->_id, ImVec2(w, h), ImVec2(0, flip ? 0 : 1), ImVec2(1, flip ? 1 : 0), ImVec4(1, 1, 1, 1), ImVec4(1, 1, 0, 1));
        }
    }

    static void Texture(GLuint id, unsigned width, unsigned height, GLenum internalFormat = 0)
    {
        if (internalFormat == GL_COMPRESSED_RED_GREEN_RGTC2_EXT) // add more if necessary
        {
            ImGui::Text("(Preview not available)");
        }
        else
        {
            ImGui::Image((void*)(std::intptr_t)id, ImVec2((float)width, (float)height), ImVec2(0, 1), ImVec2(1, 0), ImVec4(1, 1, 1, 1), ImVec4(1, 1, 0, 1));
        }
    }

    static int InputTextCallback(ImGuiInputTextCallbackData* data)
    {
        std::string* str = (std::string*)data->UserData;
        if (data->EventFlag == ImGuiInputTextFlags_CallbackResize)
        {
            // Resize string callback
            IM_ASSERT(data->Buf == str->c_str());
            str->resize(data->BufTextLen);
            data->Buf = (char*)str->c_str();
        }
        return 0;
    }

    static bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size)
    {
        ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
        return ImGui::InputTextMultiline(label, (char*)str->c_str(), str->capacity() + 1, size, flags, InputTextCallback, (void*)str);
    }

    /**
    * Structure for quickly defining menus in ImGui.
    * Create a top-level "empty" MenuDef, and start adding children as menus.
    * 
    * To define it:
    *   MenuDef menu;
    *   menu["File"]["Open"] = []() { ... };
    *   menu["File"]["Save"] = []() { ... };
    *   menu["Edit"]["Copy"] = []() { ... };
    *   menu["Edit"]["Paste"] = []() { ... };
    * 
    * To render it:
    *   DrawMenu(menu);
    */
    struct MenuDef
    {
        using Func = std::function<void()>;

        std::string name;
        Func function;
        std::vector<MenuDef> children;

        MenuDef& operator = (const Func& f) {
            function = f;
            return *this;
        }
        MenuDef& operator [] (const std::string& name) {
            auto it = std::find_if(children.begin(), children.end(), [&](const MenuDef& a) { return a.name == name; });
            if (it == children.end()) {
                children.emplace_back(MenuDef{ name });
                return children.back();
            }
            return *it;
        }
    };

    // Render a MenuDef.
    static bool DrawMenu(const MenuDef& root)
    {
        std::function<void(const MenuDef&)> renderAction = [&renderAction](const MenuDef& a)
            {
                ImGui::PushID((std::uintptr_t)&a);
                for (auto& sub : a.children) {
                    if (sub.function) {
                        if (ImGui::MenuItem(sub.name.c_str()))
                            sub.function();
                    }
                    else if (!sub.children.empty() && ImGui::BeginMenu(sub.name.c_str())) {
                        renderAction(sub);
                        ImGui::EndMenu();
                    }
                }
                ImGui::PopID();
            };

        renderAction(root);

        return true;
    }

    class ImGuiPreview
    {
    public:
        ImGuiPreview()
        {
            const int size = 256;

            _tex = new osg::Texture2D();
            _tex->setTextureSize(size, size);
            _tex->setInternalFormat(GL_RGBA8);

            _rtt = new osg::Camera();
            _rtt->setRenderOrder(osg::Camera::POST_RENDER);
            _rtt->setViewport(0, 0, 256, 256);
            _rtt->attach(osg::Camera::COLOR_BUFFER, _tex);
            _rtt->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
            _rtt->setProjectionMatrixAsPerspective(35.0f, 1.0f, 1.0f, 1000.0f);
            _rtt->setClearColor(osg::Vec4(0,0,0,1));

            _sceneView = new osgUtil::SceneView();
            _sceneView->setCamera(_rtt);
            _sceneView->setViewport(0, 0, size, size);
        }

        void setNode(osg::Node* node)
        {
            if (!node || dynamic_cast<osg::Camera*>(node)) return;
            osgEarth::Registry::shaderGenerator().run(node);
            auto group = new osg::Group();
            osgEarth::VirtualProgram::getOrCreate(group->getOrCreateStateSet())->setInheritShaders(false);
            group->addChild(node);
            _sceneView->setSceneData(group);
        }

        void render(osg::RenderInfo& ri)
        {
            auto node = _sceneView->getSceneData();
            if (!node) return;

            auto bs = node->getBound();
            auto r = bs.radius() * 3.0f;
            auto center = bs.center();

            angle += osg::PI / 180.0;
            auto x = r * cos(angle);
            auto y = r * sin(angle);

            auto eye = center + osg::Vec3d(x, y, 0);
            auto up = osg::Vec3d(0, 0, 1);

            _rtt->setViewMatrixAsLookAt(eye, center, up);

            _sceneView->setRenderInfo(ri);
            _sceneView->init();
            _sceneView->update();
            _sceneView->cull();
            _sceneView->draw();

            ImGuiEx::OSGTexture(_tex.get(), ri);
        }

    private:
        double angle = 0;
        osg::ref_ptr<osgUtil::SceneView> _sceneView;
        osg::ref_ptr<osg::Camera> _rtt;
        osg::ref_ptr<osg::Texture2D> _tex;
    };
}
