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

  Copyright (C) 2003-2021 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:

      Inttime    inttime         Time interpolation
*/

#include <cdi.h>

#include <utility>

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

int get_tunits(const char *unit, int &incperiod, int &incunit, int &tunit);

size_t
inttime(double fac1, double fac2, size_t gridsize, const double *single1, const double *single2, Varray<double> &array,
        bool withMissval, double missval1, double missval2)
{
  size_t nmiss = 0;

  if (withMissval)
    {
      for (size_t i = 0; i < gridsize; i++)
        {
          if (!DBL_IS_EQUAL(single1[i], missval1) && !DBL_IS_EQUAL(single2[i], missval2))
            array[i] = single1[i] * fac1 + single2[i] * fac2;
          else if (DBL_IS_EQUAL(single1[i], missval1) && !DBL_IS_EQUAL(single2[i], missval2) && fac2 >= 0.5)
            array[i] = single2[i];
          else if (DBL_IS_EQUAL(single2[i], missval2) && !DBL_IS_EQUAL(single1[i], missval1) && fac1 >= 0.5)
            array[i] = single1[i];
          else
            {
              array[i] = missval1;
              nmiss++;
            }
        }
    }
  else
    {
      for (size_t i = 0; i < gridsize; i++) array[i] = single1[i] * fac1 + single2[i] * fac2;
    }

  return nmiss;
}

static void
juldateAddIncrement(JulianDate &juldate, int64_t ijulinc, int calendar, int tunit)
{
  if (tunit == TUNIT_MONTH || tunit == TUNIT_YEAR)
    {
      int64_t vdate;
      int vtime;
      julian_date_decode(calendar, juldate, vdate, vtime);

      int year, month, day;
      cdiDecodeDate(vdate, &year, &month, &day);

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

      vdate = cdiEncodeDate(year, month, day);

      juldate = julian_date_encode(calendar, vdate, vtime);
    }
  else
    {
      juldate = julian_date_add_seconds(ijulinc, juldate);
    }
}

void *
Inttime(void *process)
{
  CdoStreamID streamID2 = CDO_STREAM_UNDEF;
  int64_t vdate;
  int vtime;
  int incperiod = 0, incunit = 3600, tunit = TUNIT_HOUR;
  int year, month, day, hour, minute, second;

  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!");

  const auto datestr = cdo_operator_argv(0).c_str();
  const auto timestr = cdo_operator_argv(1).c_str();

  if (strchr(datestr, '-'))
    {
      year = 1;
      month = 1;
      day = 1;
      sscanf(datestr, "%d-%d-%d", &year, &month, &day);
      vdate = cdiEncodeDate(year, month, day);
    }
  else
    {
      vdate = parameter_to_long(datestr);
    }

  if (strchr(timestr, ':'))
    {
      hour = 0;
      minute = 0;
      second = 0;
      sscanf(timestr, "%d:%d:%d", &hour, &minute, &second);
      vtime = cdiEncodeTime(hour, minute, second);
    }
  else
    {
      vtime = parameter_to_int(timestr);
    }

  if (cdo_operator_argc() == 3)
    {
      auto timeunits = cdo_operator_argv(2).c_str();
      incperiod = (int) strtol(timeunits, nullptr, 10);
      if (timeunits[0] == '-' || timeunits[0] == '+') timeunits++;
      while (isdigit((int) *timeunits)) timeunits++;

      get_tunits(timeunits, incperiod, incunit, tunit);
    }
  /* increment in seconds */
  const int64_t ijulinc = (int64_t) incperiod * incunit;

  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> array(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 juldate = julian_date_encode(calendar, vdate, vtime);

  if (Options::cdoVerbose)
    {
      cdo_print("date %ld  time %d", vdate, vtime);
      cdo_print("juldate  = %f", julian_date_to_seconds(juldate));
      cdo_print("ijulinc = %lld", ijulinc);
    }

  int curFirst = 0, curSecond = 1;

  int tsID = 0;
  auto nrecs = cdo_stream_inq_timestep(streamID1, tsID++);
  auto juldate1 = julian_date_encode(calendar, taxisInqVdate(taxisID1), taxisInqVtime(taxisID1));
  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("date %ld  time %d", taxisInqVdate(taxisID1), taxisInqVtime(taxisID1));
      cdo_print("juldate1  = %f", julian_date_to_seconds(juldate1));
    }

  if (julian_date_to_seconds(juldate1) > julian_date_to_seconds(juldate))
    cdo_warning("start time %ld %d out of range!", vdate, vtime);

  int tsIDo = 0;
  while (julian_date_to_seconds(juldate1) <= julian_date_to_seconds(juldate))
    {
      nrecs = cdo_stream_inq_timestep(streamID1, tsID++);
      if (nrecs == 0) break;

      auto juldate2 = julian_date_encode(calendar, taxisInqVdate(taxisID1), taxisInqVtime(taxisID1));
      if (Options::cdoVerbose)
        {
          cdo_print("date %ld  time %d", taxisInqVdate(taxisID1), taxisInqVtime(taxisID1));
          cdo_print("juldate2  = %f", julian_date_to_seconds(juldate2));
        }

      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 = vlistInqVarTimetype(vlistID1, varID) == TIME_CONSTANT;

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

      while (julian_date_to_seconds(juldate) <= julian_date_to_seconds(juldate2))
        {
          if (julian_date_to_seconds(juldate) >= julian_date_to_seconds(juldate1)
              && julian_date_to_seconds(juldate) <= julian_date_to_seconds(juldate2))
            {
              julian_date_decode(calendar, juldate, vdate, vtime);

              if (Options::cdoVerbose)
                cdo_print("%s %s  %f  %d", date_to_string(vdate).c_str(), time_to_string(vtime).c_str(),
                          julian_date_to_seconds(juldate), calendar);

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

              taxisDefVdate(taxisID2, vdate);
              taxisDefVtime(taxisID2, vtime);
              cdo_def_timestep(streamID2, tsIDo++);

              const auto diff = julian_date_to_seconds(julian_date_sub(juldate2, juldate1));
              const auto fac1 = julian_date_to_seconds(julian_date_sub(juldate2, juldate)) / diff;
              const auto fac2 = julian_date_to_seconds(julian_date_sub(juldate, juldate1)) / 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 bool withMissval = nmiss[curFirst][varID][levelID] || nmiss[curSecond][varID][levelID];
                  const auto nmiss3 = inttime(fac1, fac2, gridsize, single1, single2, array, withMissval, varList1[varID].missval,
                                              varList2[varID].missval);

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

          if (ijulinc == 0) break;

          juldateAddIncrement(juldate, ijulinc, calendar, tunit);
        }

      juldate1 = juldate2;
      std::swap(curFirst, curSecond);
    }

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

  if (tsIDo == 0) cdo_warning("date/time out of time axis, no time step interpolated!");

  cdo_finish();

  return nullptr;
}
