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

#include <vector>
#include <algorithm>

#include "functs.h"
#include "process_int.h"
#include "percentiles.h"

static size_t
fillReducedPoints(const int gridID, const size_t ny, std::vector<int> &reducedPoints, std::vector<int> &cumreducedPoints)
{
  reducedPoints.resize(ny);
  cumreducedPoints.resize(ny);
  gridInqReducedPoints(gridID, reducedPoints.data());
  cumreducedPoints[0] = 0;
  for (size_t j = 1; j < ny; j++) cumreducedPoints[j] = cumreducedPoints[j - 1] + reducedPoints[j - 1];
  size_t nx = reducedPoints[0];
  for (size_t j = 1; j < ny; j++)
    if (reducedPoints[j] > (int) nx) nx = reducedPoints[j];
  return nx;
}

using funcType = double (size_t, const Varray<double> &);
using funcTypeMV = double (size_t, const Varray<double> &, double);
using funcTypeNmissMV = double (size_t, const Varray<double> &, size_t, double);

static void
fieldZonKernel1(const Field &field1, Field &field2, funcTypeNmissMV funcNmissMV)
{
  size_t rnmiss = 0;
  const auto nmiss = field1.nmiss;
  const auto missval = field1.missval;

  const auto ny = gridInqYsize(field1.grid);
  const bool lreduced = gridInqType(field1.grid) == GRID_GAUSSIAN_REDUCED;
  std::vector<int> reducedPoints, cumreducedPoints;
  auto nx = lreduced ? fillReducedPoints(field1.grid, ny, reducedPoints, cumreducedPoints) : gridInqXsize(field1.grid);

  Varray<double> v(nx);

  for (size_t j = 0; j < ny; ++j)
    {
      if (lreduced) nx = reducedPoints[j];
      const size_t offset = lreduced ? cumreducedPoints[j] : j * nx;
      std::copy(field1.vec_d.begin() + offset, field1.vec_d.begin() + offset + nx, v.begin());

      const auto result = funcNmissMV(nx, v, nmiss, missval);
      if (DBL_IS_EQUAL(result, missval)) rnmiss++;
      field2.vec_d[j] = result;
    }

  field2.nmiss = rnmiss;
}

static void
fieldZonKernel2(const Field &field1, Field &field2, funcType func, funcTypeMV funcMV)
{
  size_t rnmiss = 0;
  const auto nmiss = field1.nmiss;
  const auto missval = field1.missval;

  const auto ny = gridInqYsize(field1.grid);
  const bool lreduced = gridInqType(field1.grid) == GRID_GAUSSIAN_REDUCED;
  std::vector<int> reducedPoints, cumreducedPoints;
  auto nx = lreduced ? fillReducedPoints(field1.grid, ny, reducedPoints, cumreducedPoints) : gridInqXsize(field1.grid);

  Varray<double> v(nx);

  for (size_t j = 0; j < ny; ++j)
    {
      if (lreduced) nx = reducedPoints[j];
      const size_t offset = lreduced ? cumreducedPoints[j] : j * nx;
      std::copy(field1.vec_d.begin() + offset, field1.vec_d.begin() + offset + nx, v.begin());

      const auto result = nmiss ? funcMV(nx, v, missval) : func(nx, v);
      if (DBL_IS_EQUAL(result, missval)) rnmiss++;
      field2.vec_d[j] = result;
    }

  field2.nmiss = rnmiss;
}

void
fieldZonMin(const Field &field1, Field &field2)
{
  fieldZonKernel2(field1, field2, varrayMin, varrayMinMV);
}

void
fieldZonMax(const Field &field1, Field &field2)
{
  fieldZonKernel2(field1, field2, varrayMax, varrayMaxMV);
}

void
fieldZonRange(const Field &field1, Field &field2)
{
  fieldZonKernel2(field1, field2, varrayRange, varrayRangeMV);
}

void
fieldZonSum(const Field &field1, Field &field2)
{
  fieldZonKernel2(field1, field2, varraySum, varraySumMV);
}

void
fieldZonMean(const Field &field1, Field &field2)
{
  fieldZonKernel2(field1, field2, varrayMean, varrayMeanMV);
}

void
fieldZonAvg(const Field &field1, Field &field2)
{
  fieldZonKernel2(field1, field2, varrayMean, varrayAvgMV);
}

void
fieldZonVar(const Field &field1, Field &field2)
{
  fieldZonKernel1(field1, field2, varrayVar);
}

void
fieldZonVar1(const Field &field1, Field &field2)
{
  fieldZonKernel1(field1, field2, varrayVar1);
}

void
fieldZonStd(const Field &field1, Field &field2)
{
  size_t rnmiss = 0;
  const auto missval = field1.missval;
  const auto ny = gridInqYsize(field1.grid);

  fieldZonVar(field1, field2);

  for (size_t j = 0; j < ny; j++)
    {
      const auto rstd = varToStd(field2.vec_d[j], missval);
      if (DBL_IS_EQUAL(rstd, missval)) rnmiss++;
      field2.vec_d[j] = rstd;
    }

  field2.nmiss = rnmiss;
}

void
fieldZonStd1(const Field &field1, Field &field2)
{
  size_t rnmiss = 0;
  const auto missval = field1.missval;
  const auto ny = gridInqYsize(field1.grid);

  fieldZonVar1(field1, field2);

  for (size_t j = 0; j < ny; j++)
    {
      const auto rstd = varToStd(field2.vec_d[j], missval);
      if (DBL_IS_EQUAL(rstd, missval)) rnmiss++;
      field2.vec_d[j] = rstd;
    }

  field2.nmiss = rnmiss;
}

void
fieldZonKurt(const Field &field1, Field &field2)
{
  fieldZonKernel1(field1, field2, varrayKurt);
}

void
fieldZonSkew(const Field &field1, Field &field2)
{
  fieldZonKernel1(field1, field2, varraySkew);
}

void
fieldZonPctl(const Field &field1, Field &field2, const int p)
{
  size_t rnmiss = 0;
  const auto missval = field1.missval;

  const auto ny = gridInqYsize(field1.grid);
  const bool lreduced = gridInqType(field1.grid) == GRID_GAUSSIAN_REDUCED;
  std::vector<int> reducedPoints, cumreducedPoints;
  auto nx = lreduced ? fillReducedPoints(field1.grid, ny, reducedPoints, cumreducedPoints) : gridInqXsize(field1.grid);

  Varray<double> v(nx);

  if (field1.nmiss)
    {
      Varray<double> array2(nx);

      for (size_t j = 0; j < ny; j++)
        {
          if (lreduced) nx = reducedPoints[j];
          const size_t offset = lreduced ? cumreducedPoints[j] : j * nx;
          std::copy(field1.vec_d.begin() + offset, field1.vec_d.begin() + offset + nx, v.begin());

          size_t l = 0;
          for (size_t i = 0; i < nx; i++)
            if (!DBL_IS_EQUAL(field1.vec_d[j * nx + i], missval)) array2[l++] = v[i];

          if (l > 0)
            {
              field2.vec_d[j] = percentile(array2.data(), l, p);
            }
          else
            {
              field2.vec_d[j] = missval;
              rnmiss++;
            }
        }
    }
  else
    {
      for (size_t j = 0; j < ny; j++)
        {
          if (lreduced) nx = reducedPoints[j];
          const size_t offset = lreduced ? cumreducedPoints[j] : j * nx;
          std::copy(field1.vec_d.begin() + offset, field1.vec_d.begin() + offset + nx, v.begin());

          if (nx > 0)
            {
              field2.vec_d[j] = percentile(v.data(), nx, p);
            }
          else
            {
              field2.vec_d[j] = missval;
              rnmiss++;
            }
        }
    }

  field2.nmiss = rnmiss;
}

void
fieldZonFunction(const Field &field1, Field &field2, int function)
{
  // clang-format off
  switch (function)
    {
    case func_min:    return fieldZonMin(field1, field2);
    case func_max:    return fieldZonMax(field1, field2);
    case func_range:  return fieldZonRange(field1, field2);
    case func_sum:    return fieldZonSum(field1, field2);
    case func_mean:   return fieldZonMean(field1, field2);
    case func_avg:    return fieldZonAvg(field1, field2);
    case func_std:    return fieldZonStd(field1, field2);
    case func_std1:   return fieldZonStd1(field1, field2);
    case func_var:    return fieldZonVar(field1, field2);
    case func_var1:   return fieldZonVar1(field1, field2);
    case func_kurt:   return fieldZonKurt(field1, field2);
    case func_skew:   return fieldZonSkew(field1, field2);
    default: cdoAbort("%s: function %d not implemented!",  __func__, function);
    }
  // clang-format on
}
