#include <glib.h>
#include <gtk/gtk.h>
#include <wintc/comgtk.h>

#include "../public/menubind.h"

//
// PRIVATE ENUMS
//
enum
{
    PROP_MENU_SHELL = 1,
    PROP_MENU_MODEL
};

//
// PRIVATE STRUCTURES
//
typedef struct _WinTCCtlMenuBindingMenu
{
    WinTCCtlMenuBinding* menu_binding;

    GtkMenuShell* menu_shell;
    GMenuModel*   menu_model;

    GList* sections;
} WinTCCtlMenuBindingMenu;

typedef struct _WinTCCtlMenuBindingSection
{
    WinTCCtlMenuBindingMenu* parent_menu;

    GMenuModel* menu_model;
    gint        item_count;
} WinTCCtlMenuBindingSection;

//
// FORWARD DECLARATIONS
//
static void wintc_ctl_menu_binding_constructed(
    GObject* object
);
static void wintc_ctl_menu_binding_dispose(
    GObject* object
);
static void wintc_ctl_menu_binding_finalize(
    GObject* object
);
static void wintc_ctl_menu_binding_get_property(
    GObject*    object,
    guint       prop_id,
    GValue*     value,
    GParamSpec* pspec
);
static void wintc_ctl_menu_binding_set_property(
    GObject*      object,
    guint         prop_id,
    const GValue* value,
    GParamSpec*   pspec
);

static GList* wintc_ctl_menu_binding_find_section_pos(
    WinTCCtlMenuBindingMenu* menu,
    gint                     dst_pos,
    gint*                    mid_pos
);
static void wintc_ctl_menu_binding_insert_item(
    WinTCCtlMenuBindingMenu* menu,
    GMenuModel*	             menu_model,
    gint                     src_pos,
    gint                     dst_pos
);
static void wintc_ctl_menu_binding_track_menu(
    WinTCCtlMenuBinding* menu_binding,
    GtkMenuShell*        menu_shell,
    GMenuModel*          menu_model
);

static void wintc_ctl_menu_binding_menu_free(
    WinTCCtlMenuBindingMenu* menu
);

//
// GTK OOP CLASS/INSTANCE DEFINITIONS
//
typedef struct _WinTCCtlMenuBinding
{
    GObject __parent__;

    GtkMenuShell* menu_shell;
    GMenuModel*   menu_model;

    GSList* tracked_menus;
} WinTCCtlMenuBinding;

//
// GTK TYPE DEFINITIONS & CTORS
//
G_DEFINE_TYPE(
    WinTCCtlMenuBinding,
    wintc_ctl_menu_binding,
    G_TYPE_OBJECT
)

static void wintc_ctl_menu_binding_class_init(
    WinTCCtlMenuBindingClass* klass
)
{
    GObjectClass* object_class = G_OBJECT_CLASS(klass);

    object_class->constructed  = wintc_ctl_menu_binding_constructed;
    object_class->dispose      = wintc_ctl_menu_binding_dispose;
    object_class->finalize     = wintc_ctl_menu_binding_finalize;
    object_class->get_property = wintc_ctl_menu_binding_get_property;
    object_class->set_property = wintc_ctl_menu_binding_set_property;

    g_object_class_install_property(
        object_class,
        PROP_MENU_SHELL,
        g_param_spec_object(
            "menu-shell",
            "MenuShell",
            "The GTK menu shell to manage.",
            GTK_TYPE_MENU_SHELL,
            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY
        )
    );
    g_object_class_install_property(
        object_class,
        PROP_MENU_MODEL,
        g_param_spec_object(
            "menu-model",
            "MenuModel",
            "The menu model to bind to.",
            G_TYPE_MENU_MODEL,
            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY
        )
    );
}

static void wintc_ctl_menu_binding_init(
    WINTC_UNUSED(WinTCCtlMenuBinding* self)
) {}

//
// CLASS VIRTUAL METHODS
//
static void wintc_ctl_menu_binding_constructed(
    GObject* object
)
{
    WinTCCtlMenuBinding* menu_binding = WINTC_CTL_MENU_BINDING(object);

    wintc_container_clear(
        GTK_CONTAINER(menu_binding->menu_shell)
    );

    wintc_ctl_menu_binding_track_menu(
        menu_binding,
        menu_binding->menu_shell,
        menu_binding->menu_model
    );

    (G_OBJECT_CLASS(wintc_ctl_menu_binding_parent_class))
        ->constructed(object);
}

static void wintc_ctl_menu_binding_dispose(
    GObject* object
)
{
    WinTCCtlMenuBinding* menu_binding = WINTC_CTL_MENU_BINDING(object);

    g_clear_object(&(menu_binding->menu_shell));
    g_clear_object(&(menu_binding->menu_model));

    (G_OBJECT_CLASS(wintc_ctl_menu_binding_parent_class))
        ->dispose(object);
}

static void wintc_ctl_menu_binding_finalize(
    GObject* object
)
{
    WinTCCtlMenuBinding* menu_binding = WINTC_CTL_MENU_BINDING(object);

    g_clear_slist(
        &(menu_binding->tracked_menus),
        (GDestroyNotify) wintc_ctl_menu_binding_menu_free
    );

    (G_OBJECT_CLASS(wintc_ctl_menu_binding_parent_class))
        ->finalize(object);
}

static void wintc_ctl_menu_binding_get_property(
    GObject*    object,
    guint       prop_id,
    WINTC_UNUSED(GValue* value),
    GParamSpec* pspec
)
{
    switch (prop_id)
    {
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
            break;
    }
}

static void wintc_ctl_menu_binding_set_property(
    GObject*      object,
    guint         prop_id,
    const GValue* value,
    GParamSpec*   pspec
)
{
    WinTCCtlMenuBinding* menu_binding = WINTC_CTL_MENU_BINDING(object);

    switch (prop_id)
    {
        case PROP_MENU_SHELL:
            menu_binding->menu_shell =
                g_value_dup_object(value);

            break;

        case PROP_MENU_MODEL:
            menu_binding->menu_model =
                g_value_dup_object(value);

            break;

        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
            break;
    }
}

//
// PUBLIC FUNCTIONS
//
WinTCCtlMenuBinding* wintc_ctl_menu_binding_new(
    GtkMenuShell* menu_shell,
    GMenuModel*   menu_model
)
{
    return WINTC_CTL_MENU_BINDING(
        g_object_new(
            WINTC_TYPE_CTL_MENU_BINDING,
            "menu-shell",      menu_shell,
            "menu-model",      menu_model,
            NULL
        )
    );
}

//
// PRIVATE FUNCTIONS
//
static GList* wintc_ctl_menu_binding_find_section_pos(
    WinTCCtlMenuBindingMenu* menu,
    gint                     dst_pos,
    gint*                    mid_pos
)
{
    gint   i    = 0;
    GList* iter = menu->sections;

    if (!iter)
    {
        *mid_pos = 0;
        return NULL;
    }

    for (; iter; iter = iter->next)
    {
        WinTCCtlMenuBindingSection* section =
            (WinTCCtlMenuBindingSection*) iter->data;

        // Have we hit our target?
        //
        if (i == dst_pos)
        {
            *mid_pos = 0;
            return iter;
        }

        if (!(section->menu_model))
        {
            // Check if section should be inserted in the middle of a fake
            // section
            //
            if (i + section->item_count > dst_pos)
            {
                *mid_pos = dst_pos - i;
                return iter;
            }

            // Progress num of items in the fake section
            //
            i += section->item_count;
        }
        else
        {
            // Progress only 1 as a real section is considered 1 item
            i++;
        }
    }

    *mid_pos = 0;
    return NULL;
}

static void wintc_ctl_menu_binding_insert_item(
    WinTCCtlMenuBindingMenu* menu,
    GMenuModel*	             menu_model,
    gint                     src_pos,
    gint                     dst_pos
)
{
    gboolean is_menubar = GTK_IS_MENU_BAR(menu->menu_shell);

    // If this is a section, get it set up - find where to insert it in the
    // list... it could be in the middle of normal menu items
    //
    GMenuModel* section =
        g_menu_model_get_item_link(
            menu_model,
            src_pos,
            G_MENU_LINK_SECTION
        );

    if (section)
    {
        // Create new section
        //
        WinTCCtlMenuBindingSection* new_section =
            g_new(WinTCCtlMenuBindingSection, 1);

        new_section->parent_menu = menu;
        new_section->menu_model  = section;
        new_section->item_count  = 0;

        // Find where to insert
        //
        gint   mid_pos    = 0;
        GList* target_li  =
            wintc_ctl_menu_binding_find_section_pos(menu, dst_pos, &mid_pos);

        if (!target_li)
        {
            menu->sections =
                g_list_append(menu->sections, new_section);
        }
        else
        {
            if (mid_pos)
            {
                WinTCCtlMenuBindingSection* target =
                    (WinTCCtlMenuBindingSection*) target_li->data;

                // Need to split up 'fake' section
                //
                WinTCCtlMenuBindingSection* new_subsection =
                    g_new(WinTCCtlMenuBindingSection, 1);

                new_subsection->parent_menu = menu;
                new_subsection->menu_model  = NULL;
                new_subsection->item_count  = target->item_count - mid_pos;

                target->item_count -= new_subsection->item_count;

                // Append the new section + the latter half of the split fake
                // section
                //
                menu->sections =
                    g_list_insert_before(
                        menu->sections,
                        target_li->next,
                        new_section
                    );
                menu->sections =
                    g_list_insert_before(
                        menu->sections,
                        target_li->next,
                        new_subsection
                    );
            }
            else
            {
                menu->sections =
                    g_list_insert_before(
                        menu->sections,
                        target_li,
                        new_section
                    );
            }
        }

        // Insert items from the section
        //
        gint n_items = g_menu_model_get_n_items(section);

        for (gint i = 0; i < n_items; i++)
        {
            wintc_ctl_menu_binding_insert_item(
                menu,
                section,
                i,
                i
            );
        }

        return;
    }

    // Actually have an item - pull its data
    //
    GIcon*    icon = NULL;
    GVariant* icon_var;
    gchar*    label = NULL;

    g_menu_model_get_item_attribute(
        menu_model,
        src_pos,
        G_MENU_ATTRIBUTE_LABEL,
        "s",
        &label
    );

    icon_var =
        g_menu_model_get_item_attribute_value(
            menu_model,
            src_pos,
            G_MENU_ATTRIBUTE_ICON,
            NULL
        );

    if (icon_var)
    {
        icon = g_icon_deserialize(icon_var);
        g_variant_unref(icon_var);
    }

    // Get action information
    //
    gchar*    action_name = NULL;
    GVariant* action_target;

    if (
        g_menu_model_get_item_attribute(
            menu_model,
            src_pos,
            G_MENU_ATTRIBUTE_ACTION,
            "s",
            &action_name
        )
    )
    {
        action_target =
            g_menu_model_get_item_attribute_value(
                menu_model,
                src_pos,
                G_MENU_ATTRIBUTE_TARGET,
                NULL
            );
    }

    // Create the item
    //
    GtkWidget* box_container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    GtkWidget* menu_item = gtk_menu_item_new();

    gtk_container_add(GTK_CONTAINER(menu_item), box_container);

    if (!is_menubar) // Only add icons to submenus
    {
        GtkWidget* img_icon = gtk_image_new();

        gtk_widget_set_margin_end(img_icon, 6); // Got this from Mousepad
        gtk_widget_set_size_request(img_icon, 16, 16);

        if (icon)
        {
            gtk_image_set_from_gicon(
                GTK_IMAGE(img_icon),
                icon,
                GTK_ICON_SIZE_MENU
            );
            gtk_image_set_pixel_size(
                GTK_IMAGE(img_icon),
                16
            );

            g_object_unref(icon);
        }

        gtk_box_pack_start(
            GTK_BOX(box_container),
            img_icon,
            FALSE,
            FALSE,
            0
        );
    }

    if (action_name)
    {
        gtk_actionable_set_action_name(
            GTK_ACTIONABLE(menu_item),
            action_name
        );
        gtk_actionable_set_action_target_value(
            GTK_ACTIONABLE(menu_item),
            action_target
        );

        g_free(action_name);
        g_variant_unref(action_target);
    }

    if (label)
    {
        GtkWidget* label_title = gtk_label_new(label);

        gtk_label_set_xalign(GTK_LABEL(label_title), 0.0f);

        gtk_box_pack_start(
            GTK_BOX(box_container),
            label_title,
            TRUE,
            TRUE,
            0
        );

        g_free(label);
    }

    // Do we have a submenu for this item?
    //
    GMenuModel* submenu_model =
        g_menu_model_get_item_link(
            menu_model,
            src_pos,
            G_MENU_LINK_SUBMENU
        );

    if (submenu_model)
    {
        GtkWidget* submenu = gtk_menu_new();

        gtk_menu_set_reserve_toggle_size(GTK_MENU(submenu), FALSE);

        wintc_ctl_menu_binding_track_menu(
            menu->menu_binding,
            GTK_MENU_SHELL(submenu),
            submenu_model
        );

        gtk_menu_item_set_submenu(
            GTK_MENU_ITEM(menu_item),
            submenu
        );
    }

    //
    // Insert the item
    //
    GList* iter     = menu->sections;
    gint   real_pos = 0;

    // menu_model could either be the menu itself or one of its child sections
    //
    gboolean is_subsection = menu_model != menu->menu_model;

    // If its a subsection then locate it and append the item
    //
    if (is_subsection)
    {
        for (; iter; iter = iter->next)
        {
            WinTCCtlMenuBindingSection* check_section =
                (WinTCCtlMenuBindingSection*) iter->data;

            // Is this the correct section? If so, insert and move on
            //
            if (menu_model == check_section->menu_model)
            {
                check_section->item_count++;
                real_pos += dst_pos; // Location within the section

                gtk_menu_shell_insert(menu->menu_shell, menu_item, real_pos);

                return;
            }

            // No? Increment real_pos as needed
            //
            real_pos += check_section->item_count;
        }

        // This should never happen - an attempt to add an item within a
        // section into a menu which does not contain the section
        //
        g_critical(
            "%s",
            "ctl - menubind - section not found in menu for item?"
        );
        return;
    }

    // Continue - add the item directly into the menu
    //
    if (!iter)
    {
        // If there isn't yet any sections, then this must be the first item
        //
        if (dst_pos > 0)
        {
            g_warning(
                "ctl - menubind - first item not at 0? (wants %d)",
                dst_pos
            );
        }

        // Create fake section
        //
        WinTCCtlMenuBindingSection* fake_section =
            g_new(WinTCCtlMenuBindingSection, 1);

        fake_section->parent_menu = menu;
        fake_section->menu_model  = NULL;
        fake_section->item_count  = 1;

        menu->sections =
            g_list_append(menu->sections, fake_section);

        gtk_menu_shell_append(menu->menu_shell, menu_item);

        return;
    }

    // Need to iterate through sections to find where to add this item
    //
    gint i = 0;

    while (i < dst_pos)
    {
        WinTCCtlMenuBindingSection* subsection =
            (WinTCCtlMenuBindingSection*) iter->data;

        // Check we're not overshooting - could be within a 'fake' section...
        //
        if (
            !(subsection->menu_model) &&
            i + subsection->item_count >= dst_pos
        )
        {
            real_pos += (dst_pos - i);
            break;
        }

        // No where else to go?
        //
        if (!(iter->next))
        {
            break;
        }

        // Moving on...
        //
        iter      = iter->next;
        real_pos += subsection->item_count;

        if (subsection->menu_model)
        {
            i++;
        }
        else
        {
            i += subsection->item_count;
        }
    }

    // If we fell out, we need to add the item to the end of the menu
    //
    WinTCCtlMenuBindingSection* last_section =
        (WinTCCtlMenuBindingSection*) iter->data;

    if (last_section->menu_model)
    {
        // If the last section is not fake, then we need to append a
        // fake section now with the item in it
        //
        last_section = g_new(WinTCCtlMenuBindingSection, 1);

        last_section->parent_menu = menu;
        last_section->menu_model  = NULL;
        last_section->item_count  = 0; // Will be incremented in a moment

        menu->sections =
            g_list_append(menu->sections, last_section);

        // FIXME: Probably append a separator here first
    }

    last_section->item_count++;
    gtk_menu_shell_append(menu->menu_shell, menu_item);
}

static void wintc_ctl_menu_binding_track_menu(
    WinTCCtlMenuBinding* menu_binding,
    GtkMenuShell*        menu_shell,
    GMenuModel*          menu_model
)
{
    // Initialise new tracker
    //
    WinTCCtlMenuBindingMenu* tracker =
        g_new(WinTCCtlMenuBindingMenu, 1);

    tracker->menu_binding = menu_binding;
    tracker->menu_shell   = menu_shell;
    tracker->menu_model   = menu_model;
    tracker->sections     = NULL;

    menu_binding->tracked_menus =
        g_slist_append(
            menu_binding->tracked_menus,
            tracker
        );

    // Iterate over the menu model
    //
    gint n_menu_items = g_menu_model_get_n_items(menu_model);

    for (gint i = 0; i < n_menu_items; i++)
    {
        wintc_ctl_menu_binding_insert_item(
            tracker,
            tracker->menu_model,
            i,
            i
        );
    }
}

static void wintc_ctl_menu_binding_menu_free(
    WinTCCtlMenuBindingMenu* menu
)
{
    g_clear_list(&(menu->sections), (GDestroyNotify) g_free);
    g_free(menu);
}
