/********************************************************************
 * gnc-html-graph-gog.c -- GNC/HTML Graphing support via GOG        *
 *                                                                  *
 * Copyright (C) 2005 Joshua Sled <jsled@asynchronous.org>          *
 *                                                                  *
 * 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, contact:                        *
 *                                                                  *
 * Free Software Foundation           Voice:  +1-617-542-5942       *
 * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
 * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
 ********************************************************************/

#include "config.h"

#include <gtk/gtk.h>
#include <gtkhtml/gtkhtml.h>
#include <gtkhtml/gtkhtml-embedded.h>
#include <string.h>

#include "gnc-ui-util.h"
#include "gnc-html-graph-gog.h"
#include "gnc-html.h"
#include "gnc-engine.h"
#include <goffice/goffice.h>
#include <goffice/graph/gog-graph.h>
#include <goffice/graph/gog-object.h>
#include <goffice/graph/gog-renderer-pixbuf.h>
#include <goffice/graph/gog-renderer-gnome-print.h>
#include <goffice/graph/gog-style.h>
#include <goffice/graph/gog-styled-object.h>
#include <goffice/graph/gog-plot.h>
#include <goffice/graph/gog-series.h>
#include <goffice/utils/go-color.h>
#include <goffice/graph/gog-data-set.h>
#include <goffice/graph/gog-renderer-svg.h>
#include <goffice/data/go-data-simple.h>
#include <goffice/app/go-plugin.h>
#include <goffice/app/go-plugin-loader-module.h>
#include <gsf/gsf.h>
#include <gsf/gsf-output-memory.h>

/**
 * TODO:
 * - scatter-plot marker selection
 * - series-color, piecharts (hard, not really supported by GOG)
 *   and scatters (or drop feature)
 * - title-string freeing (fixmes)
 * - general graph cleanup
 **/

#define LOG_MOD "gnc.gui.html.graph.gog"
static QofLogModule log_module = LOG_MOD;
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN LOG_MOD

static int handle_piechart(gnc_html * html, GtkHTMLEmbedded * eb, gpointer d);
static int handle_barchart(gnc_html * html, GtkHTMLEmbedded * eb, gpointer d);
static int handle_scatter(gnc_html * html, GtkHTMLEmbedded * eb, gpointer d);

static void draw_print_cb(GtkHTMLEmbedded *eb, GnomePrintContext *context, gpointer graph);

static gboolean create_basic_plot_elements(const char *plot_type, GogObject **out_graph, GogObject **out_chart, GogPlot **out_plot);

static double * read_doubles(const char * string, int nvalues);

static void set_chart_titles_from_hash(GogObject *chart, GtkHTMLEmbedded * eb);
static void set_chart_titles(GogObject *chart, const char *title, const char* sub_title);
static void set_chart_axis_labels_from_hash(GogObject *chart, GtkHTMLEmbedded * eb);
static void set_chart_axis_labels(GogObject *chart, const char *x_axis_label, const char* y_axis_label);
static void gtkhtml_pre_3_10_1_bug_workaround(GtkHTMLEmbedded *eb);

void
gnc_html_graph_gog_init(void)
{

  g_debug( "init gog graphing" );
  
  libgoffice_init();
  
  /* Initialize plugins manager */
  go_plugins_init (NULL, NULL, NULL, NULL, TRUE, GO_PLUGIN_LOADER_MODULE_TYPE);

  gnc_html_register_object_handler( "gnc-guppi-pie", handle_piechart );
  gnc_html_register_object_handler( "gnc-guppi-bar", handle_barchart );
  gnc_html_register_object_handler( "gnc-guppi-scatter", handle_scatter );
}

static double * 
read_doubles(const char * string, int nvalues)
{
  int    n;
  gchar *next;
  double * retval = g_new0(double, nvalues);

  // guile is going to (puts ...) the elements of the double array
  // together. In non-POSIX locales, that will be in a format that
  // the locale-specific sscanf will not be able to parse.
  gnc_push_locale("C");
  {
    for (n=0; n<nvalues; n++) {
      retval[n] = strtod(string, &next);
      string = next;
     }
  }
  gnc_pop_locale();

  return retval;
}

static char ** 
read_strings(const char * string, int nvalues)
{
  int n;
  int choffset=0;
  int accum = 0;
  char ** retval = g_new0(char *, nvalues);
  char thischar;
  const char * inptr = string;
  int escaped = FALSE;

  for (n=0; n < nvalues; n++) {
    retval[n] = g_new0(char, strlen(string+accum)+1);
    retval[n][0] = 0;
    choffset = 0;
    while ((thischar = *inptr) != 0) {
      if (thischar == '\\') {
        escaped = TRUE;
        inptr++;
      }
      else if ((thischar != ' ') || escaped) {
        retval[n][choffset] = thischar;
        retval[n][choffset+1] = 0;    
        choffset++;
        escaped = FALSE;
        inptr++;
      }
      else {
        /* an unescaped space */
        escaped = FALSE;
        inptr++;
        break;
      }
    }
    accum += choffset;
    /* printf("retval[%d] = '%s'\n", n, retval[n]); */
  }
  
  return retval;  
}

static void
add_pixbuf_graph_widget( GtkHTMLEmbedded *eb, GogObject *graph )
{
  GtkWidget *widget;
  GogRendererPixbuf *pixbuf_renderer;
  GdkPixbuf *buf;
  gboolean update_status;

  // Note that this shouldn't be necessary as per discussion with Jody...
  // ... but it is because we don't embed in a control which passes the
  // update requests back to the graph widget, a-la the foo-canvas that
  // gnumeric uses.  We probably _should_ do something like that, though.
  gog_object_update (GOG_OBJECT (graph));

#if 0
  // example SVG use.  Also, nice for debugging.
  {
    GsfOutput *mem;
    gboolean output;

    mem = gsf_output_memory_new();
    output = gog_graph_export_to_svg( graph, mem, eb->width, eb->height, 1. );
    printf( "svg: [%s]\n", (guchar*)gsf_output_memory_get_bytes( GSF_OUTPUT_MEMORY(mem) ) );
  }
#endif // 0

  pixbuf_renderer = GOG_RENDERER_PIXBUF (g_object_new (GOG_RENDERER_PIXBUF_TYPE,
						       "model", graph,
						       NULL));
  update_status = gog_renderer_pixbuf_update (pixbuf_renderer,
					      eb->width, eb->height, 1.0);
  buf = gog_renderer_pixbuf_get (pixbuf_renderer);
  widget = gtk_image_new_from_pixbuf (buf);
  gtk_widget_set_size_request (widget, eb->width, eb->height);
  gtk_widget_show_all (widget);
  gtk_container_add (GTK_CONTAINER (eb), widget);

  // blindly copied from gnc-html-guppi.c..
  gtk_widget_set_size_request (GTK_WIDGET (eb), eb->width, eb->height);

  g_object_set_data_full (G_OBJECT (eb), "graph", graph, g_object_unref);
  g_signal_connect (G_OBJECT (eb), "draw_print",
		    G_CALLBACK (draw_print_cb), NULL);
}

static gboolean
create_basic_plot_elements(const char *plot_type_name,
                           GogObject **out_graph,
                           GogObject **out_chart,
                           GogPlot **out_plot)
{
  *out_graph = g_object_new(GOG_GRAPH_TYPE, NULL);
  *out_chart = gog_object_add_by_name(*out_graph, "Chart", NULL);
  *out_plot = gog_plot_new_by_name(plot_type_name);
  if (!*out_plot)
  {
    // FIXME - log betterer; should probably use GError?
    g_warning("gog: unable to load %s plugin", plot_type_name);
    return FALSE;
  }
  gog_object_add_by_name(*out_chart, "Plot", GOG_OBJECT(*out_plot) );
  return TRUE;
}

static void
set_chart_titles_from_hash(GogObject *chart, GtkHTMLEmbedded * eb)
{
  set_chart_titles(chart,
                   (const char *)g_hash_table_lookup(eb->params, "title"), 
                   (const char *)g_hash_table_lookup(eb->params, "subtitle"));
}

static void
set_chart_titles(GogObject *chart, const char *title, const char* sub_title)
{
  gchar *my_sub_title, *total_title;
  GOData *title_scalar;
  GogObject *tmp;

  if (sub_title)
    my_sub_title = g_strdup_printf("%s(%s)", title ? " " : "", sub_title);
  else
    my_sub_title = g_strdup("");

  total_title = g_strdup_printf("%s%s", title ? title : "", my_sub_title);

  tmp = gog_object_add_by_name(chart, "Title", NULL);
  title_scalar = go_data_scalar_str_new(total_title, TRUE);
  gog_dataset_set_dim(GOG_DATASET(tmp), 0, title_scalar, NULL);

  g_free(my_sub_title);
}

static void
set_chart_axis_labels_from_hash(GogObject *chart, GtkHTMLEmbedded * eb)
{
  set_chart_axis_labels(chart,
                        (const char *)g_hash_table_lookup(eb->params, "x_axis_label"),
                        (const char *)g_hash_table_lookup(eb->params, "y_axis_label"));
}

static void
set_chart_axis_labels(GogObject *chart, const char *x_axis_label, const char* y_axis_label)
{
  if (x_axis_label != NULL)
  {
    GogObject *xaxis, *label;
    GOData *data;
    xaxis = gog_object_get_child_by_role(chart, gog_object_find_role_by_name(chart, "X-Axis"));
    label = gog_object_add_by_name(xaxis, "Label", NULL);
    data = go_data_scalar_str_new(x_axis_label, FALSE);
    gog_dataset_set_dim(GOG_DATASET(label), 0, data, NULL);
  }

  if (y_axis_label != NULL)
  {
    GogObject *yaxis, *label;
    GOData *data;
    yaxis = gog_object_get_child_by_role(chart, gog_object_find_role_by_name(chart, "Y-Axis"));
    label = gog_object_add_by_name(yaxis, "Label", NULL);
    data = go_data_scalar_str_new(y_axis_label, FALSE);
    gog_dataset_set_dim(GOG_DATASET(label), 0, data, NULL);
  }
}

static void
gtkhtml_pre_3_10_1_bug_workaround(GtkHTMLEmbedded *eb)
{
  /* HACK ALERT! Compensate for bug in gtkhtml < 3.10.1
     Gtkhtml set the width parameter twice (=width, =height), so both,
     width (==height) and height (<1) were incorrect. */
  if (eb->height < 1)
  {
      eb->height = eb->width;  /* only squares here :( */
  }
}

/*
 * Handle the following parameters:
 * title: text
 * subtitle: text
 * datasize: (length data), sscanf( .., %d, (int)&datasize )
 * data: (foreach (lambda (datum) (push datum) (push " ")) data)
 * colors: string; space-seperated?
 * labels: string; space-seperated?
 * slice_urls_[123]: ?
 * legend_urls_[123]: ?
 */
static gboolean
handle_piechart(gnc_html * html, GtkHTMLEmbedded * eb, gpointer unused)
{
  GogObject *graph, *chart;
  GogPlot *plot;
  GogSeries *series;
  GOData *labelData, *sliceData;
  int datasize;
  double *data = NULL;
  char **labels = NULL, **colors = NULL;

  gtkhtml_pre_3_10_1_bug_workaround(eb);

  // parse data from the text-ized params.
  {
    char *datasizeStr, *dataStr, *labelsStr, *colorStr;

    datasizeStr = g_hash_table_lookup(eb->params, "datasize");
    dataStr = g_hash_table_lookup(eb->params, "data" );
    labelsStr = g_hash_table_lookup(eb->params, "labels");
    colorStr = g_hash_table_lookup(eb->params, "colors");
    g_return_val_if_fail( datasizeStr != NULL
                          && dataStr != NULL
                          && labelsStr != NULL
                          && colorStr != NULL, FALSE );
    datasize = atoi( datasizeStr );
    data = read_doubles( dataStr, datasize );
    labels = read_strings( labelsStr, datasize );
    colors = read_strings( colorStr, datasize );
  }

  if (!create_basic_plot_elements("GogPiePlot", &graph, &chart, &plot))
  {
    return FALSE;
  }
  gog_object_add_by_name(chart, "Legend", NULL);

  GOG_STYLED_OBJECT(graph)->style->outline.width = 5;
  GOG_STYLED_OBJECT(graph)->style->outline.color = RGBA_BLACK;

  series = gog_plot_new_series(plot);
  labelData = go_data_vector_str_new((char const * const *)labels, datasize, NULL);
  gog_series_set_dim(series, 0, labelData, NULL);
  go_data_emit_changed(GO_DATA(labelData));

  sliceData = go_data_vector_val_new(data, datasize, NULL);
  gog_series_set_dim(series, 1, sliceData, NULL);
  go_data_emit_changed(GO_DATA(sliceData));

  // fixme: colors
  set_chart_titles_from_hash(chart, eb);

  add_pixbuf_graph_widget (eb, graph);

  return TRUE;
}

/**
 * data_rows:int
 * data_cols:int
 * data:doubles[], data_rows*data_cols
 * x_axis_label:string
 * y_axis_label:string
 * row_labels:string[]
 * col_labels:string[]
 * col_colors:string[]
 * rotate_row_labels:boolean
 * stacked:boolean
 **/
static gboolean
handle_barchart(gnc_html * html, GtkHTMLEmbedded * eb, gpointer unused)
{
  GogObject *graph, *chart;
  GogPlot *plot;
  GogSeries *series;
  GogStyle *style;
  GOData *label_data, *slice_data;
  int data_rows, data_cols;
  double *data = NULL;
  char **col_labels = NULL, **row_labels = NULL, **col_colors = NULL;
  gboolean rotate_row_labels = FALSE;
  gboolean stacked = FALSE;
  char *bar_type = "normal";
  int bar_overlap = 0 /*percent*/; // seperate bars; no overlap.

  gtkhtml_pre_3_10_1_bug_workaround (eb);

  // parse data from the text-ized params
  // series => bars [gnc:cols]
  // series-elements => segments [gnc:rows]
  {
    char *data_rows_str, *data_cols_str, *data_str, *col_labels_str, *row_labels_str;
    char *col_colors_str, *rotate_row_labels_str = NULL, *stacked_str = NULL;

    data_rows_str         = g_hash_table_lookup (eb->params, "data_rows");
    data_cols_str         = g_hash_table_lookup (eb->params, "data_cols");
    data_str              = g_hash_table_lookup (eb->params, "data" );
    row_labels_str        = g_hash_table_lookup (eb->params, "row_labels");
    col_labels_str        = g_hash_table_lookup (eb->params, "col_labels");
    col_colors_str        = g_hash_table_lookup (eb->params, "col_colors");
    rotate_row_labels_str = g_hash_table_lookup (eb->params, "rotate_row_labels");
    stacked_str           = g_hash_table_lookup (eb->params, "stacked");

    rotate_row_labels     = (gboolean) atoi (rotate_row_labels_str);
    stacked               = (gboolean) atoi (stacked_str);

#if 0 // too strong at the moment.
    g_return_val_if_fail (data_rows_str != NULL
                          && data_cols_str != NULL
                          && data_str != NULL
                          && col_labels_str != NULL
                          && row_labels_str != NULL
                          && col_colors_str != NULL, FALSE );
#endif // 0
    data_rows = atoi (data_rows_str);
    data_cols = atoi (data_cols_str);
    data = read_doubles (data_str, data_rows*data_cols);
    row_labels = read_strings (row_labels_str, data_rows);
    col_labels = read_strings (col_labels_str, data_cols);
    col_colors = read_strings (col_colors_str, data_cols);
  }

  if (!create_basic_plot_elements("GogBarColPlot", &graph, &chart, &plot)) {
    return FALSE;
  }
  gog_object_add_by_name(chart, "Legend", NULL);

  if ( stacked ) {
    // when stacked, we want the bars on _top_ of eachother.
    bar_type = "stacked";
    bar_overlap = 100 /*percent*/;
  }

  g_object_set (G_OBJECT (plot),
                //"vary_style_by_element",	TRUE,
                "type",                         bar_type,
                "overlap_percentage",           bar_overlap, 
                NULL);
  label_data = go_data_vector_str_new ((char const * const *)row_labels, data_rows, NULL);
  {
    // foreach row:
    //   series = row
    GdkColor color;
    int i;
    for (i = 0; i < data_cols; i++) {
      GError *err = NULL;

      series = gog_plot_new_series (plot);
      gog_object_set_name (GOG_OBJECT (series), col_labels[i], &err);
      if (err != NULL)
      {
           g_warning("error setting name [%s] on series [%d]: [%s]",
                     col_labels[i], i, err->message);
      }

      g_object_ref (label_data);
      gog_series_set_dim (series, 0, label_data, NULL);
      go_data_emit_changed (GO_DATA (label_data));

      slice_data = go_data_vector_val_new (data + (i*data_rows), data_rows, NULL);
      gog_series_set_dim (series, 1, slice_data, NULL);
      go_data_emit_changed (GO_DATA (slice_data));

      style = gog_styled_object_get_style (GOG_STYLED_OBJECT (series));
      style->fill.type = GOG_FILL_STYLE_PATTERN;
      if (gdk_color_parse (col_colors[i], &color)) {
           style->fill.auto_back = FALSE;
           go_pattern_set_solid (&style->fill.pattern, GDK_TO_UINT (color));
      } else {
           g_warning("cannot parse color [%s]", col_colors[i]);
      }
    }
  }

  if (rotate_row_labels) {
    GogObject *object = gog_object_get_child_by_role (
      chart, gog_object_find_role_by_name (chart, "X-Axis"));
    style = gog_styled_object_get_style (GOG_STYLED_OBJECT (object));
    gog_style_set_text_angle (style, 90.0);
  }

  set_chart_titles_from_hash (chart, eb);
  set_chart_axis_labels_from_hash (chart, eb);

  // we need to do this twice for the barchart... :p
  gog_object_update (GOG_OBJECT (graph));

  add_pixbuf_graph_widget (eb, graph);

  g_debug("barchart rendered.");
  return TRUE;
}

static gboolean
handle_scatter(gnc_html * html, GtkHTMLEmbedded * eb, gpointer unused)
{
  GogObject *graph, *chart;
  GogPlot *plot;
  GogSeries *series;
  GOData *sliceData;
  int datasize;
  double *xData, *yData;

  gtkhtml_pre_3_10_1_bug_workaround(eb);

  {
    char *datasizeStr, *xDataStr, *yDataStr;

    datasizeStr = g_hash_table_lookup( eb->params, "datasize" );
    datasize = atoi( datasizeStr );

    xDataStr = g_hash_table_lookup( eb->params, "x_data" );
    xData = read_doubles( xDataStr, datasize );

    yDataStr = g_hash_table_lookup( eb->params, "y_data" );
    yData = read_doubles( yDataStr, datasize );
  }

  if (!create_basic_plot_elements("GogXYPlot", &graph, &chart, &plot))
  {
    return FALSE;
  }

  series = gog_plot_new_series( plot );

  sliceData = go_data_vector_val_new( xData, datasize, NULL );
  gog_series_set_dim( series, 0, sliceData, NULL );
  go_data_emit_changed (GO_DATA (sliceData));

  sliceData = go_data_vector_val_new( yData, datasize, NULL );
  gog_series_set_dim( series, 1, sliceData, NULL );
  go_data_emit_changed (GO_DATA (sliceData));

  set_chart_titles_from_hash(chart, eb);
  set_chart_axis_labels_from_hash(chart, eb);

  // And twice for the scatter, too... :p
  gog_object_update(GOG_OBJECT(graph));

  add_pixbuf_graph_widget (eb, graph);

  return TRUE;
}

static void
draw_print_cb(GtkHTMLEmbedded *eb, GnomePrintContext *context, gpointer unused)
{
  GogGraph *graph = GOG_GRAPH (g_object_get_data (G_OBJECT (eb), "graph"));

  /* assuming pixel size is 0.5, cf. gtkhtml/src/htmlprinter.c */
  gnome_print_scale (context, 0.5, 0.5);

  gnome_print_translate (context, 0, eb->height);
  gog_graph_print_to_gnome_print (graph, context, eb->width, eb->height);
}
