/*************************************************************************
 * The following code has come from Planner. This code implements a
 * GtkCalendar in a custom GtkCellEditable popup from GtkCellRenderer.
 *
 * These files have been renamed and changed to remove code not required
 * and to remove a dependency on libplanner.
 *
 * Copyright (C) 2012 Robert Fewell
 *
 * Copyright (C) 2001-2002 CodeFactory AB
 * Copyright (C) 2001-2002 Richard Hult <richard@imendio.com>
 * Copyright (C) 2001-2002 Mikael Hallendal <micke@imendio.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *************************************************************************/
#include <config.h>

#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <gdk/gdkkeysyms.h>

#include "gnc-cell-renderer-popup.h"
#include "gnc-cell-renderer-popup-entry.h"

enum {
    SHOW_POPUP,
    HIDE_POPUP,
    LAST_SIGNAL
};

static void      gcrp_init               (GncCellRendererPopup      *popup);
static void      gcrp_class_init         (GncCellRendererPopupClass *klass);

static GtkCellEditable *gcrp_start_editing (GtkCellRenderer          *cell,
                                            GdkEvent                 *event,
                                            GtkWidget                *widget,
                                            const gchar              *path,
                                            const GdkRectangle       *background_area,
                                            const GdkRectangle       *cell_area,
                                            GtkCellRendererState      flags);

static void      gcrp_show_popup           (GncCellRendererPopup     *cell,
                                            const gchar              *path,
                                            gint                      x1,
                                            gint                      y1,
                                            gint                      x2,
                                            gint                      y2);

static void      gcrp_hide_popup           (GncCellRendererPopup     *cell);

static void      gcrp_get_size             (GtkCellRenderer          *cell,
                                            GtkWidget                *widget,
                                            const GdkRectangle       *cell_area,
                                            gint                     *x_offset,
                                            gint                     *y_offset,
                                            gint                     *width,
                                            gint                     *height);

static void      gcrp_style_set            (GtkWidget                *widget,
                                            GtkStyle                 *old_style,
                                            GncCellRendererPopup     *popup);

static gboolean  gcrp_key_press_event      (GtkWidget                *popup_window,
                                            GdkEventKey              *event,
                                            GncCellRendererPopup     *cell);

static gboolean  gcrp_button_press_event   (GtkWidget                *widget,
                                            GdkEventButton           *event,
                                            GncCellRendererPopup     *popup);

void gnc_marshal_VOID__STRING_INT_INT_INT_INT (GClosure              *closure,
                                               GValue                *return_value,
                                               guint                  n_param_values,
                                               const GValue          *param_values,
                                               gpointer               invocation_hint,
                                               gpointer               marshal_data);


static GtkCellRendererTextClass *parent_class;
static guint signals[LAST_SIGNAL];

#define GNC_CELL_RENDERER_POPUP_PATH "gnc-cell-renderer-popup-path"

GType
gnc_cell_renderer_popup_get_type (void)
{
    static GType cell_text_type = 0;

    if (!cell_text_type) {
        static const GTypeInfo cell_text_info = {
            sizeof (GncCellRendererPopupClass),
            NULL,           /* base_init */
            NULL,           /* base_finalize */
            (GClassInitFunc) gcrp_class_init,
            NULL,           /* class_finalize */
            NULL,           /* class_data */
            sizeof (GncCellRendererPopup),
            0,              /* n_preallocs */
            (GInstanceInitFunc) gcrp_init,
        };

        cell_text_type = g_type_register_static (GTK_TYPE_CELL_RENDERER_TEXT,
                                                 "GncCellRendererPopup",
                                                 &cell_text_info,
                                                 0);
    }

    return cell_text_type;
}

static void
gcrp_init (GncCellRendererPopup *popup)
{
    popup->popup_window = gtk_window_new (GTK_WINDOW_POPUP);

    popup->button_width = -1;

    g_signal_connect (popup->popup_window,
              "button-press-event",
              G_CALLBACK(gcrp_button_press_event),
              popup);

    g_signal_connect (popup->popup_window,
              "key-press-event",
              G_CALLBACK(gcrp_key_press_event),
              popup);

    g_signal_connect (popup->popup_window,
              "style-set",
              G_CALLBACK(gcrp_style_set),
              popup);
}

static void
gcrp_class_init (GncCellRendererPopupClass *klass)
{
    GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS(klass);

    parent_class = GTK_CELL_RENDERER_TEXT_CLASS(g_type_class_peek_parent (klass));

    cell_class->start_editing = gcrp_start_editing;
    cell_class->get_size      = gcrp_get_size;

    klass->show_popup = gcrp_show_popup;
    klass->hide_popup = gcrp_hide_popup;

    signals[SHOW_POPUP] = g_signal_new (
        "show-popup",
        G_TYPE_FROM_CLASS (klass),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (GncCellRendererPopupClass, show_popup),
        NULL, NULL,
        gnc_marshal_VOID__STRING_INT_INT_INT_INT,
        G_TYPE_NONE, 5,
        G_TYPE_STRING,
        G_TYPE_INT,
        G_TYPE_INT,
        G_TYPE_INT,
        G_TYPE_INT);

    signals[HIDE_POPUP] = g_signal_new (
        "hide-popup",
        G_TYPE_FROM_CLASS (klass),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (GncCellRendererPopupClass, hide_popup),
        NULL, NULL,
        g_cclosure_marshal_VOID__VOID,
        G_TYPE_NONE, 0);
}

static void
gcrp_editing_done (GtkCellEditable      *editable,
                   GncCellRendererPopup *cell)
{
    gchar       *path;
    const gchar *new_text;

    if (GNC_POPUP_ENTRY(editable)->editing_canceled ||
        cell->editing_canceled) {
                gtk_cell_renderer_stop_editing (GTK_CELL_RENDERER(cell), TRUE);
        return;
    }

    path = g_object_get_data (G_OBJECT(editable),
                  GNC_CELL_RENDERER_POPUP_PATH);

    new_text = gnc_popup_entry_get_text (GNC_POPUP_ENTRY(editable));

    gtk_cell_renderer_stop_editing (GTK_CELL_RENDERER(cell), FALSE);

    g_signal_emit_by_name (cell, "edited", path, new_text);
}

static void
gcrp_style_set (GtkWidget            *widget,
                GtkStyle             *old_style,
                GncCellRendererPopup *popup)
{
    /* Invalidate the cache. */
    popup->button_width = -1;
}

static gboolean
gcrp_grab_on_window (GdkWindow *window,
                     guint32    activate_time)
{
    GdkDisplay *display = gdk_display_get_default ();
#if GTK_CHECK_VERSION(3,20,0)
    GdkSeat *seat;
#else
    GdkDeviceManager *device_manager;
    GdkDevice *device;
#endif

#if GTK_CHECK_VERSION(3,20,0)
    GdkEvent *event = gtk_get_current_event ();
#endif

#if GTK_CHECK_VERSION(3,20,0)
    seat = gdk_display_get_default_seat (display);
#else
    device_manager = gdk_display_get_device_manager (display);
    device = gdk_device_manager_get_client_pointer (device_manager);
#endif

#if GTK_CHECK_VERSION(3,20,0)
    if ((gdk_seat_grab (seat, window, GDK_SEAT_CAPABILITY_POINTER, TRUE, NULL,
                        event, NULL, NULL) == GDK_GRAB_SUCCESS )) {
        if (gdk_seat_grab (seat, window, GDK_SEAT_CAPABILITY_KEYBOARD, TRUE, NULL,
                        event, NULL, NULL) == GDK_GRAB_SUCCESS )
#else
    if ((gdk_device_grab (device, window, GDK_OWNERSHIP_WINDOW, TRUE,
                          GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
                          GDK_POINTER_MOTION_MASK,
                          NULL, activate_time) == GDK_GRAB_SUCCESS)) {

        if (gdk_device_grab (device, window, GDK_OWNERSHIP_WINDOW, TRUE,
                             GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
                 NULL, activate_time) == GDK_GRAB_SUCCESS)
#endif
            return TRUE;
        else {
#if GTK_CHECK_VERSION(3,20,0)
            gdk_seat_ungrab (seat);
#else
            gdk_device_ungrab (device, activate_time);
#endif
            return FALSE;
        }
    }
    return FALSE;
}

static void
gcrp_show_popup (GncCellRendererPopup *cell,
                 const gchar          *path,
                 gint                  x1,
                 gint                  y1,
                 gint                  x2,
                 gint                  y2)
{
#if GTK_CHECK_VERSION(3,22,0)
        GdkWindow *win;
        GdkMonitor *mon;
        GdkRectangle monitor_size;
#endif
    GtkAllocation alloc;
    gint          x, y;
    gint          screen_height, screen_width;
    gint          button_height;

    cell->shown = TRUE;

    gtk_widget_realize (cell->popup_window);

    /* I'm not sure this is ok to do, but we need to show the window to be
     * able to get the allocation right.
     */
    gtk_window_move (GTK_WINDOW (cell->popup_window), -500, -500);
    gtk_widget_show (cell->popup_window);

    gtk_widget_get_allocation (cell->popup_window, &alloc);

    x = x2;
    y = y2;

    button_height = y2 - y1;

#if GTK_CHECK_VERSION(3,22,0)
    win = gdk_screen_get_root_window (gtk_window_get_screen (GTK_WINDOW(cell->popup_window)));
    mon = gdk_display_get_monitor_at_window (gtk_widget_get_display (GTK_WIDGET(cell->popup_window)), win);
    gdk_monitor_get_geometry (mon, &monitor_size);

    screen_width = monitor_size.width;
    screen_height = monitor_size.height - y;
#else
    screen_width = gdk_screen_width();
    screen_height = gdk_screen_height() - y;
#endif

    /* Check if it fits in the available height. */
    if (alloc.height > screen_height)
    {
        /* It doesn't fit, so we see if we have the minimum space needed. */
        if (alloc.height > screen_height && y - button_height > screen_height)
        {
            /* We don't, so we show the popup above the cell
               instead of below it. */
            y -= (alloc.height + button_height);
            if (y < 0) {
                y = 0;
            }
        }
    }

    /* We try to line it up with the right edge of the column, but we don't
     * want it to go off the edges of the screen.
     */
    if (x > screen_width)
        x = screen_width;

    x -= alloc.width;

    if (x < 0)
        x = 0;

    gtk_grab_add (cell->popup_window);

    gtk_window_move (GTK_WINDOW(cell->popup_window), x, y);
    gtk_widget_show (cell->popup_window);

    gtk_widget_grab_focus (cell->focus_window);

    gcrp_grab_on_window (gtk_widget_get_window (cell->popup_window),
                 gtk_get_current_event_time ());
}

static void
gcrp_hide_popup (GncCellRendererPopup *cell)
{
    gtk_grab_remove (cell->popup_window);
    gtk_widget_hide (cell->popup_window);

    if (cell->editable)
        gtk_cell_editable_editing_done (GTK_CELL_EDITABLE(cell->editable));

    /* This may look weird (the test), but the weak pointer will actually be
     * nulled out for some cells, like the date cell.
     */
    if (cell->editable)
        gtk_cell_editable_remove_widget (GTK_CELL_EDITABLE(cell->editable));

    cell->shown = FALSE;
    cell->editing_canceled = FALSE;
}

static void
gcrp_arrow_clicked (GtkCellEditable      *entry,
                    GncCellRendererPopup *cell)
{
    GtkAllocation  alloc;
    gint           x, y;
    const gchar   *path;

    if (cell->shown)
    {
        cell->editing_canceled = TRUE;
        gnc_cell_renderer_popup_hide (cell);
        return;
    }

    path = g_object_get_data (G_OBJECT(entry),
                  GNC_CELL_RENDERER_POPUP_PATH);

    /* Temporarily grab pointer and keyboard on a window we know exists; we
     * do this so that the grab (with owner events == TRUE) affects
     * events generated when the window is mapped, such as enter
     * notify events on subwidgets. If the grab fails, bail out.
     */
    if (!gcrp_grab_on_window (gtk_widget_get_window (GTK_WIDGET(entry)),
                  gtk_get_current_event_time ())) {
        return;
    }

    gtk_editable_select_region (GTK_EDITABLE(GNC_POPUP_ENTRY(entry)->entry), 0, 0);

    gdk_window_get_origin (gtk_widget_get_window (GTK_WIDGET(entry)), &x, &y);

    gtk_widget_get_allocation (GTK_WIDGET(entry), &alloc);

    g_signal_emit (cell, signals[SHOW_POPUP], 0,
                   path,
                   x,
                   y,
                   x + alloc.width,
                   y + alloc.height);
}

static GtkCellEditable *
gcrp_start_editing (GtkCellRenderer      *cell,
                    GdkEvent             *event,
                    GtkWidget            *widget,
                    const gchar          *path,
                    const GdkRectangle   *background_area,
                    const GdkRectangle   *cell_area,
                    GtkCellRendererState  flags)
{
    GncCellRendererPopup *popup = GNC_CELL_RENDERER_POPUP(cell);
    GtkWidget            *editable;
    gchar                *text;
    gboolean              iseditable;

    g_object_get (G_OBJECT (popup), "editable", &iseditable, NULL);

    /* If the cell isn't editable we return NULL. */
    if (iseditable == FALSE)
        return NULL;

    editable = g_object_new (GNC_TYPE_POPUP_ENTRY, NULL);

    g_object_get (G_OBJECT(cell), "text", &text, NULL);
                  popup->cell_text = text;

    gnc_popup_entry_set_text (GNC_POPUP_ENTRY(editable), text ? text : "");

    g_object_set_data_full (G_OBJECT(editable),
                            GNC_CELL_RENDERER_POPUP_PATH,
                            g_strdup (path),
                            g_free);

    gtk_widget_show (editable);

    g_signal_connect (editable,
                      "editing-done",
                      G_CALLBACK(gcrp_editing_done),
                      popup);

    g_signal_connect (editable,
                      "arrow-clicked",
                      G_CALLBACK(gcrp_arrow_clicked),
                      popup);

    popup->editable = editable;

    g_object_add_weak_pointer (G_OBJECT(popup->editable),
                               (gpointer) &popup->editable);

    return GTK_CELL_EDITABLE(editable);
}

GtkCellRenderer *
gnc_cell_renderer_popup_new (void)
{
    return GTK_CELL_RENDERER(
        g_object_new (gnc_cell_renderer_popup_get_type (), NULL));
}

void
gnc_cell_renderer_popup_hide (GncCellRendererPopup *cell)
{
    g_return_if_fail (GNC_IS_CELL_RENDERER_POPUP(cell));

    g_signal_emit (cell, signals[HIDE_POPUP], 0);
}

static void
gcrp_get_size (GtkCellRenderer    *cell,
               GtkWidget          *widget,
               const GdkRectangle *cell_area,
               gint               *x_offset,
               gint               *y_offset,
               gint               *width,
               gint               *height)
{
    GncCellRendererPopup *popup = GNC_CELL_RENDERER_POPUP (cell);

    if (GTK_CELL_RENDERER_CLASS(parent_class)->get_size) {
        (* GTK_CELL_RENDERER_CLASS(parent_class)->get_size) (cell,
                                      widget,
                                      cell_area,
                                      x_offset,
                                      y_offset,
                                      width,
                                      height);
    }

    /* We cache this because it takes really long to get the width. */
    if (popup->button_width == -1)
        popup->button_width = gnc_popup_get_button_width ();

    *width += popup->button_width;
}

static gboolean
gcrp_key_press_event (GtkWidget            *popup_window,
                      GdkEventKey          *event,
                      GncCellRendererPopup *cell)
{
    if (event->keyval != GDK_KEY_Escape &&
        event->keyval != GDK_KEY_Return &&
        event->keyval != GDK_KEY_KP_Enter &&
        event->keyval != GDK_KEY_ISO_Enter &&
        event->keyval != GDK_KEY_3270_Enter) {
        return FALSE;
    }
    if (event->keyval == GDK_KEY_Escape) {
        cell->editing_canceled = TRUE;
    } else {
        cell->editing_canceled = FALSE;
    }
    gnc_cell_renderer_popup_hide (cell);

    return TRUE;
}

static gboolean
gcrp_button_press_event (GtkWidget            *widget,
                         GdkEventButton       *event,
                         GncCellRendererPopup *popup)
{
    GtkAllocation alloc;
    gdouble       x, y;
    gint          xoffset, yoffset;
    gint          x1, y1;
    gint          x2, y2;

    if (event->button != 1)
        return FALSE;

    /* If the event happened outside the popup, cancel editing.
     */

    /*gdk_event_get_root_coords ((GdkEvent *) event, &x, &y);*/
    x = event->x_root;
    y = event->y_root;

    gdk_window_get_root_origin (gtk_widget_get_window (widget),
                                &xoffset,
                                &yoffset);

    gtk_widget_get_allocation (widget, &alloc);
    xoffset += alloc.x;
    yoffset += alloc.y;

    gtk_widget_get_allocation (popup->popup_window, &alloc);
    x1 = alloc.x + xoffset;
    y1 = alloc.y + yoffset;
    x2 = x1 + alloc.width;
    y2 = y1 + alloc.height;

    if (x > x1 && x < x2 && y > y1 && y < y2)
        return FALSE;

    popup->editing_canceled = TRUE;
    gnc_cell_renderer_popup_hide (popup);

    return FALSE;
}


#define g_marshal_value_peek_int(v)      (v)->data[0].v_int
#define g_marshal_value_peek_string(v)   (v)->data[0].v_pointer

void
gnc_marshal_VOID__STRING_INT_INT_INT_INT (GClosure     *closure,
                                          GValue       *return_value G_GNUC_UNUSED,
                                          guint         n_param_values,
                                          const GValue *param_values,
                                          gpointer      invocation_hint G_GNUC_UNUSED,
                                          gpointer      marshal_data)
{
    typedef void (*GMarshalFunc_VOID__STRING_INT_INT_INT_INT) (gpointer     data1,
                                                               gpointer     arg_1,
                                                               gint         arg_2,
                                                               gint         arg_3,
                                                               gint         arg_4,
                                                               gint         arg_5,
                                                               gpointer     data2);

    register GMarshalFunc_VOID__STRING_INT_INT_INT_INT callback;
    register GCClosure *cc = (GCClosure*) closure;
    register gpointer data1, data2;

    g_return_if_fail (n_param_values == 6);

    if (G_CCLOSURE_SWAP_DATA (closure))
    {
        data1 = closure->data;
        data2 = g_value_peek_pointer (param_values + 0);
    }
    else
    {
        data1 = g_value_peek_pointer (param_values + 0);
        data2 = closure->data;
    }
    callback = (GMarshalFunc_VOID__STRING_INT_INT_INT_INT) (marshal_data ? marshal_data : cc->callback);

    callback (data1,
              g_marshal_value_peek_string (param_values + 1),
              g_marshal_value_peek_int (param_values + 2),
              g_marshal_value_peek_int (param_values + 3),
              g_marshal_value_peek_int (param_values + 4),
              g_marshal_value_peek_int (param_values + 5),
              data2);
}
