/* Copyright (C) 2011, 2012 Matthias Vogelgesang <matthias.vogelgesang@kit.edu>
   (Karlsruhe Institute of Technology)

   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.

   This library 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 Lesser General Public License for more
   details.

   You should have received a copy of the GNU Lesser General Public License along
   with this library; if not, write to the Free Software Foundation, Inc., 51
   Franklin St, Fifth Floor, Boston, MA 02110, USA */

#include <stdlib.h>
#include "egg-property-cell-renderer.h"

G_DEFINE_TYPE (EggPropertyCellRenderer, egg_property_cell_renderer, GTK_TYPE_CELL_RENDERER)

#define EGG_PROPERTY_CELL_RENDERER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), EGG_TYPE_PROPERTY_CELL_RENDERER, EggPropertyCellRendererPrivate))


struct _EggPropertyCellRendererPrivate
{
    GObject         *object;
    GtkListStore    *list_store;
    GtkCellRenderer *renderer;
    GtkCellRenderer *text_renderer;
    GtkCellRenderer *spin_renderer;
    GtkCellRenderer *toggle_renderer;
    GtkCellRenderer *combo_renderer;
    GHashTable      *combo_models;
};

enum
{
    PROP_0,
    PROP_PROP_NAME,
    N_PROPERTIES
};

enum
{
    COMBO_COLUMN_VALUE_NAME,
    COMBO_COLUMN_VALUE,
    N_COMBO_COLUMNS
};

static GParamSpec *egg_property_cell_renderer_properties[N_PROPERTIES] = { NULL, };

GtkCellRenderer *
egg_property_cell_renderer_new (GObject         *object,
                                GtkListStore    *list_store)
{
    EggPropertyCellRenderer *renderer;

    renderer = EGG_PROPERTY_CELL_RENDERER (g_object_new (EGG_TYPE_PROPERTY_CELL_RENDERER, NULL));
    renderer->priv->object = object;
    renderer->priv->list_store = list_store;
    return GTK_CELL_RENDERER (renderer);
}

static GParamSpec *
get_pspec_from_object (GObject *object, const gchar *prop_name)
{
    GObjectClass *oclass = G_OBJECT_GET_CLASS (object);
    return g_object_class_find_property (oclass, prop_name);
}

static void
get_string_double_repr (GObject *object, const gchar *prop_name, gchar **text, gdouble *number)
{
    GParamSpec *pspec;
    GValue from = { 0 };
    GValue to_string = { 0 };
    GValue to_double = { 0 };

    pspec = get_pspec_from_object (object, prop_name);
    g_value_init (&from, pspec->value_type);
    g_value_init (&to_string, G_TYPE_STRING);
    g_value_init (&to_double, G_TYPE_DOUBLE);
    g_object_get_property (object, prop_name, &from);

    if (g_value_transform (&from, &to_string))
        *text = g_strdup (g_value_get_string (&to_string));
    else
        g_warning ("Could not convert from %s gchar*\n", g_type_name (pspec->value_type));

    if (g_value_transform (&from, &to_double))
        *number = g_value_get_double (&to_double);
    else
        g_warning ("Could not convert from %s to gdouble\n", g_type_name (pspec->value_type));
}

static void
clear_adjustment (GObject *object)
{
    GtkAdjustment *adjustment;

    g_object_get (object,
            "adjustment", &adjustment,
            NULL);

    if (adjustment)
        g_object_unref (adjustment);

    g_object_set (object,
            "adjustment", NULL,
            NULL);
}

static void
egg_property_cell_renderer_set_renderer (EggPropertyCellRenderer    *renderer,
                                         const gchar                *prop_name)
{
    EggPropertyCellRendererPrivate *priv;
    GParamSpec *pspec;
    gchar *text = NULL;
    gdouble number;

    priv = EGG_PROPERTY_CELL_RENDERER_GET_PRIVATE (renderer);
    pspec = get_pspec_from_object (priv->object, prop_name);

    /*
     * Set this renderers mode, so that any actions can be forwarded to our
     * child renderers.
     */
    switch (pspec->value_type) {
        /* toggle renderers */
        case G_TYPE_BOOLEAN:
            priv->renderer = priv->toggle_renderer;
            g_object_set (renderer, "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
            break;

        /* spin renderers */
        case G_TYPE_FLOAT:
        case G_TYPE_DOUBLE:
            priv->renderer = priv->spin_renderer;
            g_object_set (renderer, "mode", GTK_CELL_RENDERER_MODE_EDITABLE, NULL);
            g_object_set (priv->renderer, "digits", 5, NULL);
            break;

        case G_TYPE_INT:
        case G_TYPE_UINT:
        case G_TYPE_LONG:
        case G_TYPE_ULONG:
        case G_TYPE_INT64:
        case G_TYPE_UINT64:
            priv->renderer = priv->spin_renderer;
            g_object_set (renderer, "mode", GTK_CELL_RENDERER_MODE_EDITABLE, NULL);
            g_object_set (priv->renderer, "digits", 0, NULL);
            break;

        /* text renderers */
        case G_TYPE_POINTER:
        case G_TYPE_STRING:
            priv->renderer = priv->text_renderer;
            g_object_set (renderer, "mode", GTK_CELL_RENDERER_MODE_EDITABLE, NULL);
            break;

        /* combo renderers */
        default:
            if (G_TYPE_IS_ENUM (pspec->value_type)) {
                priv->renderer = priv->combo_renderer;
                g_object_set (renderer, "mode", GTK_CELL_RENDERER_MODE_EDITABLE, NULL);
            }
            break;
    }

    /*
     * Set the content from the objects property.
     */
    switch (pspec->value_type) {
        case G_TYPE_BOOLEAN:
            {
                gboolean val;

                g_object_get (priv->object, prop_name, &val, NULL);
                g_object_set (priv->renderer,
                        "active", val,
                        "activatable", pspec->flags & G_PARAM_WRITABLE ? TRUE : FALSE,
                        NULL);
                break;
            }

        case G_TYPE_INT:
        case G_TYPE_UINT:
        case G_TYPE_LONG:
        case G_TYPE_ULONG:
        case G_TYPE_INT64:
        case G_TYPE_UINT64:
        case G_TYPE_FLOAT:
        case G_TYPE_DOUBLE:
            get_string_double_repr (priv->object, prop_name, &text, &number);
            break;

        case G_TYPE_STRING:
            g_object_get (priv->object, prop_name, &text, NULL);
            break;

        case G_TYPE_POINTER:
            {
                gpointer val;

                g_object_get (priv->object, prop_name, &val, NULL);
                text = g_strdup_printf ("0x%x", GPOINTER_TO_INT (val));
            }
            break;

        default:
            if (G_TYPE_IS_ENUM (pspec->value_type)) {
                GParamSpecEnum *pspec_enum;
                GEnumClass *enum_class;
                GtkTreeModel *combo_model;
                GtkTreeIter iter;
                gint value;

                g_object_get (priv->object, prop_name, &value, NULL);

                pspec_enum = G_PARAM_SPEC_ENUM (pspec);
                enum_class = pspec_enum->enum_class;
                combo_model = g_hash_table_lookup (priv->combo_models, prop_name);

                if (combo_model == NULL) {
                    combo_model = GTK_TREE_MODEL (gtk_list_store_new (N_COMBO_COLUMNS, G_TYPE_STRING, G_TYPE_INT));
                    g_hash_table_insert (priv->combo_models, g_strdup (prop_name), combo_model);

                    for (guint i = 0; i < enum_class->n_values; i++) {
                        gtk_list_store_append (GTK_LIST_STORE (combo_model), &iter);
                        gtk_list_store_set (GTK_LIST_STORE (combo_model), &iter,
                                COMBO_COLUMN_VALUE_NAME, enum_class->values[i].value_name,
                                COMBO_COLUMN_VALUE, enum_class->values[i].value,
                                -1);
                    }
                }


                for (guint i = 0; i < enum_class->n_values; i++) {
                    if (enum_class->values[i].value == value)
                        text = g_strdup (enum_class->values[i].value_name);
                }

                g_object_set (priv->renderer,
                        "model", combo_model,
                        "text-column", 0,
                        NULL);
            }
            break;
    }

    if (pspec->flags & G_PARAM_WRITABLE) {
        if (GTK_IS_CELL_RENDERER_TOGGLE (priv->renderer))
            g_object_set (priv->renderer, "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
        else
            g_object_set (priv->renderer, "foreground", "#000000", NULL);

        if (GTK_IS_CELL_RENDERER_TEXT (priv->renderer)) {
            g_object_set (priv->renderer,
                    "editable", TRUE,
                    "mode", GTK_CELL_RENDERER_MODE_EDITABLE,
                    NULL);
        }

        if (GTK_IS_CELL_RENDERER_SPIN (priv->renderer)) {
            GtkObject *adjustment = NULL;

#define gtk_typed_adjustment_new(type, pspec, val, step_inc, page_inc) \
    gtk_adjustment_new (val, ((type *) pspec)->minimum, ((type *) pspec)->maximum, step_inc, page_inc, 0)

            switch (pspec->value_type) {
                case G_TYPE_INT:
                    adjustment = gtk_typed_adjustment_new (GParamSpecInt, pspec, number, 1, 10);
                    break;
                case G_TYPE_UINT:
                    adjustment = gtk_typed_adjustment_new (GParamSpecUInt, pspec, number, 1, 10);
                    break;
                case G_TYPE_LONG:
                    adjustment = gtk_typed_adjustment_new (GParamSpecLong, pspec, number, 1, 10);
                    break;
                case G_TYPE_ULONG:
                    adjustment = gtk_typed_adjustment_new (GParamSpecULong, pspec, number, 1, 10);
                    break;
                case G_TYPE_INT64:
                    adjustment = gtk_typed_adjustment_new (GParamSpecInt64, pspec, number, 1, 10);
                    break;
                case G_TYPE_UINT64:
                    adjustment = gtk_typed_adjustment_new (GParamSpecUInt64, pspec, number, 1, 10);
                    break;
                case G_TYPE_FLOAT:
                    adjustment = gtk_typed_adjustment_new (GParamSpecFloat, pspec, number, 0.05, 10);
                    break;
                case G_TYPE_DOUBLE:
                    adjustment = gtk_typed_adjustment_new (GParamSpecDouble, pspec, number, 0.05, 10);
                    break;
            }

            clear_adjustment (G_OBJECT (priv->renderer));
            g_object_set (priv->renderer, "adjustment", adjustment, NULL);
        }
    }
    else {
        g_object_set (priv->renderer, "mode", GTK_CELL_RENDERER_MODE_INERT, NULL);

        if (!GTK_IS_CELL_RENDERER_TOGGLE (priv->renderer))
            g_object_set (priv->renderer, "foreground", "#aaaaaa", NULL);
    }

    if (text != NULL) {
        g_object_set (priv->renderer, "text", text, NULL);
        g_free (text);
    }
}

static gchar *
get_prop_name_from_tree_model (GtkTreeModel *model, const gchar *path)
{
    GtkTreeIter iter;
    gchar *prop_name = NULL;

    /* TODO: don't assume column 0 to contain the prop name */
    if (gtk_tree_model_get_iter_from_string (model, &iter, path))
        gtk_tree_model_get (model, &iter, 0, &prop_name, -1);

    return prop_name;
}

static void
egg_property_cell_renderer_toggle_cb (GtkCellRendererToggle *renderer,
                                      gchar                 *path,
                                      gpointer               user_data)
{
    EggPropertyCellRendererPrivate *priv;
    gchar *prop_name;

    priv = (EggPropertyCellRendererPrivate *) user_data;
    prop_name = get_prop_name_from_tree_model (GTK_TREE_MODEL (priv->list_store), path);

    if (prop_name != NULL) {
        gboolean activated;

        g_object_get (priv->object, prop_name, &activated, NULL);
        g_object_set (priv->object, prop_name, !activated, NULL);
        g_free (prop_name);
    }
}

static void
egg_property_cell_renderer_text_edited_cb (GtkCellRendererText  *renderer,
                                           gchar                *path,
                                           gchar                *new_text,
                                           gpointer              user_data)
{
    EggPropertyCellRendererPrivate *priv;
    gchar *prop_name;

    priv = (EggPropertyCellRendererPrivate *) user_data;
    prop_name = get_prop_name_from_tree_model (GTK_TREE_MODEL (priv->list_store), path);

    if (prop_name != NULL) {
        g_object_set (priv->object, prop_name, new_text, NULL);
        g_free (prop_name);
    }
}

static void
egg_property_cell_renderer_spin_edited_cb (GtkCellRendererText  *renderer,
                                           gchar                *path,
                                           gchar                *new_text,
                                           gpointer              user_data)
{
    EggPropertyCellRendererPrivate *priv;
    gchar *prop_name;

    priv = (EggPropertyCellRendererPrivate *) user_data;
    prop_name = get_prop_name_from_tree_model (GTK_TREE_MODEL (priv->list_store), path);

    if (prop_name != NULL) {
        GParamSpec *pspec;
        GValue from = { 0 };
        GValue to = { 0 };

        pspec = get_pspec_from_object (priv->object, prop_name);

        g_value_init (&from, G_TYPE_DOUBLE);
        g_value_init (&to, pspec->value_type);
        g_value_set_double (&from, strtod (new_text, NULL));

        if (g_value_transform (&from, &to))
            g_object_set_property (priv->object, prop_name, &to);
        else
            g_warning ("Could not transform %s to %s\n",
                    g_value_get_string (&from), g_type_name (pspec->value_type));

        g_free (prop_name);
    }
}

static void
egg_property_cell_renderer_changed_cb (GtkCellRendererCombo *combo,
                                       gchar                *path,
                                       GtkTreeIter          *new_iter,
                                       gpointer              user_data)
{
    EggPropertyCellRendererPrivate *priv;
    gchar *prop_name;

    priv = (EggPropertyCellRendererPrivate *) user_data;
    prop_name = get_prop_name_from_tree_model (GTK_TREE_MODEL (priv->list_store), path);

    if (prop_name != NULL) {
        GtkTreeModel *combo_model;
        gchar *value_name;
        gint value;

        combo_model = g_hash_table_lookup (priv->combo_models, prop_name);

        gtk_tree_model_get (combo_model, new_iter,
                COMBO_COLUMN_VALUE_NAME, &value_name,
                COMBO_COLUMN_VALUE, &value,
                -1);

        g_object_set (priv->object, prop_name, value, NULL);
        g_free (value_name);
        g_free (prop_name);
    }
}

static void
egg_property_cell_renderer_get_size (GtkCellRenderer    *cell,
                                     GtkWidget          *widget,
                                     GdkRectangle       *cell_area,
                                     gint               *x_offset,
                                     gint               *y_offset,
                                     gint               *width,
                                     gint               *height)
{

    EggPropertyCellRendererPrivate *priv = EGG_PROPERTY_CELL_RENDERER_GET_PRIVATE (cell);
    gtk_cell_renderer_get_size (priv->renderer, widget, cell_area, x_offset, y_offset, width, height);
}

static void
egg_property_cell_renderer_render (GtkCellRenderer      *cell,
                                   GdkDrawable          *window,
                                   GtkWidget            *widget,
                                   GdkRectangle         *background_area,
                                   GdkRectangle         *cell_area,
                                   GdkRectangle         *expose_area,
                                   GtkCellRendererState  flags)
{
    EggPropertyCellRendererPrivate *priv = EGG_PROPERTY_CELL_RENDERER_GET_PRIVATE (cell);
    gtk_cell_renderer_render (priv->renderer, window, widget, background_area, cell_area, expose_area, flags);
}

static gboolean
egg_property_cell_renderer_activate (GtkCellRenderer        *cell,
                                     GdkEvent               *event,
                                     GtkWidget              *widget,
                                     const gchar            *path,
                                     GdkRectangle           *background_area,
                                     GdkRectangle           *cell_area,
                                     GtkCellRendererState    flags)
{
    EggPropertyCellRendererPrivate *priv = EGG_PROPERTY_CELL_RENDERER_GET_PRIVATE (cell);
    return gtk_cell_renderer_activate (priv->renderer, event, widget, path, background_area, cell_area, flags);
}

static GtkCellEditable *
egg_property_cell_renderer_start_editing (GtkCellRenderer        *cell,
                                          GdkEvent               *event,
                                          GtkWidget              *widget,
                                          const gchar            *path,
                                          GdkRectangle           *background_area,
                                          GdkRectangle           *cell_area,
                                          GtkCellRendererState    flags)
{
    EggPropertyCellRendererPrivate *priv = EGG_PROPERTY_CELL_RENDERER_GET_PRIVATE (cell);
    return gtk_cell_renderer_start_editing (priv->renderer, event, widget, path, background_area, cell_area, flags);
}

static void
egg_property_cell_renderer_dispose (GObject *object)
{
    EggPropertyCellRenderer *renderer = EGG_PROPERTY_CELL_RENDERER (object);
    g_hash_table_destroy (renderer->priv->combo_models);
}

static void
egg_property_cell_renderer_set_property (GObject        *object,
                                         guint           property_id,
                                         const GValue   *value,
                                         GParamSpec     *pspec)
{
    g_return_if_fail (EGG_IS_PROPERTY_CELL_RENDERER (object));
    EggPropertyCellRenderer *renderer = EGG_PROPERTY_CELL_RENDERER (object);

    switch (property_id) {
        case PROP_PROP_NAME:
            egg_property_cell_renderer_set_renderer (renderer, g_value_get_string (value));
            break;

        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
            return;
    }
}

static void
egg_property_cell_renderer_get_property (GObject    *object,
                                         guint       property_id,
                                         GValue     *value,
                                         GParamSpec *pspec)
{
    g_return_if_fail (EGG_IS_PROPERTY_CELL_RENDERER (object));

    switch (property_id) {
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
            return;
    }
}

static void
egg_property_cell_renderer_class_init (EggPropertyCellRendererClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
    GtkCellRendererClass *cellrenderer_class = GTK_CELL_RENDERER_CLASS (klass);

    gobject_class->set_property = egg_property_cell_renderer_set_property;
    gobject_class->get_property = egg_property_cell_renderer_get_property;
    gobject_class->dispose = egg_property_cell_renderer_dispose;

    cellrenderer_class->render = egg_property_cell_renderer_render;
    cellrenderer_class->get_size = egg_property_cell_renderer_get_size;
    cellrenderer_class->activate = egg_property_cell_renderer_activate;
    cellrenderer_class->start_editing = egg_property_cell_renderer_start_editing;

    egg_property_cell_renderer_properties[PROP_PROP_NAME] =
            g_param_spec_string("prop-name",
                                "Property name", "Property name", "",
                                G_PARAM_READWRITE);

    g_object_class_install_property(gobject_class, PROP_PROP_NAME, egg_property_cell_renderer_properties[PROP_PROP_NAME]);

    g_type_class_add_private (klass, sizeof (EggPropertyCellRendererPrivate));
}

static void
egg_property_cell_renderer_init (EggPropertyCellRenderer *renderer)
{
    EggPropertyCellRendererPrivate *priv;

    renderer->priv = priv = EGG_PROPERTY_CELL_RENDERER_GET_PRIVATE (renderer);

    priv->text_renderer = gtk_cell_renderer_text_new ();
    priv->spin_renderer = gtk_cell_renderer_spin_new ();
    priv->toggle_renderer = gtk_cell_renderer_toggle_new ();
    priv->combo_renderer = gtk_cell_renderer_combo_new ();
    priv->renderer = priv->text_renderer;
    priv->combo_models = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);

    g_object_set (priv->text_renderer,
            "editable", TRUE,
            NULL);

    g_object_set (priv->spin_renderer,
            "editable", TRUE,
            NULL);

    g_object_set (priv->toggle_renderer,
            "xalign", 0.0f,
            "activatable", TRUE,
            "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE,
            NULL);

    g_object_set (priv->combo_renderer,
            "has-entry", FALSE,
            NULL);

    g_signal_connect (priv->spin_renderer, "edited",
            G_CALLBACK (egg_property_cell_renderer_spin_edited_cb), priv);

    g_signal_connect (priv->text_renderer, "edited",
            G_CALLBACK (egg_property_cell_renderer_text_edited_cb), NULL);

    g_signal_connect (priv->toggle_renderer, "toggled",
            G_CALLBACK (egg_property_cell_renderer_toggle_cb), priv);

    g_signal_connect (priv->combo_renderer, "changed",
            G_CALLBACK (egg_property_cell_renderer_changed_cb), priv);
}