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

  Copyright (C) 2003-2021 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 <cstring>

#include <algorithm>

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

enum class PercentileMethod
{
  NRANK = 1,
  NIST,
  NUMPY,
  NR8
};

enum class InterpolationMethod
{
  LINEAR = 1,
  LOWER,
  HIGHER,
  NEAREST
};

static PercentileMethod percentile_method = PercentileMethod::NRANK;
static InterpolationMethod interpolation_method = InterpolationMethod::LINEAR;

template <typename T>
static double
get_nth_element(T *array, size_t length, size_t n)
{
  std::nth_element(array, array + n, array + length);
  return array[n];
}

template <typename T>
static double
percentile_nrank(T *array, size_t len, double pn)
{
  auto irank = (size_t) std::ceil(len * (pn / 100.0));
  irank = std::min(std::max(irank, static_cast<size_t>(1)), len);
  return get_nth_element(array, len, irank - 1);
}

template <typename T>
static double
percentile_nist(T *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.0;
  if (k == 0)
    percentil = get_nth_element(array, len, 0);
  else if (k >= len)
    percentil = get_nth_element(array, len, len - 1);
  else
    {
      const auto vk = get_nth_element(array, len, k - 1);
      const auto vk2 = get_nth_element(array, len, k);
      percentil = vk + d * (vk2 - vk);
    }

  return percentil;
}

template <typename T>
static double
percentile_numpy(T *array, size_t len, double pn)
{
  const double rank = (len - 1) * (pn / 100.0) + 1.0;
  const size_t k = (size_t) rank;
  const double d = rank - k;

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

          irank = std::min(std::max(irank, static_cast<size_t>(1)), len);

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

  return percentil;
}

template <typename T>
static double
percentile_Rtype8(T *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.0;
  if (k == 0)
    percentil = get_nth_element(array, len, 0);
  else if (k >= len)
    percentil = get_nth_element(array, len, len - 1);
  else
    {
      const auto vk = get_nth_element(array, len, k - 1);
      const auto vk2 = get_nth_element(array, len, k);
      percentil = vk + d * (vk2 - vk);
    }

  return percentil;
}

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

static void
print_percentile_method(size_t len)
{
  const char *method = "unknown";
  // clang-format off
  if      (percentile_method == PercentileMethod::NR8)    method = "NR8 (R’s type=8)";
  else if (percentile_method == PercentileMethod::NRANK)  method = "NRANK (Nearest Rank)";
  else if (percentile_method == PercentileMethod::NIST)   method = "NIST (recommended by NIST)";
  else if (percentile_method == PercentileMethod::NUMPY)
    {
      if      (interpolation_method == InterpolationMethod::LINEAR)
        method = "NUMPY LINEAR (numpy.percentile with linear interpolation option)";
      else if (interpolation_method == InterpolationMethod::LOWER)
        method = "NUMPY LOWER (numpy.percentile with lower interpolation option)";
      else if (interpolation_method == InterpolationMethod::HIGHER)
        method = "NUMPY HIGHER (numpy.percentile with higher interpolation option)";
      else if (interpolation_method == InterpolationMethod::NEAREST)
        method = "NUMPY NEAREST (numpy.percentile with nearest interpolation option)";
    }
  // clang-format on

  cdo_print("Using percentile method: %s with %zu values", method, len);
}

template <typename T>
double
percentile(T *array, size_t len, double pn)
{
  static auto lprint = true;
  if (lprint && Options::cdoVerbose)
    {
      lprint = false;
      print_percentile_method(len);
    }

  percentile_check_number(pn);

  double percentil = 0.0;

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

  return percentil;
}

// Explicit instantiation
template double percentile(float *array, size_t len, double pn);
template double percentile(double *array, size_t len, double pn);

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

  // clang-format off
  if      ("nrank"  == methodname) percentile_method = PercentileMethod::NRANK;
  else if ("nist"   == methodname) percentile_method = PercentileMethod::NIST;
  else if ("numpy"  == methodname) percentile_method = PercentileMethod::NUMPY;
  else if ("rtype8" == methodname) percentile_method = PercentileMethod::NR8;
  else if ("numpy_linear" == methodname)
    {
      percentile_method = PercentileMethod::NUMPY;
      interpolation_method = InterpolationMethod::LINEAR;
    }
  else if ("numpy_lower" == methodname)
    {
      percentile_method = PercentileMethod::NUMPY;
      interpolation_method = InterpolationMethod::LOWER;
    }
  else if ("numpy_higher" == methodname)
    {
      percentile_method = PercentileMethod::NUMPY;
      interpolation_method = InterpolationMethod::HIGHER;
    }
  else if ("numpy_nearest" == methodname)
    {
      percentile_method = PercentileMethod::NUMPY;
      interpolation_method = InterpolationMethod::NEAREST;
    }
  else
    cdo_abort("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 rtype8 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')
*/
