#include <dirent.h>
#include <sstream>
#include <iostream>
#include <cassert>

#include <giomm.h>
#include <glibmm/spawn.h>

#include "menu.hpp"
#include "gtk-utils.hpp"
#include "wf-autohide-window.hpp"
#include "wf-popover.hpp"

#include "categorybutton.hpp"
#include "item.hpp"

const std::string default_icon = "wayfire";


void WayfireMenu::load_menu_item(AppInfo app_info)
{
    if (!app_info)
    {
        return;
    }

    if (app_info->get_nodisplay())
    {
        return;
    }

    auto name = app_info->get_name();
    auto exec = app_info->get_executable();
    /* If we don't have the following, then the entry won't be useful anyway,
     * so we should skip it */
    if (name.empty() || !app_info->get_icon() || exec.empty())
    {
        return;
    }

    /* Already created such a launcher, skip */
    if (loaded_apps.count({name, exec}))
    {
        return;
    }

    loaded_apps.insert({name, exec});

    /* Check if this has a 'OnlyShownIn' for a different desktop env
    *  If so, we throw it in a pile at the bottom just to be safe */
    if (!app_info->should_show())
    {
        add_category_app("Hidden", app_info);
        return;
    }

    add_category_app("All", app_info);

    /* Split the Categories, iterate to place into submenus */
    std::stringstream categories_stream(app_info->get_categories());
    std::string segment;

    while (std::getline(categories_stream, segment, ';'))
    {
        add_category_app(segment, app_info);
    }
}

void WayfireMenu::add_category_app(std::string category, AppInfo app)
{
    /* Filter for allowed categories */
    if (category_list.count(category) == 1)
    {
        category_list[category]->items.push_back(app);
    }
}

void WayfireMenu::populate_menu_categories()
{
    // Ensure the category list is empty
    for (auto child : category_box.get_children())
    {
        category_box.remove(*child);
        delete child;
    }

    // Iterate allowed categories in order
    for (auto category_name : category_order)
    {
        if (category_list.count(category_name) == 1)
        {
            auto& category = category_list[category_name];
            if (category->items.size() > 0)
            {
                auto icon_name = category->get_icon_name();
                auto name = category->get_name();
                auto category_button = new WfMenuCategoryButton(this, category_name, name, icon_name);
                category_box.append(*category_button);
            }
        } else
        {
            std::cerr << "Category in orderlist without Category object : " << category << std::endl;
        }
    }
}

void WayfireMenu::populate_menu_items()
{
    /* Ensure the flowbox is empty */
    for (auto child : flowbox.get_children())
    {
        flowbox.remove(*child);
        delete child;
    }

    for (auto app_info : category_list["All"]->items)
    {
        auto app = new WfMenuItem(this, app_info);
        flowbox.append(*app);
    }
}

static bool ends_with(std::string text, std::string pattern)
{
    if (text.length() < pattern.length())
    {
        return false;
    }

    return text.substr(text.length() - pattern.length()) == pattern;
}

void WayfireMenu::load_menu_items_from_dir(std::string path)
{
    /* Expand path */
    auto dir = opendir(path.c_str());
    if (!dir)
    {
        return;
    }

    /* Iterate over all files in the directory */
    dirent *file;
    while ((file = readdir(dir)) != 0)
    {
        /* Skip hidden files and folders */
        if (file->d_name[0] == '.')
        {
            continue;
        }

        auto fullpath = path + "/" + file->d_name;

        if (ends_with(fullpath, ".desktop"))
        {
            load_menu_item(Gio::DesktopAppInfo::create_from_filename(fullpath));
        }
    }
}

void WayfireMenu::load_menu_items_all()
{
    std::string home_dir = getenv("HOME");
    auto app_list = Gio::AppInfo::get_all();
    for (auto app : app_list)
    {
        auto desktop_app = std::dynamic_pointer_cast<Gio::DesktopAppInfo>(app);
        load_menu_item(desktop_app);
    }

    load_menu_items_from_dir(home_dir + "/Desktop");
}

void WayfireMenu::on_search_changed()
{
    if (menu_show_categories)
    {
        if (search_entry.get_text().length() == 0)
        {
            /* Text has been unset, show categories again */
            flowbox.invalidate_filter();
            category_scrolled_window.show();
        } else
        {
            /* User is filtering, hide categories, ignore chosen category */
            flowbox.invalidate_filter();
        }
    }

    m_sort_names  = search_entry.get_text().length() == 0;
    fuzzy_filter  = false;
    count_matches = 0;
    flowbox.unselect_all();
    flowbox.invalidate_filter();
    flowbox.invalidate_sort();

    /* We got no matches, try to fuzzy-match */
    if ((count_matches <= 0) && fuzzy_search_enabled)
    {
        fuzzy_filter = true;
        flowbox.unselect_all();
        flowbox.invalidate_filter();
        flowbox.invalidate_sort();
    }

    select_first_flowbox_item();
}

bool WayfireMenu::on_filter(Gtk::FlowBoxChild *child)
{
    auto button = dynamic_cast<WfMenuItem*>(child);
    assert(button);

    if (this->category != "All")
    {
        const auto& target_items = category_list[this->category]->items;
        auto it = std::find(target_items.begin(), target_items.end(), button->get_app_info());

        if (it == target_items.end())
        {
            return false;
        }
    }

    auto text = search_entry.get_text();
    uint32_t match_score = this->fuzzy_filter ?
        button->fuzzy_match(text) : button->matches(text);

    button->set_search_value(match_score);
    if (match_score > 0)
    {
        this->count_matches++;
        return true;
    }

    return false;
}

bool WayfireMenu::on_sort(Gtk::FlowBoxChild *a, Gtk::FlowBoxChild *b)
{
    auto b1 = dynamic_cast<WfMenuItem*>(a);
    auto b2 = dynamic_cast<WfMenuItem*>(b);
    assert(b1 && b2);

    if (m_sort_names)
    {
        return *b2 < *b1;
    }

    return b2->get_search_value() > b1->get_search_value();
}

void WayfireMenu::on_popover_shown()
{
    search_entry.delete_text(0, search_entry.get_text().length());
    on_search_changed();
    set_category("All");
    flowbox.unselect_all();
    search_entry.grab_focus();
}

bool WayfireMenu::update_icon()
{
    std::string icon;
    if (((std::string)menu_icon).empty())
    {
        icon = default_icon;
    } else
    {
        icon = menu_icon;
    }

    IconProvider::image_set_icon(main_image, icon);
    return true;
}

void WayfireMenu::setup_popover_layout()
{
    button->set_popup_child(popover_layout_box);

    popover_layout_box.append(app_scrolled_window);
    popover_layout_box.append(category_scrolled_window);
    popover_layout_box.append(search_entry);
    popover_layout_box.append(separator);
    popover_layout_box.append(box_bottom);

    flowbox.set_selection_mode(Gtk::SelectionMode::SINGLE);
    flowbox.set_activate_on_single_click(true);
    flowbox.set_valign(Gtk::Align::START);
    flowbox.set_homogeneous(true);
    flowbox.set_sort_func(sigc::mem_fun(*this, &WayfireMenu::on_sort));
    flowbox.set_filter_func(sigc::mem_fun(*this, &WayfireMenu::on_filter));
    flowbox.add_css_class("app-list");
    flowbox.set_vexpand(true);
    flowbox.set_hexpand(true);

    flowbox_container.append(flowbox);
    app_scrolled_window.set_child(flowbox_container);
    app_scrolled_window.add_css_class("app-list-scroll");
    app_scrolled_window.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);

    category_box.add_css_class("category-list");
    category_box.set_orientation(Gtk::Orientation::VERTICAL);

    category_scrolled_window.set_child(category_box);
    category_scrolled_window.add_css_class("categtory-list-scroll");
    category_scrolled_window.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
    category_scrolled_window.set_vexpand(true);

    search_entry.add_css_class("app-search");

    signals.push_back((search_entry.signal_changed().connect(
        [this] ()
    {
        on_search_changed();
    })));
    auto typing_gesture = Gtk::EventControllerKey::create();
    typing_gesture->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
    signals.push_back(typing_gesture->signal_key_pressed().connect([=] (guint keyval, guint keycode,
                                                                        Gdk::ModifierType state)
    {
        Gtk::Widget *focused = nullptr;
        auto root = popover_layout_box.get_root();
        if (root)
        {
            focused = root->get_focus();
        }

        if ((keyval == GDK_KEY_Return) || (keyval == GDK_KEY_KP_Enter))
        {
            auto children = flowbox.get_selected_children();
            if (children.size() == 1)
            {
                auto child = dynamic_cast<WfMenuItem*>(children[0]);
                child->on_click();
            }

            return true;
        } else if (keyval == GDK_KEY_Escape)
        {
            button->popdown();
            return true;
        } else if ((keyval == GDK_KEY_Up) ||
                   (keyval == GDK_KEY_Down) ||
                   (keyval == GDK_KEY_Left) ||
                   (keyval == GDK_KEY_Right))
        {
            return false;
        } else if (focused && focused->is_ancestor(search_entry))
        {
            return false;
        } else
        {
            if (!search_entry.grab_focus())
            {
                std::cerr << "Unable to steal focus to entry" << std::endl;
            }

            // this is a hack to still try to get the key propersly to resume
            // search, as we can’t focus the entry and have the key be used by
            // it. Better than nothing, but it fails to account for keys other
            // than simple letters, and still focuses on other keys.
            std::string input = gdk_keyval_name(keyval);
            if (input.length() == 1)
            {
                search_entry.set_text(search_entry.get_text() + input);
            }

            auto pos = search_entry.get_text().length();
            search_entry.select_region(pos, pos);
            on_search_changed();
            return false;
        }
    }, false));
    popover_layout_box.add_controller(typing_gesture);

    layout = std::make_shared<WfMenuLayout>(this);
    popover_layout_box.set_layout_manager(layout);
}

void WayfireMenu::update_popover_layout()
{
    if (!menu_show_categories)
    {
        category_scrolled_window.hide();
    } else
    {
        category_scrolled_window.show();
    }

    if (menu_list)
    {
        flowbox.set_max_children_per_line(1);
    } else
    {
        flowbox.set_max_children_per_line(-1);
    }
}

void WayfireMenu::on_logout_click()
{
    button->popdown();
    if (!menu_logout_command.value().empty())
    {
        g_spawn_command_line_async(menu_logout_command.value().c_str(), NULL);
        return;
    }

    /* If no command specified for logout, show our own logout window */
    logout_ui->ui.present();
}

void WayfireMenu::refresh()
{
    loaded_apps.clear();
    for (auto& [key, category] : category_list)
    {
        category->items.clear();
    }

    for (auto child : flowbox.get_children())
    {
        flowbox.remove(*child);
        delete child;
    }

    load_menu_items_all();
    populate_menu_categories();
    populate_menu_items();
}

static void app_info_changed(GAppInfoMonitor *gappinfomonitor, gpointer user_data)
{
    WayfireMenu *menu = (WayfireMenu*)user_data;

    menu->refresh();
}

void WayfireMenu::init(Gtk::Box *container)
{
    /* https://specifications.freedesktop.org/menu-spec/latest/apa.html#main-category-registry
     * Using the 'Main' categories, with names and icons assigned
     * Any Categories in .desktop files that are not in this list are ignored */
    category_list["All"]     = std::make_unique<WfMenuCategory>("All", "applications-other");
    category_list["Network"] = std::make_unique<WfMenuCategory>("Internet",
        "applications-internet");
    category_list["Education"] = std::make_unique<WfMenuCategory>("Education",
        "applications-education");
    category_list["Office"] = std::make_unique<WfMenuCategory>("Office",
        "applications-office");
    category_list["Development"] = std::make_unique<WfMenuCategory>("Development",
        "applications-development");
    category_list["Graphics"] = std::make_unique<WfMenuCategory>("Graphics",
        "applications-graphics");
    category_list["AudioVideo"] = std::make_unique<WfMenuCategory>("Multimedia",
        "applications-multimedia");
    category_list["Game"] = std::make_unique<WfMenuCategory>("Games",
        "applications-games");
    category_list["Science"] = std::make_unique<WfMenuCategory>("Science",
        "applications-science");
    category_list["Settings"] = std::make_unique<WfMenuCategory>("Settings",
        "preferences-desktop");
    category_list["System"] = std::make_unique<WfMenuCategory>("System",
        "applications-system");
    category_list["Utility"] = std::make_unique<WfMenuCategory>("Accessories",
        "applications-utilities");
    category_list["Hidden"] = std::make_unique<WfMenuCategory>("Other Desktops",
        "user-desktop");

    main_image.add_css_class("default-icon");
    main_image.add_css_class("menu-icon");

    signals.push_back(output->toggle_menu_signal().connect(sigc::mem_fun(*this, &WayfireMenu::toggle_menu)));

    // configuration reloading callbacks
    menu_icon.set_callback([=] () { update_icon(); });
    flowbox_spacing.set_callback([=] ()
    {
        flowbox.set_column_spacing(flowbox_spacing.value());
        flowbox.set_column_spacing(flowbox_spacing.value());
    });
    menu_min_category_width.set_callback([=] () { update_size(); });
    menu_min_content_height.set_callback([=] () { update_size(); });
    menu_min_content_width.set_callback([=] () { update_size(); });
    panel_position.set_callback([=] () { update_popover_layout(); });
    menu_show_categories.set_callback([=] () { update_popover_layout(); });
    menu_list.set_callback([=] () { update_popover_layout(); });

    button = std::make_unique<WayfireMenuWidget>("panel", "menu");
    button->set_child(main_image);
    button->set_keyboard_interactive(true);
    button->add_css_class("menu-button");
    button->open_on(1); /* Open menu on left click */
    signals.push_back(button->signal_popup().connect(
        sigc::mem_fun(*this, &WayfireMenu::on_popover_shown)));

    if (!update_icon())
    {
        return;
    }

    signals.push_back(button->property_scale_factor().signal_changed().connect(
        [=] () {update_icon(); }));

    button->set_popup_child(popover_layout_box);

    container->append(box);
    box.append(*button);

    logout_image.set_icon_size(Gtk::IconSize::LARGE);
    logout_image.set_from_icon_name("system-shutdown");
    logout_button.add_css_class("flat");
    signals.push_back(logout_button.signal_clicked().connect(
        sigc::mem_fun(*this, &WayfireMenu::on_logout_click)));
    logout_button.set_margin_end(35);
    logout_button.set_child(logout_image);
    box_bottom.append(logout_button);
    box_bottom.set_halign(Gtk::Align::END);

    popover_layout_box.set_orientation(Gtk::Orientation::VERTICAL);

    logout_ui = std::make_unique<WayfireLogoutUI>();

    load_menu_items_all();
    setup_popover_layout();
    update_popover_layout();
    populate_menu_categories();
    populate_menu_items();
    app_info_monitor = g_app_info_monitor_get();
    app_info_monitor_changed_handler_id =
        g_signal_connect(app_info_monitor, "changed", G_CALLBACK(app_info_changed), this);

    box.show();
    main_image.show();
    button->show();

    signals.push_back(button->get_scroll().signal_map().connect([=] ()
    {
        update_size();
    }));
}

void WayfireMenu::update_size()
{
    int width  = button->get_scroll().get_width();
    int height = button->get_scroll().get_height();
    if ((width <= 0) || (height <= 0))
    {
        /* Not yet allocated, do it next tick */
        width  = menu_min_content_width;
        height = menu_min_content_height;
        if (menu_show_categories.value())
        {
            width += menu_min_category_width;
        }

        layout->set_limit(width, height);

        Glib::signal_idle().connect_once([=] ()
        {
            update_size();
        });
        return;
    }

    /* We know the size of the outside of the scrollbox, use it */
    layout->set_limit(width, height);
}

void WayfireMenu::toggle_menu()
{
    search_entry.set_text("");

    button->toggle();
}

void WayfireMenu::hide_menu()
{
    button->popdown();
}

void WayfireMenu::set_category(std::string in_category)
{
    category = in_category;
    flowbox.invalidate_filter();
}

void WayfireMenu::select_first_flowbox_item()
{
    auto child = flowbox.get_child_at_index(0);
    if (child)
    {
        auto cast_child = dynamic_cast<WfMenuItem*>(child);
        if (cast_child)
        {
            flowbox.select_child(*cast_child);
        }
    }
}

WayfireMenu::~WayfireMenu()
{
    if (app_info_monitor)
    {
        g_signal_handler_disconnect(app_info_monitor, app_info_monitor_changed_handler_id);
    }

    for (auto signal : signals)
    {
        signal.disconnect();
    }
}
