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

/*
   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 "functs.h"
#include "process_int.h"
#include "param_conversion.h"
#include <mpim_grid.h>
#include "pmlist.h"
#include "cdo_zaxis.h"

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 = cdoZaxisInqLevel(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 == func_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;
            }
        }
    }
}

static void
fldstatGetParameter(bool *weights)
{
  const auto pargc = operatorArgc();
  if (pargc)
    {
      auto pargv = cdoGetOperArgv();

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

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

          if (key == "weights")
            *weights = parameter2bool(value);
          else
            cdoAbort("Invalid parameter key >%s<!", key.c_str());
        }
    }
}

static void
addOperators(void)
{
  // clang-format off
  cdoOperatorAdd("fldrange", func_range,  0, nullptr);
  cdoOperatorAdd("fldmin",   func_min,    0, nullptr);
  cdoOperatorAdd("fldmax",   func_max,    0, nullptr);
  cdoOperatorAdd("fldsum",   func_sum,    0, nullptr);
  cdoOperatorAdd("fldmean",  func_meanw,  1, nullptr);
  cdoOperatorAdd("fldavg",   func_avgw,   1, nullptr);
  cdoOperatorAdd("fldstd",   func_stdw,   1, nullptr);
  cdoOperatorAdd("fldstd1",  func_std1w,  1, nullptr);
  cdoOperatorAdd("fldvar",   func_varw,   1, nullptr);
  cdoOperatorAdd("fldvar1",  func_var1w,  1, nullptr);
  cdoOperatorAdd("fldskew",  func_skew,   0, nullptr);
  cdoOperatorAdd("fldkurt",  func_kurt,   0, nullptr);
  cdoOperatorAdd("fldpctl",  func_pctl,   0, nullptr);
  // clang-format on
}

void *
Fldstat(void *process)
{
  int lastgrid = -1;
  int nrecs;
  int varID, levelID;

  cdoInitialize(process);

  addOperators();

  const auto operatorID = cdoOperatorID();
  const auto operfunc = cdoOperatorF1(operatorID);
  const bool needWeights = cdoOperatorF2(operatorID) != 0;

  double pn = 0;
  if (operfunc == func_pctl)
    {
      operatorInputArg("percentile number");
      pn = parameter2double(cdoOperatorArgv(0));
    }

  bool useweights = true;
  if (needWeights) fldstatGetParameter(&useweights);

  const auto streamID1 = cdoOpenRead(0);

  const auto vlistID1 = cdoStreamInqVlist(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 = cdoOpenWrite(1);

  cdoDefVlist(streamID2, vlistID2);

  const auto gridsizemax = vlistGridsizeMax(vlistID1);

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

  VarList varList1;
  varListInit(varList1, vlistID1);

  int tsID = 0;
  while ((nrecs = cdoStreamInqTimestep(streamID1, tsID)))
    {
      taxisCopyTimestep(taxisID2, taxisID1);
      cdoDefTimestep(streamID2, tsID);

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

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

          if (needWeights && field.grid != lastgrid)
            {
              lastgrid = field.grid;
              field.weightv[0] = 1;
              if (useweights && field.size > 1)
                {
                  const bool wstatus = gridWeights(field.grid, field.weightv.data()) != 0;
                  if (wstatus && tsID == 0 && levelID == 0)
                    {
                      if (ngrids == 1)
                        cdoWarning("Grid cell bounds not available, using constant grid cell area weights!");
                      else
                        cdoWarning("Grid cell bounds not available, using constant grid cell area weights for variable %s!",
                                   varList1[varID].name);
                    }
                }
            }

          auto singleValue = (operfunc == func_pctl) ? fieldPctl(field, pn) : fieldFunction(field, operfunc);

          if (Options::cdoVerbose && (operfunc == func_min || operfunc == func_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);

          cdoDefRecord(streamID2, varID, levelID);
          cdoWriteRecord(streamID2, &singleValue, nmiss);
        }

      tsID++;
    }

  cdoStreamClose(streamID2);
  cdoStreamClose(streamID1);

  vlistDestroy(vlistID2);

  cdoFinish();

  return nullptr;
}
