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

  Copyright (C) 2003-2020 Uwe Schulzweida, <uwe.schulzweida AT mpimet.mpg.de>
  See COPYING file for copying and redistribution conditions.

  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; version 2 of the License.

  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.
*/

#include <cmath>
#include <string.h>

#include "compare.h"
#include "percentiles.h"
#include "nth_element.h"
#include "util_string.h"
#include "cdo_output.h"

enum percentile_methods
{
  NRANK = 1,
  NIST,
  NUMPY,
  NR8 = 2
};

enum interpolation_methods
{
  LINEAR = 1,
  LOWER,
  HIGHER,
  NEAREST
};

static int percentile_method = NRANK;
static int interpolation_method = LINEAR;

static double
percentile_nrank(double *array, size_t len, double pn)
{
  auto irank = (size_t) std::ceil(len * (pn / 100.0));
  if (irank < 1) irank = 1;
  if (irank > len) irank = len;
  return nth_element(array, len, irank - 1);
}

static double
percentile_nist(double *array, size_t len, double pn)
{
  const double rank = (len + 1) * (pn / 100.0);
  const size_t k = (size_t) rank;
  const double d = rank - k;

  double percentil = 0;
  if (k == 0)
    percentil = nth_element(array, len, 0);
  else if (k >= len)
    percentil = nth_element(array, len, len - 1);
  else
    {
      const auto vk1 = nth_element(array, len, k);
      const auto vk = nth_element(array, len, k - 1);
      percentil = vk + d * (vk1 - vk);
    }

  return percentil;
}

static double
percentile_numpy(double *array, size_t len, double pn)
{
  const double rank = (len - 1) * (pn / 100.0) + 1;
  const size_t k = (size_t) rank;
  const double d = rank - k;

  double percentil = 0;
  if (k == 0)
    percentil = nth_element(array, len, 0);
  else if (k >= len)
    percentil = nth_element(array, len, len - 1);
  else
    {
      if (interpolation_method == LINEAR)
        {
          const auto vk1 = nth_element(array, len, k);
          const auto vk = nth_element(array, len, k - 1);
          percentil = vk + d * (vk1 - vk);
        }
      else
        {
          size_t irank = 0;
          // clang-format off
          if      (interpolation_method == LOWER)   irank = (size_t) std::floor(rank);
          else if (interpolation_method == HIGHER)  irank = (size_t) std::ceil(rank);
          else if (interpolation_method == NEAREST) irank = (size_t) std::lround(rank);
          // clang-format on
          // numpy is using around(), with rounds to the nearest even value

          if (irank < 1) irank = 1;
          if (irank > len) irank = len;

          percentil = nth_element(array, len, irank - 1);
        }
    }

  return percentil;
}

static double
percentile_Rtype8(double *array, size_t len, double pn)
{
  const double rank = 1./3. + (len + 1./3.) * (pn / 100.0);
  const size_t k = (size_t) rank;
  const double d = rank - k;

  double percentil = 0;
  if (k == 0)
    percentil = nth_element(array, len, 0);
  else if (k >= len)
    percentil = nth_element(array, len, len - 1);
  else
    {
      const auto vk1 = nth_element(array, len, k);
      const auto vk = nth_element(array, len, k - 1);
      percentil = vk + d * (vk1 - vk);
    }

  return percentil;
}

static void
percentile_check_number(double pn)
{
  if (pn < 0 || pn > 100) cdoAbort("Percentile number %g out of range! Percentiles must be in the range [0,100].", pn);
}

double
percentile(double *array, size_t len, double pn)
{
  percentile_check_number(pn);

  double percentil = 0;

  // clang-format off
  if      (percentile_method == NR8)    percentil = percentile_Rtype8(array, len, pn);
  else if (percentile_method == NRANK)  percentil = percentile_nrank(array, len, pn);
  else if (percentile_method == NIST)   percentil = percentile_nist(array, len, pn);
  else if (percentile_method == NUMPY)  percentil = percentile_numpy(array, len, pn);
  else cdoAbort("Internal error: percentile method %d not implemented!", percentile_method);
  // clang-format on

  return percentil;
}

void
percentile_set_method(const std::string &methodstr)
{
  auto methodname = stringToLower(methodstr);

  // clang-format off
  if      ("nrank" == methodname) percentile_method = NRANK;
  else if ("nist"  == methodname) percentile_method = NIST;
  else if ("numpy" == methodname) percentile_method = NUMPY;
  else if ("rtype8"== methodname) percentile_method = NR8;
  else if ("numpy_linear" == methodname)
    {
      percentile_method = NUMPY;
      interpolation_method = LINEAR;
    }
  else if ("numpy_lower" == methodname)
    {
      percentile_method = NUMPY;
      interpolation_method = LOWER;
    }
  else if ("numpy_higher" == methodname)
    {
      percentile_method = NUMPY;
      interpolation_method = HIGHER;
    }
  else if ("numpy_nearest" == methodname)
    {
      percentile_method = NUMPY;
      interpolation_method = NEAREST;
    }
  else
    cdoAbort("Percentile method %s not available!", methodstr.c_str());
  // clang-format on
}

/*
  CDO check
#/bin/sh
CDO=cdo
#
cdo -f nc input,r5x1 testfile <<EOF
 15 20 35 40 50
EOF
cdo -f nc input,r6x1 testfile <<EOF
 15 20 35 40 50 55
EOF
#
PERS="30 40 50 75 100"
METS="nrank nist numpy numpy_lower numpy_higher numpy_nearest"
#
for MET in $METS; do
    for PER in $PERS; do
        echo "$MET: $PER"
        $CDO -s --percentile $MET output -fldpctl,$PER testfile
    done
done
*/
/*
  numpy check
#python with numpy 1.9.0
import numpy as np
np.version.version
a=np.array([15, 20, 35, 40, 50, 55])
for p in [30, 40, 50, 75, 100] : print np.percentile(a, p, interpolation='linear')
*/
