/*
 *  Copyright (c) 2000-2022 Inria
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *  this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *  * Neither the name of the ALICE Project-Team nor the names of its
 *  contributors may be used to endorse or promote products derived from this
 *  software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 *
 *  Contact: Bruno Levy
 *
 *     https://www.inria.fr/fr/bruno-levy
 *
 *     Inria,
 *     Domaine de Voluceau,
 *     78150 Le Chesnay - Rocquencourt
 *     FRANCE
 *
 */

#include <geogram_gfx/imgui_ext/imgui_ext.h>
#include <geogram_gfx/imgui_ext/icon_font.h>

#ifdef GEO_COMPILER_GCC_FAMILY
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wconversion"
#ifdef GEO_COMPILER_CLANG
#pragma GCC diagnostic ignored "-Wunknown-warning-option"
#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
#pragma GCC diagnostic ignored "-Wc++98-compat-pedantic"
#endif
#endif

#include <geogram_gfx/third_party/imgui/imgui_internal.h>

#if defined(GEO_COMPILER_GCC_FAMILY)
#pragma GCC diagnostic pop
#endif


#include <geogram/basic/string.h>
#include <geogram/basic/logger.h>
#include <geogram/basic/file_system.h>
#include <geogram/basic/command_line.h>
#include <map>

namespace {
    using namespace GEO;

    bool initialized = false;
    bool tooltips_enabled = true;

    /**
     * \brief Manages the GUI of a color editor.
     * \details This creates a custom dialog with the color editor and
     *  a default palette, as in ImGUI example.
     * \param[in] label the label of the widget, passed to ImGUI
     * \param[in,out] color_in a pointer to an array of 3 floats if
     *  with_alpha is false or 4 floats if with_alpha is true
     * \param[in] with_alpha true if transparency is edited, false otherwise
     * \retval true if the color was changed
     * \retval false otherwise
     */
    bool ColorEdit3or4WithPalette(
        const char* label, float* color_in, bool with_alpha
    ) {
        bool result = false;
        static bool saved_palette_initialized = false;
        static ImVec4 saved_palette[40];
        static ImVec4 backup_color;
        ImGui::PushID(label);
        int flags =
            ImGuiColorEditFlags_PickerHueWheel |
            ImGuiColorEditFlags_Float |
	    ImGuiColorEditFlags_AlphaOpaque ;

        if(!with_alpha) {
            flags |= ImGuiColorEditFlags_NoAlpha ;
        }

        ImVec4& color = *(ImVec4*)color_in;

        if (!saved_palette_initialized) {

            saved_palette[0] = ImVec4(0.0f, 0.0f, 0.0f, 1.0f);
            saved_palette[1] = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
            saved_palette[2] = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
            saved_palette[3] = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
            saved_palette[4] = ImVec4(1.0f, 1.0f, 0.0f, 1.0f);
            saved_palette[5] = ImVec4(0.0f, 0.0f, 1.0f, 1.0f);
            saved_palette[6] = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
            saved_palette[7] = ImVec4(0.0f, 1.0f, 1.0f, 1.0f);

            for (int n = 0; n < 32; n++) {
                ImGui::ColorConvertHSVtoRGB(
                    float(n) / 31.0f, 0.8f, 0.8f,
                    saved_palette[n+8].x,
                    saved_palette[n+8].y,
                    saved_palette[n+8].z
                );
		saved_palette[n+8].w = 1.0f;
            }
            saved_palette_initialized = true;
        }

        bool open_popup = ImGui::ColorButton(label, color, flags);

        if(label[0] != '#') {
            ImGui::SameLine();
            ImGui::Text("%s",label);
        }
        if (open_popup) {
            ImGui::OpenPopup("##PickerPopup");
            backup_color = color;
        }
        if (ImGui::BeginPopup("##PickerPopup")) {
            if(label[0] != '#') {
                ImGui::Text("%s",label);
                ImGui::Separator();
            }
            if(ImGui::ColorPicker4(
                   "##picker", (float*)&color,
                   flags | ImGuiColorEditFlags_NoSidePreview
                   | ImGuiColorEditFlags_NoSmallPreview
               )
              ) {
                result = true;
            }
            ImGui::SameLine();
            ImGui::BeginGroup();
            ImGui::Text("Current");
            ImGui::ColorButton(
                "##current", color,
                ImGuiColorEditFlags_NoPicker |
                ImGuiColorEditFlags_AlphaPreviewHalf,
                ImVec2(60,40)
            );
            ImGui::Text("Previous");
            if (ImGui::ColorButton(
                    "##previous", backup_color,
                    ImGuiColorEditFlags_NoPicker |
                    ImGuiColorEditFlags_AlphaPreviewHalf,
                    ImVec2(60,40))
               ) {
                color = backup_color;
                result = true;
            }
            ImGui::Separator();
            ImGui::Text("Palette");

#ifdef GEO_OS_ANDROID
            int nb_btn_per_row = 4;
            float btn_size = 35.0;
#else
            int nb_btn_per_row = 8;
            float btn_size = 20.0;
#endif
            for (int n = 0; n < 40; n++) {
                ImGui::PushID(n);
                if ( (n % nb_btn_per_row) != 0 ) {
                    ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y);
                }
                if (ImGui::ColorButton(
                        "##palette",
                        saved_palette[n],
                        ImGuiColorEditFlags_NoPicker |
                        ImGuiColorEditFlags_NoTooltip,
                        ImVec2(btn_size,btn_size))
                   ) {
                    color = ImVec4(
                        saved_palette[n].x,
                        saved_palette[n].y,
                        saved_palette[n].z,
                        color.w
                    ); // Preserve alpha!
                    result = true;
                }
                ImGui::PopID();
            }
            ImGui::Separator();
            if(ImGui::Button(
                   "OK", ImVec2(-1,-1)
               )
              ) {
                ImGui::CloseCurrentPopup();
            }
            ImGui::EndGroup();
            ImGui::EndPopup();
        }
        ImGui::PopID();
        return result;
    }

    /**************************************************************************/

    /**
     * \brief Safer version of strncpy()
     * \param[in] dest a pointer to the destination string
     * \param[in] source a pointer to the source string
     * \param[in] max_dest_size number of characters available in
     *  destination string
     * \return the length of the destination string after copy. If
     *  the source string + null terminator was greater than max_dest_size,
     *  then it is cropped. On exit, dest is always null-terminated (in
     *  contrast with strncpy()).
     */
    size_t safe_strncpy(
        char* dest, const char* source, size_t max_dest_size
    ) {
        strncpy(dest, source, max_dest_size-1);
        dest[max_dest_size-1] = '\0';
        return strlen(dest);
    }

    /**
     * \brief Converts a complete path to a file to a label
     *  displayed in the file browser.
     * \details Strips viewer_path from the input path.
     * \param[in] path the complete path, can be either a directory or
     *  a file
     * \return the label to be displayed in the menu
     */
    std::string path_to_label(
        const std::string& viewer_path, const std::string& path
    ) {
        std::string result = path;
        if(GEO::String::string_starts_with(result, viewer_path)) {
            result = result.substr(
                viewer_path.length(), result.length()-viewer_path.length()
            );
        }
        return result;
    }


    /**
     * \brief Converts an icon symbolic name and a label to a string.
     * \param[in] icon_sym the symbolic name of the icon.
     * \param[in] label the label to be displayed.
     * \param[in] no_label if true, do not display the label
     * \return a UTF8 string with the icon and label, or just the label
     *  if the icon font is not initialized.
     */
    std::string icon_label(
        const char* icon_sym,
        const char* label = nullptr,
        bool no_label = false
    ) {
        wchar_t str[2];
        str[0] = icon_wchar(icon_sym);
        if(str[0] == '\0') {
            return std::string(label);
        }
        str[1] = '\0';
        if(label == nullptr) {
            return GEO::String::wchar_to_UTF8(str);
        }
        if(no_label) {
            return GEO::String::wchar_to_UTF8(str) + "##" + label;
        }
        if(label[0] == '#') {
            return GEO::String::wchar_to_UTF8(str) + label;
        }
        return GEO::String::wchar_to_UTF8(str) + " " + label;
    }

    /**************************************************************************/

    /**
     * \brief The state for OpenFileDialog() and FileDialog()
     */
    class FileDialog {
    public:

        /**
         * \brief FileDialog constructor.
         * \param[in] save_mode if true, FileDialog is used to create files
         * \param[in] default_filename the default file name used if save_mode
         *  is set
         */
        FileDialog(
            bool save_mode=false,
            const std::string& default_filename="",
            FileSystem::Node* root = nullptr
        ) : visible_(false),
            root_(nullptr),
            current_write_extension_index_(0),
            pinned_(false),
            show_hidden_(false),
            scroll_to_file_(false),
            save_mode_(save_mode),
            are_you_sure_(false)
            {
                set_root(root);
                set_default_filename(default_filename);
                current_file_index_ = 0;
                current_directory_index_ = 0;
                current_write_extension_index_ = 0;
                no_docking_ =
                    !CmdLine::get_arg_bool("gui:expert") &&
                    !CmdLine::get_arg_bool("gui:phone_screen");
            }


        /**
         * \brief Sets the root node to be used with the FileDialog.
         * \param[in] root a pointer to the root node.
         */
        void set_root(FileSystem::Node* root) {
            FileSystem::Node* prev_root = root_;
            root_ = root;
            if(root_ == nullptr) {
                FileSystem::get_root(root_);
            }
            if(prev_root != root_) {
#if defined(GEO_OS_WINDOWS) || defined(GEO_OS_ANDROID)
                directory_ = root_->documents_directory();
#else
                directory_ = root_->get_current_working_directory();
#endif
                if(
                    directory_ == "" ||
                    directory_[directory_.length()-1] != '/'
                ) {
                    directory_ += "/";
                }
            }
        }

        /**
         * \brief Sets the default file.
         * \details Only valid if save_mode is set.
         * \param[in] default_filename the default file name.
         */
        void set_default_filename(const std::string& default_filename) {
            safe_strncpy(
                current_file_, default_filename.c_str(), sizeof(current_file_)
            );
        }

        /**
         * \brief Makes this FileDialog visible.
         */
        void show() {
            update_files();
            visible_ = true;
        }

        /**
         * \brief Makes this FileDialog invisibile.
         */
        void hide() {
            visible_ = false;
        }

        /**
         * \brief Draws this FileDialog and handles the gui.
         */
        void draw() {
            if(!visible_) {
                return;
            }

            bool phone_screen = CmdLine::get_arg_bool("gui:phone_screen");

            if(!phone_screen) {
                ImGui::SetNextWindowSize(
                    ImVec2(ImGui::scaling()*400.0f, ImGui::scaling()*415.0f),
                    ImGuiCond_Once
                );
            }

            std::string label = std::string(
                save_mode_ ? "Save as...##" : "Load...##"
            );

            if(!phone_screen) {
                label += String::to_string(this);
            }

            ImGui::Begin(
                label.c_str(),
                &visible_,
                ImGuiWindowFlags_NoCollapse | (
                    (no_docking_ && !pinned_) ? ImGuiWindowFlags_NoDocking : 0
                )
            );

            float s = ImGui::CalcTextSize(icon_UTF8("thumbtack").c_str()).x;
            float spacing = 0.15f*s;

            bool compact = (ImGui::GetContentRegionAvail().x < s*10.0f);

            if(phone_screen) {
                if(ImGui::SimpleButton(icon_label(
                                           "window-close","##file_dialog_close",
                                           compact
                                       ))) {
                    visible_ = false;
                }
                ImGui::SameLine();
                ImGui::Dummy(ImVec2(spacing,1.0f));
                ImGui::SameLine();
            }

            if(ImGui::SimpleButton(icon_label(
                                       "arrow-circle-up","parent", compact
                                   ))) {
                set_directory("../");
            }
            ImGui::SameLine();
            ImGui::Dummy(ImVec2(spacing,1.0f));
            ImGui::SameLine();
            if(ImGui::SimpleButton(icon_label("home","home",compact))) {
                set_directory(root_->documents_directory());
                update_files();
            }
            ImGui::SameLine();
            ImGui::Dummy(ImVec2(spacing,1.0f));
            ImGui::SameLine();
            if(ImGui::SimpleButton(icon_label("sync-alt","refresh",compact))) {
                update_files();
            }

            if(!save_mode_ && !phone_screen) {
                ImGui::SameLine();
                ImGui::Dummy(
                    ImVec2(
                        ImGui::GetContentRegionAvail().x - s*1.1f,1.0f
                    )
                );
                ImGui::SameLine();
                if(pinned_) {
                    if(ImGui::SimpleButton(
                           icon_UTF8("dot-circle") + "##pin"
                       )) {
                        pinned_ = !pinned_;
                    }
                } else {
                    if(ImGui::SimpleButton(
                           icon_UTF8("thumbtack") + "##pin"
                       )) {
                        pinned_ = !pinned_;
                    }
                }
                if(ImGui::IsItemHovered()) {
                    ImGui::SetTooltip("Keeps this dialog open.");
                }
            }

            draw_disk_drives();
            ImGui::Separator();

            {
                std::vector<std::string> path;
                String::split_string(directory_, '/', path);
                for(index_t i=0; i<path.size(); ++i) {
                    if(i != 0) {
                        ImGui::SameLine();
                        if(
                            ImGui::GetContentRegionAvail().x <
                            ImGui::CalcTextSize(path[i].c_str()).x +
                            10.0f*ImGui::scaling()
                        ) {
                            ImGui::NewLine();
                        }
                    }
                    // We need to generate a unique id, else there is an id
                    // clash with the "home" button right before !!
                    if(ImGui::SimpleButton(
                           (path[i] + "##path" + String::to_string(i))
                       )) {
                        std::string new_dir;
                        if(path[0].length() >= 2 && path[0][1] == ':') {
                            new_dir = path[0];
                        } else {
                            new_dir += "/";
                            new_dir += path[0];
                        }
                        for(index_t j=1; j<=i; ++j) {
                            new_dir += "/";
                            new_dir += path[j];
                        }
                        set_directory(new_dir);
                    }
                    ImGui::SameLine();
                    ImGui::Text("/");
                }
            }

            const float footer_size =
                phone_screen ? 0.0f : 35.0f*ImGui::scaling();

            if(phone_screen) {
                draw_footer();
            }
            {
                ImGui::BeginChild(
                    "##directories",
                    ImVec2(
                        ImGui::GetWindowWidth()*0.5f-10.0f*ImGui::scaling(),
                        -footer_size
                    ),
                    true
                );
                for(index_t i=0; i<directories_.size(); ++i) {
                    if(ImGui::Selectable(
                           directories_[i].c_str(),
                           (i == current_directory_index_)
                       )) {
                        current_directory_index_ = i;
                        set_directory(directories_[current_directory_index_]);
                    }
                }
                ImGui::EndChild();
            }
            ImGui::SameLine();
            {
                ImGui::BeginChild(
                    "##files",
                    ImVec2(
                        ImGui::GetWindowWidth()*0.5f-10.0f*ImGui::scaling(),
                        -footer_size
                    ),
                    true
                );
                for(index_t i=0; i<files_.size(); ++i) {
                    if(ImGui::Selectable(
                           files_[i].c_str(),
                           (i == current_file_index_)
                       )) {
                        safe_strncpy(
                            current_file_,files_[i].c_str(),
                            sizeof(current_file_)
                        );
                        if(current_file_index_ == i) {
                            file_selected();
                        } else {
                            current_file_index_ = i;
                        }
                    }
                    if(scroll_to_file_ && i == current_file_index_) {
                        ImGui::SetScrollHereY();
                        scroll_to_file_ = false;
                    }
                }
                ImGui::EndChild();
            }
            if(!phone_screen) {
                draw_footer();
            }
            ImGui::End();
            draw_are_you_sure();
        }

        /**
         * \brief Sets whether this file dialog is for
         *  saving file.
         * \details If this file dialog is for saving file,
         *  then the user can enter the name of a non-existing
         *  file, else he can only select existing files.
         * \param[in] x true if this file dialog is for
         *  saving file.
         */
        void set_save_mode(bool x) {
            save_mode_ = x;
        }

        /**
         * \brief Gets the selected file if any and resets it
         *  to the empty string.
         * \return the selected file if there is any or the
         *  empty string otherwise.
         */
        std::string get_and_reset_selected_file() {
            std::string result;
            std::swap(result,selected_file_);
            return result;
        }

        /**
         * \brief Defines the file extensions managed by this
         *  FileDialog.
         * \param[in] extensions a ';'-separated list of extensions
         */
        void set_extensions(const std::string& extensions) {
            extensions_.clear();
            GEO::String::split_string(extensions, ';', extensions_);
        }

    protected:

        void draw_footer() {
            if(ImGui::Button(
                   save_mode_ ?
                   icon_label("save","Save as").c_str() :
                   icon_label("folder-open","Load").c_str()
               )) {
                file_selected();
            }
            ImGui::SameLine();
            ImGui::PushItemWidth(
                save_mode_ ?
                -80.0f*ImGui::scaling() : -5.0f*ImGui::scaling()
            );
            if(ImGui::InputText(
                   "##filename",
                   current_file_, geo_imgui_string_length,
                   ImGuiInputTextFlags_EnterReturnsTrue    |
                   ImGuiInputTextFlags_CallbackHistory     |
                   ImGuiInputTextFlags_CallbackCompletion ,
                   text_input_callback,
                   this
               )
              ) {
                scroll_to_file_ = true;
                std::string file = current_file_;
                for(index_t i=0; i<files_.size(); ++i) {
                    if(files_[i] == file) {
                        current_file_index_ = i;
                    }
                }
                file_selected();
            }
            ImGui::PopItemWidth();
            // Keep auto focus on the input box
            if (ImGui::IsItemHovered()) {
                // Auto focus previous widget
                ImGui::SetKeyboardFocusHere(-1);
            }

            if(save_mode_) {
                ImGui::SameLine();
                ImGui::PushItemWidth(-5.0f*ImGui::scaling());

                std::vector<const char*> write_extensions;
                for(index_t i=0; i<extensions_.size(); ++i) {
                    write_extensions.push_back(&extensions_[i][0]);
                }
                if(ImGui::Combo(
                       "##extension",
                       (int*)(&current_write_extension_index_),
                       &write_extensions[0],
                       int(write_extensions.size())
                   )
                  ) {
                    std::string file = current_file_;
                    file = root_->base_name(file) + "." +
                        extensions_[current_write_extension_index_];
                    safe_strncpy(
                        current_file_, file.c_str(),
                        sizeof(current_file_)
                    );
                }
                ImGui::PopItemWidth();
            }
        }

        /**
         * \brief Tests whether a file can be read.
         * \param[in] filename the file name to be tested.
         * \retval true if this file can be read.
         * \retval false otherwise.
         */
        bool can_load(const std::string& filename) {
            if(!root_->is_file(filename)) {
                return false;
            }
            std::string ext = root_->extension(filename);
            for(size_t i=0; i<extensions_.size(); ++i) {
                if(extensions_[i] == ext || extensions_[i] == "*") {
                    return true;
                }
            }
            return false;
        }

        /**
         * \brief Updates the list of files and directories
         *  displayed by this FileDialog.
         */
        void update_files() {
            directories_.clear();
            files_.clear();

            directories_.push_back("../");

            std::vector<std::string> entries;
            root_->get_directory_entries(directory_, entries);
            std::sort(entries.begin(), entries.end());
            for(index_t i=0; i<entries.size(); ++i) {
                if(can_load(entries[i])) {
                    files_.push_back(path_to_label(directory_,entries[i]));
                } else if(root_->is_directory(entries[i])) {
                    std::string subdir =
                        path_to_label(directory_,entries[i]) + "/";
                    if(show_hidden_ || subdir[0] != '.') {
                        directories_.push_back(subdir);
                    }
                }
            }
            if(current_directory_index_ >= directories_.size()) {
                current_directory_index_ = 0;
            }
            if(current_file_index_ >= files_.size()) {
                current_file_index_ = 0;
            }
            if(!save_mode_) {
                if(current_file_index_ >= files_.size()) {
                    current_file_[0] = '\0';
                } else {
                    safe_strncpy(
                        current_file_,
                        files_[current_file_index_].c_str(),
                        sizeof(current_file_)
                    );
                }
            }
        }

        /**
         * \brief Changes the current directory.
         * \param[in] directory either the path relative to the
         *  current directory or an absolute path
         */
        void set_directory(const std::string& directory) {
            current_directory_index_ = 0;
            current_file_index_ = 0;
            if(directory[0] == '/' || directory[1] == ':') {
                directory_ = directory;
            } else {
                directory_ = root_->normalized_path(
                    directory_ + "/" +
                    directory
                );
            }
            if(directory_[directory_.length()-1] != '/') {
                directory_ += "/";
            }
            update_files();
        }

        /**
         * \brief The callback for handling the text input.
         * \param[in,out] data a pointer to the callback data
         */
        static int text_input_callback(ImGuiInputTextCallbackData* data) {
            FileDialog* dlg = static_cast<FileDialog*>(data->UserData);
            if(
                (data->EventFlag &
                 ImGuiInputTextFlags_CallbackCompletion) != 0
            ) {
                dlg->tab_callback(data);
            } else if(
                (data->EventFlag & ImGuiInputTextFlags_CallbackHistory) != 0
            ) {
                if(data->EventKey == ImGuiKey_UpArrow) {
                    dlg->updown_callback(data,-1);
                } else if(data->EventKey == ImGuiKey_DownArrow) {
                    dlg->updown_callback(data,1);
                }
            }
            return 0;
        }

        /**
         * \brief Called whenever the up or down arrows are pressed.
         * \param[in,out] data a pointer to the callback data
         * \param[in] direction -1 if the up arrow was pressed, 1 if the
         *  down arrow was pressed
         */
        void updown_callback(ImGuiInputTextCallbackData* data, int direction) {
            int next = int(current_file_index_) + direction;
            if(next < 0) {
                if(files_.size() == 0) {
                    current_file_index_ = 0;
                } else {
                    current_file_index_ = index_t(files_.size()-1);
                }
            } else if(next >= int(files_.size())) {
                current_file_index_ = 0;
            } else {
                current_file_index_ = index_t(next);
            }

            if(files_.size() == 0) {
                current_file_[0] = '\0';
            } else {
                safe_strncpy(
                    current_file_,
                    files_[current_file_index_].c_str(),
                    sizeof(current_file_)
                );
            }
            update_text_edit_callback_data(data);
            scroll_to_file_ = true;
        }

        /**
         * \brief Called whenever the tab key is pressed.
         * \param[in,out] data a pointer to the callback data
         */
        void tab_callback(ImGuiInputTextCallbackData* data) {
            std::string file(current_file_);
            bool found = false;
            for(index_t i=0; i<files_.size(); ++i) {
                if(String::string_starts_with(files_[i],file)) {
                    current_file_index_ = i;
                    found = true;
                    break;
                }
            }
            if(found) {
                safe_strncpy(
                    current_file_,
                    files_[current_file_index_].c_str(),
                    sizeof(current_file_)
                );
                update_text_edit_callback_data(data);
                scroll_to_file_ = true;
            }
        }

        /**
         * \brief Copies the currently selected file into the
         *  string currently manipulated by InputText.
         * \param[out] data a pointer to the callback data
         */
        void update_text_edit_callback_data(
            ImGuiInputTextCallbackData* data
        ) {
            data->BufTextLen = int(
                safe_strncpy(
                    data->Buf, current_file_, (size_t)data->BufSize
                )
            );
            data->CursorPos = data->BufTextLen;
            data->SelectionStart = data->BufTextLen;
            data->SelectionEnd = data->BufTextLen;
            data->BufDirty = true;
        }

        /**
         * \brief Called whenever a file is selected.
         * \param[in] force in save_mode, if set,
         *  overwrites the file even if it already
         *  exists.
         */
        void file_selected(bool force=false) {
            std::string file =
                root_->normalized_path(directory_+"/"+current_file_);

            if(save_mode_) {
                if(!force && root_->is_file(file)) {
                    are_you_sure_ = true;
                    return;
                } else {
                    selected_file_ = file;
                }
            } else {
                selected_file_ = file;
            }

            if(!pinned_) {
                hide();
            }
        }

        /**
         * \brief Handles the "are you sure ?" dialog
         *  when a file is about to be overwritten.
         */
        void draw_are_you_sure() {
            if(are_you_sure_) {
                ImGui::OpenPopup("File exists");
            }
            if(
                ImGui::BeginPopupModal(
                    "File exists", nullptr, ImGuiWindowFlags_AlwaysAutoResize
                )
            ) {
                ImGui::Text(
                    "%s",
                    (std::string("File ") + current_file_ +
                     " already exists\nDo you want to overwrite it ?"
                    ).c_str()
                );
                ImGui::Separator();
                if (ImGui::Button(
                        "Overwrite",
                        ImVec2(-ImGui::GetContentRegionAvail().x/2.0f,0.0f))
                   ) {
                    are_you_sure_ = false;
                    ImGui::CloseCurrentPopup();
                    file_selected(true);
                }
                ImGui::SameLine();
                if (ImGui::Button("Cancel", ImVec2(-1.0f, 0.0f))) {
                    are_you_sure_ = false;
                    ImGui::CloseCurrentPopup();
                }
                ImGui::EndPopup();
            }
        }

        /**
         * \brief Under Windows, add buttons to change
         *  disk drive.
         */
        void draw_disk_drives() {
#ifdef GEO_OS_WINDOWS
            DWORD drives = GetLogicalDrives();
            for(DWORD b=0; b<16; ++b) {
                if((drives & (1u << b)) != 0) {
                    std::string drive;
                    drive += char('A' + char(b));
                    drive += ":";
                    if(ImGui::Button(drive)) {
                        set_directory(drive);
                    }
                    ImGui::SameLine();
                    if(
                        ImGui::GetContentRegionAvail().x <
                        ImGui::CalcTextSize("X:").x + 10.0f*ImGui::scaling()
                    ) {
                        ImGui::NewLine();
                    }
                }
            }
#endif
        }

    private:
        bool visible_;
        FileSystem::Node* root_;
        std::string directory_;
        index_t current_directory_index_;
        index_t current_file_index_;
        std::vector<std::string> directories_;
        std::vector<std::string> files_;
        std::vector<std::string> extensions_;
        index_t current_write_extension_index_;
        char current_file_[geo_imgui_string_length];
        bool pinned_;
        bool show_hidden_;
        bool scroll_to_file_;
        bool save_mode_;
        bool are_you_sure_;
        std::string selected_file_;
        bool no_docking_;
    };

    std::map<std::string, FileDialog*> file_dialogs;

    void terminate_imgui_ext() {
        for(auto& it : file_dialogs) {
            delete it.second;
        }
    }

    void initialize_imgui_ext() {
        if(!initialized) {
            initialized = true;
            atexit(terminate_imgui_ext);
        }
    }
}

namespace ImGui {

    float scaling() {
        ImGuiContext& g = *GImGui;
        float s = 1.0;
        if(g.Font->FontSize > 40.0f) {
            s = g.Font->FontSize / 30.0f;
        } else {
            s = g.Font->FontSize / 20.0f;
        }
        return s * ImGui::GetIO().FontGlobalScale;
    }

    void set_scaling(float x) {
        ImGui::GetIO().FontGlobalScale = x;
    }

    /*******************************************************************/

    bool ColorEdit3WithPalette(const char* label, float* color_in) {
        return ColorEdit3or4WithPalette(label, color_in, false);
    }

    bool ColorEdit4WithPalette(const char* label, float* color_in) {
        return ColorEdit3or4WithPalette(label, color_in, true);
    }

    /*******************************************************************/

    void OpenFileDialog(
        const char* label,
        const char* extensions,
        const char* filename,
        ImGuiExtFileDialogFlags flags,
        FileSystem::Node* root
    ) {
        initialize_imgui_ext();
        ::FileDialog* dlg = nullptr;
        if(file_dialogs.find(label) == file_dialogs.end()) {
            file_dialogs[label] = new ::FileDialog();
        }
        dlg = file_dialogs[label];
        dlg->set_extensions(extensions);
        if(flags == ImGuiExtFileDialogFlags_Save) {
            dlg->set_save_mode(true);
            dlg->set_default_filename(filename);
        } else {
            dlg->set_save_mode(false);
        }
        dlg->set_root(root);
        dlg->show();
    }

    bool FileDialog(
        const char* label, char* filename, size_t filename_buff_len
    ) {
        if(file_dialogs.find(label) == file_dialogs.end()) {
            filename[0] = '\0';
            return false;
        }
        ::FileDialog* dlg = file_dialogs[label];
        dlg->draw();

        std::string result = dlg->get_and_reset_selected_file();
        if(result != "") {
            if(result.length() + 1 >= filename_buff_len) {
                GEO::Logger::err("ImGui_ext") << "filename_buff_len exceeded"
                                              << std::endl;
                return false;
            } else {
                strcpy(filename, result.c_str());
                return true;
            }
        } else {
            return false;
        }
    }

    /****************************************************************/

    void Tooltip(const char* str) {
        if(
            tooltips_enabled && (str != nullptr) && (*str != '\0') &&
            IsItemHovered()
        ) {
            SetTooltip("%s",str);
        }
    }

    void EnableTooltips() {
        tooltips_enabled = true;
    }

    void DisableTooltips() {
        tooltips_enabled = false;
    }

    /****************************************************************/

    bool SimpleButton(const char* label) {
        std::string str(label);
        size_t off = str.find("##");
        if(off != std::string::npos) {
            str = str.substr(0, off);
        }
        ImVec2 label_size = ImGui::CalcTextSize(str.c_str(), NULL, true);
        return ImGui::Selectable(label, false, 0, label_size);
    }

    void CenteredText(const char* text) {
        ImVec2 text_size = ImGui::CalcTextSize(text, NULL, true);
        ImVec2 avail_size = ImGui::GetContentRegionAvail();
        float w = 0.95f*(avail_size.x - text_size.x) / 2.0f;
        if(w > 0.0f) {
            ImGui::Dummy(ImVec2(w, text_size.y));
        }
        ImGui::SameLine();
        ImGui::Text("%s",text);
    }
}
