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

      Inttime    inttime         Time interpolation
*/

#include "cdi.h"
#include "julian_date.h"

#include <utility>

#include "cdo_options.h"
#include "process_int.h"
#include "cdo_vlist.h"
#include "datetime.h"
#include "printinfo.h"


size_t
interp_time(double fac1, double fac2, size_t gridsize, const double *array1, const double *array2, Varray<double> &array3,
            bool withMissval, double missval1, double missval2)
{
  size_t nmiss3 = 0;

  if (withMissval)
    {
      for (size_t i = 0; i < gridsize; ++i)
        {
          if (!DBL_IS_EQUAL(array1[i], missval1) && !DBL_IS_EQUAL(array2[i], missval2))
            array3[i] = array1[i] * fac1 + array2[i] * fac2;
          else if (DBL_IS_EQUAL(array1[i], missval1) && !DBL_IS_EQUAL(array2[i], missval2) && fac2 >= 0.5)
            array3[i] = array2[i];
          else if (DBL_IS_EQUAL(array2[i], missval2) && !DBL_IS_EQUAL(array1[i], missval1) && fac1 >= 0.5)
            array3[i] = array1[i];
          else
            {
              array3[i] = missval1;
              nmiss3++;
            }
        }
    }
  else
    {
      for (size_t i = 0; i < gridsize; ++i) array3[i] = array1[i] * fac1 + array2[i] * fac2;
    }

  return nmiss3;
}

static void
julianDate_add_increment(JulianDate &julianDate, int64_t ijulinc, int calendar, int tunit)
{
  if (tunit == TUNIT_MONTH || tunit == TUNIT_YEAR)
    {
      auto vDateTime = julianDate_decode(calendar, julianDate);

      int year, month, day;
      cdiDate_decode(vDateTime.date, &year, &month, &day);

      month += (int) ijulinc;
      adjust_month_and_year(month, year);

      vDateTime.date = cdiDate_encode(year, month, day);
      julianDate = julianDate_encode(calendar, vDateTime);
    }
  else
    {
      julianDate = julianDate_add_seconds(julianDate, ijulinc);
    }
}

void *
Inttime(void *process)
{
  CdoStreamID streamID2 = CDO_STREAM_UNDEF;
  CdiDateTime sDateTime = { };
  int incrPeriod = 0, incrUnits = 3600, timeUnits = TUNIT_HOUR;

  cdo_initialize(process);

  operator_input_arg("date,time<,increment> (format YYYY-MM-DD,hh:mm:ss)");
  if (cdo_operator_argc() < 2) cdo_abort("Too few arguments!");

  sDateTime.date = decode_datestring(cdo_operator_argv(0));
  sDateTime.time = decode_timestring(cdo_operator_argv(1));
  if (cdo_operator_argc() == 3) decode_timeunits(cdo_operator_argv(2), incrPeriod, incrUnits, timeUnits);

  // increment in seconds
  const auto ijulinc = (int64_t) incrPeriod * incrUnits;

  const auto streamID1 = cdo_open_read(0);

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

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

  if (ijulinc == 0) vlistDefNtsteps(vlistID2, 1);

  const auto nvars = vlistNvars(vlistID1);

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

  const auto gridsizemax = vlistGridsizeMax(vlistID1);
  Varray<double> array3(gridsizemax);

  Varray3D<size_t> nmiss(2);
  nmiss[0].resize(nvars);
  nmiss[1].resize(nvars);
  Varray3D<double> vardata(2);
  vardata[0].resize(nvars);
  vardata[1].resize(nvars);

  for (int varID = 0; varID < nvars; ++varID)
    {
      const auto gridsize = varList1[varID].gridsize;
      const auto nlevel = varList1[varID].nlevels;
      nmiss[0][varID].resize(nlevel);
      nmiss[1][varID].resize(nlevel);
      vardata[0][varID].resize(gridsize * nlevel);
      vardata[1][varID].resize(gridsize * nlevel);
    }

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

  const auto calendar = taxisInqCalendar(taxisID1);

  auto julianDate = julianDate_encode(calendar, sDateTime);

  if (Options::cdoVerbose)
    {
      cdo_print("Start date/time %s", datetime_to_string(sDateTime));
      cdo_print("julianDate = %f", julianDate_to_seconds(julianDate));
      cdo_print("ijulinc = %lld", ijulinc);
    }

  int curFirst = 0, curSecond = 1;

  int tsID = 0;
  auto nrecs = cdo_stream_inq_timestep(streamID1, tsID++);
  const auto vDateTime1 = taxisInqVdatetime(taxisID1);
  auto julianDate1 = julianDate_encode(calendar, vDateTime1);
  for (int recID = 0; recID < nrecs; ++recID)
    {
      int varID, levelID;
      cdo_inq_record(streamID1, &varID, &levelID);
      const auto offset = varList1[varID].gridsize * levelID;
      auto single1 = &vardata[curFirst][varID][offset];
      cdo_read_record(streamID1, single1, &nmiss[curFirst][varID][levelID]);
    }

  if (Options::cdoVerbose)
    {
      cdo_print("Dataset begins on %s", datetime_to_string(vDateTime1));
      cdo_print("julianDate1 = %f", julianDate_to_seconds(julianDate1));
    }

  if (julianDate_to_seconds(julianDate1) > julianDate_to_seconds(julianDate))
    {
      cdo_print("Dataset begins on %s", datetime_to_string(vDateTime1));
      cdo_warning("The start time %s is before the beginning of the dataset!", datetime_to_string(sDateTime));
    }

  int tsIDo = 0;
  while (julianDate_to_seconds(julianDate1) <= julianDate_to_seconds(julianDate))
    {
      nrecs = cdo_stream_inq_timestep(streamID1, tsID++);
      if (nrecs == 0) break;

      const auto vDateTime = taxisInqVdatetime(taxisID1);
      const auto julianDate2 = julianDate_encode(calendar, vDateTime);
      if (Options::cdoVerbose)
        {
          cdo_print("date/time: %s", datetime_to_string(vDateTime));
          cdo_print("julianDate2 = %f", julianDate_to_seconds(julianDate2));
        }

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

          recList[recID].varID = varID;
          recList[recID].levelID = levelID;
          recList[recID].lconst = (varList1[varID].timetype == TIME_CONSTANT);

          const auto offset = varList1[varID].gridsize * levelID;
          auto single2 = &vardata[curSecond][varID][offset];
          cdo_read_record(streamID1, single2, &nmiss[curSecond][varID][levelID]);
        }

      while (julianDate_to_seconds(julianDate) <= julianDate_to_seconds(julianDate2))
        {
          if (julianDate_to_seconds(julianDate) >= julianDate_to_seconds(julianDate1)
              && julianDate_to_seconds(julianDate) <= julianDate_to_seconds(julianDate2))
            {
              const auto dt = julianDate_decode(calendar, julianDate);

              if (Options::cdoVerbose)
                cdo_print("%s %s  %f  %d", date_to_string(dt.date), time_to_string(dt.time),
                          julianDate_to_seconds(julianDate), calendar);

              if (streamID2 == CDO_STREAM_UNDEF)
                {
                  streamID2 = cdo_open_write(1);
                  cdo_def_vlist(streamID2, vlistID2);
                }

              taxisDefVdatetime(taxisID2, dt);
              cdo_def_timestep(streamID2, tsIDo++);

              const auto diff = julianDate_to_seconds(julianDate_sub(julianDate2, julianDate1));
              const auto fac1 = julianDate_to_seconds(julianDate_sub(julianDate2, julianDate)) / diff;
              const auto fac2 = julianDate_to_seconds(julianDate_sub(julianDate, julianDate1)) / diff;

              for (int recID = 0; recID < nrecs; ++recID)
                {
                  const auto varID = recList[recID].varID;
                  const auto levelID = recList[recID].levelID;
                  const auto gridsize = varList1[varID].gridsize;
                  const auto offset = gridsize * levelID;
                  const auto single1 = &vardata[curFirst][varID][offset];
                  const auto single2 = &vardata[curSecond][varID][offset];
                  const auto missval1 = varList1[varID].missval;
                  const auto missval2 = varList2[varID].missval;

                  const auto withMissval = (nmiss[curFirst][varID][levelID] || nmiss[curSecond][varID][levelID]);
                  const auto nmiss3 = interp_time(fac1, fac2, gridsize, single1, single2, array3, withMissval, missval1, missval2);

                  cdo_def_record(streamID2, varID, levelID);
                  cdo_write_record(streamID2, array3.data(), nmiss3);
                }
            }

          if (ijulinc == 0) break;

          julianDate_add_increment(julianDate, ijulinc, calendar, timeUnits);
        }

      julianDate1 = julianDate2;
      std::swap(curFirst, curSecond);
    }

  if (streamID2 != CDO_STREAM_UNDEF) cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  if (tsIDo == 0) cdo_warning("Start date/time %s out of range, no time steps interpolated!", datetime_to_string(sDateTime));

  cdo_finish();

  return nullptr;
}
