/* PSPP - a program for statistical analysis.
   Copyright (C) 2004, 2008, 2009, 2011, 2012 Free Software Foundation, Inc.

   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 3 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, see <http://www.gnu.org/licenses/>. */

#include <config.h>

#include "math/histogram.h"

#include <gsl/gsl_histogram.h>
#include <math.h>

#include "data/settings.h"
#include "libpspp/message.h"
#include "libpspp/assertion.h"
#include "libpspp/cast.h"
#include "math/chart-geometry.h"

#include "gettext.h"
#define _(msgid) gettext (msgid)
#define N_(msgid) msgid


#include "gl/xalloc.h"

void
histogram_add (struct histogram *h, double y, double c)
{
  gsl_histogram_accumulate (h->gsl_hist, y, c);
}

static void
destroy (struct statistic *s)
{
  struct histogram *h = UP_CAST (s, struct histogram, parent);
  gsl_histogram_free (h->gsl_hist);
  free (s);
}


/* Find a bin width which is adapted to the scaling of the x axis
In the example here, the binwidth is half of the tick interval.

        binwidth
         >   <
     |....+....+....+.      .+....|
   LOWER  1    2    3     N_TICKS
        ^LOWDBL                 ^HIGHDBL

This only works, when the min and max value for the histogram are adapted
such that (max-min) is a multiple of the binwidth. Then the location of the
first bin has to be aligned to the ticks.
*/
static int
hist_find_pretty_no_of_bins (double bin_width_in, double min, double max,
                             double *adjusted_min, double *adjusted_max)
{
  /* Choose an initial chart scale, without considering bin width. */
  double lower, interval;
  int n_ticks;
  chart_get_scale (max, min, &lower, &interval, &n_ticks);

  /* Then adjust things. */
  double binwidth;
  if (bin_width_in >= 2 * interval)
    {
      binwidth = floor(bin_width_in/interval) * interval;
      *adjusted_min = lower;
    }
  else if (bin_width_in >= 1.5 * interval)
    {
      binwidth = 1.5 * interval;
      if (min < (lower + 0.5 * interval))
        *adjusted_min = lower;
      else
        *adjusted_min = lower + 0.5 * interval;
    }
  else if (bin_width_in >= interval)
    {
      binwidth = interval;
      *adjusted_min = lower;
    }
  else if (bin_width_in >= (2.0/3.0 * interval))
    {
      binwidth = (2.0/3.0 * interval);
      if (min >= lower + binwidth)
        *adjusted_min = lower + binwidth;
      else
        *adjusted_min = lower;
    }
  else
    {
      int i;
      for(i = 2; bin_width_in < interval/i; i++);
      binwidth = interval/i;
      *adjusted_min = floor((min - lower)/binwidth)*binwidth + lower;
    }

  /* This should not happen, but sometimes it does. */
  if (*adjusted_min > min)
    *adjusted_min = min;

  int nbins = ceil((max-*adjusted_min)/binwidth);
  *adjusted_max = nbins*binwidth + *adjusted_min;

  /* adjusted_max should never be smaller than max but if it is equal
     then the gsl_histogram will not add the cases which have max value */
  if (*adjusted_max <= max)
    {
      *adjusted_max += binwidth;
      nbins++;
    }
  assert (*adjusted_min <= min);

  return nbins;
}

/* Creates and returns a new histogram for data that ranges from MIN to MAX
   with a suggested bin width of BIN_WIDTH_IN.  The implementation will adjust
   the bin width.  The caller should add each data point to the histogram with
   histogram_add(), and eventually destroy it with statistic_destroy().

   Returns NULL if the histogram cannot be created. */
struct histogram *
histogram_create (double bin_width_in, double min, double max)
{
  if (max == min)
    {
      msg (MW, _("Not creating histogram because the data contains less than 2 "
                 "distinct values"));
      return NULL;
  }

  assert (bin_width_in > 0);

  double adjusted_min, adjusted_max;
  int bins = hist_find_pretty_no_of_bins (bin_width_in, min, max, &adjusted_min,
                                          &adjusted_max);

  struct histogram *h = xmalloc (sizeof *h);
  *h = (struct histogram) {
    .parent.destroy = destroy,
    .gsl_hist = gsl_histogram_alloc (bins),
  };

  /* The bin ranges could be computed with gsl_histogram_set_ranges_uniform,
     but the number of bins is adapted to the ticks of the axis such that for
     example data ranging from 1.0 to 7.0 with 6 bins will have bin limits at
     2.0,3.0,4.0 and 5.0. Due to numerical accuracy the computed bin limits
     might be 4.99999999 for a value which is expected to be 5.0. But the limits
     of the histogram bins should be that what is expected from the displayed
     ticks. Therefore the bin limits are computed from the rounded values which
     is similar to the procedure at the chart_get_ticks_format. Actual bin
     limits should be exactly what is displayed at the ticks. But... I cannot
     reproduce the problem that I see with gsl_histogram_set_ranges_uniform with
     the code below without rounding...
  */
  double *ranges = xmalloc (sizeof *ranges * (bins + 1));
  double interval = (adjusted_max - adjusted_min) / bins;
  for (int i = 0; i < bins; i++)
    ranges[i] = adjusted_min + interval * i;
  ranges[bins] = adjusted_max;
  gsl_histogram_set_ranges (h->gsl_hist, ranges, bins + 1);
  free (ranges);

  return h;
}
