/*
     This file is part of GNUnet
     Copyright (C) 2011 GNUnet e.V.

     GNUnet 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 3, or (at your
     option) any later version.

     GNUnet 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 GNUnet; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.

*/

/**
 * @file src/statistics/gtk_statistics.c
 * @brief widget to display statistics
 * @author Christian Grothoff
 */
#include "gnunet_gtk.h"
#include "gtk_statistics.h"

#define MAX_HISTORY 1280

/**
 * Number of ticks on the x-axis.
 */
#define XTICKS 4

/**
 * Number of ticks on the y-axis.
 */
#define YTICKS 5

/**
 * Additional distance between text and lines / borders in pixels.
 */
#define BORDER 10.0


/**
 * Information about a value we received.
 */
struct HistoricValue
{
  /**
   * Time when the value was recorded.
   */
  uint64_t x;

  /**
   * A value we recorded.
   */
  uint64_t y;
};

/**
 * Historic information we recorded for a value.
 */
struct ValueHistory
{

  /**
   * ID for value updates.
   */
  char *id;

  /**
   * Label for the subsystem.
   */
  char *label;

  /**
   * Color values (rgb).
   */
  double red, green, blue;

  /**
   * Recent values for this number.
   */
  struct HistoricValue history[MAX_HISTORY];

  /**
   * Last offset we wrote to in history.
   */
  unsigned int last_history_offset;

  /**
   * Number of valid entries in the history.
   */
  unsigned int history_size;
};


struct _GtkStatisticsPrivate
{

  /**
   * Values we plot.
   */
  struct ValueHistory **values;

  /**
   * Size of the 'values' array.
   */
  unsigned int num_values;
};


static gboolean
gtk_statistics_draw (GtkWidget *widget, cairo_t *cr);

static void
gtk_statistics_finalize (GObject *object);


G_DEFINE_TYPE_EXTENDED (GtkStatistics,
                        gtk_statistics,
                        GTK_TYPE_WIDGET,
                        0,
                        G_ADD_PRIVATE (GtkStatistics))


#if GTK_MAJOR_VERSION < 3
static gboolean
statistics_expose (GtkWidget *statistics, GdkEventExpose *event)
{
  GtkAllocation alloc;
  cairo_t *cr;
  gint x;
  gint y;

  gtk_widget_translate_coordinates (statistics,
                                    GTK_WIDGET (
                                      gtk_widget_get_toplevel (statistics)),
                                    0,
                                    0,
                                    &x,
                                    &y);
  gtk_widget_get_allocation (statistics, &alloc);
  cr = gdk_cairo_create (statistics->window);
  cairo_translate (cr, x, y);
  cairo_rectangle (cr, 0, 0, alloc.width, alloc.height);
  cairo_clip (cr);

  gtk_statistics_draw (statistics, cr);
  cairo_destroy (cr);
  return FALSE;
}
#endif


static void
gtk_statistics_class_init (GtkStatisticsClass *class)
{
  GObjectClass *gobject_class;
  GtkWidgetClass *widget_class;

  gobject_class = (GObjectClass *) class;
  widget_class = (GtkWidgetClass *) class;

  gobject_class->finalize = gtk_statistics_finalize;
#if GTK_MAJOR_VERSION >= 3
  widget_class->draw = gtk_statistics_draw;
#endif
}


static void
gtk_statistics_init (GtkStatistics *statistics)
{
  GtkStatisticsPrivate *priv;

  statistics->priv = gtk_statistics_get_instance_private (statistics);
  priv = statistics->priv;
#if GTK_MAJOR_VERSION < 3
  g_signal_connect (statistics,
                    "expose-event",
                    G_CALLBACK (statistics_expose),
                    statistics);
#endif
  gtk_widget_set_has_window (GTK_WIDGET (statistics), FALSE);
  priv->values = NULL;
  priv->num_values = 0;
}


/**
 * gtk_statistics_new:
 *
 * Creates a new #GtkStatistics widget.
 *
 * Returns: the new #GtkStatistics widget.
 */
GtkWidget *
gtk_statistics_new ()
{
  GtkStatisticsPrivate *priv;
  GtkStatistics *statistics;

  statistics = g_object_new (GTK_TYPE_STATISTICS, NULL);

  priv = statistics->priv;
  priv->values = NULL;
  priv->num_values = 0;

  return GTK_WIDGET (statistics);
}


/**
 * Add another data series to plot by the statistics widget.
 *
 * @param statistics widget to modify
 * @param id identifier for the series
 * @param label label to use
 * @param color_name name of the color to use
 */
void
gtk_statistics_add_line (GtkStatistics *statistics,
                         const char *id,
                         const char *label,
                         const char *color_name)
{
  GtkStatisticsPrivate *priv;
  struct ValueHistory *vh;
  GdkRGBA c;

  g_return_if_fail (GTK_IS_STATISTICS (statistics));
  g_return_if_fail (gdk_rgba_parse (&c, color_name));
  priv = statistics->priv;
  priv->values =
    g_realloc (priv->values,
               sizeof (struct ValueHistory *) * (1 + priv->num_values));
  priv->values[priv->num_values++] = vh =
    g_malloc (sizeof (struct ValueHistory));
  vh->id = strdup (id);
  vh->label = strdup (label);
  vh->red = c.red;
  vh->green = c.green;
  vh->blue = c.blue;
  vh->last_history_offset = 0;
  vh->history_size = 0;
}


/**
 * Add another value to a data series.
 *
 * @param statistics widget to update
 * @param id identifier of the series
 * @param x new x-value
 * @param y new y-value
 */
void
gtk_statistics_update_value (GtkStatistics *statistics,
                             const char *id,
                             uint64_t x,
                             uint64_t y)
{
  GtkStatisticsPrivate *priv;
  GtkWidget *widget;
  struct ValueHistory *vh;
  unsigned int i;

  g_return_if_fail (GTK_IS_STATISTICS (statistics));
  priv = statistics->priv;
  for (i = 0; i < priv->num_values; i++)
  {
    vh = priv->values[i];
    if (0 != strcmp (id, vh->id))
      continue;

    if (++vh->last_history_offset == MAX_HISTORY)
      vh->last_history_offset = 0;
    if (vh->history_size < MAX_HISTORY)
      vh->history_size++;
    vh->history[vh->last_history_offset].x = x;
    vh->history[vh->last_history_offset].y = y;
    widget = GTK_WIDGET (statistics);
    if (gtk_widget_is_drawable (widget))
      gtk_widget_queue_draw (widget);
  }
}


/**
 * Convert a number to a nice label for the axis.
 *
 * @param num number to convert
 * @param label where to store the string (must be big enough!)
 */
static void
num_to_label (unsigned long long num, char *label)
{
  if (num > 1000LL * 1000 * 1000 * 1000 * 3)
    sprintf (label, "%llu t", num / 1000LL / 1000LL / 1000LL / 1000LL);
  else if (num > 1000LL * 1000 * 1000 * 3)
    sprintf (label, "%llu g", num / 1000LL / 1000LL / 1000LL);
  else if (num > 1000LL * 1000 * 3)
    sprintf (label, "%llu m", num / 1000LL / 1000LL);
  else if (num > 1000LL * 3)
    sprintf (label, "%llu k", num / 1000LL);
  else
    sprintf (label, "%llu", num);
}


/**
 * Draw the statistics widget.
 *
 * @param widget widget to draw
 * @param cr drawing context
 */
static gboolean
gtk_statistics_draw (GtkWidget *widget, cairo_t *cr)
{
  GtkStatistics *statistics = GTK_STATISTICS (widget);
  GtkStatisticsPrivate *priv = statistics->priv;
  int width;
  int height;
  uint64_t xmin;
  uint64_t xmax;
  uint64_t ymax;
  unsigned int i;
  unsigned int j;
  struct ValueHistory *vh;
  struct HistoricValue *hv;
  char label[64];
  cairo_text_extents_t te;
  cairo_text_extents_t tex_max;
  cairo_text_extents_t tey_max;
  double rx;
  double ry;
  unsigned int h;
  GtkAllocation alloc;

  /* collect basic data */
  xmin = UINT64_MAX;
  xmax = 0;
  ymax = 0;
  for (i = 0; i < priv->num_values; i++)
  {
    vh = priv->values[i];
    for (j = 0; j < vh->history_size; j++)
    {
      hv =
        &vh->history[(vh->last_history_offset - j + MAX_HISTORY) % MAX_HISTORY];
      xmin = GNUNET_MIN (hv->x, xmin);
      xmax = GNUNET_MAX (hv->x, xmax);
      ymax = GNUNET_MAX (hv->y, ymax);
    }
  }
  /* round to 10x for nicer legends */
  while (0 != (ymax % 10))
    ymax++;
  while (0 != (xmax % 10))
    xmax++;
  while (0 != (xmin % 10))
    xmin--;

  gtk_widget_get_allocation (widget, &alloc);
  width = alloc.width;
  height = alloc.height;

  /* fill with black background */
  cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
  cairo_paint (cr);

  if ((0 == priv->num_values) || (ymax == 0))
    return FALSE; /* done */
  /* select font */
  cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
  cairo_select_font_face (cr,
                          "Georgia",
                          CAIRO_FONT_SLANT_NORMAL,
                          CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size (cr, GNUNET_MIN (20, height / (2 * XTICKS)));

  /* find out needed space for axis labels */
  tex_max.width = 0;
  tex_max.height = 0;
  for (i = 0; i < XTICKS; i++)
  {
    num_to_label ((unsigned long long) (xmin + (xmax - xmin) * ((uint64_t) i) /
                                                 (XTICKS - 1)),
                  label);
    cairo_text_extents (cr, label, &te);
    tex_max.width = GNUNET_MAX (te.width, tex_max.width);
    tex_max.height = GNUNET_MAX (te.height, tex_max.height);
  }
  tey_max.width = 0;
  tey_max.height = 0;
  for (i = 0; i < YTICKS; i++)
  {
    num_to_label ((unsigned long long) ymax * ((uint64_t) i) / (YTICKS - 1),
                  label);
    cairo_text_extents (cr, label, &te);
    tey_max.width = GNUNET_MAX (te.width, tey_max.width);
    tey_max.height = GNUNET_MAX (te.height, tey_max.height);
  }

  /* draw y-axis labels */
  for (i = 0; i < YTICKS; i++)
  {
    num_to_label ((unsigned long long) ymax * ((uint64_t) i) / (YTICKS - 1),
                  label);
    cairo_text_extents (cr, label, &te);
    cairo_move_to (cr,
                   BORDER + tey_max.width - te.width,
                   BORDER + tey_max.height +
                     (YTICKS - i - 1) *
                       (height - 2.0 * BORDER - tey_max.height -
                        tex_max.height) /
                       (double) (YTICKS - 1));
    cairo_show_text (cr, label);


    cairo_move_to (cr,
                   2.0 * BORDER + tey_max.width,
                   BORDER + tey_max.height / 2.0 +
                     (YTICKS - i - 1) *
                       (height - 2.0 * BORDER - tex_max.height -
                        tey_max.height) /
                       (double) (YTICKS - 1));
    cairo_line_to (cr,
                   2.0 * BORDER + tey_max.width + BORDER / 2.0,
                   BORDER + tey_max.height / 2.0 +
                     (YTICKS - i - 1) *
                       (height - 2.0 * BORDER - tex_max.height -
                        tey_max.height) /
                       (double) (YTICKS - 1));

    cairo_stroke (cr);
  }

  /* draw x-axis labels */
  for (i = 0; i < XTICKS; i++)
  {
    num_to_label ((unsigned long long) (xmin + (xmax - xmin) * ((uint64_t) i) /
                                                 (XTICKS - 1)),
                  label);
    cairo_text_extents (cr, label, &te);
    if (i != 0)
    {
      cairo_move_to (cr,
                     2.0 * BORDER + tey_max.width +
                       (width - tey_max.width - tex_max.width / 2.0 -
                        3.0 * BORDER) *
                         i / (XTICKS - 1.0) -
                       te.width / 2.0,
                     height - BORDER / 2.0 - tex_max.height / 2.0);
      cairo_show_text (cr, label);
    }
    cairo_move_to (cr,
                   2.0 * BORDER + tey_max.width +
                     (width - tey_max.width - tex_max.width / 2.0 -
                      3.0 * BORDER) *
                       i / (XTICKS - 1.0),
                   height - BORDER - tey_max.height / 2.0 - tex_max.height);
    cairo_line_to (cr,
                   2.0 * BORDER + tey_max.width +
                     (width - tey_max.width - tex_max.width / 2.0 -
                      3.0 * BORDER) *
                       i / (XTICKS - 1.0),
                   height - BORDER - tey_max.height / 2.0 - tex_max.height -
                     BORDER / 2.0);

    cairo_stroke (cr);
  }

  /* plot border */
  cairo_set_line_width (cr, 1.0);
  cairo_rectangle (cr,
                   tey_max.width + BORDER * 2.0,
                   BORDER + tey_max.height / 2.0,
                   width - BORDER * 3.0 - tey_max.width - tex_max.width / 2.0,
                   height - BORDER * 2.0 - tey_max.height - tex_max.height);
  cairo_stroke (cr);

  /* finally, plot lines */
  cairo_set_line_width (cr, 2.0);

  cairo_set_font_size (cr,
                       GNUNET_MIN (20,
                                   (height - 3.0 * BORDER - tex_max.height -
                                    tey_max.height / 2) /
                                     (priv->num_values + 1)));

  h = 2.0 * BORDER + tey_max.height / 2;

  for (i = 0; i < priv->num_values; i++)
  {
    vh = priv->values[i];
    cairo_set_source_rgb (cr, vh->red, vh->green, vh->blue);
    cairo_text_extents (cr, vh->label, &te);
    h += te.height / 2;
    cairo_move_to (cr, 3.0 * BORDER + tey_max.width, h);
    h += te.height / 2 + 1.0;
    cairo_show_text (cr, vh->label);
    if (xmax == xmin)
    {
      hv = &vh->history[vh->last_history_offset % MAX_HISTORY];
      ry = hv->y / (double) ymax;
      ry =
        BORDER + tex_max.height / 2.0 +
        (1.0 - ry) * (height - BORDER * 2.0 - tey_max.height - tex_max.height);
      /* if y-values are small, offset y-values a bit to allow overlapping curves to still show up */
      if (ymax < height / (priv->num_values * 4))
        ry += priv->num_values * 2 - (4 * i);
      cairo_move_to (cr, width - BORDER - tex_max.width / 2.0, ry);
      cairo_line_to (cr, rx = tey_max.width + BORDER * 2.0, ry);
      cairo_stroke (cr);
      continue;
    }

    for (j = 0; j < vh->history_size; j++)
    {
      hv =
        &vh->history[(vh->last_history_offset - j + MAX_HISTORY) % MAX_HISTORY];
      rx = (hv->x - xmin) / (double) (xmax - xmin);
      ry = hv->y / (double) ymax;

      rx = tey_max.width + BORDER * 2.0 +
           (rx * (width - BORDER * 3.0 - tey_max.width - tex_max.width / 2.0));
      ry =
        BORDER + tex_max.height / 2.0 +
        (1.0 - ry) * (height - BORDER * 2.0 - tey_max.height - tex_max.height);

      /* if y-values are small, offset y-values a bit to allow overlapping curves to still show up */
      if (ymax < height / (priv->num_values * 4))
        ry += priv->num_values * 2 - (4 * i);
      if (j == 0)
        cairo_move_to (cr, width - BORDER - tex_max.width / 2.0, ry);
      cairo_line_to (cr, rx, ry);
    }
    cairo_stroke (cr);
  }
  return FALSE;
}


/**
 * Free memory used by statistics object.
 *
 * @param object object to release
 */
static void
gtk_statistics_finalize (GObject *object)
{
  GtkStatistics *label = GTK_STATISTICS (object);
  GtkStatisticsPrivate *priv = label->priv;
  unsigned int i;

  for (i = 0; i < priv->num_values; i++)
  {
    g_free (priv->values[i]->label);
    g_free (priv->values[i]->id);
    g_free (priv->values[i]);
  }
  g_free (priv->values);
  G_OBJECT_CLASS (gtk_statistics_parent_class)->finalize (object);
}


/* end of gtk_statistics.c */
