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

  Copyright (C) 2006 Brockmann Consult
  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:

      Ydrunpctl    ydrunpctl         Multi-year daily running percentiles
*/

#include <cdi.h>

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

void *
Ydrunpctl(void *process)
{
  constexpr int MaxDays = 373;
  int its;
  size_t nmiss;
  int64_t vdates1[MaxDays] = { 0 }, vdates2[MaxDays] = { 0 };
  int vtimes1[MaxDays] = { 0 } /*, vtimes2[MaxDays]*/;
  int nsets[MaxDays] = { 0 };
  FieldVector2D vars2[MaxDays];
  HistogramSet hsets[MaxDays];

  cdoInitialize(process);
  cdoOperatorAdd("ydrunpctl", func_pctl, 0, nullptr);

  const auto pn = parameter2double(cdoOperatorArgv(0));
  const auto ndates = parameter2int(cdoOperatorArgv(1));

  char *percMethod = nullptr, *readMethod = nullptr;
  if (operatorArgc() > 2)
    {
      auto params = cdoGetOperArgv();
      params = std::vector<std::string>(params.begin() + 2, params.end());
      KVList kvlist;
      if (kvlist.parseArguments(operatorArgc()-2, params) != 0) cdoAbort("Argument parse error!"); 
      auto kv = kvlist.search("pm");
      if (kv && kv->nvalues > 0) percMethod = strdup(kv->values[0].c_str());
      kv = kvlist.search("rm");
      if (kv && kv->nvalues > 0) readMethod = strdup(kv->values[0].c_str());
    }

  if (percMethod)
    {
      if (strcmp(percMethod,"r8") == 0) percentile_set_method("rtype8");
      free(percMethod);
    }

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

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

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

  VarList varList1, varList2;
  varListInit(varList1, vlistID1);
  varListInit(varList2, vlistID2);

  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 = cdoOpenWrite(3);
  cdoDefVlist(streamID4, vlistID4);

  const auto nvars = vlistNvars(vlistID1);

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

  const auto gridsizemax = vlistGridsizeMax(vlistID1);

  Field field;
  field.resize(gridsizemax);

  std::vector<CdoDateTime> datetime(ndates + 1);

  FieldVector3D vars1(ndates + 1);
  for (its = 0; its < ndates; its++) fieldsFromVlist(vlistID1, vars1[its], FIELD_VEC);

  int tsID = 0;
  int startYear = 0, endYear = 0, mon = 0, day = 0;
  while (true)
    {
      const auto nrecs = cdoStreamInqTimestep(streamID2, tsID);
      if (nrecs == 0) break;

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

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

      if (vdate != taxisInqVdate(taxisID3))
        cdoAbort("Verification dates at time step %d of %s and %s differ!", tsID + 1, cdoGetStreamName(1), cdoGetStreamName(2));

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

      const auto dayoy = decodeDayOfYear(vdate);
      if (dayoy < 0 || dayoy >= MaxDays) cdoAbort("Day %d out of range!", dayoy);

      vdates2[dayoy] = vdate;
      // vtimes2[dayoy] = vtime;

      if (!vars2[dayoy].size())
        {
          fieldsFromVlist(vlistID2, vars2[dayoy], FIELD_VEC);
          hsets[dayoy].create(nvars);

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

      for (int recID = 0; recID < nrecs; recID++)
        {
          int varID, levelID;
          cdoInqRecord(streamID2, &varID, &levelID);
          cdoReadRecord(streamID2, vars2[dayoy][varID][levelID].vec_d.data(), &nmiss);
          vars2[dayoy][varID][levelID].nmiss = nmiss;
        }

      for (int recID = 0; recID < nrecs; recID++)
        {
          int varID, levelID;
          cdoInqRecord(streamID3, &varID, &levelID);
          cdoReadRecord(streamID3, field.vec_d.data(), &field.nmiss);
          field.size = varList2[varID].gridsize;
          field.grid = varList2[varID].gridID;
          field.missval = varList2[varID].missval;

          hsets[dayoy].defVarLevelBounds(varID, levelID, vars2[dayoy][varID][levelID], field);
        }

      tsID++;
    }

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

      datetime[tsID].date = taxisInqVdate(taxisID1);
      datetime[tsID].time = taxisInqVtime(taxisID1);

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

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

          cdoReadRecord(streamID1, vars1[tsID][varID][levelID].vec_d.data(), &nmiss);
          vars1[tsID][varID][levelID].nmiss = nmiss;
        }
    }

  while (true)
    {
      datetime_avg(dpy, ndates, datetime.data());

      const auto vdate = datetime[ndates].date;
      const auto vtime = datetime[ndates].time;

      const auto dayoy = decodeDayOfYear(vdate);
      if (dayoy < 0 || dayoy >= MaxDays) cdoAbort("Day %d out of range!", dayoy);

      vdates1[dayoy] = vdate;
      vtimes1[dayoy] = vtime;

      if (!vars2[dayoy].size()) cdoAbort("No data for day %d in %s and %s", dayoy, cdoGetStreamName(1), cdoGetStreamName(2));

      for (int varID = 0; varID < nvars; varID++)
        {
          if (vlistInqVarTimetype(vlistID1, varID) == TIME_CONSTANT) continue;

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

      datetime[ndates] = datetime[0];
      vars1[ndates] = vars1[0];

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

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

      datetime[ndates - 1].date = taxisInqVdate(taxisID1);
      datetime[ndates - 1].time = taxisInqVtime(taxisID1);

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

          cdoReadRecord(streamID1, vars1[ndates - 1][varID][levelID].vec_d.data(), &nmiss);
          vars1[ndates - 1][varID][levelID].nmiss = nmiss;
        }

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

  if (readMethod && readMethod[0] == 'c' && cdoAssertFilesOnly() )
    {
      cdiDecodeDate(datetime[ndates - 1].date, &endYear, &mon, &day);
      const auto cdiStream = streamOpenRead(cdoGetStreamName(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;

          int year;
          cdiDecodeDate(taxisInqVdate(cdiTaxisID), &year, &mon, &day);
          datetime[ndates - 1].date = cdiEncodeDate(endYear+1, mon, day);
          datetime[ndates - 1].time = taxisInqVtime(cdiTaxisID);

          for (int recID = 0; recID < nrecs; recID++)
            {
              int varID, levelID;
              streamInqRecord(cdiStream, &varID, &levelID);
              streamReadRecord(cdiStream, vars1[ndates - 1][varID][levelID].vec_d.data(), &nmiss);
              vars1[ndates - 1][varID][levelID].nmiss = nmiss;
            }

          datetime_avg(dpy, ndates, datetime.data());
          cdiDecodeDate(datetime[ndates].date, &year, &mon, &day);
          const auto vdate = (year > endYear) ? cdiEncodeDate(startYear, mon, day) : datetime[ndates].date;

          const auto dayoy = decodeDayOfYear(vdate);
          if (dayoy < 0 || dayoy >= MaxDays) cdoAbort("Day %d out of range!", dayoy);

          nsets[dayoy] += ndates;

          for (int varID = 0; varID < nvars; varID++)
            {
              if (vlistInqVarTimetype(vlistID1, varID) == TIME_CONSTANT) continue;

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

          datetime[ndates] = datetime[0];
          vars1[ndates] = vars1[0];

          for (int inp = 0; inp < ndates; inp++)
            {
              datetime[inp] = datetime[inp + 1];
              vars1[inp] = vars1[inp + 1];
	    }
        }
      if ( missTimes != ndates-1 )
        cdoAbort("Addding the missing values when using the 'readMethodar' method was not possible");
      streamClose(cdiStream);    
    }
  else if (readMethod && readMethod[0] == 'c' )
    cdoWarning("Operators cannot be piped in circular mode");

  if (readMethod) free(readMethod);

  /*
  int outyear = 1e9;
  for ( dayoy = 0; dayoy < MaxDays; dayoy++ )
    if ( nsets[dayoy] )
      {
        int year, month, day;
        cdiDecodeDate(vdates1[dayoy], &year, &month, &day);
        if ( year < outyear ) outyear = year;
      }

  for ( dayoy = 0; dayoy < MaxDays; dayoy++ )
    if ( nsets[dayoy] )
      {
        int year, month, day;
        cdiDecodeDate(vdates1[dayoy], &year, &month, &day);
        vdates1[dayoy] = cdiEncodeDate(outyear, month, day);
      }
  */
  int otsID = 0;
  for (int dayoy = 0; dayoy < MaxDays; dayoy++)
    if (nsets[dayoy])
      {
        if (decodeMonthAndDay(vdates1[dayoy]) != decodeMonthAndDay(vdates2[dayoy]))
          cdoAbort("Verification dates for day %d of %s, %s and %s are different!", dayoy, cdoGetStreamName(0),
                   cdoGetStreamName(1));

        for (int varID = 0; varID < nvars; varID++)
          {
            if (vlistInqVarTimetype(vlistID1, varID) == TIME_CONSTANT) continue;

            const auto nlevels = varList1[varID].nlevels;
            for (int levelID = 0; levelID < nlevels; levelID++)
              hsets[dayoy].getVarLevelPercentiles(vars2[dayoy][varID][levelID], varID, levelID, pn, FIELD_VEC);
          }

        taxisDefVdate(taxisID4, vdates1[dayoy]);
        taxisDefVtime(taxisID4, vtimes1[dayoy]);
        cdoDefTimestep(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;
            cdoDefRecord(streamID4, varID, levelID);
            cdoWriteRecord(streamID4, vars2[dayoy][varID][levelID].vec_d.data(), vars2[dayoy][varID][levelID].nmiss);
          }

        otsID++;
      }

  cdoStreamClose(streamID4);
  cdoStreamClose(streamID3);
  cdoStreamClose(streamID2);
  cdoStreamClose(streamID1);

  cdoFinish();

  return nullptr;
}
