/*
  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 "functs.h"
#include "process_int.h"
#include "percentiles.h"

using funcType1 = double (size_t, const Varray<double> &);
using funcTypeMV1 = double (size_t, const Varray<double> &, double);
using funcType2 = double (size_t, const Varray<double> &, const Varray<double> &, double);
using funcTypeMV2 = double (size_t, const Varray<double> &, const Varray<double> &, double);
using funcType3 = double (size_t, const Varray<double> &, const Varray<double> &, size_t, double);
using funcType4 = double (size_t, const Varray<double> &, size_t, double);

static void
fieldMerKernel1(const Field &field1, Field &field2, funcType1 func, funcTypeMV1 funcMV)
{
  size_t rnmiss = 0;
  const auto nmiss = field1.nmiss;
  const auto missval = field1.missval;
  const auto nx = gridInqXsize(field1.grid);
  const auto ny = gridInqYsize(field1.grid);

  Varray<double> v(ny);

  for (size_t i = 0; i < nx; ++i)
    {
      for (size_t j = 0; j < ny; j++) v[j] = field1.vec_d[j * nx + i];

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

  field2.nmiss = rnmiss;
}

static void
fieldMerKernel2(const Field &field1, Field &field2, funcType2 func, funcTypeMV2 funcMV)
{
  size_t rnmiss = 0;
  const auto nmiss = field1.nmiss;
  const auto missval = field1.missval;
  const auto nx = gridInqXsize(field1.grid);
  const auto ny = gridInqYsize(field1.grid);

  Varray<double> v(ny), w(ny);

  for (size_t i = 0; i < nx; ++i)
    {
      for (size_t j = 0; j < ny; j++) v[j] = field1.vec_d[j * nx + i];
      for (size_t j = 0; j < ny; j++) w[j] = field1.weightv[j * nx + i];

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

  field2.nmiss = rnmiss;
}

static void
fieldMerKernel3(const Field &field1, Field &field2, funcType3 func)
{
  size_t rnmiss = 0;
  const auto nmiss = field1.nmiss;
  const auto missval = field1.missval;
  const auto nx = gridInqXsize(field1.grid);
  const auto ny = gridInqYsize(field1.grid);

  Varray<double> v(ny), w(ny);

  for (size_t i = 0; i < nx; ++i)
    {
      for (size_t j = 0; j < ny; j++) v[j] = field1.vec_d[j * nx + i];
      for (size_t j = 0; j < ny; j++) w[j] = field1.weightv[j * nx + i];

      const auto result = func(ny, v, w, nmiss, missval);
      if (DBL_IS_EQUAL(result, missval)) rnmiss++;
      field2.vec_d[i] = result;
    }

  field2.nmiss = rnmiss;
}

static void
fieldMerKernel4(const Field &field1, Field &field2, funcType4 func)
{
  size_t rnmiss = 0;
  const auto nmiss = field1.nmiss;
  const auto missval = field1.missval;
  const auto nx = gridInqXsize(field1.grid);
  const auto ny = gridInqYsize(field1.grid);

  Varray<double> v(ny);

  for (size_t i = 0; i < nx; ++i)
    {
      for (size_t j = 0; j < ny; j++) v[j] = field1.vec_d[j * nx + i];

      const auto result = func(ny, v, nmiss, missval);
      if (DBL_IS_EQUAL(result, missval)) rnmiss++;
      field2.vec_d[i] = result;
    }

  field2.nmiss = rnmiss;
}

static void
fieldMerMin(const Field &field1, Field &field2)
{
  fieldMerKernel1(field1, field2, varrayMin, varrayMinMV);
}

static void
fieldMerMax(const Field &field1, Field &field2)
{
  fieldMerKernel1(field1, field2, varrayMax, varrayMaxMV);
}

static void
fieldMerRange(const Field &field1, Field &field2)
{
  fieldMerKernel1(field1, field2, varrayRange, varrayRangeMV);
}

static void
fieldMerSum(const Field &field1, Field &field2)
{
  fieldMerKernel1(field1, field2, varraySum, varraySumMV);
}

static void
fieldMerMeanw(const Field &field1, Field &field2)
{
  fieldMerKernel2(field1, field2, varrayWeightedMean, varrayWeightedMeanMV);
}

static void
fieldMerAvgw(const Field &field1, Field &field2)
{
  fieldMerKernel2(field1, field2, varrayWeightedMean, varrayWeightedAvgMV);
}

static void
fieldMerVarw(const Field &field1, Field &field2)
{
  fieldMerKernel3(field1, field2, varrayWeightedVar);
}

static void
fieldMerVar1w(const Field &field1, Field &field2)
{
  fieldMerKernel3(field1, field2, varrayWeightedVar1);
}

static void
fieldMerStdw(const Field &field1, Field &field2)
{
  size_t rnmiss = 0;
  const auto missval = field1.missval;

  const auto nx = gridInqXsize(field1.grid);

  fieldMerVarw(field1, field2);

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

  field2.nmiss = rnmiss;
}

static void
fieldMerStd1w(const Field &field1, Field &field2)
{
  size_t rnmiss = 0;
  const auto missval = field1.missval;

  const auto nx = gridInqXsize(field1.grid);

  fieldMerVar1w(field1, field2);

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

  field2.nmiss = rnmiss;
}

static void
fieldMerKurt(const Field &field1, Field &field2)
{
  fieldMerKernel4(field1, field2, varrayKurt);
}

static void
fieldMerSkew(const Field &field1, Field &field2)
{
  fieldMerKernel4(field1, field2, varraySkew);
}

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

  const auto nx = gridInqXsize(field1.grid);
  const auto ny = gridInqYsize(field1.grid);

  Varray<double> array2(nx);

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

          if (l > 0)
            {
              field2.vec_d[i] = percentile(array2.data(), l, p);
            }
          else
            {
              field2.vec_d[i] = missval;
              rnmiss++;
            }
        }
    }
  else
    {
      for (size_t i = 0; i < nx; i++)
        {
          if (ny > 0)
            {
              for (size_t j = 0; j < ny; j++) array2[j] = field1.vec_d[j * nx + i];
              field2.vec_d[i] = percentile(array2.data(), ny, p);
            }
          else
            {
              field2.vec_d[i] = missval;
              rnmiss++;
            }
        }
    }

  field2.nmiss = rnmiss;
}

void
fieldMerFunction(const Field &field1, Field &field2, int function)
{
  // clang-format off
  switch (function)
    {
    case func_min:    return fieldMerMin(field1, field2);
    case func_max:    return fieldMerMax(field1, field2);
    case func_range:  return fieldMerRange(field1, field2);
    case func_sum:    return fieldMerSum(field1, field2);
    case func_meanw:  return fieldMerMeanw(field1, field2);
    case func_avgw:   return fieldMerAvgw(field1, field2);
    case func_stdw:   return fieldMerStdw(field1, field2);
    case func_std1w:  return fieldMerStd1w(field1, field2);
    case func_varw:   return fieldMerVarw(field1, field2);
    case func_var1w:  return fieldMerVar1w(field1, field2);
    case func_kurt:   return fieldMerKurt(field1, field2);
    case func_skew:   return fieldMerSkew(field1, field2);
    default: cdoAbort("%s: function %d not implemented!", __func__, function);
    }
  // clang-format on
}
