/*
  This file is part of CDO. CDO is a collection of Operators to manipulate and analyse Climate model Data.

  Author: Uwe Schulzweida
          Ralf Quast

*/

#include <climits>
#include <algorithm>
#include <stdlib.h>

#include <cdi.h>

#include "cdo_output.h"
#include "percentiles.h"
#include "percentiles_hist.h"

constexpr int sizeOfUint = sizeof(unsigned int);

#define FLT_CAPACITY(n, s) ((int) (((n) * (s)) / sizeof(float)))
#define FLT_PTR(p) ((float *) (p))
#define INT_PTR(p) ((unsigned int *) (p))
#define SHR_PTR(p) ((unsigned short *) (p))

static int
histGetEnvNBins()
{
  constexpr int NBINS_DEFAULT = 101;
  constexpr int NBINS_MINIMUM = 11;

  const auto str = getenv("CDO_PCTL_NBINS");

  return str != nullptr ? std::max(atoi(str), NBINS_MINIMUM) : NBINS_DEFAULT;
}

static void
histDefBounds(Histogram &hist, float a, float b)
{
  assert(hist.nbins > 0);

  hist.min = std::min(a, b);
  hist.max = std::max(a, b);
  hist.step = (hist.max - hist.min) / hist.nbins;
  hist.nsamp = 0;

  if (hist.vsize == sizeOfUint)
    for (int i = 0; i < hist.nbins; i++) INT_PTR(hist.ptr)[i] = 0;
  else
    for (int i = 0; i < hist.nbins; i++) SHR_PTR(hist.ptr)[i] = 0;
}

static void
histBinAddValue(Histogram &hist, float value)
{
  assert(hist.step > 0.0f);

  const int bin = std::min((int) ((value - hist.min) / hist.step), hist.nbins - 1);
  if (bin >= 0 && bin < hist.nbins)
    {
      if (hist.vsize == sizeOfUint)
        INT_PTR(hist.ptr)[bin]++;
      else
        SHR_PTR(hist.ptr)[bin]++;
    }
}

static void
histBinSubValue(Histogram &hist, float value)
{
  assert(hist.step > 0.0f);

  const int bin = std::min((int) ((value - hist.min) / hist.step), hist.nbins - 1);
  if (bin >= 0 && bin < hist.nbins)
    {
      if (hist.vsize == sizeOfUint)
        {
          if (INT_PTR(hist.ptr)[bin] > 0) INT_PTR(hist.ptr)[bin]--;
        }
      else
        {
          if (SHR_PTR(hist.ptr)[bin] > 0) SHR_PTR(hist.ptr)[bin]--;
        }
    }
}

static void
histBin(Histogram &hist)
{
  assert(hist.nsamp == FLT_CAPACITY(hist.nbins, hist.vsize));

  std::vector<float> values(hist.nsamp);

  const auto fltptr = FLT_PTR(hist.ptr);
  for (int i = 0; i < hist.nsamp; i++) values[i] = fltptr[i];
  if (hist.vsize == sizeOfUint)
    for (int i = 0; i < hist.nbins; i++) INT_PTR(hist.ptr)[i] = 0;
  else
    for (int i = 0; i < hist.nbins; i++) SHR_PTR(hist.ptr)[i] = 0;
  for (int i = 0; i < hist.nsamp; i++) histBinAddValue(hist, values[i]);
}
/* unused
static int
histReset(Histogram &hist)
{
  assert(hist.nbins > 0);

  if (hist.nsamp < FLT_CAPACITY(hist.nbins, hist.vsize))
    {
      for (int i = 0; i < hist.nsamp; i++) FLT_PTR(hist.ptr)[i] = 0.;
    }
  else
    {
      if (hist.vsize == sizeOfUint)
        for (int i = 0; i < hist.nbins; i++) INT_PTR(hist.ptr)[i] = 0;
      else
        for (int i = 0; i < hist.nbins; i++) SHR_PTR(hist.ptr)[i] = 0;
    }

  hist.nsamp = 0;

  return 0;
}
*/

static void
histCheckValue(const Histogram &hist, float &value)
{
  // 2011-08-01 Uwe Schulzweida: added check for rounding errors
  if (value < hist.min && (hist.min - value) < 1.e5f) value = hist.min;
  if (value > hist.max && (value - hist.max) < 1.e5f) value = hist.max;
}

static int
histAddValue(Histogram &hist, float value)
{
  assert(hist.nbins > 0);

  histCheckValue(hist, value);

  if (IS_EQUAL(hist.min, hist.max)) return 0;
  if (value < hist.min || value > hist.max) return 1;

  if (hist.nsamp < FLT_CAPACITY(hist.nbins, hist.vsize))
    {
      FLT_PTR(hist.ptr)[hist.nsamp] = value;
    }
  else if (hist.nsamp > FLT_CAPACITY(hist.nbins, hist.vsize))
    {
      histBinAddValue(hist, value);
    }
  else
    {
      histBin(hist);
      histBinAddValue(hist, value);
    }

  hist.nsamp++;

  return 0;
}

static void
histRemoveValue(Histogram &hist, float value)
{
  auto fltptr = FLT_PTR(hist.ptr);
  int i = 0;
  for (i = 0; i < hist.nsamp; i++)
    {
      if (IS_EQUAL(fltptr[i], value))
        {
          if (i != hist.nsamp - 1) fltptr[i] = fltptr[hist.nsamp - 1];
          break;
        }
    }
  if (i == hist.nsamp)
    cdo_warning("'%f' not found in histogram!", value);
  else
    hist.nsamp--;
}

static int
histSubValue(Histogram &hist, float value)
{
  assert(hist.nbins > 0);

  histCheckValue(hist, value);

  if (IS_EQUAL(hist.min, hist.max)) return 0;
  if (value < hist.min || value > hist.max) return 1;

  if (hist.nsamp < FLT_CAPACITY(hist.nbins, hist.vsize))
    {
      histRemoveValue(hist, value);
    }
  else if (hist.nsamp > FLT_CAPACITY(hist.nbins, hist.vsize))
    {
      histBinSubValue(hist, value);
      hist.nsamp--;
    }
  else
    return 1;

  return 0;
}

template <typename T>
static double
histGetBin(int nbins, double s, const T *ptr)
{
  int i = 0, count = 0;

  do
    count += ptr[i++];
  while (count < s);

  assert(i > 0);
  assert(i - 1 < nbins);
  assert(ptr[i - 1] > 0);

  const double t = (count - s) / ptr[i - 1];

  return (i - t);
}

static double
histGetPercentile(const Histogram &hist, const double p)
{
  assert(hist.nsamp > 0);
  assert(hist.nbins > 0);
  assert(p >= 0.0);
  assert(p <= 100.0);

  if (hist.nsamp > FLT_CAPACITY(hist.nbins, hist.vsize))
    {
      static auto lprint = true;
      if (lprint && Options::cdoVerbose)
        {
          lprint = false;
          cdo_print("Using percentile method: histogram with %d bins", hist.nbins);
        }

      const double s = hist.nsamp * (p / 100.0);

      auto bin = (hist.vsize == sizeOfUint) ? histGetBin(hist.nbins, s, INT_PTR(hist.ptr))
                                            : histGetBin(hist.nbins, s, SHR_PTR(hist.ptr));

      assert(hist.step > 0.0f);

      return hist.min + bin * hist.step;
    }
  else
    {
      return percentile(FLT_PTR(hist.ptr), hist.nsamp, p);
    }
}

void
HistogramSet::createVarLevels(const int varID, const int nlevels, const size_t nhists)
{
  const auto nbins = histGetEnvNBins();

  assert(nbins > 0);
  assert(nlevels > 0);
  assert(nhists > 0);

  if (varID < 0 || varID >= nvars) cdo_abort("Illegal argument: varID %d is undefined (%s)", varID, __func__);

  this->var_nlevels[varID] = nlevels;
  this->var_nhists[varID] = nhists;
  this->histograms[varID].resize(nlevels);

  for (int levelID = 0; levelID < nlevels; levelID++)
    {
      this->histograms[varID][levelID].resize(nhists);
      auto &hists = this->histograms[varID][levelID];

      for (size_t histID = 0; histID < nhists; histID++)
        {
          hists[histID].min = 0.0f;
          hists[histID].max = 0.0f;
          hists[histID].step = 0.0f;
          hists[histID].nbins = nbins;
          hists[histID].nsamp = 0;
          hists[histID].vsize = (this->nsteps > 0 && this->nsteps < USHRT_MAX) ? sizeof(unsigned short) : sizeof(unsigned int);
          hists[histID].ptr = (int *) malloc(nbins * hists[histID].vsize);
          if (hists[histID].ptr == nullptr) cdo_abort("Not enough memory (%s)", __func__);
        }
    }
}

template <typename T1, typename T2>
static void
defBounds(size_t nhists, std::vector<Histogram> &hists, const Varray<T1> &v1, const Varray<T2> &v2, float mv1, float mv2)
{
  assert(!v1.empty());
  assert(!v2.empty());

  for (size_t i = 0; i < nhists; i++)
    {
      const float a = v1[i];
      const float b = v2[i];

      if (DBL_IS_EQUAL(a, mv1) || DBL_IS_EQUAL(b, mv2) || DBL_IS_EQUAL(a, b))
        histDefBounds(hists[i], 0.0f, 0.0f);
      else
        histDefBounds(hists[i], a, b);
    }
}

void
HistogramSet::defVarLevelBounds(int varID, int levelID, const Field &field1, const Field &field2)
{
  if (varID < 0 || varID >= nvars) cdo_abort("Illegal argument: varID %d is undefined (%s)", varID, __func__);

  const auto nlevels = this->var_nlevels[varID];
  if (levelID < 0 || levelID >= nlevels) cdo_abort("Illegal argument: levelID %d is undefined (%s)", levelID, __func__);

  const auto nhists = this->var_nhists[varID];
  if (nhists != gridInqSize(field1.grid) || nhists != gridInqSize(field2.grid)) cdo_abort("Grids are different (%s)", __func__);

  auto &hists = this->histograms[varID][levelID];

  const float mv1 = (float) field1.missval;
  const float mv2 = (float) field2.missval;

  // clang-format off
  if      (field1.memType == MemType::Float && field2.memType == MemType::Float)
    defBounds(nhists, hists, field1.vec_f, field2.vec_f, mv1, mv2);
  else if (field1.memType == MemType::Float && field2.memType == MemType::Double)
    defBounds(nhists, hists, field1.vec_f, field2.vec_d, mv1, mv2);
  else if (field1.memType == MemType::Double && field2.memType == MemType::Float)
    defBounds(nhists, hists, field1.vec_d, field2.vec_f, mv1, mv2);
  else if (field1.memType == MemType::Double && field2.memType == MemType::Double)
    defBounds(nhists, hists, field1.vec_d, field2.vec_d, mv1, mv2);
  // clang-format on
}

template <typename T>
static int
histAddVarLevelValues(size_t nhists, std::vector<Histogram> &hists, const Varray<T> &v, size_t nmiss, T mv)
{
  assert(!v.empty());

  int nign = 0;
  if (nmiss)
    {
      for (size_t i = 0; i < nhists; i++)
        if (!DBL_IS_EQUAL(v[i], mv)) nign += histAddValue(hists[i], v[i]);
    }
  else
    {
      for (size_t i = 0; i < nhists; i++) nign += histAddValue(hists[i], v[i]);
    }

  return nign;
}

template <typename T>
static int
histSubVarLevelValues(size_t nhists, std::vector<Histogram> &hists, const Varray<T> &v, size_t nmiss, T mv)
{
  int nign = 0;

  assert(!v.empty());

  if (nmiss)
    {
      for (size_t i = 0; i < nhists; i++)
        if (!DBL_IS_EQUAL(v[i], mv)) nign += histSubValue(hists[i], v[i]);
    }
  else
    {
      for (size_t i = 0; i < nhists; i++) nign += histSubValue(hists[i], v[i]);
    }

  return nign;
}

int
HistogramSet::addVarLevelValues(const int varID, const int levelID, const Field &field)
{
  if (varID < 0 || varID >= nvars) cdo_abort("Illegal argument: varID %d is undefined (%s)", varID, __func__);

  const auto nlevels = this->var_nlevels[varID];
  if (levelID < 0 || levelID >= nlevels) cdo_abort("Illegal argument: levelID %d is undefined (%s)", levelID, __func__);

  const auto nhists = this->var_nhists[varID];
  if (nhists != gridInqSize(field.grid)) cdo_abort("Grids are different (%s)", __func__);

  auto &hists = this->histograms[varID][levelID];

  int nign = 0;

  if (field.memType == MemType::Float)
    nign = histAddVarLevelValues(nhists, hists, field.vec_f, field.nmiss, (float) field.missval);
  else
    nign = histAddVarLevelValues(nhists, hists, field.vec_d, field.nmiss, field.missval);

  if (nign)
    {
      cdo_warning("%d out of %d grid values are out of bounds and have been ignored (%s)", nign, nhists, __func__);
      return 1;
    }

  return 0;
}

int
HistogramSet::subVarLevelValues(const int varID, const int levelID, const Field &field)
{
  if (varID < 0 || varID >= nvars) cdo_abort("Illegal argument: varID %d is undefined (%s)", varID, __func__);

  const auto nlevels = this->var_nlevels[varID];
  if (levelID < 0 || levelID >= nlevels) cdo_abort("Illegal argument: levelID %d is undefined (%s)", levelID, __func__);

  const auto nhists = this->var_nhists[varID];
  if (nhists != gridInqSize(field.grid)) cdo_abort("Grids are different (%s)", __func__);

  auto &hists = this->histograms[varID][levelID];

  int nign = 0;

  if (field.memType == MemType::Float)
    nign = histSubVarLevelValues(nhists, hists, field.vec_f, field.nmiss, (float) field.missval);
  else
    nign = histSubVarLevelValues(nhists, hists, field.vec_d, field.nmiss, field.missval);

  if (nign)
    {
      cdo_warning("%d out of %d grid values are out of bounds and have been ignored (%s)", nign, nhists, __func__);
      return 1;
    }

  return 0;
}
/* unused
void
HistogramSet::reset(const int varID, const int levelID)
{
  assert(nvars > 0);

  if (varID < 0 || varID >= nvars) cdo_abort("Illegal argument: varID %d is undefined (%s)", varID, __func__);

  const auto nlevels = this->var_nlevels[varID];
  assert(nlevels > 0);

  if (levelID < 0 || levelID >= nlevels) cdo_abort("Illegal argument: levelID %d is undefined (%s)", levelID, __func__);

  const auto nhists = this->var_nhists[varID];
  assert(nhists > 0);

  auto &hists = this->histograms[varID][levelID];
  for (size_t i = 0; i < nhists; i++) histReset(hists[i]);
}
*/

template <typename T>
static size_t
calcPercentile(size_t nhists, const std::vector<Histogram> &hists, const double p, Varray<T> &v, T mv)
{
  assert(!v.empty());

  size_t nmiss = 0;

  for (size_t i = 0; i < nhists; i++)
    {
      if (hists[i].nsamp)
        v[i] = histGetPercentile(hists[i], p);
      else
        {
          v[i] = mv;
          nmiss++;
        }
    }

  return nmiss;
}

void
HistogramSet::getVarLevelPercentiles(Field &field, const int varID, const int levelID, const double p)
{
  if (varID < 0 || varID >= nvars) cdo_abort("Illegal argument: varID %d is undefined (%s)", varID, __func__);

  const auto nlevels = this->var_nlevels[varID];
  if (levelID < 0 || levelID >= nlevels) cdo_abort("Illegal argument: levelID %d is undefined (%s)", levelID, __func__);

  const auto nhists = this->var_nhists[varID];
  if (nhists != gridInqSize(field.grid)) cdo_abort("Grids are different (%s)", __func__);

  const auto &hists = this->histograms[varID][levelID];

  if (field.memType == MemType::Float)
    field.nmiss = calcPercentile(nhists, hists, p, field.vec_f, (float) field.missval);
  else
    field.nmiss = calcPercentile(nhists, hists, p, field.vec_d, field.missval);
}
