/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2018 Pelican Mapping
 * http://osgearth.org
 *
 * osgEarth is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
#pragma once

#include <osgEarthImGui/ImGuiPanel>
#include <osgEarth/MapNode>
#include <osgEarth/EarthManipulator>
#include <osgEarth/ViewFitter>
#include <osgEarth/ThreeDTilesLayer>
#include <osgEarth/ImageLayer>
#include <osgEarth/CoverageLayer>
#include <osgEarth/MapboxGLImageLayer>
#include <osgEarth/FeatureModelLayer>
#include <osgEarth/TiledFeatureModelLayer>
#include <osgEarth/ModelLayer>
#include <osgEarth/TerrainConstraintLayer>
#include <osgEarth/NodeUtils>
#include <osgEarth/TerrainEngineNode>
#include <osgEarth/ExampleResources>
#include <osgEarth/ContourMap>
#include <osgEarth/GDALDEM>
#include <osgEarth/Sky>
#include <osg/Camera>

#ifdef OSGEARTH_HAVE_CESIUM_NODEKIT
#include <osgEarthCesium/CesiumLayer>
#endif

#ifndef NOMINMAX
#define NOMINMAX
#endif
#if defined(__has_include)
#if __has_include(<third_party/portable-file-dialogs/portable-file-dialogs.h>)
#include <third_party/portable-file-dialogs/portable-file-dialogs.h>
#define HAS_PFD
#endif
#endif

#include <osgEarth/BuildConfig>
#include <osgEarth/Registry>
#include <osgEarth/XYZ>
#include <osgEarth/TMS>
#include <osgEarth/GDAL>
#include <osgEarth/WMS>
#include <osgEarth/DebugImageLayer>

#ifdef OSGEARTH_HAVE_MBTILES
#include <osgEarth/MBTiles>
#endif

#ifdef OSGEARTH_HAVE_PROCEDURAL_NODEKIT
#include <osgEarthProcedural/LifeMapLayer>
#include <osgEarthProcedural/TextureSplattingLayer>
#include <osgEarthProcedural/VegetationLayer>
#include <osgEarthProcedural/BiomeLayer>
#include <osgEarthProcedural/BridgeLayer>
#endif

namespace osgEarth
{
    using namespace osgEarth::Contrib;
    using namespace osgEarth::Util;

    namespace detail
    {
        struct AddWMSDialog
        {
            AddWMSDialog()
            {
                strcpy(url, "http://readymap.org/readymap/tiles");
                memset(name, 0, sizeof(name));
            }

            void draw(osgEarth::MapNode* mapNode)
            {
                if (!visible) return;

                ImGui::Begin("Add WMS", &visible);
                ImGui::InputText("URL", url, IM_ARRAYSIZE(url));
                const std::string wmsVersion = "1.1.1";

                if (ImGui::Button("Fetch layers from server"))
                {
                    std::string wmsString = std::string(url);
                    char sep = wmsString.find_first_of('?') == std::string::npos ? '?' : '&';

                    std::string capUrl = wmsString +
                        sep +
                        std::string("SERVICE=WMS") +
                        std::string("&VERSION=") + wmsVersion +
                        std::string("&REQUEST=GetCapabilities");

                    capabilities = WMS::CapabilitiesReader::read(capUrl, nullptr);
                }

                static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
                ImVec2 outer_size = ImVec2(0.0f, 300.0f);
                if (ImGui::BeginTable("wms_layers", 3, flags, outer_size))
                {
                    // The first column will use the default _WidthStretch when ScrollX is Off and _WidthFixed when ScrollX is On
                    ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoHide);
                    ImGui::TableSetupColumn("Title", ImGuiTableColumnFlags_NoHide);
                    ImGui::TableSetupColumn("Abstract", ImGuiTableColumnFlags_NoHide);
                    ImGui::TableHeadersRow();

                    if (capabilities.valid())
                    {
                        for (auto& layer : capabilities->getLayers())
                        {
                            displayWMSLayer(layer);
                        }
                    }
                    ImGui::EndTable();
                }

                ImGui::InputText("Name", name, IM_ARRAYSIZE(name));

                if (ImGui::Button("OK"))
                {
                    if (selectedWMSLayer)
                    {
                        WMSImageLayer* wms = new WMSImageLayer;
                        if (strlen(name) > 0)
                            wms->setName(name);
                        else
                            wms->setName(selectedWMSLayer->getTitle());
                        wms->setURL(url);
                        wms->setLayers(selectedWMSLayer->getName());
                        mapNode->getMap()->addLayer(wms);
                    }
                    visible = false;
                }

                ImGui::SameLine();
                if (ImGui::Button("Cancel"))
                {
                    visible = false;
                }
                ImGui::End();
            }

            void displayWMSLayer(WMS::Layer* layer)
            {
                ImGui::TableNextRow();
                ImGui::TableNextColumn();
                const bool is_folder = (layer->getLayers().size() > 0);
                if (is_folder)
                {
                    bool open = ImGui::TreeNodeEx(layer->getName().c_str(), ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_DefaultOpen);
                    ImGui::TableNextColumn();
                    ImGui::TextDisabled("%s", layer->getTitle().c_str());
                    ImGui::TableNextColumn();
                    ImGui::Text("%s", layer->getAbstract().c_str());
                    if (open)
                    {
                        for (auto& l : layer->getLayers())
                        {
                            displayWMSLayer(l);
                        }
                        ImGui::TreePop();
                    }
                }
                else
                {
                    ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_DefaultOpen;
                    if (layer == selectedWMSLayer)
                    {
                        node_flags |= ImGuiTreeNodeFlags_Selected;
                    }
                    ImGui::TreeNodeEx(layer->getName().c_str(), node_flags);
                    if (ImGui::IsItemClicked())
                    {
                        selectedWMSLayer = layer;
                        strcpy(name, selectedWMSLayer->getTitle().c_str());
                    }
                    ImGui::TableNextColumn();
                    ImGui::Text("%s", layer->getTitle().c_str());
                    ImGui::TableNextColumn();
                    ImGui::Text("%s", layer->getAbstract().c_str());
                }
            }

            bool visible = false;
            char url[128];
            char name[1024];
            osg::ref_ptr<WMS::Capabilities> capabilities;
            osg::ref_ptr< WMS::Layer > selectedWMSLayer;
        };


        struct AddTMSDialog
        {
            void draw(MapNode* mapNode)
            {
                if (!visible) return;

                ImGui::Begin("Add TMS", &visible);
                ImGui::InputText("Name", name, IM_ARRAYSIZE(name));
                ImGui::InputText("URL", url, IM_ARRAYSIZE(url));
                ImGui::Checkbox("Treat as Elevation", &isElevation);
                if (ImGui::Button("OK"))
                {
                    if (isElevation)
                    {
                        TMSElevationLayer* tms = new TMSElevationLayer;
                        tms->setName(name);
                        tms->setURL(url);
                        mapNode->getMap()->addLayer(tms);
                    }
                    else
                    {
                        TMSImageLayer* tms = new TMSImageLayer;
                        tms->setName(name);
                        tms->setURL(url);
                        mapNode->getMap()->addLayer(tms);
                    }
                    visible = false;
                }
                ImGui::SameLine();
                if (ImGui::Button("Cancel"))
                {
                    visible = false;
                }
                ImGui::End();
            }

            bool visible = false;
            char url[128] = "http://readymap.org/readymap/tiles/1.0.0/7/";
            char name[64] = "New Layer";
            bool isElevation = false;
        };

        struct AddXYZDialog
        {
            void draw(MapNode* mapNode)
            {
                if (!visible) return;

                ImGui::Begin("Add XYZ", &visible);
                ImGui::InputText("Name", name, IM_ARRAYSIZE(name));
                ImGui::InputText("URL", url, IM_ARRAYSIZE(url));

                static int profile = 1;
                ImGui::Text("Profile");
                if (ImGui::RadioButton("Global Geodetic", profile == 0)) { profile = 0; } ImGui::SameLine();
                if (ImGui::RadioButton("Spherical Mercator", profile == 1)) { profile = 1; }

                ImGui::Checkbox("Treat as Elevation", &isElevation);
                if (ImGui::Button("OK"))
                {
                    if (isElevation)
                    {
                        XYZElevationLayer* xyz = new XYZElevationLayer;
                        xyz->setName(name);
                        xyz->setURL(url);
                        if (profile == 0)      xyz->setProfile(osgEarth::Registry::instance()->getGlobalGeodeticProfile());
                        else if (profile == 1) xyz->setProfile(osgEarth::Registry::instance()->getSphericalMercatorProfile());
                        mapNode->getMap()->addLayer(xyz);
                    }
                    else
                    {
                        XYZImageLayer* xyz = new XYZImageLayer;
                        xyz->setName(name);
                        xyz->setURL(url);
                        if (profile == 0)      xyz->setProfile(osgEarth::Registry::instance()->getGlobalGeodeticProfile());
                        else if (profile == 1) xyz->setProfile(osgEarth::Registry::instance()->getSphericalMercatorProfile());
                        xyz->setProfile(osgEarth::Registry::instance()->getSphericalMercatorProfile());
                        mapNode->getMap()->addLayer(xyz);
                    }
                    visible = false;
                }
                ImGui::SameLine();
                if (ImGui::Button("Cancel"))
                {
                    visible = false;
                }
                ImGui::End();
            }

            bool visible = false;
            char url[128] = "http://[abc].tile.openstreetmap.org/{z}/{x}/{y}.png";
            char name[64] = "New Layer";
            bool isElevation = false;
        };

        struct EditContoursDialog
        {
            bool visible = false;
            osg::TransferFunction1D::ColorMap ramp;

            void draw(ContourMapLayer* layer)
            {
                bool dirty = false;
                if (!visible) return;
                ImGui::Begin("Edit Contours", &visible);
                auto xfer = layer->getTransferFunction();
                if (ramp.empty())
                    ramp = xfer->getColorMap();

                ImGui::BeginTable("contours", 3, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersV | 
                    ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody);
                float to_remove = FLT_MAX;
                float to_add = FLT_MAX;
                osg::Vec4f to_add_color;

                ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
                ImGui::TableSetupColumn("Elevation", ImGuiTableColumnFlags_WidthFixed, 200.0f);
                ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthFixed, 400.0f);
                ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 20.0f);
                ImGui::TableHeadersRow();

                for (auto& item : ramp)
                {
                    ImGui::TableNextColumn();
                    float elev = item.first;
                    ImGui::PushID((std::uintptr_t)&item);
                    std::string label = "##" + std::to_string(elev);
                    if (ImGui::InputFloat(label.c_str(), &elev, 0.0f, 0.0f, "%.0f", ImGuiInputTextFlags_EnterReturnsTrue)) {
                        to_remove = item.first;
                        to_add = elev;
                        to_add_color = item.second;
                    }
                    ImGui::TableNextColumn();
                    std::string label2 = "##c" + std::to_string(elev);
                    if (ImGui::ColorEdit3(label2.c_str(), item.second.ptr())) {
                        to_add = item.first;
                        to_add_color = item.second;
                    }
                    ImGui::TableNextColumn();
                    std::string label3 = "##t" + std::to_string(elev);
                    if (ImGui::Button("x")) {
                        to_remove = item.first;
                    }
                    ImGui::PopID();
                }
                ImGui::EndTable();

                if (ImGui::Button("Add stop"))
                {
                    if (ramp.empty())
                        to_add = 0.0f;
                    else
                        to_add = ramp.rbegin()->first + 1.0f;
                    to_add_color = Color::White;
                }

                if (to_remove != FLT_MAX)
                    ramp.erase(to_remove);
                if (to_add != FLT_MAX)
                    ramp.emplace(to_add, to_add_color);
                if (to_remove != FLT_MAX || to_add != FLT_MAX)
                {
                    xfer->setColorMap(ramp);
                    layer->setTransferFunction(xfer);
                }

                ImGui::SameLine();
                if (ImGui::Button("Export XML"))
                {
                    std::ostringstream buf;
                    buf << "<stops>\n";
                    for (auto& item : ramp)
                    {
                        buf << "    <stop elevation=\"" << item.first << "\" color=\"" << Color(item.second).toHTML() << "\"/>\n";
                    }
                    buf << "</stops>\n";
                    std::cout << buf.str();
                }
                ImGui::SameLine();
                if (ImGui::Button("Done"))
                {
                    ramp.clear();
                    visible = false;
                }
                ImGui::End();
            }
        };
    }

    class LayersGUI : public ImGuiPanel
    {
    private:
        osg::observer_ptr<MapNode> _mapNode;
        bool _showDisabled = false;
        bool _sortByCat = false;
        LayerVector _layers;
        std::unordered_map<Layer*, bool> _layerExpanded;
        int _mapRevision = -1;
        bool _first = true;
        std::unordered_map<Layer*, float> _maxMaxRanges;
        std::unordered_map<Layer*, float> _maxMinRanges;
        std::unordered_map<Layer*, float> _maxAttenRanges;
        bool _addXYZ = false;
        ImageLayer* _mouseOverImageLayer = nullptr;
        CoverageLayer* _mouseOverCoverageLayer = nullptr;

        static const int NUM_CATS = 8;

        enum LayerCat {
            IMAGE = 0,
            ELEVATION,
            FEATURE,
            MODEL,
            PROCEDURAL,
            CONSTRAINT,
            DATA,
            OTHER
        };
        const std::string _layerCatName[NUM_CATS] = {
            "Image",
            "Elevation",
            "Feature",
            "Model",
            "Procedural",
            "Constraint",
            "Data",
            "Other"
        };

        std::vector<osg::ref_ptr<Layer>> _layersByCat[NUM_CATS];

        detail::AddWMSDialog _addWMSDialog;
        detail::AddTMSDialog _addTMSDialog;
        detail::AddXYZDialog _addXYZDialog;
        detail::EditContoursDialog _editContoursDialog;

        using ValueUnderMouse = struct {
            osg::Vec4 pixel;
            GLenum pixelFormat;
            GLenum dataType;
        };
        Future<Result<ValueUnderMouse>> _imageLayerValueUnderMouse;

#ifdef OSGEARTH_HAVE_PROCEDURAL_NODEKIT
        Future<Result<osgEarth::Procedural::LandCoverSample>> _coverageLayerValueUnderMouse;
#endif

        std::function<bool(const Layer*)>
            _showPred,
            _showVisibleLayers,
            _showImageLayers,
            _showTerrainSurfaceLayers,
            _showFeatureModelLayers,
            _showConstraintLayers;

    public:

        LayersGUI() : ImGuiPanel("Map")
        {
            _showVisibleLayers = [this](const Layer* layer)
                {
                    return
                        dynamic_cast<const VisibleLayer*>(layer) &&
                        layer->getUserProperty("show_in_ui", true);
                };

            _showImageLayers = [this](const Layer* layer)
                {
                    return
                        dynamic_cast<const ImageLayer*>(layer) &&
                        layer->getUserProperty("show_in_ui", true);
                };

            _showTerrainSurfaceLayers = [this](const Layer* layer)
                {
                    return
                        layer->getRenderType() == layer->RENDERTYPE_TERRAIN_SURFACE &&
                        layer->getUserProperty("show_in_ui", true);
                };

            _showFeatureModelLayers = [this](const Layer* layer)
                {
                    return
                        dynamic_cast<const FeatureModelLayer*>(layer) &&
                        layer->getUserProperty("show_in_ui", true);
                };

            _showConstraintLayers = [this](const Layer* layer)
                {
                    return
                        dynamic_cast<const TerrainConstraintLayer*>(layer) &&
                        layer->getUserProperty("show_in_ui", true);
                };

            _showPred = _showVisibleLayers;
        }

        //! Sets a predicate that decides whether to include a layer in the GUI
        void setShowPredicate(std::function<bool(const Layer*)> func)
        {
            _showPred = func;
        }

        void load(const Config& conf) override
        {
            conf.get("ShowDisabled", _showDisabled);
            conf.get("SortByCategory", _sortByCat);
        }

        void save(Config& conf) override
        {
            conf.set("ShowDisabled", _showDisabled);
            conf.set("SortByCategory", _sortByCat);
        }

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

            bool mapNodeWasValid = _mapNode.valid();

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

            if (_first)
            {
                EventRouter::get(view(ri))
                    .onMove([&](osg::View* v, float x, float y) { onMove(v, x, y); });
                _first = false;
            }

            refreshMap(ri, mapNodeWasValid);

            if (ImGui::Begin(name(), visible(), ImGuiWindowFlags_MenuBar))
            {
                drawAddLayerMenu(ri);

                // Map name:
                if (_mapNode->getMap()->getName().empty() == false)
                {
                    ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", _mapNode->getMap()->getName().c_str());
                }

                ImGui::SameLine();

                ImGui::Text("(%d)", (int)_layers.size());
                ImGui::SameLine();

                if (ImGui::Checkbox("Sort", &_sortByCat))
                    dirtySettings();

                ImGui::SameLine();
                if (ImGui::Checkbox("Closed", &_showDisabled))
                    dirtySettings();

                ImGui::Separator();

                if (_sortByCat)
                {
                    for (int cat = 0; cat < NUM_CATS; ++cat)
                    {
                        auto& layers = _layersByCat[cat];
                        if (layers.empty())
                            continue;

                        if (ImGui::TreeNode(std::string(_layerCatName[cat] + " Layers").c_str()))
                        {
                            drawLayers(layers, ri);
                            ImGui::TreePop();
                        }
                    }
                }
                else
                {
                    drawLayers(_layers, ri);
                }

                ImGui::End();
            }
        }

        void refreshMap(osg::RenderInfo& ri, bool mapNodeWasValid)
        {
            const Map* map = _mapNode->getMap();

            Revision rev = map->getDataModelRevision();
            if (rev != _mapRevision || !mapNodeWasValid)
            {
                _layers.clear();
                _layerExpanded.clear();

                if (_showPred)
                    _mapRevision = map->getLayers(_layers, _showPred);
                else
                    _mapRevision = map->getLayers(_layers);

                for (auto& layer : _layers)
                    _layerExpanded[layer.get()] = false;

                _maxMaxRanges.clear();
                _maxMinRanges.clear();
                _maxAttenRanges.clear();
                for (auto layer : _layers)
                {
                    VisibleLayer* v = dynamic_cast<VisibleLayer*>(layer.get());
                    if (v)
                    {
                        _maxMaxRanges[layer.get()] = v->getMaxVisibleRange() * 2.0f;
                        _maxMinRanges[layer.get()] = v->getMinVisibleRange() * 2.0f;
                        _maxAttenRanges[layer.get()] = v->getAttenuationRange() * 2.0f;
                    }
                }

                for (int cat = 0; cat < NUM_CATS; ++cat)
                    _layersByCat[cat].clear();

                for (auto& layer : _layers)
                {
#ifdef OSGEARTH_HAVE_PROCEDURAL_NODEKIT
                    if (dynamic_cast<osgEarth::Procedural::LifeMapLayer*>(layer.get()) ||
                        dynamic_cast<osgEarth::Procedural::VegetationLayer*>(layer.get()) ||
                        dynamic_cast<osgEarth::Procedural::TextureSplattingLayer*>(layer.get()) ||
                        dynamic_cast<osgEarth::Procedural::BiomeLayer*>(layer.get()))
                        _layersByCat[PROCEDURAL].push_back(layer);
                    else if (dynamic_cast<osgEarth::Procedural::BridgeLayer*>(layer.get()))
                        _layersByCat[FEATURE].push_back(layer);                    
                    else
#endif
                        if (dynamic_cast<ImageLayer*>(layer.get()) || dynamic_cast<ContourMapLayer*>(layer.get()))
                            _layersByCat[IMAGE].push_back(layer);
                        else if (dynamic_cast<ElevationLayer*>(layer.get()))
                            _layersByCat[ELEVATION].push_back(layer);
                        else if (dynamic_cast<FeatureModelLayer*>(layer.get()) || dynamic_cast<TiledFeatureModelLayer*>(layer.get()))
                            _layersByCat[FEATURE].push_back(layer);
                        else if (dynamic_cast<ModelLayer*>(layer.get()))
                            _layersByCat[MODEL].push_back(layer);
                        else if (dynamic_cast<TerrainConstraintLayer*>(layer.get()))
                            _layersByCat[CONSTRAINT].push_back(layer);
                        else if (dynamic_cast<FeatureSource*>(layer.get()))
                            _layersByCat[DATA].push_back(layer);
                        else
                            _layersByCat[OTHER].push_back(layer);
                }
            }
        }

        void drawLayers(std::vector<osg::ref_ptr<Layer>>& layers, osg::RenderInfo& ri)
        {
            auto camera = view(ri)->getCamera();
            auto map = _mapNode->getMap();

            for (int i = layers.size() - 1; i >= 0; --i)
            {
                osgEarth::Layer* layer = layers[i];

                if (!_showDisabled && !layer->isOpen() && !layer->getOpenAutomatically())
                    continue;

                ImGui::PushID(layer);
                bool stylePushed = false;

                bool error =
                    layer->getStatus().isError() &&
                    layer->getStatus().message() != "Layer closed" &&
                    layer->getStatus().message() != "Layer disabled";

                if (error)
                    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(ImColor(255, 72, 72))), stylePushed = true;
                else if (!layer->isOpen())
                    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(ImColor(127, 127, 127))), stylePushed = true;

                auto visibleLayer = dynamic_cast<osgEarth::VisibleLayer*>(layer);

                if (visibleLayer)
                {
                    if (layer->isOpen())
                    {
                        bool visible = visibleLayer->getVisible();
                        if (ImGui::Checkbox("", &visible))
                        {
                            visibleLayer->setVisible(visible);
                        }
                    }
                    else // closed? the checkbox will try to open it.
                    {
                        bool dummy = false;
                        if (ImGui::Checkbox("", &dummy))
                        {
                            Registry::instance()->clearBlacklist();
                            layer->open();
                        }
                    }

                    ImGui::SameLine();

                    ImGui::PushID("selectable");
                    bool layerNameClicked = false;
                    if (layer->isOpen())
                    {
                        ImGui::Selectable(layer->getName().c_str(), &layerNameClicked);
                    }
                    else
                    {
                        std::string text = layer->getName() + " (closed)";
                        ImGui::Selectable(text.c_str(), &layerNameClicked);
                    }

                    if (layerNameClicked)
                    {
                        _layerExpanded[layer] = !_layerExpanded[layer];
                    }
                    ImGui::PopID(); // "selectable"
                }
                else
                {
                    ImGui::Text("%s", layer->getName().c_str());
                }

                if (_layerExpanded[layer])
                {
                    ImGui::Indent();

                    ImGui::TextColored(ImVec4(.8, .8, .8, 1), "%s", layer->className());

                    if (layer->isOpen())
                    {
                        auto visibleLayer = dynamic_cast<osgEarth::VisibleLayer*>(layer);
                        auto tileLayer = dynamic_cast<osgEarth::TileLayer*>(layer);
                        auto imageLayer = dynamic_cast<osgEarth::ImageLayer*>(layer);
                        auto elevationLayer = dynamic_cast<osgEarth::ElevationLayer*>(layer);

                        if (tileLayer)
                        {
                            const Profile* profile = tileLayer->getProfile();
                            if (profile)
                            {
                                std::string srsname = profile->getSRS()->getName();
                                if (srsname == "unknown")
                                    srsname = profile->getSRS()->getHorizInitString();
                                ImGui::TextColored(ImVec4(.8, .8, .8, 1), "%s", srsname.c_str());

                                if (elevationLayer)
                                {
                                    ImGui::SameLine();
                                    auto vdatum = profile->getSRS()->getVertInitString();
                                    if (vdatum.empty()) vdatum = "geodetic";
                                    ImGui::TextColored(ImVec4(1, .8, .8, 1), " (vdatum = %s)", vdatum.c_str());
                                }
                            }
                        }

                        const GeoExtent& extent = layer->getExtent();
                        if (extent.isValid())
                        {
                            const std::string fmt[] = { "%5.1f", "%4.1f  *  %4.1f", "%1.f", "%.1f * %.1f" };
                            int i = _mapNode->getMapSRS()->isGeographic() ? 0 : 2;
                            ImGuiEx::TextCentered(fmt[i].c_str(), extent.north());
                            ImGuiEx::TextCentered(fmt[i + 1].c_str(), extent.west(), extent.east());
                            ImGuiEx::TextCentered(fmt[i].c_str(), extent.south());
                        }

                        ImGuiLTable::Begin("Layer");

                        if (visibleLayer && !elevationLayer)
                        {
                            float opacity = visibleLayer->getOpacity();
                            if (ImGuiLTable::SliderFloat("Opacity", &opacity, 0.0f, 1.0f))
                                visibleLayer->setOpacity(opacity);

                            if (visibleLayer->options().maxVisibleRange().isSet())
                            {
                                float value = visibleLayer->getMaxVisibleRange();
                                if (value < FLT_MAX) {
                                    if (ImGuiLTable::SliderFloat("Max range", &value, 0.0f, _maxMaxRanges[layer]))
                                        visibleLayer->setMaxVisibleRange(value);
                                }
                            }

                            if (visibleLayer->options().minVisibleRange().isSet())
                            {
                                float value = visibleLayer->getMinVisibleRange();
                                if (ImGuiLTable::SliderFloat("Min range", &value, 0.0f, _maxMinRanges[layer]))
                                    visibleLayer->setMinVisibleRange(value);
                            }

                            if (visibleLayer->options().attenuationRange().isSet())
                            {
                                float value = visibleLayer->getAttenuationRange();
                                if (ImGuiLTable::SliderFloat("Attenuation range", &value, 0.0f, _maxAttenRanges[layer]))
                                    visibleLayer->setAttenuationRange(value);
                            }

                            if (visibleLayer->getNode() != nullptr)
                            {
                                float value = visibleLayer->getDepthOffset();
                                if (ImGuiLTable::SliderFloat("Depth offset", &value, -1.0f, 1.0f, "%.3f", 0))
                                    visibleLayer->setDepthOffset(value);
                            }

                            bool debugView = visibleLayer->getEnableDebugView();
                            if (ImGuiLTable::Checkbox("Highlight", &debugView)) {
                                visibleLayer->setEnableDebugView(debugView);
                            }
                        }

                        if (imageLayer)
                        {
                            bool modulate = imageLayer->getColorBlending() == BLEND_MODULATE;
                            if (ImGuiLTable::Checkbox("Modulate", &modulate))
                            {
                                imageLayer->setColorBlending(modulate ? BLEND_MODULATE : BLEND_INTERPOLATE);
                            }
                        }

#ifdef OSGEARTH_HAVE_CESIUM_NODEKIT
                        auto cesiumNativeLayer = dynamic_cast<osgEarth::Cesium::CesiumNative3DTilesLayer*>(layer);
                        if (cesiumNativeLayer)
                        {
                            float sse = cesiumNativeLayer->getMaximumScreenSpaceError();
                            ImGui::PushID("sse");
                            ImGuiLTable::SliderFloat("SSE", &sse, 0.0f, 50.0f);
                            cesiumNativeLayer->setMaximumScreenSpaceError(sse);
                            ImGui::PopID();
                        }
#endif

                        auto threedTiles = dynamic_cast<osgEarth::Contrib::ThreeDTilesLayer*>(layer);
                        if (threedTiles)
                        {
                            float sse = threedTiles->getMaximumScreenSpaceError();
                            ImGui::PushID("sse");
                            ImGuiLTable::SliderFloat("SSE", &sse, 0.0f, 50.0f);
                            threedTiles->setMaximumScreenSpaceError(sse);
                            ImGui::PopID();

                            ImGui::PushID("debugVolumes");
                            bool showBoundingVolumes = threedTiles->getTilesetNode()->getShowBoundingVolumes();
                            ImGuiLTable::Checkbox("Show debug volumes", &showBoundingVolumes);
                            threedTiles->getTilesetNode()->setShowBoundingVolumes(showBoundingVolumes);
                            ImGui::PopID();

                            ImGui::PushID("debugColors");
                            bool colorPerTile = threedTiles->getTilesetNode()->getColorPerTile();
                            ImGuiLTable::Checkbox("Show color per tile", &colorPerTile);
                            threedTiles->getTilesetNode()->setColorPerTile(colorPerTile);
                            ImGui::PopID();
                        }

                        auto mapboxGLLayer = dynamic_cast<MapBoxGLImageLayer*>(layer);
                        if (mapboxGLLayer)
                        {
                            bool disableText = mapboxGLLayer->getDisableText();
                            if (ImGuiLTable::Checkbox("Disable text", &disableText))
                            {
                                mapboxGLLayer->setDisableText(disableText);
                            }

                            float pixelScale = mapboxGLLayer->getPixelScale();
                            if (ImGuiLTable::InputFloat("Pixel Scale", &pixelScale))
                            {
                                mapboxGLLayer->setPixelScale(pixelScale);
                            }
                        }

                        if (tileLayer)
                        {
                            if (tileLayer->options().minLevel().isSet())
                            {
                                ImGuiLTable::Text("Min level", "%d", tileLayer->getMinLevel());
                            }
                            if (tileLayer->options().maxLevel().isSet())
                            {
                                ImGuiLTable::Text("Max level", "%d", tileLayer->getMaxLevel());
                            }
                            if (tileLayer->options().maxDataLevel().isSet())
                            {
                                ImGuiLTable::Text("Max data level", "%d", tileLayer->getMaxDataLevel());
                            }

                            if (tileLayer->options().upsample().isSet())
                            {
                                bool upsampling = tileLayer->options().upsample().value();
                                ImGuiLTable::Text("Upsampling", "%s", (upsampling ? "ON" : "off"));
                            }
                        }

                        auto report = layer->reportStats();
                        for (auto& kv : report)
                        {
                            ImGuiLTable::Text(kv.first.c_str(), "%s", kv.second.c_str()); // (kv.first + ": " + kv.second).c_str());
                        }

                        const DateTimeExtent& dtextent = layer->getDateTimeExtent();
                        if (dtextent.valid())
                        {
                            //ImGui::Text("Time Series:");
                            ImGuiLTable::Text("Start time", "%s", dtextent.getStart().asISO8601().c_str());
                            ImGuiLTable::Text("End time", "%s", dtextent.getEnd().asISO8601().c_str());
                        }

                        ImGuiLTable::End();


                        auto contourmap = dynamic_cast<osgEarth::ContourMapLayer*>(layer);
                        if (contourmap)
                        {
                            if (ImGui::Button("Edit contours"))
                            {
                                _editContoursDialog.visible = true;
                            }
                            _editContoursDialog.draw(contourmap);
                        }

#ifdef OSGEARTH_HAVE_PROCEDURAL_NODEKIT
                        auto coverage = dynamic_cast<osgEarth::CoverageLayer*>(layer);
                        if (coverage)
                        {
                            bool queryOn = (_mouseOverCoverageLayer == coverage);

                            if (ImGui::Checkbox("Show value under mouse", &queryOn))
                            {
                                _mouseOverCoverageLayer = queryOn ? coverage : nullptr;
                            }

                            if (_mouseOverCoverageLayer == coverage)
                            {
                                if (_coverageLayerValueUnderMouse.available())
                                {
                                    if (_coverageLayerValueUnderMouse->isOK())
                                    {
                                        auto& value = _coverageLayerValueUnderMouse->value();
                                        if (value.biomeid().isSet())
                                            ImGui::Text("Biome ID: %s", value.biomeid()->c_str());
                                        if (value.dense().isSet())
                                            ImGui::Text("Dense: %.2f", value.dense().value());
                                        if (value.lush().isSet())
                                            ImGui::Text("Lush: %.2f", value.lush().value());
                                        if (value.rugged().isSet())
                                            ImGui::Text("Rugged: %.2f", value.rugged().value());    
                                        if (value.soiltype().isSet())
                                            ImGui::Text("Soil Type: %s", value.soiltype()->c_str());
                                    }
                                    else
                                    {
                                        ImGui::Text("%s", _coverageLayerValueUnderMouse->message().c_str());
                                    }
                                }
                                else
                                {
                                    ImGui::Text("  (searching...)");                                
                                }
                            }
                        }
#endif

                        if (imageLayer)
                        {
                            bool queryOn = (_mouseOverImageLayer == imageLayer);
                            if (ImGui::Checkbox("Show value under mouse", &queryOn))
                            {
                                _mouseOverImageLayer = queryOn ? imageLayer : nullptr;
                            }

                            if (_mouseOverImageLayer == imageLayer)
                            {
                                if (_imageLayerValueUnderMouse.available())
                                {
                                    if (_imageLayerValueUnderMouse->isOK())
                                    {
                                        auto& value = _imageLayerValueUnderMouse->value();
                                        if (value.dataType == GL_UNSIGNED_BYTE)
                                        {
                                            auto& p = value.pixel;
                                            if (value.pixelFormat == GL_RED)
                                            {
                                                ImGui::Text(" f32 (%.2f)", p.r());
                                                ImGui::Text(" int (%d)", (int)(255.f * p.r()));
                                                ImGui::Text(" hex (%.2X)", (int)(255.f * p.r()));
                                            }
                                            if (value.pixelFormat == GL_RG)
                                            {
                                                ImGui::Text(" f32 (%.2f %.2f)", p.r(), p.g());
                                                ImGui::Text(" int (%d %d)", (int)(255.f * p.r()), (int)(255.f * p.g()));
                                                ImGui::Text(" hex (%.2X %.2X)", (int)(255.f * p.r()), (int)(255.f * p.g()));
                                            }
                                            else if (value.pixelFormat == GL_RGB)
                                            {
                                                ImGui::Text(" f32 (%.2f %.2f %.2f)", p.r(), p.g(), p.b());
                                                ImGui::Text(" int (%d %d %d)", (int)(255.f * p.r()), (int)(255.f * p.g()), (int)(255.0f * p.b()));
                                                ImGui::Text(" hex (%.2X %.2X %.2X)", (int)(255.f * p.r()), (int)(255.f * p.g()), (int)(255.0f * p.b()));
                                            }
                                            else
                                            {
                                                ImGui::Text(" f32 (%.2f %.2f %.2f %.2f)", p.r(), p.g(), p.b(), p.a());
                                                ImGui::Text(" int (%d %d %d %d)", (int)(255.f * p.r()), (int)(255.f * p.g()), (int)(255.0f * p.b()), (int)(255.0f * p.a()));
                                                ImGui::Text(" hex (%.2X %.2X %.2X %.2X)", (int)(255.f * p.r()), (int)(255.f * p.g()), (int)(255.0f * p.b()), (int)(255.0f * p.a()));
                                            }
                                        }
                                        else if (value.dataType == GL_FLOAT)
                                        {
                                            int v = (int)value.pixel.r();
                                            ImGui::Text(" f32 (%0.4f)", value.pixel.r());
                                            ImGui::Text(" int (%d)", v);
                                            ImGui::Text(" hex (%#.8X)", v);
                                        }
                                    }
                                    else
                                    {
                                        ImGui::Text("%s", _imageLayerValueUnderMouse->message().c_str());
                                    }
                                }
                                else
                                {
                                    ImGui::Text("  (searching)");
                                    ImGui::Text(" ");
                                }
                            }
                        }

                        if (visibleLayer)
                        {
                            if ((extent.isValid() && !extent.isWholeEarth()) ||
                                (layer->getNode() && layer->getNode()->getBound().valid()) ||
                                (dtextent.valid()))
                            {
                                if (ImGui::Button("Zoom"))
                                {
                                    if (extent.isValid())
                                    {
                                        std::vector<GeoPoint> points;
                                        points.push_back(GeoPoint(extent.getSRS(), extent.west(), extent.south()));
                                        points.push_back(GeoPoint(extent.getSRS(), extent.east(), extent.north()));

                                        ViewFitter fitter(_mapNode->getMap()->getSRS(), camera);
                                        Viewpoint vp;
                                        if (fitter.createViewpoint(points, vp))
                                        {
                                            auto manip = dynamic_cast<EarthManipulator*>(view(ri)->getCameraManipulator());
                                            if (manip) manip->setViewpoint(vp, 2.0);
                                        }
                                    }
                                    else if (layer->getNode())
                                    {
                                        ViewFitter fitter(map->getSRS(), camera);
                                        Viewpoint vp;
                                        if (fitter.createViewpoint(layer->getNode(), vp))
                                        {
                                            auto manip = dynamic_cast<EarthManipulator*>(view(ri)->getCameraManipulator());
                                            if (manip) manip->setViewpoint(vp, 2.0);
                                        }
                                    }

                                    if (dtextent.valid())
                                    {
                                        auto sky = osgEarth::findRelativeNodeOfType<SkyNode>(_mapNode.get());
                                        if (sky)
                                        {
                                            sky->setDateTime(dtextent.getStart());
                                        }
                                    }
                                }

                                ImGui::SameLine();
                            }

                            if (ImGui::Button("Refresh"))
                            {
                                Registry::instance()->clearBlacklist();
                                layer->dirty();
                                auto cp = layer->getCachePolicy();
                                cp.minTime() = DateTime().asTimeStamp();
                                layer->setCachePolicy(cp);
                                std::vector<const Layer*> layers = { layer };
                                _mapNode->getTerrainEngine()->invalidateRegion(layers, GeoExtent());
                            }

                            if (layer->isOpen())
                            {
                                ImGui::SameLine();
                                if (ImGui::Button("Close"))
                                {
                                    layer->close();
                                }
                            }

                            ImGui::SameLine();
                            if (ImGui::Button("JSON"))
                            {
                                auto conf = layer->getConfig();
                                std::cout << conf.toJSON(true) << std::endl;
                            }
                        }
                    }

                    else if (layer->getStatus().isError() && layer->getStatus().message() != "Layer closed")
                    {
                        ImGui::TextWrapped("%s", layer->getStatus().message().c_str());
                    }

                    ImGui::Unindent();
                }

                ImGui::PopID();

                if (stylePushed)
                    ImGui::PopStyleColor(1);

                ImGui::Separator();
            }
        }

        void drawAddLayerMenu(osg::RenderInfo& ri)
        {
            if (ImGui::BeginMenuBar())
            {
                if (ImGui::BeginMenu("Add Layer"))
                {
#ifdef HAS_PFD
                    if (ImGui::MenuItem("Local File"))
                    {
                        auto f = pfd::open_file("Choose files to read", pfd::path::home(),
                            { "All Files", "*" },
                            pfd::opt::multiselect);

                        if (f.result().size() > 0)
                        {
                            auto m = pfd::message("Imagery",
                                "Are these files imagery?  Select No for elevation.",
                                pfd::choice::yes_no,
                                pfd::icon::question);
                            bool imagery = m.result() == pfd::button::yes;

                            _mapNode->getMap()->beginUpdate();

                            if (imagery)
                            {
                                for (auto const& name : f.result())
                                {
                                    std::string ext = osgDB::getLowerCaseFileExtension(name);
                                    if (ext == "mbtiles")
                                    {
#ifdef OSGEARTH_HAVE_MBTILES
                                        MBTilesImageLayer* mbtilesImage = new MBTilesImageLayer;
                                        mbtilesImage->setName(osgDB::getSimpleFileName(name));
                                        mbtilesImage->setURL(name);
                                        _mapNode->getMap()->addLayer(mbtilesImage);
#else
                                        OE_WARN << "MBTiles driver not available" << std::endl;
#endif
                                    }
                                    else

                                    {
                                        GDALImageLayer* gdalImage = new GDALImageLayer;
                                        gdalImage->setName(osgDB::getSimpleFileName(name));
                                        gdalImage->setURL(name);
                                        _mapNode->getMap()->addLayer(gdalImage);
                                    }
                                }
                            }
                            else
                            {
                                for (auto const& name : f.result())
                                {
                                    std::string ext = osgDB::getLowerCaseFileExtension(name);
                                    if (ext == "mbtiles")
                                    {
#ifdef OSGEARTH_HAVE_MBTILES
                                        MBTilesElevationLayer* mbtilesElevation = new MBTilesElevationLayer;
                                        mbtilesElevation->setName(osgDB::getSimpleFileName(name));
                                        mbtilesElevation->setURL(name);
                                        _mapNode->getMap()->addLayer(mbtilesElevation);
#else
                                        OE_WARN << "MBTiles driver not available" << std::endl;
#endif
                                    }
                                    else
                                    {
                                        GDALElevationLayer* gdalElevation = new GDALElevationLayer;
                                        gdalElevation->setName(osgDB::getSimpleFileName(name));
                                        gdalElevation->setURL(name);
                                        _mapNode->getMap()->addLayer(gdalElevation);
                                    }
                                }
                            }
                            _mapNode->getMap()->endUpdate();
                        }
                    }
#endif
                    if (ImGui::MenuItem("TMS")) _addTMSDialog.visible = true;
                    if (ImGui::MenuItem("XYZ")) _addXYZDialog.visible = true;
                    if (ImGui::MenuItem("WMS")) _addWMSDialog.visible = true;

                    drawUsefulLayers();

                    ImGui::EndMenu();
                }
                ImGui::EndMenuBar();
            }

            // Draw the add dialogs
            _addXYZDialog.draw(_mapNode.get());
            _addTMSDialog.draw(_mapNode.get());
            _addWMSDialog.draw(_mapNode.get());
        }

        void drawUsefulLayers()
        {
            ImGui::Separator();

            //if (ImGui::BeginMenu("Useful Layers"))
            {
                // ReadyMap Imagery
                if (_mapNode->getMap()->getLayerByName("ReadyMap Imagery") == nullptr)
                {
                    if (ImGui::MenuItem("ReadyMap imagery"))
                    {
                        TMSImageLayer* readymap = new TMSImageLayer();
                        readymap->setName("ReadyMap Imagery");
                        readymap->setURL("https://readymap.org/readymap/tiles/1.0.0/7/");
                        _mapNode->getMap()->addLayer(readymap);
                    }
                }

                // ReadyMap Elevation
                if (_mapNode->getMap()->getLayerByName("ReadyMap Elevation") == nullptr)
                {
                    if (ImGui::MenuItem("ReadyMap elevation"))
                    {
                        TMSElevationLayer* readymap = new TMSElevationLayer();
                        readymap->setName("ReadyMap Elevation");
                        readymap->setURL("https://readymap.org/readymap/tiles/1.0.0/116/");
                        _mapNode->getMap()->addLayer(readymap);
                    }
                }

                // OpenStreetMap
                if (_mapNode->getMap()->getLayerByName("OpenStreetMap") == nullptr)
                {
                    if (ImGui::MenuItem("OpenStreetMap"))
                    {
                        XYZImageLayer* osm = new XYZImageLayer();
                        osm->setName("OpenStreetMap");
                        osm->setURL("https://[abc].tile.openstreetmap.org/{z}/{x}/{y}.png");
                        osm->setProfile(osgEarth::Registry::instance()->getSphericalMercatorProfile());
                        osm->setAttribution("&#169;OpenStreetMap contributors");
                        _mapNode->getMap()->addLayer(osm);
                    }
                }

                // Contourmap
                if (_mapNode->getMap()->getLayer<ContourMapLayer>() == nullptr)
                {
                    if (ImGui::MenuItem("Contour map"))
                    {
                        auto* layer = new ContourMapLayer();
                        layer->setName("Contour Map");
                        _mapNode->getMap()->addLayer(layer);
                    }
                }

                // Hillshade Shading
                if (_mapNode->getMap()->getLayer<GDALDEMLayer>() == nullptr)
                {
                    if (ImGui::MenuItem("Hill shading"))
                    {
                        auto* layer = new GDALDEMLayer();
                        layer->setName("Hill shading");
                        layer->setOpacity(0.25);
                        _mapNode->getMap()->addLayer(layer);
                    }
                }

                // Debug tiles
                if (ImGui::MenuItem("Debug tiles (map profile)"))
                {
                    DebugImageLayer* debugImage = new DebugImageLayer();
                    debugImage->setName("Debug tiles (Map profile)");
                    _mapNode->getMap()->addLayer(debugImage);
                }

                if (ImGui::MenuItem("Debug tiles (Mercator)"))
                {
                    DebugImageLayer* debugImage = new DebugImageLayer();
                    debugImage->setProfile(Profile::create(Profile::SPHERICAL_MERCATOR));
                    debugImage->setName("Debug tiles (Mercator)");
                    _mapNode->getMap()->addLayer(debugImage);
                }


                //ImGui::EndMenu();
            }
        }

        void onMove(osg::View* view, float x, float y)
        {
            if (_mouseOverImageLayer)
            {
                _imageLayerValueUnderMouse.reset();

                TerrainTile* tile = _mapNode->getTerrain()->getTerrainTileUnderMouse(view, x, y);
                if (tile)
                {
                    GeoPoint p = _mapNode->getGeoPointUnderMouse(view, x, y);
                    TileKey key = _mouseOverImageLayer->getProfile()->createTileKey(p, tile->getKey().getLOD());
                    key = _mouseOverImageLayer->getBestAvailableTileKey(key);

                    if (key.valid())
                    {
                        auto task = [this, key, p](Cancelable& c)
                            {
                                ValueUnderMouse value;
                                osg::ref_ptr<ProgressCallback> prog = new ProgressCallback(&c);
                                GeoImage g = _mouseOverImageLayer->createImage(key, prog.get());
                                if (g.valid())
                                {
                                    g.getReader().setBilinear(false);
                                    if (g.read(value.pixel, p))
                                    {
                                        value.pixelFormat = g.getImage()->getPixelFormat();
                                        value.dataType = g.getImage()->getDataType();
                                        return Result<ValueUnderMouse>(value);
                                    }
                                }
                                return Result<ValueUnderMouse>(Status::Error("No value"));
                            };

                        _imageLayerValueUnderMouse = jobs::dispatch(task);
                    }
                }
            }

#ifdef OSGEARTH_HAVE_PROCEDURAL_NODEKIT
            if (_mouseOverCoverageLayer)
            {
                _coverageLayerValueUnderMouse.reset();

                TerrainTile* tile = _mapNode->getTerrain()->getTerrainTileUnderMouse(view, x, y);
                if (tile)
                {
                    GeoPoint p = _mapNode->getGeoPointUnderMouse(view, x, y);
                    TileKey key = _mouseOverCoverageLayer->getProfile()->createTileKey(p, tile->getKey().getLOD());
                    key = _mouseOverCoverageLayer->getBestAvailableTileKey(key);

                    if (key.valid())
                    {
                        auto task = [this, key, p](Cancelable& c)
                            {
                                osg::ref_ptr<ProgressCallback> prog = new ProgressCallback(&c);

                                auto factory = osgEarth::Procedural::LandCoverSample::Factory::create(_mouseOverCoverageLayer);
                                auto coverage = factory->createCoverage(key, prog.get());
                                const osgEarth::Procedural::LandCoverSample* value = nullptr;
                                TileKey key2 = key;
                                for(TileKey key2 = key; !value && key2.valid(); key2.makeParent()) {
                                    value = coverage.readAtCoords(p.x(), p.y());
                                }
                                if (value)
                                {
                                    return Result<osgEarth::Procedural::LandCoverSample>(*value);
                                }
                                return Result<osgEarth::Procedural::LandCoverSample>(Status::Error("No value"));
                            };

                        _coverageLayerValueUnderMouse = jobs::dispatch(task);
                    }
                }
            }
#endif
        }
    };
}

