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

  Author: Uwe Schulzweida

*/

/*
   This module contains the following operators:

      Fldstat    fldrange        Field range (max-min)
      Fldstat    fldmin          Field minimum
      Fldstat    fldmax          Field maximum
      Fldstat    fldsum          Field sum
      Fldstat    fldmean         Field mean
      Fldstat    fldavg          Field average
      Fldstat    fldstd          Field standard deviation
      Fldstat    fldstd1         Field standard deviation [Normalize by (n-1)]
      Fldstat    fldvar          Field variance
      Fldstat    fldvar1         Field variance [Normalize by (n-1)]
      Fldstat    fldpctl         Field percentiles
*/

#include <cdi.h>

#include "cdo_options.h"
#include "process_int.h"
#include "param_conversion.h"
#include <mpim_grid.h>
#include "pmlist.h"
#include "cdo_zaxis.h"

void grid_cell_area(int gridID, double *array);

template <typename T>
static void
print_location_LL(int operfunc, int vlistID, int varID, int levelID, int gridID, T sglval, const Varray<T> &fieldvec, int64_t vdate,
                  int vtime)
{
  static auto showHeader = true;
  const auto code = vlistInqVarCode(vlistID, varID);

  int year, month, day, hour, minute, second;
  cdiDecodeDate(vdate, &year, &month, &day);
  cdiDecodeTime(vtime, &hour, &minute, &second);

  const auto lreg2d = (gridInqType(gridID) == GRID_GAUSSIAN || gridInqType(gridID) == GRID_LONLAT);

  if (lreg2d || gridInqType(gridID) == GRID_CURVILINEAR || gridInqType(gridID) == GRID_UNSTRUCTURED)
    {
      const auto zaxisID = vlistInqVarZaxis(vlistID, varID);
      const auto level = cdo_zaxis_inq_level(zaxisID, levelID);
      const auto gridsize = gridInqSize(gridID);
      const auto nlon = gridInqXsize(gridID);
      for (size_t ij = 0; ij < gridsize; ++ij)
        {
          if (DBL_IS_EQUAL(fieldvec[ij], sglval))
            {
              const auto j = ij / nlon;
              const auto i = ij - j * nlon;
              const auto xval = gridInqXval(gridID, lreg2d ? i : ij);
              const auto yval = gridInqYval(gridID, lreg2d ? j : ij);
              if (showHeader)
                {
                  fprintf(stdout, "  Date     Time     Code  Level   Lon      Lat          %s\n",
                          operfunc == FieldFunc_Min ? "Minval" : "Maxval");
                  showHeader = false;
                }

              fprintf(stdout, "%4.4d-%2.2d-%2.2d %2.2d:%2.2d:%2.2d %3d %7g %9.7g %9.7g %12.5g\n", year, month, day, hour, minute,
                      second, code, level, xval, yval, sglval);
              break;
            }
        }
    }
}

template <typename T>
static void
field_mul_weights(size_t len, Varray<T> &v1, const Varray<double> &v2, size_t nmiss, T missval)
{
  if (nmiss)
    {
      for (size_t i = 0; i < len; ++i)
        if (!DBL_IS_EQUAL(v1[i], missval)) v1[i] *= v2[i];
    }
  else
    {
      for (size_t i = 0; i < len; ++i) v1[i] *= v2[i];
    }
}

static void
field_mul_weights(Field &field)
{
  if (field.memType == MemType::Float)
    field_mul_weights(field.size, field.vec_f, field.weightv, field.nmiss, (float) field.missval);
  else
    field_mul_weights(field.size, field.vec_d, field.weightv, field.nmiss, field.missval);
}

static void
printWeightsWarning(int ngrids, const char *varname)
{
  if (ngrids == 1)
    cdo_warning("Grid cell bounds not available, using constant grid cell area weights!");
  else
    cdo_warning("Grid cell bounds not available, using constant grid cell area weights for variable %s!", varname);
}

static void
fldstatGetParameter(bool &useweights)
{
  const auto pargc = cdo_operator_argc();
  if (pargc)
    {
      auto pargv = cdo_get_oper_argv();

      KVList kvlist;
      kvlist.name = "FLDSTAT";
      if (kvlist.parse_arguments(pargc, pargv) != 0) cdo_abort("Parse error!");
      if (Options::cdoVerbose) kvlist.print();

      for (const auto &kv : kvlist)
        {
          const auto &key = kv.key;
          if (kv.nvalues > 1) cdo_abort("Too many values for parameter key >%s<!", key.c_str());
          if (kv.nvalues < 1) cdo_abort("Missing value for parameter key >%s<!", key.c_str());
          const auto &value = kv.values[0];

          if (key == "weights")
            useweights = parameter_to_bool(value);
          else
            cdo_abort("Invalid parameter key >%s<!", key.c_str());
        }
    }
}

void *
Fldstat(void *process)
{
  cdo_initialize(process);

  // clang-format off
                cdo_operator_add("fldrange",  FieldFunc_Range,  0, nullptr);
                cdo_operator_add("fldmin",    FieldFunc_Min,    0, nullptr);
                cdo_operator_add("fldmax",    FieldFunc_Max,    0, nullptr);
                cdo_operator_add("fldsum",    FieldFunc_Sum,    0, nullptr);
  auto FLDINT = cdo_operator_add("fldint",    FieldFunc_Sum,    0, nullptr);
                cdo_operator_add("fldmean",   FieldFunc_Meanw,  1, nullptr);
                cdo_operator_add("fldavg",    FieldFunc_Avgw,   1, nullptr);
                cdo_operator_add("fldstd",    FieldFunc_Stdw,   1, nullptr);
                cdo_operator_add("fldstd1",   FieldFunc_Std1w,  1, nullptr);
                cdo_operator_add("fldvar",    FieldFunc_Varw,   1, nullptr);
                cdo_operator_add("fldvar1",   FieldFunc_Var1w,  1, nullptr);
                cdo_operator_add("fldskew",   FieldFunc_Skew,   0, nullptr);
                cdo_operator_add("fldkurt",   FieldFunc_Kurt,   0, nullptr);
                cdo_operator_add("fldmedian", FieldFunc_Median, 0, nullptr);
                cdo_operator_add("fldpctl",   FieldFunc_Pctl,   0, nullptr);
  // clang-format on

  const auto operatorID = cdo_operator_id();
  const auto operfunc = cdo_operator_f1(operatorID);
  const auto needWeights = (cdo_operator_f2(operatorID) != 0);
  auto useweights = true;
  const auto needCellarea = (operatorID == FLDINT);

  double pn = 0.0;
  if (operfunc == FieldFunc_Pctl)
    {
      operator_input_arg("percentile number");
      pn = parameter_to_double(cdo_operator_argv(0));
    }
  else if (needWeights)
    {
      fldstatGetParameter(useweights);
    }
  else
    {
      operator_check_argc(0);
    }

  const auto streamID1 = cdo_open_read(0);

  const auto vlistID1 = cdo_stream_inq_vlist(streamID1);
  const auto vlistID2 = vlistDuplicate(vlistID1);

  const auto taxisID1 = vlistInqTaxis(vlistID1);
  const auto taxisID2 = taxisDuplicate(taxisID1);
  vlistDefTaxis(vlistID2, taxisID2);

  const auto slon = 0.0, slat = 0.0;
  const auto gridID2 = gridCreate(GRID_LONLAT, 1);
  gridDefXsize(gridID2, 1);
  gridDefYsize(gridID2, 1);
  gridDefXvals(gridID2, &slon);
  gridDefYvals(gridID2, &slat);

  const auto ngrids = vlistNgrids(vlistID1);

  for (int index = 0; index < ngrids; index++) vlistChangeGridIndex(vlistID2, index, gridID2);

  const auto streamID2 = cdo_open_write(1);

  cdo_def_vlist(streamID2, vlistID2);

  const auto gridsizemax = vlistGridsizeMax(vlistID1);

  Field field;
  if (needWeights || needCellarea)
    {
      field.weightv.resize(gridsizemax);
      if (needWeights && !useweights)
        {
          cdo_print("Using constant grid cell area weights!");
          for (size_t i = 0; i < gridsizemax; ++i) field.weightv[i] = 1;
        }
    }

  VarList varList1;
  varListInit(varList1, vlistID1);

  int lastgrid = -1;
  int tsID = 0;
  while (true)
    {
      const auto nrecs = cdo_stream_inq_timestep(streamID1, tsID);
      if (nrecs == 0) break;

      cdo_taxis_copy_timestep(taxisID2, taxisID1);
      cdo_def_timestep(streamID2, tsID);

      const auto vdate = taxisInqVdate(taxisID1);
      const auto vtime = taxisInqVtime(taxisID1);

      for (int recID = 0; recID < nrecs; recID++)
        {
          int varID, levelID;
          cdo_inq_record(streamID1, &varID, &levelID);
          field.init(varList1[varID]);
          cdo_read_record(streamID1, field);

          if (needWeights && field.grid != lastgrid)
            {
              lastgrid = field.grid;
              field.weightv[0] = 1;
              if (useweights && field.size > 1)
                {
                  const auto wstatus = (gridWeights(field.grid, field.weightv.data()) != 0);
                  if (wstatus && tsID == 0 && levelID == 0) printWeightsWarning(ngrids, varList1[varID].name);
                }
            }

          if (needCellarea && field.grid != lastgrid)
            {
              lastgrid = field.grid;
              grid_cell_area(field.grid, field.weightv.data());
            }

          if (needCellarea) field_mul_weights(field);

          auto singleValue = (operfunc == FieldFunc_Pctl) ? field_pctl(field, pn) : field_function(field, operfunc);

          if (Options::cdoVerbose && (operfunc == FieldFunc_Min || operfunc == FieldFunc_Max))
            {
              if (field.memType == MemType::Float)
                print_location_LL(operfunc, vlistID1, varID, levelID, field.grid, (float) singleValue, field.vec_f, vdate, vtime);
              else
                print_location_LL(operfunc, vlistID1, varID, levelID, field.grid, singleValue, field.vec_d, vdate, vtime);
            }

          const size_t nmiss = DBL_IS_EQUAL(singleValue, field.missval);

          cdo_def_record(streamID2, varID, levelID);
          cdo_write_record(streamID2, &singleValue, nmiss);
        }

      tsID++;
    }

  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  vlistDestroy(vlistID2);

  cdo_finish();

  return nullptr;
}
