/* 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 <math.h>
#include "egg-histogram-view.h"

G_DEFINE_TYPE (EggHistogramView, egg_histogram_view, GTK_TYPE_DRAWING_AREA)

#define EGG_HISTOGRAM_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), EGG_TYPE_HISTOGRAM_VIEW, EggHistogramViewPrivate))

#define MIN_WIDTH   128
#define MIN_HEIGHT  128
#define BORDER      2

struct _EggHistogramViewPrivate
{
    GdkCursorType cursor_type;
    gboolean      grabbing;

    /* This could be moved into a real histogram class */
    guint n_bins;
    gint *bins;
    /* gdouble *grabbed; */
    enum {
        GRAB_MIN,
        GRAB_MAX
    } grabbed;

    gdouble max;
    gdouble min_value;    /* lowest value of the first bin */
    gdouble max_value;    /* highest value of the last bin */

    gint     n_elements;
    gint     n_bits;
};

enum
{
    PROP_0,
    PROP_MINIMUM_BIN_VALUE,
    PROP_MAXIMUM_BIN_VALUE,
    N_PROPERTIES
};

enum
{
    CHANGED,
    LAST_SIGNAL
};

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

static guint egg_histogram_view_signals[LAST_SIGNAL] = { 0 };


GtkWidget *
egg_histogram_view_new (guint n_elements,
                        guint n_bits,
                        guint n_bins)
{
    EggHistogramView *view;
    EggHistogramViewPrivate *priv;

    view = EGG_HISTOGRAM_VIEW (g_object_new (EGG_TYPE_HISTOGRAM_VIEW, NULL));
    priv = view->priv;

    priv->bins = g_malloc0 (n_bins * sizeof (guint));
    priv->n_bins = n_bins;
    priv->n_bits = n_bits;
    priv->n_elements = n_elements;

    priv->min_value = 0.0;
    priv->max_value = priv->max = (gint) pow (2, n_bits) - 1;

    return GTK_WIDGET (view);
}

void
egg_histogram_view_update (EggHistogramView *view,
                           gpointer buffer)
{
    EggHistogramViewPrivate *priv;
    guint n_bins;

    g_return_if_fail (EGG_IS_HISTOGRAM_VIEW (view));
    priv = view->priv;
    n_bins = priv->n_bins - 1;

    for (guint i = 0; i < priv->n_bins; i++)
        priv->bins[i] = 0;

    if (priv->n_bits == 8) {
        guint8 *data = (guint8 *) buffer;

        for (guint i = 0; i < priv->n_elements; i++) {
            guint8 v = data[i];

            guint index = (guint) round (((gdouble) v) / priv->max * n_bins);
            priv->bins[index]++;
        }
    }
    else {
        guint16 *data = (guint16 *) buffer;

        for (guint i = 0; i < priv->n_elements; i++) {
            guint16 v = data[i];

            guint index = (guint) floor (((gdouble ) v) / priv->max * n_bins);
            priv->bins[index]++;
        }
    }
}

void
egg_histogram_get_range (EggHistogramView *view,
                         gdouble *min,
                         gdouble *max)
{
    EggHistogramViewPrivate *priv;

    g_return_if_fail (EGG_IS_HISTOGRAM_VIEW (view));
    priv = view->priv;
    *min = priv->min_value;
    *max = priv->max_value;
}

static void
set_cursor_type (EggHistogramView *view, GdkCursorType cursor_type)
{
    if (cursor_type != view->priv->cursor_type) {
        GdkCursor *cursor = gdk_cursor_new (cursor_type);

        gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET(view)), cursor);
        gdk_cursor_unref (cursor);
        view->priv->cursor_type = cursor_type;
    }
}

static void
egg_histogram_view_size_request (GtkWidget *widget,
                                 GtkRequisition *requisition)
{
    requisition->width = MIN_WIDTH;
    requisition->height = MIN_HEIGHT;
}

static void
draw_bins (EggHistogramViewPrivate *priv,
           cairo_t *cr,
           gint width,
           gint height)
{
    gdouble skip = ((gdouble) width) / priv->n_bins;
    gdouble x = BORDER;
    gdouble ys = height + BORDER - 1;
    gint max_value = 0;

    for (guint i = 0; i < priv->n_bins; i++) {
        if (priv->bins[i] > max_value)
            max_value = priv->bins[i];
    }

    if (max_value == 0)
        return;

    for (guint i = 0; i < priv->n_bins && x < (width - BORDER); i++, x += skip) {
        if (priv->bins[i] == 0)
            continue;

        gint y = (gint) ((height - 2) * priv->bins[i]) / max_value;
        cairo_move_to (cr, round (x), ys);
        cairo_line_to (cr, round (x), ys - y);
        cairo_stroke (cr);
    }
}

static gboolean
egg_histogram_view_expose (GtkWidget *widget,
                           GdkEventExpose *event)
{
    EggHistogramViewPrivate *priv;
    GtkAllocation allocation;
    GtkStyle *style;
    cairo_t *cr;
    gint width, height;
    gdouble left, right;

    priv = EGG_HISTOGRAM_VIEW_GET_PRIVATE (widget);
    cr = gdk_cairo_create (gtk_widget_get_window (widget));

    gdk_cairo_region (cr, event->region);
    cairo_clip (cr);

    style = gtk_widget_get_style (widget);
    gdk_cairo_set_source_color (cr, &style->base[GTK_STATE_NORMAL]);
    cairo_paint (cr);

    /* Draw the background */
    gdk_cairo_set_source_color (cr, &style->base[GTK_STATE_NORMAL]);
    cairo_paint (cr);

    gtk_widget_get_allocation (widget, &allocation);
    width  = allocation.width - 2 * BORDER;
    height = allocation.height - 2 * BORDER;

    gdk_cairo_set_source_color (cr, &style->dark[GTK_STATE_NORMAL]);
    cairo_set_line_width (cr, 1.0);
    cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
    cairo_translate (cr, 0.5, 0.5);
    cairo_rectangle (cr, BORDER, BORDER, width - 1, height - 1);
    cairo_stroke (cr);

    if (priv->bins == NULL)
        goto cleanup;

    /* Draw border areas */
    gdk_cairo_set_source_color (cr, &style->dark[GTK_STATE_NORMAL]);

    left = ((gint) (priv->min_value / priv->max * width)) + 0.5;
    cairo_rectangle (cr, BORDER, BORDER, left, height - 1);
    cairo_fill (cr);

    right = ((gint) (priv->max_value / priv->max * width)) + 0.5;

    cairo_rectangle (cr, right, BORDER, width - right, height - 1);
    cairo_fill (cr);

    /* Draw spikes */
    gdk_cairo_set_source_color (cr, &style->black);
    draw_bins (priv, cr, width, height);

cleanup:
    cairo_destroy (cr);
    return FALSE;
}

static void
egg_histogram_view_finalize (GObject *object)
{
    EggHistogramViewPrivate *priv;

    priv = EGG_HISTOGRAM_VIEW_GET_PRIVATE (object);

    if (priv->bins)
        g_free (priv->bins);

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

static void
egg_histogram_view_dispose (GObject *object)
{
    G_OBJECT_CLASS (egg_histogram_view_parent_class)->dispose (object);
}

static void
egg_histogram_view_set_property (GObject *object,
                                 guint property_id,
                                 const GValue *value,
                                 GParamSpec *pspec)
{
    EggHistogramViewPrivate *priv;

    g_return_if_fail (EGG_IS_HISTOGRAM_VIEW (object));
    priv = EGG_HISTOGRAM_VIEW_GET_PRIVATE (object);

    switch (property_id) {
        case PROP_MINIMUM_BIN_VALUE:
            {
                gdouble v = g_value_get_double (value);

                if (v > priv->max_value) {
                    g_warning ("Minimum value `%f' larger than maximum value `%f'",
                               v, priv->max_value);
                }
                else {
                    priv->min_value = v;
                    gtk_widget_queue_draw (GTK_WIDGET (object));
                }
            }
            break;

        case PROP_MAXIMUM_BIN_VALUE:
            {
                gdouble v = g_value_get_double (value);

                if (v < priv->min_value) {
                    g_warning ("Maximum value `%f' larger than minimum value `%f'",
                               v, priv->min_value);
                }
                else {
                    priv->max_value = v;
                    gtk_widget_queue_draw (GTK_WIDGET (object));
                }
            }
            break;

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

static void
egg_histogram_view_get_property (GObject *object,
                                 guint property_id,
                                 GValue *value,
                                 GParamSpec *pspec)
{
    EggHistogramViewPrivate *priv;

    g_return_if_fail (EGG_IS_HISTOGRAM_VIEW (object));
    priv = EGG_HISTOGRAM_VIEW_GET_PRIVATE (object);

    switch (property_id) {
        case PROP_MINIMUM_BIN_VALUE:
            g_value_set_double (value, priv->min_value);
            break;

        case PROP_MAXIMUM_BIN_VALUE:
            g_value_set_double (value, priv->max_value);
            break;

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

static gdouble
screen_to_histogram_coordinate (EggHistogramViewPrivate *priv,
                                GtkAllocation *allocation,
                                gint x)
{
    return (((gdouble) x) / (allocation->width - BORDER)) * priv->max;
}

static gboolean
is_on_border (EggHistogramViewPrivate *priv,
              GtkAllocation *allocation,
              gint x)
{
    gdouble coord;

    coord = screen_to_histogram_coordinate (priv, allocation , x);
    return (ABS (coord - priv->min_value) < 6) || (ABS (coord - priv->max_value) < 6);
}

static void
set_grab_value (EggHistogramView *view,
                gdouble value)
{
    if (view->priv->grabbed == GRAB_MIN) {
        view->priv->min_value = value;
        g_object_notify_by_pspec (G_OBJECT (view),
                                  properties[PROP_MINIMUM_BIN_VALUE]);
    }
    else {
        view->priv->max_value = value;
        g_object_notify_by_pspec (G_OBJECT (view),
                                  properties[PROP_MAXIMUM_BIN_VALUE]);
    }
}

static gboolean
egg_histogram_view_motion_notify (GtkWidget *widget,
                                  GdkEventMotion *event)
{
    EggHistogramView *view;
    EggHistogramViewPrivate *priv;
    GtkAllocation allocation;

    view = EGG_HISTOGRAM_VIEW (widget);
    priv = view->priv;
    gtk_widget_get_allocation (widget, &allocation);

    if (priv->grabbing) {
        gdouble coord;

        coord = screen_to_histogram_coordinate (priv, &allocation, event->x);

        if (ABS (priv->max_value - priv->min_value) > 8.0 && coord > 0 && coord < priv->max) {
            set_grab_value (view, coord);
            gtk_widget_queue_draw (widget);
        }
    }
    else {
        if (is_on_border (priv, &allocation, event->x))
            set_cursor_type (view, GDK_FLEUR);
        else
            set_cursor_type (view, GDK_ARROW);
    }

    return TRUE;
}

static gboolean
egg_histogram_view_button_release (GtkWidget *widget,
                                   GdkEventButton *event)
{
    EggHistogramView *view;

    view = EGG_HISTOGRAM_VIEW (widget);
    set_cursor_type (view, GDK_ARROW);
    view->priv->grabbing = FALSE;

    g_signal_emit (widget, egg_histogram_view_signals[CHANGED], 0);

    return TRUE;
}

static gboolean
egg_histogram_view_button_press (GtkWidget *widget,
                                 GdkEventButton *event)
{
    EggHistogramView *view;
    EggHistogramViewPrivate *priv;
    GtkAllocation allocation;

    view = EGG_HISTOGRAM_VIEW (widget);
    priv = view->priv;
    gtk_widget_get_allocation (widget, &allocation);

    if (is_on_border (priv, &allocation, event->x)) {
        gdouble coord;

        priv->grabbing = TRUE;
        coord = screen_to_histogram_coordinate (priv, &allocation, event->x);

        if (ABS (coord - priv->min_value < 6))
            priv->grabbed = GRAB_MIN;
        else if (ABS (coord - priv->max_value < 6))
            priv->grabbed = GRAB_MAX;

        set_grab_value (view, coord);
        set_cursor_type (view, GDK_FLEUR);
    }

    return TRUE;
}

static void
egg_histogram_view_class_init (EggHistogramViewClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

    object_class->set_property = egg_histogram_view_set_property;
    object_class->get_property = egg_histogram_view_get_property;
    object_class->dispose      = egg_histogram_view_dispose;
    object_class->finalize     = egg_histogram_view_finalize;

    widget_class->size_request = egg_histogram_view_size_request;
    widget_class->expose_event = egg_histogram_view_expose;
    widget_class->button_press_event   = egg_histogram_view_button_press;
    widget_class->button_release_event = egg_histogram_view_button_release;
    widget_class->motion_notify_event  = egg_histogram_view_motion_notify;

    properties[PROP_MINIMUM_BIN_VALUE] =
        g_param_spec_double ("minimum-bin-value",
                             "Smallest possible bin value",
                             "Smallest possible bin value, everything below is discarded.",
                             0.0, G_MAXDOUBLE, 0.0,
                             G_PARAM_READWRITE);

    properties[PROP_MAXIMUM_BIN_VALUE] =
        g_param_spec_double ("maximum-bin-value",
                             "Largest possible bin value",
                             "Largest possible bin value, everything above is discarded.",
                             0.0, G_MAXDOUBLE, 256.0,
                             G_PARAM_READWRITE);

    g_object_class_install_property (object_class, PROP_MINIMUM_BIN_VALUE, properties[PROP_MINIMUM_BIN_VALUE]);
    g_object_class_install_property (object_class, PROP_MAXIMUM_BIN_VALUE, properties[PROP_MAXIMUM_BIN_VALUE]);

    egg_histogram_view_signals[CHANGED] =
        g_signal_new ("changed",
                      G_OBJECT_CLASS_TYPE (klass),
                      G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                      G_STRUCT_OFFSET (EggHistogramViewClass, changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);

    g_type_class_add_private (klass, sizeof (EggHistogramViewPrivate));
}

static void
egg_histogram_view_init (EggHistogramView *view)
{
    EggHistogramViewPrivate *priv;

    view->priv = priv = EGG_HISTOGRAM_VIEW_GET_PRIVATE (view);

    priv->bins = NULL;
    priv->n_bins = 0;
    priv->n_elements = 0;
    priv->min_value = 0;
    priv->max_value = 256;

    priv->cursor_type = GDK_ARROW;
    priv->grabbing = FALSE;

    gtk_widget_add_events (GTK_WIDGET (view),
                           GDK_BUTTON_PRESS_MASK   |
                           GDK_BUTTON_RELEASE_MASK |
                           GDK_BUTTON1_MOTION_MASK |
                           GDK_POINTER_MOTION_MASK);
}