/*
 * This file is part of budgie-desktop.
 *
 * Copyright © 2016-2019 Budgie Desktop Developers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */

#define _GNU_SOURCE

#include "util.h"

BUDGIE_BEGIN_PEDANTIC
#include "budgie-enums.h"
#include "popover.h"
#include <gtk/gtk.h>
BUDGIE_END_PEDANTIC

/**
 * SECTION:popover
 * @Short_description: Budgie Panel GTK+ Popover Widget
 * @Title: BudgiePopover
 *
 * The BudgiePopover is a specialised top level window with a tail pointer,
 * providing a decorative approach to panel windows. These windows point
 * at the source of an event, such as a button, and allow rich user interfaces
 * to be built with a focus on Budgie Panel usage.
 *
 * The BudgiePopover should be used in conjunction with the #BudgiePopoverManager.
 * Simply add your content to the popover, and ensure that you call
 * #GtkWidget:show_all to display the contents.
 *
 * Your popover may be dismissed from screen in response to an event, such
 * as the user pressing the button again, or automatically, as the user
 * clicked outside of the window, or even because the #BudgiePopoverManager
 * switched to a new active popover. You may connect to the #BudgiePopover::closed
 * signal to check for this event.
 *
 */

/*
 * We'll likely take this from a style property in future, but for now it
 * is both the width and height of a tail
 */
#define TAIL_DIMENSION 16
#define TAIL_HEIGHT TAIL_DIMENSION / 2
#define SHADOW_DIMENSION 4
#define BORDER_WIDTH 1

/*
 * Used for storing BudgieTail calculations
 */
typedef struct BudgieTail {
        double start_x;
        double start_y;
        double end_x;
        double end_y;
        double x;
        double y;
        double x_offset;
        double y_offset;
        GtkPositionType position;
} BudgieTail;

struct _BudgiePopoverPrivate {
        GtkWidget *add_area;
        GtkWidget *relative_to;
        BudgieTail tail;
        GdkRectangle widget_rect;
        BudgiePopoverPositionPolicy policy;
};

enum { PROP_RELATIVE_TO = 1, PROP_POLICY, N_PROPS };

static GParamSpec *obj_properties[N_PROPS] = {
        NULL,
};

/*
 * IDs for our signals
 */
enum { POPOVER_SIGNAL_CLOSED = 0, N_SIGNALS };

static guint popover_signals[N_SIGNALS] = { 0 };

G_DEFINE_TYPE_WITH_PRIVATE(BudgiePopover, budgie_popover, GTK_TYPE_WINDOW)

static gboolean budgie_popover_draw(GtkWidget *widget, cairo_t *cr);
static void budgie_popover_map(GtkWidget *widget);
static void budgie_popover_unmap(GtkWidget *widget);
static void budgie_popover_size_allocate(GtkWidget *widget, GdkRectangle *rectangle,
                                         gpointer udata);
static void budgie_popover_add(GtkContainer *container, GtkWidget *widget);
static gboolean budgie_popover_button_press(GtkWidget *widget, GdkEventButton *button,
                                            gpointer udata);
static gboolean budgie_popover_key_press(GtkWidget *widget, GdkEventKey *key, gpointer udata);
static void budgie_popover_set_property(GObject *object, guint id, const GValue *value,
                                        GParamSpec *spec);
static void budgie_popover_get_property(GObject *object, guint id, GValue *value, GParamSpec *spec);
static void budgie_popover_compute_positition(BudgiePopover *self, GdkRectangle *target);
static void budgie_popover_compute_widget_geometry(BudgiePopover *self);
static void budgie_popover_compute_tail(BudgiePopover *self);
static void budgie_popover_update_position_hints(BudgiePopover *self);
static void budgie_popover_get_screen_for_widget(GtkWidget *widget, GdkRectangle *rectangle);

/**
 * budgie_popover_dispose:
 *
 * Clean up a BudgiePopover instance
 */
static void budgie_popover_dispose(GObject *obj)
{
        G_OBJECT_CLASS(budgie_popover_parent_class)->dispose(obj);
}

/**
 * budgie_popover_constructor:
 *
 * Override initial properties to be sane
 *
 * Influence:
 * https://stackoverflow.com/questions/16557905/change-g-param-construct-only-property-via-inheritance
 */
static GObject *budgie_popover_constructor(GType type, guint n_properties,
                                           GObjectConstructParam *properties)
{
        GObject *o = NULL;
        const gchar *prop_name = NULL;
        GObjectConstructParam *param = NULL;

        /* Override the construct-only type property */
        for (guint i = 0; i < n_properties; i++) {
                param = &properties[i];

                prop_name = g_param_spec_get_name(param->pspec);
                if (g_str_equal(prop_name, "type")) {
                        g_value_set_enum(param->value, GTK_WINDOW_POPUP);
                }
        }

        o = G_OBJECT_CLASS(budgie_popover_parent_class)
                ->constructor(type, n_properties, properties);

        /* Blame clang-format for weird wrapping */
        g_object_set(o,
                     "app-paintable",
                     TRUE,
                     "decorated",
                     FALSE,
                     "deletable",
                     FALSE,
                     "focus-on-map",
                     TRUE,
                     "gravity",
                     GDK_GRAVITY_NORTH_WEST,
                     "modal",
                     FALSE,
                     "resizable",
                     FALSE,
                     "skip-pager-hint",
                     TRUE,
                     "skip-taskbar-hint",
                     TRUE,
                     "type-hint",
                     GDK_WINDOW_TYPE_HINT_POPUP_MENU,
                     "window-position",
                     GTK_WIN_POS_NONE,
                     NULL);

        return o;
}

/**
 * budgie_popover_class_init:
 *
 * Handle class initialisation
 */
static void budgie_popover_class_init(BudgiePopoverClass *klazz)
{
        GObjectClass *obj_class = G_OBJECT_CLASS(klazz);
        GtkWidgetClass *wid_class = GTK_WIDGET_CLASS(klazz);
        GtkContainerClass *cont_class = GTK_CONTAINER_CLASS(klazz);

        /* gobject vtable hookup */
        obj_class->constructor = budgie_popover_constructor;
        obj_class->dispose = budgie_popover_dispose;
        obj_class->set_property = budgie_popover_set_property;
        obj_class->get_property = budgie_popover_get_property;

        /* widget vtable hookup */
        wid_class->draw = budgie_popover_draw;
        wid_class->map = budgie_popover_map;
        wid_class->unmap = budgie_popover_unmap;

        /* container vtable */
        cont_class->add = budgie_popover_add;

        /**
         * BudgiePopover::closed
         * @popover: The popover that has been closed
         *
         * This signal is emitted when the popover has been dismissed, whether
         * it was deliberately from the user's perspective, or implicitly
         * through a toggling action, such as being rolled past in a
         * #BudgiePopoverManager set of popovers.
         */
        popover_signals[POPOVER_SIGNAL_CLOSED] =
            g_signal_new("closed",
                         BUDGIE_TYPE_POPOVER,
                         G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                         G_STRUCT_OFFSET(BudgiePopoverClass, closed),
                         NULL,
                         NULL,
                         NULL,
                         G_TYPE_NONE,
                         0);

        /*
         * BudgiePopover:relative-to
         *
         * Determines the GtkWidget that we'll appear next to
         */
        obj_properties[PROP_RELATIVE_TO] = g_param_spec_object("relative-to",
                                                               "Relative widget",
                                                               "Set the relative widget",
                                                               GTK_TYPE_WIDGET,
                                                               G_PARAM_READWRITE);

        /**
         * BudgiePopover:position-policy:
         *
         * Control the behavior used to place the popover on screen.
         */
        obj_properties[PROP_POLICY] = g_param_spec_enum("position-policy",
                                                        "Positioning policy",
                                                        "Get/set the popover position policy",
                                                        BUDGIE_TYPE_POPOVER_POSITION_POLICY,
                                                        BUDGIE_POPOVER_POSITION_AUTOMATIC,
                                                        G_PARAM_READWRITE);

        g_object_class_install_properties(obj_class, N_PROPS, obj_properties);
}

/**
 * budgie_popover_init:
 *
 * Handle construction of the BudgiePopover
 */
static void budgie_popover_init(BudgiePopover *self)
{
        GtkWindow *win = GTK_WINDOW(self);
        GdkScreen *screen = NULL;
        GdkVisual *visual = NULL;
        GtkStyleContext *style = NULL;

        self->priv = budgie_popover_get_instance_private(self);

        style = gtk_widget_get_style_context(GTK_WIDGET(self));
        gtk_style_context_add_class(style, "budgie-popover");

        /* Allow budgie-wm to know what we are */
        G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        gtk_window_set_wmclass(GTK_WINDOW(self), "budgie-popover", "budgie-popover");
        G_GNUC_END_IGNORE_DEPRECATIONS

        self->priv->add_area = gtk_frame_new(NULL);
        style = gtk_widget_get_style_context(GTK_WIDGET(self->priv->add_area));
        gtk_style_context_add_class(style, "container");
        gtk_container_add(GTK_CONTAINER(self), self->priv->add_area);
        gtk_widget_show_all(self->priv->add_area);

        /* Setup window specific bits */
        gtk_window_set_position(win, GTK_WIN_POS_CENTER);
        g_signal_connect(win, "button-press-event", G_CALLBACK(budgie_popover_button_press), NULL);
        g_signal_connect(win, "key-press-event", G_CALLBACK(budgie_popover_key_press), NULL);
        g_signal_connect_after(win,
                               "size-allocate",
                               G_CALLBACK(budgie_popover_size_allocate),
                               NULL);

        /* Set up RGBA ability */
        screen = gtk_widget_get_screen(GTK_WIDGET(self));
        visual = gdk_screen_get_rgba_visual(screen);
        if (visual) {
                gtk_widget_set_visual(GTK_WIDGET(self), visual);
        }
        /* We do all rendering */
        gtk_widget_set_app_paintable(GTK_WIDGET(self), TRUE);

        /* Set initial placement up for default bottom position */
        self->priv->tail.position = GTK_POS_BOTTOM;

        budgie_popover_update_position_hints(self);
}

static void budgie_popover_map(GtkWidget *widget)
{
        GdkWindow *window = NULL;
        GdkRectangle coords = { 0 };
        BudgiePopover *self = NULL;

        self = BUDGIE_POPOVER(widget);

        /* Determine our relative-to widget's location on screen */
        budgie_popover_compute_widget_geometry(self);

        /* Work out where we go on screen now */
        budgie_popover_compute_positition(self, &coords);

        /* Forcibly request focus */
        window = gtk_widget_get_window(widget);
        gdk_window_set_accept_focus(window, TRUE);
        gdk_window_focus(window, GDK_CURRENT_TIME);
        gdk_window_move(window, coords.x, coords.y);
        gtk_window_present(GTK_WINDOW(widget));

        GTK_WIDGET_CLASS(budgie_popover_parent_class)->map(widget);
}

/**
 * budgie_popover_trigger_closed:
 *
 * Used to emit the `closed` signal on the idle loop, after we've dealt
 * with the unmap event cleanly.
 */
static inline gboolean budgie_popover_trigger_closed(gpointer v)
{
        g_signal_emit(v, popover_signals[POPOVER_SIGNAL_CLOSED], 0);
        return G_SOURCE_REMOVE;
}

static void budgie_popover_unmap(GtkWidget *widget)
{
        GTK_WIDGET_CLASS(budgie_popover_parent_class)->unmap(widget);
        g_idle_add(budgie_popover_trigger_closed, widget);
}

/**
 * budgie_popover_size_allocate:
 *
 * Upon having our contents resize us, i.e. a #GtkStack or #GtkRevealer, we
 * re-calculate our position to ensure we resize in the right direction.
 */
static void budgie_popover_size_allocate(GtkWidget *widget,
                                         __budgie_unused__ GdkRectangle *rectangle,
                                         __budgie_unused__ gpointer udata)
{
        GdkRectangle coords = { 0 };
        BudgiePopover *self = NULL;
        GdkWindow *window = NULL;

        if (!gtk_widget_get_realized(widget)) {
                return;
        }

        self = BUDGIE_POPOVER(widget);

        window = gtk_widget_get_window(widget);

        /* Work out where we go on screen now */
        budgie_popover_compute_positition(self, &coords);
        gdk_window_move(window, coords.x, coords.y);
        gtk_widget_queue_draw(widget);
}

/**
 * budgie_popover_select_position_toplevel:
 *
 * Select the position of the popover tail (and extend outwards from it)
 * based on the hints provided by the toplevel
 */
static GtkPositionType budgie_popover_select_position_toplevel(BudgiePopover *self)
{
        GtkWidget *parent_window = NULL;

        /* Tail points out from the panel */
        parent_window = gtk_widget_get_toplevel(self->priv->relative_to);
        if (!parent_window) {
                return GTK_POS_BOTTOM;
        }

        GtkStyleContext *context = gtk_widget_get_style_context(parent_window);
        if (gtk_style_context_has_class(context, "top")) {
                return GTK_POS_TOP;
        } else if (gtk_style_context_has_class(context, "left")) {
                return GTK_POS_LEFT;
        } else if (gtk_style_context_has_class(context, "right")) {
                return GTK_POS_RIGHT;
        }

        return GTK_POS_BOTTOM;
}

/**
 * budgie_popover_select_position_automatic:
 *
 * Select the position based on the amount of space available in the given
 * regions.
 *
 * Typically we'll always try to display underneath first, and failing that
 * we'll try to appear above.  If we're still estate-limited, we'll then try
 * the right hand side, before finally falling back to the left hand side for display.
 *
 * The side options will also utilise Y-offsets and bounding to ensure there
 * is always some way to fit the popover sanely on screen.
 */
static GtkPositionType budgie_popover_select_position_automatic(gint our_height,
                                                                GdkRectangle screen_rect,
                                                                GdkRectangle widget_rect)
{
        /* Try to show the popover underneath */
        if (widget_rect.y + widget_rect.height + TAIL_HEIGHT + SHADOW_DIMENSION + our_height <=
            screen_rect.y + screen_rect.height) {
                return GTK_POS_TOP;
        }

        /* Now try to show the popover above the widget */
        if (widget_rect.y - TAIL_HEIGHT - SHADOW_DIMENSION - our_height >= screen_rect.y) {
                return GTK_POS_BOTTOM;
        }

        /* Work out which has more room, left or right. */
        double room_right = screen_rect.x + screen_rect.width - (widget_rect.x + widget_rect.width);
        double room_left = widget_rect.x - screen_rect.x;

        if (room_left > room_right) {
                return GTK_POS_RIGHT;
        }

        return GTK_POS_LEFT;
}

/**
 * budgie_popover_update_position_hints:
 *
 * Update our style classes and padding in response to a tail change
 */
static void budgie_popover_update_position_hints(BudgiePopover *self)
{
        GtkStyleContext *style = NULL;
        const gchar *style_class = NULL;
        static const gchar *position_classes[] = { "top", "left", "right", "bottom" };

        /* Allow themers to know what kind of popover this is, and set the
         * CSS class in accordance with the direction that the popover is
         * pointing in.
         */
        style = gtk_widget_get_style_context(GTK_WIDGET(self));
        for (guint i = 0; i < G_N_ELEMENTS(position_classes); i++) {
                gtk_style_context_remove_class(style, position_classes[i]);
        }

        switch (self->priv->tail.position) {
        case GTK_POS_BOTTOM:
                g_object_set(self->priv->add_area,
                             "margin-top",
                             5,
                             "margin-bottom",
                             13,
                             "margin-start",
                             5,
                             "margin-end",
                             5,
                             NULL);
                style_class = "bottom";
                break;
        case GTK_POS_TOP:
                g_object_set(self->priv->add_area,
                             "margin-top",
                             9,
                             "margin-bottom",
                             9,
                             "margin-start",
                             5,
                             "margin-end",
                             5,
                             NULL);
                style_class = "top";
                break;
        case GTK_POS_LEFT:
                g_object_set(self->priv->add_area,
                             "margin-top",
                             5,
                             "margin-bottom",
                             9,
                             "margin-start",
                             13,
                             "margin-end",
                             5,
                             NULL);
                style_class = "left";
                break;
        case GTK_POS_RIGHT:
                g_object_set(self->priv->add_area,
                             "margin-top",
                             5,
                             "margin-bottom",
                             9,
                             "margin-start",
                             5,
                             "margin-end",
                             13,
                             NULL);
                style_class = "right";
                break;
        default:
                break;
        }

        gtk_style_context_add_class(style, style_class);
}

/**
 * budgie_popover_compute_geometry:
 *
 * Work out the geometry for the relative_to widget in absolute coordinates
 * on the screen.
 */
static void budgie_popover_compute_widget_geometry(BudgiePopover *self)
{
        GtkAllocation alloc = { 0 };
        GtkWidget *toplevel = NULL;
        gint rx, ry = 0;
        gint x, y = 0;
        GdkRectangle display_geom = { 0 };
        gint our_height = 0, our_width = 0;
        GtkPositionType tail_position = GTK_POS_TOP;

        if (!self->priv->relative_to) {
                g_warning("compute_widget_geometry(): missing relative_widget");
                return;
        }

        budgie_popover_get_screen_for_widget(self->priv->relative_to, &display_geom);
        gtk_window_get_size(GTK_WINDOW(self), &our_width, &our_height);

        toplevel = gtk_widget_get_toplevel(self->priv->relative_to);
        gtk_window_get_position(GTK_WINDOW(toplevel), &x, &y);
        gtk_widget_translate_coordinates(self->priv->relative_to, toplevel, x, y, &rx, &ry);
        gtk_widget_get_allocation(self->priv->relative_to, &alloc);

        self->priv->widget_rect =
            (GdkRectangle){ .x = rx, .y = ry, .width = alloc.width, .height = alloc.height };

        /* Determine our position now based on the widget's geometry and our own */
        if (self->priv->policy == BUDGIE_POPOVER_POSITION_TOPLEVEL_HINT) {
                tail_position = budgie_popover_select_position_toplevel(self);
        } else {
                tail_position = budgie_popover_select_position_automatic(our_height,
                                                                         display_geom,
                                                                         self->priv->widget_rect);
        }

        /* Don't do this unless something changed */
        if (self->priv->tail.position != tail_position) {
                self->priv->tail.position = tail_position;
                budgie_popover_update_position_hints(self);
        }

        /* Update tail knowledge */
        self->priv->tail.position = tail_position;
        budgie_popover_compute_tail(self);
}

/**
 * budgie_popover_get_screen_for_widget:
 *
 * Use the appropriate function to find out the monitor's resolution for the
 * given @widget.
 */
static void budgie_popover_get_screen_for_widget(GtkWidget *widget, GdkRectangle *rectangle)
{
        GdkScreen *screen = NULL;
        GdkWindow *assoc_window = NULL;
        GdkDisplay *display = NULL;

        assoc_window = gtk_widget_get_parent_window(widget);
        screen = gtk_widget_get_screen(widget);
        display = gdk_screen_get_display(screen);

#if GTK_CHECK_VERSION(3, 22, 0)
        GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, assoc_window);
        gdk_monitor_get_geometry(monitor, rectangle);
#else
        gint monitor = gdk_screen_get_monitor_at_window(screen, assoc_window);
        gdk_screen_get_monitor_geometry(screen, monitor, rectangle);
#endif
}

/**
 * budgie_popover_compute_position:
 *
 * Work out exactly where the popover needs to appear on screen
 *
 * This will try to account for all potential positions, using a fairly
 * biased view of what the popover should do in each situation.
 *
 * Unlike a typical popover implementation, this relies on some information
 * from the toplevel window on what edge it happens to be on.
 */
static void budgie_popover_compute_positition(BudgiePopover *self, GdkRectangle *target)
{
        GtkPositionType tail_position = GTK_POS_BOTTOM;
        gint our_width = 0, our_height = 0;
        int x = 0, y = 0, width = 0, height = 0;
        GdkRectangle display_geom = { 0 };
        GdkRectangle widget_rect = { 0 };

        /* Work out our own size */
        gtk_window_get_size(GTK_WINDOW(self), &our_width, &our_height);

        /* Work out the real screen geometry involved here */
        budgie_popover_get_screen_for_widget(self->priv->relative_to, &display_geom);

        tail_position = self->priv->tail.position;
        budgie_popover_compute_tail(self);
        widget_rect = self->priv->widget_rect;

        /* Now work out where we live on screen */
        switch (tail_position) {
        case GTK_POS_BOTTOM:
                /* We need to appear above the widget */
                y = widget_rect.y - our_height;
                x = (widget_rect.x + (widget_rect.width / 2)) - (our_width / 2);
                break;
        case GTK_POS_TOP:
                /* We need to appear below the widget */
                y = widget_rect.y + widget_rect.height + (TAIL_DIMENSION / 2);
                x = (widget_rect.x + (widget_rect.width / 2)) - (our_width / 2);
                break;
        case GTK_POS_LEFT:
                /* We need to appear to the right of the widget */
                y = (widget_rect.y + (widget_rect.height / 2)) - (our_height / 2);
                x = widget_rect.x + widget_rect.width;
                break;
        case GTK_POS_RIGHT:
                y = (widget_rect.y + (widget_rect.height / 2)) - (our_height / 2);
                x = widget_rect.x - our_width;
                break;
        default:
                break;
        }

        static int pad_num = 1;

        /* Bound X to display width */
        if (x < display_geom.x) {
                self->priv->tail.x_offset += (x - (display_geom.x + pad_num));
                x -= (int)(self->priv->tail.x_offset);
        } else if ((x + our_width) >= display_geom.x + display_geom.width) {
                self->priv->tail.x_offset -=
                    ((display_geom.x + display_geom.width) - (our_width + pad_num)) - x;
                x -= (int)(self->priv->tail.x_offset);
        }

        /* Bound Y to display height */
        if (y < display_geom.y) {
                self->priv->tail.y_offset += (y - (display_geom.y + pad_num));
                y -= (int)(self->priv->tail.y_offset);
        } else if ((y + our_height) >= display_geom.y + display_geom.height) {
                self->priv->tail.y_offset -=
                    ((display_geom.y + display_geom.height) - (our_height + pad_num)) - y;
                y -= (int)(self->priv->tail.y_offset);
        }

        double display_tail_x = x + self->priv->tail.x + self->priv->tail.x_offset;
        double display_tail_y = y + self->priv->tail.y + self->priv->tail.y_offset;
        static double required_offset_x = TAIL_DIMENSION * 1.25;
        static double required_offset_y = TAIL_DIMENSION * 1.75;

        /* Prevent the tail pointer spilling outside the X bounds */
        if (display_tail_x <= display_geom.x + required_offset_x) {
                self->priv->tail.x_offset += (display_geom.x + required_offset_x) - display_tail_x;
        } else if (display_tail_x >= ((display_geom.x + display_geom.width) - required_offset_x)) {
                self->priv->tail.x_offset -=
                    (display_tail_x + required_offset_x) - (display_geom.x + display_geom.width);
        }

        /* Prevent the tail pointer spilling outside the Y bounds */
        if (display_tail_y <= display_geom.y + required_offset_y) {
                self->priv->tail.y_offset += (display_geom.y + required_offset_y) - display_tail_y;
        } else if (display_tail_y >= ((display_geom.y + display_geom.height) - required_offset_y)) {
                self->priv->tail.y_offset -=
                    (display_tail_y + required_offset_y) - (display_geom.y + display_geom.height);
        }

        /* Set the target rectangle */
        *target = (GdkRectangle){ .x = x, .y = y, .width = width, .height = height };
}

static void budgie_popover_compute_tail(BudgiePopover *self)
{
        GtkAllocation alloc = { 0 };
        BudgieTail t = { 0 };

        gtk_widget_get_allocation(GTK_WIDGET(self), &alloc);

        t.position = self->priv->tail.position;

        switch (self->priv->tail.position) {
        case GTK_POS_LEFT:
                t.x = alloc.x;
                t.y = alloc.y + (alloc.height / 2);
                t.start_y = t.y - TAIL_HEIGHT;
                t.end_y = t.y + TAIL_HEIGHT;
                t.start_x = t.end_x = t.x + TAIL_HEIGHT + SHADOW_DIMENSION;
                break;
        case GTK_POS_RIGHT:
                t.x = alloc.width;
                t.y = alloc.y + (alloc.height / 2);
                t.start_y = t.y - TAIL_HEIGHT;
                t.end_y = t.y + TAIL_HEIGHT;
                t.start_x = t.end_x = t.x - TAIL_HEIGHT - SHADOW_DIMENSION;
                break;
        case GTK_POS_TOP:
                t.x = (alloc.x + alloc.width / 2);
                t.y = alloc.y + BORDER_WIDTH;
                t.start_x = t.x - TAIL_HEIGHT;
                t.end_x = t.start_x + TAIL_DIMENSION;
                t.start_y = t.y + TAIL_HEIGHT;
                t.end_y = t.start_y;
                break;
        case GTK_POS_BOTTOM:
        default:
                t.x = (alloc.x + alloc.width / 2);
                t.y = (alloc.y + alloc.height) - SHADOW_DIMENSION;
                t.start_x = t.x - TAIL_HEIGHT;
                t.end_x = t.start_x + TAIL_DIMENSION;
                t.start_y = t.y - TAIL_HEIGHT;
                t.end_y = t.start_y;
                break;
        }

        self->priv->tail = t;
}

/**
 * budgie_popover_draw_tail:
 *
 * Draw the popover's tail section.
 */
static void budgie_popover_draw_tail(BudgiePopover *self, cairo_t *cr)
{
        BudgieTail *tail = &(self->priv->tail);

        cairo_move_to(cr, tail->start_x + tail->x_offset, tail->start_y + tail->y_offset);
        cairo_line_to(cr, tail->x + tail->x_offset, tail->y + tail->y_offset);
        cairo_line_to(cr, tail->end_x + tail->x_offset, tail->end_y + tail->y_offset);
        cairo_stroke_preserve(cr);
}

/**
 * budgie_popover_draw:
 *
 * Handle the main rendering + clipping of the BudgiePopover
 */
static gboolean budgie_popover_draw(GtkWidget *widget, cairo_t *cr)
{
        GtkStyleContext *style = NULL;
        GtkAllocation alloc = { 0 };
        GdkRGBA border_color = { 0 };
        GtkBorder border = { 0 };
        GtkStateFlags fl;
        BudgiePopover *self = NULL;
        BudgieTail *tail = NULL;
        GtkAllocation body_alloc = { 0 };
        GtkWidget *child = NULL;

        self = BUDGIE_POPOVER(widget);
        tail = &(self->priv->tail);

        /* Clear out the background before we draw anything */
        cairo_save(cr);
        cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.0);
        cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
        cairo_paint(cr);
        cairo_restore(cr);

        style = gtk_widget_get_style_context(widget);
        gtk_widget_get_allocation(widget, &alloc);
        body_alloc = alloc;

        /* Set up the offset */
        gdouble gap_start = 0, gap_end = 0;

        body_alloc.x += SHADOW_DIMENSION;
        body_alloc.width -= SHADOW_DIMENSION * 2;
        body_alloc.y += SHADOW_DIMENSION;
        body_alloc.height -= SHADOW_DIMENSION * 2;

        switch (self->priv->tail.position) {
        case GTK_POS_LEFT:
                body_alloc.height -= SHADOW_DIMENSION;
                body_alloc.width -= TAIL_HEIGHT;
                body_alloc.x += TAIL_HEIGHT;
                gap_start = (tail->start_y + tail->y_offset);
                break;
        case GTK_POS_RIGHT:
                body_alloc.height -= SHADOW_DIMENSION;
                body_alloc.width -= TAIL_HEIGHT;
                gap_start = tail->start_y + tail->y_offset;
                break;
        case GTK_POS_TOP: {
                int diff = TAIL_HEIGHT - SHADOW_DIMENSION;
                body_alloc.height -= SHADOW_DIMENSION * 2;
                body_alloc.y += diff;
                gap_start = (tail->start_x + tail->x_offset);
        } break;
        case GTK_POS_BOTTOM:
        default:
                body_alloc.height -= TAIL_HEIGHT;
                gap_start = (tail->start_x + tail->x_offset);
                break;
        }

        gap_start -= SHADOW_DIMENSION;
        gap_end = gap_start + TAIL_DIMENSION;

        fl = gtk_widget_get_state_flags(widget);

        /* Warning: Using deprecated API */
        G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        gtk_style_context_get_border_color(style, fl, &border_color);
        G_GNUC_END_IGNORE_DEPRECATIONS
        gtk_style_context_get_border(style, fl, &border);
        gtk_render_background(style,
                              cr,
                              body_alloc.x,
                              body_alloc.y,
                              body_alloc.width,
                              body_alloc.height);

        gtk_render_frame_gap(style,
                             cr,
                             body_alloc.x,
                             body_alloc.y,
                             body_alloc.width,
                             body_alloc.height,
                             self->priv->tail.position,
                             gap_start,
                             gap_end);
        gtk_style_context_set_state(style, fl);

        cairo_save(cr);
        cairo_set_line_width(cr, 1.3);
        cairo_set_source_rgba(cr,
                              border_color.red,
                              border_color.green,
                              border_color.blue,
                              border_color.alpha);
        budgie_popover_draw_tail(self, cr);
        cairo_clip(cr);
        cairo_move_to(cr, 0, 0);
        gtk_render_background(style, cr, alloc.x, alloc.y, alloc.width, alloc.height);
        cairo_restore(cr);

        child = gtk_bin_get_child(GTK_BIN(widget));
        if (child) {
                gtk_container_propagate_draw(GTK_CONTAINER(widget), child, cr);
        }

        return GDK_EVENT_PROPAGATE;
}

static void budgie_popover_add(GtkContainer *container, GtkWidget *widget)
{
        BudgiePopover *self = NULL;

        self = BUDGIE_POPOVER(container);

        /* Only add internal area to self for real. Anything else goes to add_area */
        if (widget == self->priv->add_area) {
                GTK_CONTAINER_CLASS(budgie_popover_parent_class)->add(container, widget);
                return;
        }

        gtk_container_add(GTK_CONTAINER(self->priv->add_area), widget);
}

static gboolean budgie_popover_hide_self(gpointer v)
{
        gtk_widget_hide(GTK_WIDGET(v));
        return G_SOURCE_REMOVE;
}

/**
 * budgie_popover_button_press:
 *
 * If the mouse button is pressed outside of our window, that's our cue to close.
 */
static gboolean budgie_popover_button_press(GtkWidget *widget, GdkEventButton *button,
                                            __budgie_unused__ gpointer udata)
{
        gint x, y = 0;
        gint w, h = 0;
        gtk_window_get_position(GTK_WINDOW(widget), &x, &y);
        gtk_window_get_size(GTK_WINDOW(widget), &w, &h);

        gint root_x = (gint)button->x_root;
        gint root_y = (gint)button->y_root;

        /* Inside our window? Continue as normal. */
        if ((root_x >= x && root_x <= x + w) && (root_y >= y && root_y <= y + h)) {
                return GDK_EVENT_PROPAGATE;
        }

        /* Happened outside, we're done. */
        g_idle_add(budgie_popover_hide_self, widget);
        return GDK_EVENT_PROPAGATE;
}

/**
 * budgie_popover_key_press:
 *
 * If the Escape/Super key is pressed, then we also need to close.
 */
static gboolean budgie_popover_key_press(GtkWidget *widget, GdkEventKey *key,
                                         __budgie_unused__ gpointer udata)
{
        switch (key->keyval) {
        case GDK_KEY_Escape:
        case GDK_KEY_Super_L:
        case GDK_KEY_Super_R:
                gtk_widget_hide(widget);
                return GDK_EVENT_STOP;
        default:
                return GDK_EVENT_PROPAGATE;
        }
}

/**
 * budgie_popover_disconnect:
 *
 * Our associated widget has died, so we must unref ourselves now.
 */
static void budgie_popover_disconnect(__budgie_unused__ GtkWidget *relative_to, BudgiePopover *self)
{
        self->priv->relative_to = NULL;
        gtk_widget_destroy(GTK_WIDGET(self));
}

static void budgie_popover_set_property(GObject *object, guint id, const GValue *value,
                                        GParamSpec *spec)
{
        BudgiePopover *self = BUDGIE_POPOVER(object);

        switch (id) {
        case PROP_RELATIVE_TO:
                if (self->priv->relative_to) {
                        g_signal_handlers_disconnect_by_data(self->priv->relative_to, self);
                }
                self->priv->relative_to = g_value_get_object(value);
                if (self->priv->relative_to) {
                        g_signal_connect(self->priv->relative_to,
                                         "destroy",
                                         G_CALLBACK(budgie_popover_disconnect),
                                         self);
                        budgie_popover_compute_tail(self);
                }
                break;
        case PROP_POLICY:
                self->priv->policy = g_value_get_enum(value);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID(object, id, spec);
                break;
        }
}

static void budgie_popover_get_property(GObject *object, guint id, GValue *value, GParamSpec *spec)
{
        BudgiePopover *self = BUDGIE_POPOVER(object);

        switch (id) {
        case PROP_RELATIVE_TO:
                g_value_set_object(value, self->priv->relative_to);
                break;
        case PROP_POLICY:
                g_value_set_enum(value, self->priv->policy);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID(object, id, spec);
                break;
        }
}

/**
 * budgie_popover_new:
 * @relative_to: The widget to show the popover for

 * Construct a new BudgiePopover object
 *
 * Returns: (transfer full): A newly created #BudgiePopover
 */
GtkWidget *budgie_popover_new(GtkWidget *relative_to)
{
        return g_object_new(BUDGIE_TYPE_POPOVER,
                            "relative-to",
                            relative_to,
                            "type",
                            GTK_WINDOW_POPUP,
                            NULL);
}

/**
 * budgie_popover_set_position_policy:
 * @policy:(type BudgiePopoverPositionPolicy): New policy to set
 *
 * Set the positioning policy employed by the popover
 */
void budgie_popover_set_position_policy(BudgiePopover *self, BudgiePopoverPositionPolicy policy)
{
        g_return_if_fail(self != NULL);
        g_object_set(self, "position-policy", policy, NULL);
}

/**
 * budgie_popover_get_position_policy:
 *
 * Retrieve the currently active positioning policy for this popover
 *
 * Returns: The #BudgiePopoverPositionPolicy currently in use
 */
BudgiePopoverPositionPolicy budgie_popover_get_position_policy(BudgiePopover *self)
{
        g_return_val_if_fail(self != NULL, 0);
        return self->priv->policy;
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 8
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=8 tabstop=8 expandtab:
 * :indentSize=8:tabSize=8:noTabs=true:
 */
