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

  Copyright (C) 2006 Brockmann Consult

  Author: Ralf Quast
          Uwe Schulzweida

*/

/*
   This module contains the following operators:

      Ydrunpctl    ydrunpctl         Multi-year daily running percentiles
*/

#include <cdi.h>
#include "calendar.h"

#include "cdo_options.h"
#include "cdo_vlist.h"
#include "datetime.h"
#include "process_int.h"
#include "param_conversion.h"
#include "percentiles_hist.h"
#include "percentiles.h"
#include "pmlist.h"
#include "field_functions.h"

void *
Ydrunpctl(void *process)
{
  constexpr int MaxDays = 373;
  CdiDateTime vDateTimes1[MaxDays] = { };
  CdiDateTime vDateTimes2[MaxDays] = { };
  int nsets[MaxDays] = { 0 };
  std::vector<bool> vars2(MaxDays, false);
  HistogramSet hsets[MaxDays];

  cdo_initialize(process);
  cdo_operator_add("ydrunpctl", FieldFunc_Pctl, 0, nullptr);

  const auto pn = parameter_to_double(cdo_operator_argv(0));
  const auto ndates = parameter_to_int(cdo_operator_argv(1));

  std::string percMethod, readMethod;
  if (cdo_operator_argc() > 2)
    {
      auto params = cdo_get_oper_argv();
      params = std::vector<std::string>(params.begin() + 2, params.end());
      KVList kvlist;
      if (kvlist.parse_arguments(cdo_operator_argc() - 2, params) != 0) cdo_abort("Argument parse error!");
      auto kv = kvlist.search("pm");
      if (kv && kv->nvalues > 0) percMethod = parameter_to_word(kv->values[0]);
      kv = kvlist.search("rm");
      if (kv && kv->nvalues > 0) readMethod = parameter_to_word(kv->values[0]);
    }

  if (percMethod == "r8") percentile_set_method("rtype8");

  const auto streamID1 = cdo_open_read(0);
  const auto streamID2 = cdo_open_read(1);
  const auto streamID3 = cdo_open_read(2);

  const auto vlistID1 = cdo_stream_inq_vlist(streamID1);
  const auto vlistID2 = cdo_stream_inq_vlist(streamID2);
  const auto vlistID3 = cdo_stream_inq_vlist(streamID3);
  const auto vlistID4 = vlistDuplicate(vlistID1);

  vlist_compare(vlistID1, vlistID2, CMP_ALL);
  vlist_compare(vlistID1, vlistID3, CMP_ALL);

  const auto taxisID1 = vlistInqTaxis(vlistID1);
  const auto taxisID2 = vlistInqTaxis(vlistID2);
  const auto taxisID3 = vlistInqTaxis(vlistID3);
  // TODO - check that time axes 2 and 3 are equal

  const auto taxisID4 = taxisDuplicate(taxisID1);
  if (taxisHasBounds(taxisID4)) taxisDeleteBounds(taxisID4);
  vlistDefTaxis(vlistID4, taxisID4);

  const auto dpy = calendar_dpy(taxisInqCalendar(taxisID1));

  const auto streamID4 = cdo_open_write(3);
  cdo_def_vlist(streamID4, vlistID4);

  const auto ntsteps = vlistNtsteps(vlistID1);
  const auto nvars = vlistNvars(vlistID1);

  const auto maxrecs = vlistNrecs(vlistID1);
  std::vector<RecordInfo> recList(maxrecs);

  FieldVector constFields(maxrecs);

  Field field1, field2;

  VarList varList1;
  varListInit(varList1, vlistID1);

  std::vector<CdiDateTime> cdiDateTimes(ndates + 1);

  FieldVector3D vars1(ndates + 1);
  for (int its = 0; its < ndates; its++) fields_from_vlist(vlistID1, vars1[its], FIELD_VEC | FIELD_NAT);

  int startYear = 0;
  int tsID = 0;

  while (true)
    {
      const auto nrecs = cdo_stream_inq_timestep(streamID2, tsID);
      if (nrecs == 0) break;

      if (nrecs != cdo_stream_inq_timestep(streamID3, tsID))
        cdo_abort("Number of records at time step %d of %s and %s differ!", tsID + 1, cdo_get_stream_name(1),
                  cdo_get_stream_name(2));

      const auto vDateTime = taxisInqVdatetime(taxisID2);

      if (cdiDateTime_isNE(vDateTime, taxisInqVdatetime(taxisID3)))
        cdo_abort("Verification dates at time step %d of %s and %s differ!", tsID + 1, cdo_get_stream_name(1),
                  cdo_get_stream_name(2));

      // if (Options::cdoVerbose) cdo_print("process timestep: %d %d %d", tsID + 1, vdate, vtime);

      const auto dayOfYear = decode_day_of_year(vDateTime.date);
      if (dayOfYear < 0 || dayOfYear >= MaxDays) cdo_abort("Day %d out of range!", dayOfYear);

      vDateTimes2[dayOfYear] = vDateTime;

      if (!vars2[dayOfYear])
        {
          vars2[dayOfYear] = true;
          hsets[dayOfYear].create(nvars, ntsteps);

          for (int varID = 0; varID < nvars; ++varID)
            hsets[dayOfYear].createVarLevels(varID, varList1[varID].nlevels, varList1[varID].gridsize);
        }

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

          cdo_inq_record(streamID3, &varID, &levelID);
          field2.init(varList1[varID]);
          cdo_read_record(streamID3, field2);

          hsets[dayOfYear].defVarLevelBounds(varID, levelID, field1, field2);
        }

      tsID++;
    }

  for (tsID = 0; tsID < ndates; ++tsID)
    {
      const auto nrecs = cdo_stream_inq_timestep(streamID1, tsID);
      if (nrecs == 0) cdo_abort("File has less then %d timesteps!", ndates);

      cdiDateTimes[tsID] = taxisInqVdatetime(taxisID1);

      for (int recID = 0; recID < nrecs; ++recID)
        {
          int varID, levelID;
          cdo_inq_record(streamID1, &varID, &levelID);

          if (tsID == 0)
            {
              recList[recID].varID = varID;
              recList[recID].levelID = levelID;
              recList[recID].lconst = (varList1[varID].timetype == TIME_CONSTANT);
            }

          if (tsID == 0 && recList[recID].lconst)
            {
              constFields[recID].init(varList1[varID]);
              cdo_read_record(streamID1, constFields[recID]);
            }
          else
            {
              cdo_read_record(streamID1, vars1[tsID][varID][levelID]);
            }
        }
    }

  while (true)
    {
      cdiDateTimes[ndates] = datetime_avg(dpy, ndates, cdiDateTimes);

      const auto vDateTime = cdiDateTimes[ndates];

      const auto dayOfYear = decode_day_of_year(vDateTime.date);
      if (dayOfYear < 0 || dayOfYear >= MaxDays) cdo_abort("Day %d out of range!", dayOfYear);

      vDateTimes1[dayOfYear] = vDateTime;

      if (!vars2[dayOfYear]) cdo_abort("No data for day %d in %s and %s", dayOfYear, cdo_get_stream_name(1), cdo_get_stream_name(2));

      for (int varID = 0; varID < nvars; ++varID)
        {
          if (varList1[varID].timetype == TIME_CONSTANT) continue;

          const auto nlevels = varList1[varID].nlevels;
          for (int levelID = 0; levelID < nlevels; ++levelID)
            for (int inp = 0; inp < ndates; ++inp) hsets[dayOfYear].addVarLevelValues(varID, levelID, vars1[inp][varID][levelID]);
        }

      cdiDateTimes[ndates] = cdiDateTimes[0];
      vars1[ndates] = vars1[0];

      for (int inp = 0; inp < ndates; ++inp)
        {
          cdiDateTimes[inp] = cdiDateTimes[inp + 1];
          vars1[inp] = vars1[inp + 1];
        }

      const auto nrecs = cdo_stream_inq_timestep(streamID1, tsID);
      if (nrecs == 0) break;

      cdiDateTimes[ndates - 1] = taxisInqVdatetime(taxisID1);

      for (int recID = 0; recID < nrecs; ++recID)
        {
          int varID, levelID;
          cdo_inq_record(streamID1, &varID, &levelID);
          cdo_read_record(streamID1, vars1[ndates - 1][varID][levelID]);
        }

      nsets[dayOfYear] += ndates;
      tsID++;
    }

  if (readMethod == "c" && cdo_assert_files_only())
    {
      const auto endYear = cdiDateTimes[ndates - 1].date.year;
      const auto cdiStream = streamOpenRead(cdo_get_stream_name(0));
      const auto cdiVlistID = streamInqVlist(cdiStream);
      const auto cdiTaxisID = vlistInqTaxis(cdiVlistID);
      int missTimes = 0;
      for (missTimes = 0; missTimes < ndates - 1; missTimes++)
        {
          const auto nrecs = streamInqTimestep(cdiStream, missTimes);
          if (nrecs == 0) break;

          cdiDateTimes[ndates - 1] = taxisInqVdatetime(cdiTaxisID);
          cdiDateTimes[ndates - 1].date.year = endYear + 1;

          for (int recID = 0; recID < nrecs; ++recID)
            {
              int varID, levelID;
              streamInqRecord(cdiStream, &varID, &levelID);
              auto &pvars1 = vars1[ndates - 1][varID][levelID];
              if (pvars1.memType == MemType::Float)
                streamReadRecordF(cdiStream, pvars1.vec_f.data(), &pvars1.nmiss);
              else
                streamReadRecord(cdiStream, pvars1.vec_d.data(), &pvars1.nmiss);
            }

          cdiDateTimes[ndates] = datetime_avg(dpy, ndates, cdiDateTimes);
          auto vDateTime = cdiDateTimes[ndates];
          if (vDateTime.date.year > endYear) vDateTime.date.year = startYear;

          const auto dayOfYear = decode_day_of_year(vDateTime.date);
          if (dayOfYear < 0 || dayOfYear >= MaxDays) cdo_abort("Day %d out of range!", dayOfYear);

          nsets[dayOfYear] += ndates;

          for (int varID = 0; varID < nvars; ++varID)
            {
              if (varList1[varID].timetype == TIME_CONSTANT) continue;

              const auto nlevels = varList1[varID].nlevels;
              for (int levelID = 0; levelID < nlevels; ++levelID)
                for (int inp = 0; inp < ndates; ++inp) hsets[dayOfYear].addVarLevelValues(varID, levelID, vars1[inp][varID][levelID]);
            }

          cdiDateTimes[ndates] = cdiDateTimes[0];
          vars1[ndates] = vars1[0];

          for (int inp = 0; inp < ndates; ++inp)
            {
              cdiDateTimes[inp] = cdiDateTimes[inp + 1];
              vars1[inp] = vars1[inp + 1];
            }
        }

      if (missTimes != ndates - 1) cdo_abort("Addding the missing values when using the 'readMethod' method was not possible");

      streamClose(cdiStream);
    }
  else if (readMethod == "c")
    cdo_warning("Operators cannot be piped in circular mode");

  /*
  int outyear = 1e9;
  for (dayOfYear = 0; dayOfYear < MaxDays; dayOfYear++)
    if (nsets[dayOfYear])
      {
        int year, month, day;
        cdiDate_decode(vDateTimes1[dayOfYear].date, &year, &month, &day);
        if (year < outyear) outyear = year;
      }

  for (dayOfYear = 0; dayOfYear < MaxDays; dayOfYear++)
    if (nsets[dayOfYear])
      {
        int year, month, day;
        cdiDate_decode(vDateTimes1[dayOfYear].date, &year, &month, &day);
        vDateTimes1[dayOfYear].date = cdiDate_encode(outyear, month, day);
      }
  */
  int otsID = 0;
  for (int dayOfYear = 0; dayOfYear < MaxDays; dayOfYear++)
    if (nsets[dayOfYear])
      {
        if (decode_month_and_day(vDateTimes1[dayOfYear].date) != decode_month_and_day(vDateTimes2[dayOfYear].date))
          cdo_abort("Verification dates for day %d of %s, %s and %s are different!", dayOfYear, cdo_get_stream_name(0),
                    cdo_get_stream_name(1));

        taxisDefVdatetime(taxisID4, vDateTimes1[dayOfYear]);
        cdo_def_timestep(streamID4, otsID);

        for (int recID = 0; recID < maxrecs; ++recID)
          {
            if (otsID && recList[recID].lconst) continue;

            const auto varID = recList[recID].varID;
            const auto levelID = recList[recID].levelID;
            cdo_def_record(streamID4, varID, levelID);

            if (recList[recID].lconst)
              {
                cdo_write_record(streamID4, constFields[recID]);
              }
            else
              {
                field1.init(varList1[varID]);
                hsets[dayOfYear].getVarLevelPercentiles(field1, varID, levelID, pn);
                cdo_write_record(streamID4, field1);
              }
          }

        otsID++;
      }

  cdo_stream_close(streamID4);
  cdo_stream_close(streamID3);
  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  cdo_finish();

  return nullptr;
}
