#include "gpanthera.hh"

#include <cassert>
#include <iostream>
#include <utility>
#include <libintl.h>
#include <locale.h>
#include <filesystem>
#include <ranges>
#include <nlohmann/json.hpp>
#include <unordered_set>

#define _(STRING) dgettext("gpanthera", STRING)

namespace gPanthera {
    sigc::signal<void(double, double)> add_context_menu(Gtk::Widget &widget) {
        sigc::signal<void(double, double)> signal;
        // Add a context menu to a widget
        auto gesture_click = Gtk::GestureClick::create();
        gesture_click->set_button(3);
        auto gesture_keyboard = Gtk::EventControllerKey::create();
        gesture_click->signal_pressed().connect([&widget, signal](int n_press, double x, double y) {
            if(n_press == 1) {
                signal.emit(x, y);
            }
        }, false);
        gesture_keyboard->signal_key_pressed().connect([&widget, signal](int keyval, int keycode, Gdk::ModifierType state) {
            if((keyval == GDK_KEY_F10) && (state == Gdk::ModifierType::SHIFT_MASK) || keyval == GDK_KEY_Menu) {
                // auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
                // popover->set_menu_model(menu);
                // popover->set_has_arrow(false);
                // popover->set_halign(Gtk::Align::START);
                // popover->set_pointing_to(Gdk::Rectangle(0, 0, 1, 1));
                // popover->set_parent(widget);
                // popover->popup();
                signal.emit(0, 0);
            }
            return true;
        }, false);
        widget.add_controller(gesture_click);
        widget.add_controller(gesture_keyboard);
        return signal;
    }

    std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) {
        // Get a vector of the children of a GTK widget, since the container API was removed in GTK 4
        std::vector<Gtk::Widget*> children;
        for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
            children.push_back(child);
        }
        return children;
    }

    Gtk::Image *copy_image(Gtk::Image *image) {
        // Generate a new Gtk::Image with the same contents as an existing one
        if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
            return Gtk::make_managed<Gtk::Image>(image->get_paintable());
        } else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
            auto new_image = Gtk::make_managed<Gtk::Image>();
            new_image->set_from_icon_name(image->get_icon_name());
            return new_image;
        } else if(image->get_storage_type() == Gtk::Image::Type::GICON) {
            auto new_image = Gtk::make_managed<Gtk::Image>(image->get_gicon());
            return new_image;
        } else {
            return nullptr;
        }
    }

    void init() {
        g_log_set_always_fatal(G_LOG_LEVEL_CRITICAL);
        // Set up gettext configuration
        bindtextdomain("gpanthera", "./locales");
    }

    DockablePane::DockablePane(std::shared_ptr<LayoutManager> layout, Gtk::Widget &child, const Glib::ustring &name, const Glib::ustring &label, Gtk::Image *icon, DockStack *stack, Gtk::Widget *custom_header)
        : Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
        if(icon) {
            this->icon = icon;
        }
        if(stack) {
            this->stack = stack;
        }
        this->layout = layout;
        this->layout->add_pane(this);
        this->label.set_text(label);
        // This should be replaced with a custom class in the future
        header = std::make_unique<Gtk::HeaderBar>();
        header->set_show_title_buttons(false);
        if(custom_header) {
            header->set_title_widget(*custom_header);
        } else {
            header->set_title_widget(this->label);
        }
        header->add_css_class("gpanthera-dock-titlebar");
        auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
        header_menu_button->set_direction(Gtk::ArrowType::NONE);

        // Pane menu
        this->action_group = Gio::SimpleActionGroup::create();
        header_menu_button->insert_action_group("win", action_group);

        // Close action
        auto close_action = Gio::SimpleAction::create("close");
        close_action->signal_activate().connect([this](const Glib::VariantBase&) {
            if(this->stack) {
                this->stack->set_visible_child("");
            }
        });
        action_group->add_action(close_action);
        header_menu->append(_("Close"), "win.close");

        // Pop out action
        auto pop_out_action = Gio::SimpleAction::create("pop_out");
        pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
            if(this->stack) {
                this->pop_out();
            }
        });
        action_group->add_action(pop_out_action);
        header_menu->append(_("Pop out"), "win.pop_out");

        // Move menu
        auto move_menu = Gio::Menu::create();
        for(auto &this_stack : this->layout->stacks) {
            auto action_name = "move_" + this_stack->name;
            auto move_action = Gio::SimpleAction::create(action_name);
            move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
                this_stack->add_pane(*this);
            });
            action_group->add_action(move_action);
            move_menu->append(this_stack->name, "win." + action_name);
        }

        // Add move submenu
        header_menu->append_submenu(_("Move"), move_menu);

        // Switch to traditional (nested) submenus, not sliding
        auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
        popover_menu->set_has_arrow(false);
        header_menu_button->set_popover(*popover_menu);

        // TODO: Add a context menu as well

        header->pack_end(*header_menu_button);

        this->prepend(*header);
        this->child = &child;
        this->append(child);
    }

    Gtk::Stack *DockablePane::get_stack() const {
        return stack;
    }

    void DockablePane::redock(DockStack *stack) {
        if(this->window != nullptr) {
            this->window->hide();
            // Put the titlebar back
            this->window->unset_titlebar();
            this->window->set_decorated(false);
            this->header->get_style_context()->remove_class("titlebar");
            this->prepend(*this->header);
            this->window->unset_child();
            this->window->close();
        } else if(this->get_parent() && this->stack == this->get_parent()) {
            this->stack->remove(*this);
        }
        this->stack = stack;
        this->last_stack = stack;
        this->stack->add(*this, this->get_identifier());
        if(this->window != nullptr) {
            this->window->destroy();
            delete this->window;
            this->window = nullptr;
            // Re-enable the pop out option
            auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
            action->set_enabled(true);
            this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
        }
        layout->signal_pane_moved.emit(this);
    }

    void DockablePane::pop_out() {
        if(this->stack != nullptr) {
            this->stack->remove(*this);
            this->stack = nullptr;
        }

        if(this->window == nullptr) {
            // Remove the header bar from the pane, so it can be used as the titlebar of the window
            this->remove(*this->header);
            this->window = new DockWindow(this);
            this->window->set_titlebar(*this->header);
            this->window->set_child(*this);
            this->window->set_decorated(true);
            // Grey out the pop-out option
            auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
            action->set_enabled(false);
            this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
        }
        this->window->present();
        this->signal_pane_shown.emit();
        layout->signal_pane_moved.emit(this);
    }

    Glib::ustring DockablePane::get_identifier() const {
        return name;
    }

    Gtk::Image *DockablePane::get_icon() const {
        return icon;
    }

    Gtk::Widget *DockablePane::get_child() const {
        return child;
    }

    Gtk::Label *DockablePane::get_label() {
        return &label;
    }

    LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
    }

    void LayoutManager::add_pane(DockablePane *pane) {
        panes.push_back(pane);
    }

    void LayoutManager::add_stack(DockStack *stack) {
        stacks.push_back(stack);
    }

    void LayoutManager::remove_pane(DockablePane *pane) {
        panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
    }

    void LayoutManager::remove_stack(DockStack *stack) {
        stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
    }

    BaseStack::BaseStack() : Gtk::Stack() {
    }

    DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name, const std::string &id) : BaseStack(), layout(layout), name(name), id(id) {
        auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
        this->add(*empty_child, "");
        // Add the stack to a layout manager
        this->layout->add_stack(this);

        // Hide the stack when no child is visible
        this->property_visible_child_name().signal_changed().connect([this]() {
            if(last_pane) {
                last_pane->signal_pane_hidden.emit();
            }
            if(this->get_visible_child_name() == "") {
                this->hide();
            } else {
                this->show();
            }
            last_pane = dynamic_cast<DockablePane*>(this->get_visible_child());
            if(last_pane) {
                last_pane->signal_pane_shown.emit();
            }
        });

        // Also hide when the visible child is removed
        this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
            if(this->get_visible_child_name() == "") {
                last_pane = nullptr;
                this->hide();
            }
        });

        this->set_visible_child("");
        this->hide();
    }

    DockWindow::DockWindow(DockablePane *pane) {
        this->pane = pane;
        this->set_child(*pane);
        // Attempting to close the window should redock the pane so it doesn't vanish
        this->signal_close_request().connect([this]() {
            this->pane->redock(this->pane->last_stack);
            return true;
        }, false);
    }

    DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
        auto update_callback = [this](Gtk::Widget*) {
            this->update_buttons();
        };
        this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
        stack->signal_child_added.connect(update_callback);
        stack->signal_child_removed.connect(update_callback);
        this->get_style_context()->add_class("gpanthera-dock-switcher");
        drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
        this->add_controller(drop_target);
        // Process dropped buttons
        drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
            const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();

            if(widget) {
                if(auto pane = dynamic_cast<DockablePane*>(widget)) {
                    this->stack->add_pane(*pane);
                }
            }

            return true;  // Drop OK
        }, false);
    }

    DockStack *DockStackSwitcher::get_stack() const {
        return stack;
    }

    DockStackSwitcher::~DockStackSwitcher() {
        add_handler.disconnect();
        remove_handler.disconnect();
    }

    DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
        if(pane->get_icon()) {
            if(auto icon_copy = copy_image(pane->get_icon())) {
                this->set_child(*icon_copy);
            }
        }
        this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
        this->set_tooltip_text(pane->get_label()->get_text());
        this->set_halign(Gtk::Align::CENTER);
        this->set_valign(Gtk::Align::CENTER);
        this->get_style_context()->add_class("toggle");
        this->get_style_context()->add_class("gpanthera-dock-button");
        // Add/remove CSS classes when the pane is shown/hidden
        active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
            this->update_active_style();
        });
        drag_source = Gtk::DragSource::create();
        drag_source->set_exclusive(false);
        // This is to prevent the click handler from taking over grabbing the button
        drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
        value.init(DockablePane::get_type());
        value.set(pane);
        // Add the drag source to the button
        this->add_controller(drag_source);
        this->signal_clicked().connect([this, pane]() {
            if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
                pane->get_stack()->set_visible_child("");
            } else {
                pane->get_stack()->set_visible_child(pane->get_identifier());
            }
        });
        // Provide the drag data
        drag_source->signal_prepare().connect([this](double, double) {
            drag_source->set_actions(Gdk::DragAction::MOVE);
            auto const paintable = Gtk::WidgetPaintable::create();
            paintable->set_widget(*this);
            drag_source->set_icon(paintable->get_current_image(), 0, 0);
            return Gdk::ContentProvider::create(value);
        }, false);
        drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
            this->set_opacity(0);
        }, false);
        // Pop out if dragged to an external location
        drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
            if(reason == Gdk::DragCancelReason::NO_TARGET) {
                this->pane->pop_out();
                return true;
            }
            this->set_opacity(1);
            return false;
        }, false);
        // Add a drop target to the button
        auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
        drop_target->set_actions(Gdk::DragAction::MOVE);
        drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
        this->add_controller(drop_target);
        // Process dropped buttons by inserting them after the current button
        drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
            const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();

            if(widget) {
                if(auto pane = dynamic_cast<DockablePane*>(widget)) {
                    if(pane->layout != this->pane->layout) {
                        // If the pane is not in the same layout manager, reject
                        return false;
                    }
                    auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
                    if(switcher) {
                        auto *stack = switcher->get_stack();
                        // Move the button to the new position
                        pane->redock(stack);
                        if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
                            if(x < static_cast<double>(this->get_allocated_width()) / 2) {
                                pane->insert_before(*stack, *this->pane);
                            } else {
                                pane->insert_after(*stack, *this->pane);
                            }
                        } else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
                            if(y < static_cast<double>(this->get_allocated_height()) / 2) {
                                pane->insert_before(*stack, *this->pane);
                            } else {
                                pane->insert_after(*stack, *this->pane);
                            }
                        }

                        switcher->update_buttons();
                    }
                }
            }

            return true;  // Drop OK
        }, false);
        this->update_active_style();
    }

    void DockButton::update_active_style() {
        if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
            this->add_css_class("checked");
            this->add_css_class("gpanthera-dock-button-active");
            this->set_active(true);
        } else {
            this->remove_css_class("checked");
            this->remove_css_class("gpanthera-dock-button-active");
            this->set_active(false);
        }
    }

    DockButton::~DockButton() {
        active_style_handler.disconnect();
    }

    void DockStackSwitcher::update_buttons() {
        // Clear the old buttons
        auto old_buttons = collect_children(*this);
        for(auto *button : old_buttons) {
            remove(*button);
        }
        DockButton* first_child = nullptr;
        for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
            if(auto pane = dynamic_cast<DockablePane*>(widget)) {
                auto *button = Gtk::make_managed<DockButton>(pane);
                if(!first_child) {
                    first_child = button;
                } else {
                    button->set_group(*first_child);
                }
                if(pane->get_identifier() != Glib::ustring("")) {
                    append(*button);
                }
            }
        }
    }

    void DockStack::add_pane(DockablePane &child) {
        child.redock(this);
    }

    void ContentManager::add_stack(ContentStack *stack) {
        this->stacks.push_back(stack);
    }

    void ContentManager::remove_stack(ContentStack *stack) {
        this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
    }

    void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
        Gtk::Stack::add(child, name);
        signal_child_added.emit(&child);
    }

    void BaseStack::add(Gtk::Widget &child) {
        Gtk::Stack::add(child);
        signal_child_added.emit(&child);
    }

    void BaseStack::remove(Gtk::Widget &child) {
        Gtk::Stack::remove(child);
        signal_child_removed.emit(&child);
    }

    void ContentStack::disable_children() {
        auto children = collect_children(*this);
        for(auto *child : children) {
            child->set_can_target(false);
        }
    }

    void ContentStack::enable_children() {
        auto children = collect_children(*this);
        for(auto *child : children) {
            child->set_can_target(true);
        }
    }

    ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager, std::function<bool(ContentPage*)> detach_handler)
        : BaseStack(), content_manager(content_manager) {
        this->set_name("gpanthera_content_stack");
        this->content_manager->add_stack(this);
        if(detach_handler) {
            this->detach_handler = detach_handler;
            this->signal_detach.connect([this](ContentPage *widget) {
                return this->detach_handler(widget);
            });
        }

        drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
        this->add_controller(drop_target);
        // Some widgets (such as webviews) will consume anything dropped on their ancestors. We have
        // to disable all child widgets to ensure the drop can be caught by this widget.
        drop_target->signal_enter().connect([this](double x, double y) {
            disable_children();
            return Gdk::DragAction::MOVE;
        }, false);
        drop_target->signal_leave().connect([this]() {
            enable_children();
        }, false);
        // Process dropped buttons
        drop_target->signal_drop().connect([this, content_manager](const Glib::ValueBase &value, double x, double y) {
            enable_children();
            const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();

            if(auto page = dynamic_cast<ContentPage*>(widget)) {
                if(page->get_stack() == this && !this->get_first_child()->get_next_sibling()) {
                    // Don't allow splitting if there are no more pages
                    return false;
                }
                if(!(page->content_manager == this->content_manager)) {
                    // If the page is not in the same content manager, reject
                    return false;
                }
                double width = this->get_allocated_width(), height = this->get_allocated_height();
                // Split based on the drop position
                if(!(x < width / 4 || x > width * 3 / 4 || y < height / 4 || y > height * 3 / 4)) {
                    if(page->get_stack() == this) {
                        return false;
                    }
                    page->lose_visibility();
                    // If the drop position is not at a quarter to the edges, move to the same stack
                    this->add_page(*page);
                    return true;
                }
                page->lose_visibility();
                auto new_stack = Gtk::make_managed<ContentStack>(this->content_manager, this->detach_handler);
                auto this_notebook = dynamic_cast<ContentNotebook*>(this->get_parent());
                auto new_switcher = Gtk::make_managed<ContentTabBar>(new_stack, this_notebook ? this_notebook->get_switcher()->get_orientation() : Gtk::Orientation::HORIZONTAL, this_notebook->get_switcher()->get_extra_child_function());
                auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher, this_notebook ? this_notebook->get_tab_position() : Gtk::PositionType::TOP);
                new_stack->add_page(*page);
                new_stack->set_visible_child(*page);
                if(x < width / 4) {
                    this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START);
                    if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
                        paned->set_start_child(*new_notebook);
                    }
                } else if(x > width * 3 / 4) {
                    this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END);
                    if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
                        paned->set_end_child(*new_notebook);
                    }
                } else if(y < height / 4) {
                    this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START);
                    if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
                        paned->set_start_child(*new_notebook);
                    }
                } else if(y > height * 3 / 4) {
                    this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END);
                    if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
                        paned->set_end_child(*new_notebook);
                    }
                }
                content_manager->set_last_operated_page(page);
            }

            return true;  // Drop OK
        }, false);
    }

    std::function<bool(ContentPage*)> ContentStack::get_detach_handler() const {
        return this->detach_handler;
    }

    ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher, Gtk::PositionType tab_position) : Gtk::Box(), stack(stack), switcher(switcher), tab_position(tab_position) {
        this->append(*stack);
        if(tab_position == Gtk::PositionType::TOP || tab_position == Gtk::PositionType::BOTTOM) {
            this->set_orientation(Gtk::Orientation::VERTICAL);
            if(tab_position == Gtk::PositionType::TOP) {
                this->prepend(*switcher);
                switcher->add_css_class("top");
            } else if(tab_position == Gtk::PositionType::BOTTOM) {
                this->append(*switcher);
                switcher->add_css_class("bottom");
            }
        } else if(tab_position == Gtk::PositionType::LEFT || tab_position == Gtk::PositionType::RIGHT) {
            this->set_orientation(Gtk::Orientation::HORIZONTAL);
            if(tab_position == Gtk::PositionType::LEFT) {
                this->prepend(*switcher);
                switcher->add_css_class("left");
            } else if(tab_position == Gtk::PositionType::RIGHT) {
                this->append(*switcher);
                switcher->add_css_class("right");
            }
        }
        this->add_css_class("gpanthera-content-notebook");
    }

    void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) {
        auto *parent = this->get_parent();
        if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) {
            auto *paned = Gtk::make_managed<Gtk::Paned>(orientation);
            if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) {
                if(parent_paned->get_start_child() == notebook) {
                    notebook->unparent();
                    parent_paned->set_start_child(*paned);
                } else if(parent_paned->get_end_child() == notebook) {
                    notebook->unparent();
                    parent_paned->set_end_child(*paned);
                }
            } else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) {
                auto previous_child = notebook->get_prev_sibling();
                box->remove(*notebook);
                if(previous_child) {
                    paned->insert_after(*box, *previous_child);
                } else {
                    box->prepend(*paned);
                }
            } else if(auto window = dynamic_cast<Gtk::Window*>(notebook->get_parent())) {
                window->unset_child();
                window->set_child(*paned);
            }
            if(pack_type == Gtk::PackType::START) {
                paned->set_end_child(*notebook);
            } else if(pack_type == Gtk::PackType::END) {
                paned->set_start_child(*notebook);
            }
        }
    }

    ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation, std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) : Gtk::Box(orientation), stack(stack), extra_child_function(extra_child_function) {
        this->get_style_context()->add_class("gpanthera-content-tab-bar");
        this->set_margin_top(0);
        this->set_margin_bottom(0);
        this->set_margin_start(0);
        this->set_margin_end(0);

        auto update_callback = [this](Gtk::Widget*) {
            this->update_buttons();
        };
        this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
        stack->signal_child_added.connect(update_callback);
        stack->signal_child_removed.connect(update_callback);
        drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
        this->add_controller(drop_target);
        // Process dropped buttons
        drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
            if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) {
                if(auto page = dynamic_cast<ContentPage*>(widget)) {
                    if(page->get_stack() == this->stack) {
                        this->stack->remove(*page);
                        this->stack->add(*page);
                    } else {
                        this->stack->add_page(*page);
                    }
                }
            }

            return true;  // Drop OK
        }, false);

        scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>();
        scrolled_window->add_css_class("gpanthera-content-tab-bar-scroll");
        auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr);
        viewport->add_css_class("gpanthera-content-tab-bar-viewport");
        tab_box = Gtk::make_managed<Gtk::Box>(orientation);
        tab_box->add_css_class("gpanthera-content-tab-box");
        this->prepend(*scrolled_window);
        scrolled_window->set_child(*viewport);
        viewport->set_child(*tab_box);

        this->set_orientation(orientation);

        if(this->extra_child_function) {
            this->append(*this->extra_child_function(this));
        }
    }

    std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> ContentTabBar::get_extra_child_function() const {
        return this->extra_child_function;
    }

    void ContentTabBar::set_extra_child_function(std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) {
        // Note that this doesn't remove the existing extra child
        this->extra_child_function = extra_child_function;
    }

    void ContentTabBar::set_orientation(Gtk::Orientation orientation) {
        this->Gtk::Box::set_orientation(orientation);
        if(orientation == Gtk::Orientation::HORIZONTAL) {
            scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::NEVER);
            tab_box->set_orientation(Gtk::Orientation::HORIZONTAL);
            scrolled_window->set_hexpand(true);
            scrolled_window->set_vexpand(false);
        } else if(orientation == Gtk::Orientation::VERTICAL) {
            scrolled_window->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
            tab_box->set_orientation(Gtk::Orientation::VERTICAL);
            scrolled_window->set_vexpand(true);
            scrolled_window->set_hexpand(false);
        }
    }

    ContentTabBar::~ContentTabBar() {
        add_handler.disconnect();
        remove_handler.disconnect();
    }

    ContentStack *ContentTabBar::get_stack() const {
        return stack;
    }

    void ContentTabBar::update_buttons() {
        // Clear the old buttons
        auto old_buttons = collect_children(*this->tab_box);
        for(auto *button : old_buttons) {
            if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
                button_button->unset_child();
                tab_box->remove(*button);
            }
        }
        ContentTab* first_child = nullptr;
        for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
            if(auto page = dynamic_cast<ContentPage*>(widget)) {
                auto *button = Gtk::make_managed<ContentTab>(page);
                if(!first_child) {
                    first_child = button;
                } else {
                    button->set_group(*first_child);
                }
                this->tab_box->append(*button);
            }
        }
    }

    void ContentStack::add_page(ContentPage &child) {
        child.redock(this);
    }

    void ContentStack::remove_with_paned() {
        if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
            Gtk::Widget *child = nullptr;
            if(this->get_parent() == paned->get_start_child()) {
                child = paned->get_end_child();
            } else if(this->get_parent() == paned->get_end_child()) {
                child = paned->get_start_child();
            } else {
                return;
            }

            g_object_ref(child->gobj());    // Prevent the child from being automatically deleted
            paned->property_start_child().reset_value();  // Some hacks because the Gtkmm API isn't complete; it doesn't have an unset_start_child() or unset_end_child()
            paned->property_end_child().reset_value();

            if(child) {
                if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
                    if(parent_paned->get_start_child() == paned) {
                        parent_paned->set_start_child(*child);
                    } else if(parent_paned->get_end_child() == paned) {
                        parent_paned->set_end_child(*child);
                    }
                } else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
                    child->insert_after(*box, *paned);
                    paned->unparent();
                    g_object_unref(child->gobj());
                } else if(auto window = dynamic_cast<Gtk::Window*>(paned->get_parent())) {
                    window->set_child(*child);
                    g_object_unref(child->gobj());
                }
                // Focus the visible page of the former pane's other child
                if(auto stack = dynamic_cast<ContentStack*>(child->get_last_child())) {
                    if(auto page = dynamic_cast<ContentPage*>(stack->get_visible_child())) {
                        content_manager->set_last_operated_page(page);
                    }
                }
            }
        }
    }

    ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
        this->set_child(*page->get_tab_widget());
        this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
        this->set_halign(Gtk::Align::CENTER);
        this->set_valign(Gtk::Align::CENTER);
        this->get_style_context()->add_class("toggle");
        this->get_style_context()->add_class("gpanthera-content-tab");
        // Add/remove CSS classes when the pane is shown/hidden
        active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
            this->update_active_style();
        });
        drag_source = Gtk::DragSource::create();
        drag_source->set_exclusive(false);
        // This is to prevent the click handler from taking over grabbing the button
        drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
        value.init(ContentPage::get_type());
        value.set(page);
        // Add the drag source to the button
        this->add_controller(drag_source);
        this->signal_clicked().connect([this, page]() {
            page->get_stack()->set_visible_child(*page);
            page->content_manager->set_last_operated_page(page);
            update_active_style();
        });
        // Switch tabs on depress
        auto gesture_click = Gtk::GestureClick::create();
        gesture_click->set_button(1);
        this->add_controller(gesture_click);
        gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
            page->get_stack()->set_visible_child(*page);
            page->content_manager->set_last_operated_page(page);
            update_active_style();
        });
        // Provide the drag data
        drag_source->signal_prepare().connect([this](double, double) {
            drag_source->set_actions(Gdk::DragAction::MOVE);
            auto const paintable = Gtk::WidgetPaintable::create();
            paintable->set_widget(*this);
            drag_source->set_icon(paintable->get_current_image(), 0, 0);
            return Gdk::ContentProvider::create(value);
        }, false);
        update_active_style();
        drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
            this->set_opacity(0);
        }, false);
        drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
        // Process dropped buttons by inserting them after the current button
        drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
            const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();

            if(widget) {
                if(auto page = dynamic_cast<ContentPage*>(widget)) {
                    if(page->content_manager != this->page->content_manager) {
                        // If the pane is not in the same layout manager, reject
                        return false;
                    }
                    auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent());
                    if(switcher) {
                        auto *stack = switcher->get_stack();
                        // Move the button to the new position
                        page->redock(stack);
                        if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
                            if(x < static_cast<double>(this->get_allocated_width()) / 2) {
                                page->insert_before(*stack, *this->page);
                            } else {
                                page->insert_after(*stack, *this->page);
                            }
                        } else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
                            if(y < static_cast<double>(this->get_allocated_height()) / 2) {
                                page->insert_before(*stack, *this->page);
                            } else {
                                page->insert_after(*stack, *this->page);
                            }
                        }

                        switcher->update_buttons();
                    }
                }
            }

            return true;  // Drop OK
        }, false);
        this->add_controller(drop_target);
        // Pop out if dragged to an external location
        drag_cancel_handler = drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
            if(reason == Gdk::DragCancelReason::NO_TARGET) {
                auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
                bool result = stack->signal_detach.emit(this->page);
                if(!result) {
                    this->set_opacity(1);
                }
                return result;
            }
            this->set_opacity(1);
            return false;
        }, false);
        drag_end_handler = drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) {
            if(drop_ok) {
                this->set_opacity(1);
            }
        }, false);

        // Provide a context menu
        context_menu = Gio::Menu::create();
        auto action_group = Gio::SimpleActionGroup::create();
        this->insert_action_group("win", action_group);

        auto close_action = Gio::SimpleAction::create("close");
        close_action->signal_activate().connect([this](const Glib::VariantBase&) {
            this->close();
        });
        action_group->add_action(close_action);
        context_menu->append(_("Close"), "win.close");

        auto close_all_action = Gio::SimpleAction::create("close-all");
        close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
            for(auto page : collect_children(*this->page->get_stack())) {
                if(auto content_page = dynamic_cast<ContentPage*>(page)) {
                    if(!content_page->signal_close.emit()) {
                        content_page->redock(nullptr);
                    }
                }
            }
        });
        action_group->add_action(close_all_action);
        context_menu->append(_("Close all"), "win.close-all");

        auto close_others_action = Gio::SimpleAction::create("close-others");
        close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
            for(auto page : collect_children(*this->page->get_stack())) {
                if(auto content_page = dynamic_cast<ContentPage*>(page)) {
                    if(content_page != this->page && !content_page->signal_close.emit()) {
                        content_page->redock(nullptr);
                    }
                }
            }
        });
        action_group->add_action(close_others_action);
        context_menu->append(_("Close others"), "win.close-others");

        auto detach_action = Gio::SimpleAction::create("detach");
        detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
            auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
            bool result = stack->signal_detach.emit(this->page);
        });
        action_group->add_action(detach_action);
        context_menu->append(_("New window"), "win.detach");

        // TODO: Add more actions: "New window", "Split left", "Split right", "Split top", "Split bottom", "Close all", "Close others", "Close to the right", "Close to the left"
        this->insert_action_group("win", action_group);
        // Attach the context menu to the button
        auto context_menu_signal = add_context_menu(*this);
        popover = Gtk::make_managed<Gtk::PopoverMenu>();
        popover->set_menu_model(context_menu);
        popover->set_parent(*this);
        popover->set_has_arrow(false);
        popover->set_halign(Gtk::Align::START);
        context_menu_signal.connect([this, action_group](double x, double y) {
            if(this->context_menu) {
                popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
                popover->popup();
            }
        });
        auto middle_click_controller = Gtk::GestureClick::create();
        middle_click_controller->set_button(2);
        middle_click_controller->signal_released().connect([this](int num_presses, double x, double y) {
            this->close();
        });
        this->add_controller(middle_click_controller);
        control_status_handler = this->page->signal_control_status_changed.connect([this](bool operating) {
            if(operating) {
                this->add_css_class("gpanthera-selected-tab");
            } else {
                this->remove_css_class("gpanthera-selected-tab");
            }
        });
    }

    void ContentTab::close() {
        page->close();
    }

    void ContentPage::close() {
        if(!this->signal_close.emit()) {
            if(this->get_next_sibling()) {
                this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
                this->get_stack()->set_visible_child(*this->get_next_sibling());
            } else if(this->get_prev_sibling()) {
                this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
                this->get_stack()->set_visible_child(*this->get_prev_sibling());
            }
            this->redock(nullptr);
            content_manager->signal_page_closed.emit(this);
        }
    }

    void ContentTab::update_active_style() {
        if(!this->page->get_stack()) {
            return;
        }
        if(this->page->get_stack()->get_visible_child() == this->page) {
            this->add_css_class("checked");
            this->add_css_class("gpanthera-dock-button-active");
            this->set_active(true);
        } else {
            this->remove_css_class("checked");
            this->remove_css_class("gpanthera-dock-button-active");
            this->set_active(false);
        }
    }

    ContentTab::~ContentTab() {
        active_style_handler.disconnect();
        drag_end_handler.disconnect();
        drag_cancel_handler.disconnect();
        control_status_handler.disconnect();
    }

    Gtk::Widget *ContentPage::get_tab_widget() const {
        return this->tab_widget;
    }

    ContentStack *ContentPage::get_stack() const {
        return this->stack;
    }

    void ContentPage::lose_visibility() {
        if(this->get_next_sibling()) {
            this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
            this->get_stack()->set_visible_child(*this->get_next_sibling());
        } else if(this->get_prev_sibling()) {
            this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
            this->get_stack()->set_visible_child(*this->get_prev_sibling());
        }
    }

    void ContentPage::redock(ContentStack *stack) {
        if(stack == nullptr) {
            content_manager->signal_page_closing.emit(this);
            if(this->stack) {
                this->stack->remove(*this);
                /*
                The stack may be removed, for example, if the application closes its window if
                it was the sole stack inside. Thus, it must be re-checked.
                */
                if(this->stack && dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
                    this->stack->remove_with_paned();
                }
            }
            auto old_stack = this->stack;
            if(old_stack) {
                if(!old_stack->get_first_child()) {
                    old_stack->signal_leave_empty.emit();
                }
            }
            this->stack = nullptr;
            this->last_stack = nullptr;
            return;
        }
        // Check if the stack is now empty, in which case we should remove it
        if(this->stack == stack) {
            return;
        }
        content_manager->signal_page_moving.emit(this);
        if(this->stack != nullptr) {
            if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && (!this->stack->get_first_child() || (this->stack->get_first_child() == this && !this->stack->get_first_child()->get_next_sibling()))) {
                this->stack->remove(*this);
                this->stack->remove_with_paned();
            } else if(this->get_parent() == this->stack) {
                this->stack->remove(*this);
            }
        }
        auto old_stack = this->stack;
        this->stack = stack;
        this->last_stack = stack;
        this->stack->add(*this);
        if(old_stack) {
            if(!old_stack->get_first_child()) {
                old_stack->signal_leave_empty.emit();
            }
        }
        content_manager->signal_page_moved.emit(this);
    }

    ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
        Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
        this->set_name("gpanthera_content_page");
        this->set_child(*child);
        this->set_tab_widget(tab_widget);
        this->set_margin_top(0);
        this->set_margin_bottom(0);
        this->set_margin_start(0);
        this->set_margin_end(0);
        if(stack) {
            stack->add_page(*this);
            this->content_manager->add_stack(this->stack);
        }
        auto click_controller = Gtk::GestureClick::create();
        click_controller->set_button(0);
        click_controller->signal_pressed().connect([this](int num_presses, double x, double y) {
            this->content_manager->set_last_operated_page(this);
        });
        this->property_has_focus().signal_changed().connect([this]() {
            if(this->property_has_focus()) {
                this->content_manager->set_last_operated_page(this);
            }
        });
        click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
        this->add_controller(click_controller);
    }

    ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
    }

    void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
        this->tab_widget = tab_widget;
    }

    ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
        this->set_child(*notebook);
        this->set_decorated(true);
        this->set_resizable(true);
    }

    Gtk::PositionType ContentNotebook::get_tab_position() const {
        return this->tab_position;
    }

    ContentTabBar *ContentNotebook::get_switcher() const {
        return this->switcher;
    }

    ContentStack *ContentNotebook::get_stack() const {
        return this->stack;
    }

    ContentPage *ContentManager::get_last_operated_page() const {
        return this->last_operated_page;
    }

    void ContentManager::set_last_operated_page(ContentPage *page) {
        if(this->last_operated_page) {
            this->last_operated_page->signal_control_status_changed.emit(false);
        }
        this->last_operated_page = page;
        if(page) {
            page->signal_control_status_changed.emit(true);
            this->signal_page_operated.emit(page);
        }
    }

    DockWindow *DockablePane::get_window() const {
        return this->window;
    }

    std::string LayoutManager::get_layout_as_json() const {
        nlohmann::json json;
        std::unordered_set<DockablePane*> panes_set;
        for(auto pane: panes) {
            panes_set.insert(pane);
        }
        for(auto stack: stacks) {
            for(auto pane: collect_children(*stack)) {
                if(dynamic_cast<DockablePane*>(pane)) {
                    panes_set.erase(dynamic_cast<DockablePane*>(pane));
                    json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier());
                }
            }
        }
        for(auto pane: panes_set) {
            if(pane->get_window()) {
                json[""].push_back(pane->get_identifier());
            }
        }
        return json.dump();
    }

    void LayoutManager::restore_json_layout(const std::string &json_string) {
        nlohmann::json json = nlohmann::json::parse(json_string);
        for(auto stack: stacks) {
            if(json.contains(stack->id)) {
                for(auto const &pane_id: json[stack->id]) {
                    for(auto pane: panes) {
                        // TODO: could probably be done with a better complexity if there are
                        // many panes
                        if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
                            stack->add_pane(*pane);
                            break;
                        }
                    }
                }
            }
        }
        // Process popped-out panes
        for(auto const &pane_id: json[""]) {
            for(auto pane: panes) {
                // TODO: could probably be done with a better complexity if there are
                // many panes
                if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
                    pane->pop_out();
                    break;
                }
            }
        }
    }
} // namespace gPanthera
