#include "gpanthera.hh"
#include <iostream>
#include <utility>
#include <libintl.h>
#include <locale.h>
#include <filesystem>
#define _(STRING) gettext(STRING)

namespace gPanthera {
    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 {
            return nullptr;
        }
    }

    void init() {
        // Set up gettext configuration
        setlocale(LC_ALL, "");

        bindtextdomain("gpanthera", "./locales");
        textdomain("gpanthera");
    }

    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 = std::move(layout);
        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>();
        auto header_menu = Gio::Menu::create();
        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->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");
        }
    }

    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();
    }

    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());
    }

    DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) {
        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(this->get_visible_child_name() == "") {
                this->hide();
            } else {
                this->show();
            }
        });

        // Also hide when the visible child is removed
        this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
            if(this->get_visible_child_name() == "") {
                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<Gtk::Widget*>&>(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::Button(), pane(pane) {
        if(pane->get_icon()) {
            this->set_child(*copy_image(pane->get_icon()));
        }
        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 = Glib::Value<Gtk::Widget*>();
        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, 0, 0);
            return Gdk::ContentProvider::create(value);
        }, 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;
            }
            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<Gtk::Widget*>&>(value).get();

            if(widget) {
                if(auto pane = dynamic_cast<DockablePane*>(widget)) {
                    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);
                        pane->insert_before(*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");
        } else {
            this->remove_css_class("checked");
            this->remove_css_class("gpanthera-dock-button-active");
        }
    }

    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);
        }
        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(pane->get_identifier() != Glib::ustring("")) {
                    append(*button);
                }
            }
        }
    }

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

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

    void DockStack::remove(Gtk::Widget &child) {
        Gtk::Stack::remove(child);
        signal_child_removed.emit(&child);
    }
} // namespace gPanthera
