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

      Settime    setdate         Set date
      Settime    settime         Set time
      Settime    setday          Set day
      Settime    setmon          Set month
      Settime    setyear         Set year
      Settime    settunits       Set time units
      Settime    settaxis        Set time axis
      Settime    setreftime      Set reference time
      Settime    setcalendar     Set calendar
      Settime    shifttime       Shift timesteps
*/

#include <cdi.h>

#include "cdo_options.h"
#include "process_int.h"
#include "cdo_cdi_wrapper.h"
#include "param_conversion.h"
#include "calendar.h"
#include "util_string.h"
#include "datetime.h"

int
get_tunits(const char *unit, int &incperiod, int &incunit, int &tunit)
{
  const size_t len = strlen(unit);

  // clang-format off
  if      (memcmp(unit, "seconds", len) == 0) { incunit = 1;     tunit = TUNIT_SECOND; }
  else if (memcmp(unit, "minutes", len) == 0) { incunit = 60;    tunit = TUNIT_MINUTE; }
  else if (memcmp(unit, "hours", len) == 0)   { incunit = 3600;  tunit = TUNIT_HOUR; }
  else if (memcmp(unit, "3hours", len) == 0)  { incunit = 10800; tunit = TUNIT_3HOURS; }
  else if (memcmp(unit, "6hours", len) == 0)  { incunit = 21600; tunit = TUNIT_6HOURS; }
  else if (memcmp(unit, "12hours", len) == 0) { incunit = 43200; tunit = TUNIT_12HOURS; }
  else if (memcmp(unit, "days", len) == 0)    { incunit = 86400; tunit = TUNIT_DAY; }
  else if (memcmp(unit, "months", len) == 0)  { incunit = 1;     tunit = TUNIT_MONTH; }
  else if (memcmp(unit, "years", len) == 0)   { incunit = 12;    tunit = TUNIT_YEAR; }
  else cdo_abort("Time unit >%s< unsupported!", unit);

  if (tunit == TUNIT_HOUR)
    {
      if      (incperiod ==  3) { incperiod = 1; incunit = 10800; tunit = TUNIT_3HOURS;  }
      else if (incperiod ==  6) { incperiod = 1; incunit = 21600; tunit = TUNIT_6HOURS;  }
      else if (incperiod == 12) { incperiod = 1; incunit = 43200; tunit = TUNIT_12HOURS; }
    }
  // clang-format on

  return 0;
}

static void
shifttime(int calendar, int tunit, int64_t ijulinc, int64_t &vdate, int &vtime)
{
  if (tunit == TUNIT_MONTH || tunit == TUNIT_YEAR)
    {
      int year, month, day;
      cdiDecodeDate(vdate, &year, &month, &day);

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

      vdate = cdiEncodeDate(year, month, day);
    }
  else
    {
      auto juldate = julian_date_encode(calendar, vdate, vtime);
      juldate = julian_date_add_seconds(ijulinc, juldate);
      julian_date_decode(calendar, juldate, vdate, vtime);

      if (Options::cdoVerbose)
        cdo_print("juldate, ijulinc, vdate, vtime: %g %lld %lld %d", julian_date_to_seconds(juldate), ijulinc, vdate, vtime);
    }
}

static void
time_gen_bounds(int calendar, int tunit, int incperiod, int64_t vdate, int vtime, int64_t *vdateb, int *vtimeb)
{
  vdateb[0] = vdate;
  vdateb[1] = vdate;
  vtimeb[0] = 0;
  vtimeb[1] = 0;

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

  if (tunit == TUNIT_MONTH)
    {
      vdateb[0] = cdiEncodeDate(year, month, 1);
      month++;
      if (month > 12)
        {
          month = 1;
          year++;
        }
      vdateb[1] = cdiEncodeDate(year, month, 1);
    }
  else if (tunit == TUNIT_YEAR)
    {
      vdateb[0] = cdiEncodeDate(year, 1, 1);
      vdateb[1] = cdiEncodeDate(year + 1, 1, 1);
    }
  else if (tunit == TUNIT_DAY)
    {
      vdateb[0] = vdate;
      auto juldate = julian_date_encode(calendar, vdateb[0], vtimeb[0]);
      juldate = julian_date_add_seconds(86400, juldate);
      julian_date_decode(calendar, juldate, vdateb[1], vtimeb[1]);
    }
  else if (tunit == TUNIT_HOUR || tunit == TUNIT_3HOURS || tunit == TUNIT_6HOURS || tunit == TUNIT_12HOURS)
    {
      if (incperiod == 0) incperiod = 1;
      if (incperiod > 24) cdo_abort("Time period must be less equal 24!");

      // clang-format off
      if      (tunit == TUNIT_3HOURS)  incperiod = 3;
      else if (tunit == TUNIT_6HOURS)  incperiod = 6;
      else if (tunit == TUNIT_12HOURS) incperiod = 12;
      // clang-format on

      int hour, minute, second;
      cdiDecodeTime(vtime, &hour, &minute, &second);
      int h0 = (hour / incperiod) * incperiod;
      vtimeb[0] = cdiEncodeTime(h0, 0, 0);
      int h1 = h0 + incperiod;
      if (h1 >= 24)
        {
          vdateb[1] = cdiEncodeDate(year, month, day + 1);
          auto juldate = julian_date_encode(calendar, vdateb[0], vtimeb[0]);
          juldate = julian_date_add_seconds(incperiod * 3600, juldate);
          julian_date_decode(calendar, juldate, vdateb[1], vtimeb[1]);
        }
      else
        vtimeb[1] = cdiEncodeTime(h1, 0, 0);
    }
}

int
evaluateCalendarString(int operatorID, const std::string &calendarName)
{
  int calendar = CALENDAR_STANDARD;
  const auto calendarString = string_to_lower(calendarName);
  // clang-format off
  if      (calendarString == "standard")  calendar = CALENDAR_STANDARD;
  else if (calendarString == "gregorian") calendar = CALENDAR_GREGORIAN;
  else if (calendarString == "proleptic") calendar = CALENDAR_PROLEPTIC;
  else if (calendarString == "proleptic_gregorian") calendar = CALENDAR_PROLEPTIC;
  else if (calendarString == "360days") calendar = CALENDAR_360DAYS;
  else if (calendarString == "360_day") calendar = CALENDAR_360DAYS;
  else if (calendarString == "365days") calendar = CALENDAR_365DAYS;
  else if (calendarString == "365_day") calendar = CALENDAR_365DAYS;
  else if (calendarString == "366days") calendar = CALENDAR_366DAYS;
  else if (calendarString == "366_day") calendar = CALENDAR_366DAYS;
  else cdo_abort("Calendar >%s< unsupported! Available %s", calendarName.c_str(), cdo_operator_enter(operatorID));
  // clang-format on

  return calendar;
}

static int64_t
decode_datestr(const char *datestr)
{
  if (strchr(datestr + 1, '-'))
    {
      int year = 1, month = 1, day = 1;
      sscanf(datestr, "%d-%d-%d", &year, &month, &day);
      return cdiEncodeDate(year, month, day);
    }
  else
    {
      return parameter_to_long(datestr);
    }
}

static int
decode_timestr(const char *timestr)
{
  if (strchr(timestr, ':'))
    {
      int hour = 0, minute = 0, second = 0;
      sscanf(timestr, "%d:%d:%d", &hour, &minute, &second);
      return cdiEncodeTime(hour, minute, second);
    }
  else
    {
      return parameter_to_int(timestr);
    }
}

static void
decode_timeunits(const char *timeunits, int &incperiod, int &incunit, int &tunit)
{
  const auto ich = timeunits[0];
  if (ich == '-' || ich == '+' || isdigit(ich))
    {
      incperiod = (int) strtol(timeunits, nullptr, 10);
      if (ich == '-' || ich == '+') timeunits++;
      while (isdigit((int) *timeunits)) timeunits++;
    }
  get_tunits(timeunits, incperiod, incunit, tunit);
}

static void
argument2datetimeinc(int64_t &sdate, int &stime, int &incperiod, int &incunit, int &tunit)
{
  if (cdo_operator_argc() < 1) cdo_abort("Too few arguments!");
  if (cdo_operator_argc() > 3) cdo_abort("Too many arguments!");

  sdate = decode_datestr(cdo_operator_argv(0).c_str());

  if (cdo_operator_argc() > 1)
    {
      stime = decode_timestr(cdo_operator_argv(1).c_str());
      if (cdo_operator_argc() == 3) decode_timeunits(cdo_operator_argv(2).c_str(), incperiod, incunit, tunit);
    }
}

void *
Settime(void *process)
{
  int64_t newval = 0;
  int64_t vdateb[2];
  int vtimeb[2];
  int64_t sdate = 0;
  int stime = 0;
  int taxisID2 = CDI_UNDEFID;
  int tunit = TUNIT_DAY;
  int64_t ijulinc = 0;
  int incperiod = 1, incunit = 86400;
  int year = 1, month = 1, day = 1;
  int day0 = 0;
  bool copy_timestep = false;
  int newcalendar = CALENDAR_STANDARD;
  // int nargs;
  JulianDate juldate;

  cdo_initialize(process);

  // clang-format off
  const auto SETYEAR     = cdo_operator_add("setyear",      0,  1, "year");
  const auto SETMON      = cdo_operator_add("setmon",       0,  1, "month");
  const auto SETDAY      = cdo_operator_add("setday",       0,  1, "day");
  const auto SETDATE     = cdo_operator_add("setdate",      0,  1, "date (format: YYYY-MM-DD)");
  const auto SETTIME     = cdo_operator_add("settime",      0,  1, "time (format: hh:mm:ss)");
  const auto SETTUNITS   = cdo_operator_add("settunits",    0,  1, "time units (seconds, minutes, hours, days, months, years)");
  const auto SETTAXIS    = cdo_operator_add("settaxis",     0, -2, "date<,time<,increment>> (format YYYY-MM-DD,hh:mm:ss)");
  const auto SETTBOUNDS  = cdo_operator_add("settbounds",   0,  1, "frequency (day, month, year)");
  const auto SETREFTIME  = cdo_operator_add("setreftime",   0, -2, "date<,time<,units>> (format YYYY-MM-DD,hh:mm:ss)");
  const auto SETCALENDAR = cdo_operator_add("setcalendar",  0,  1, "calendar (standard, proleptic_gregorian, 360_day, 365_day, 366_day)");
  const auto SHIFTTIME   = cdo_operator_add("shifttime",    0,  1, "shift value");
  // clang-format on

  const auto operatorID = cdo_operator_id();
  // nargs = cdo_operator_f2(operatorID);

  operator_input_arg(cdo_operator_enter(operatorID));

  if (operatorID == SETTAXIS || operatorID == SETREFTIME)
    {
      argument2datetimeinc(sdate, stime, incperiod, incunit, tunit);
      // increment in seconds
      ijulinc = (int64_t) incperiod * incunit;
    }
  else if (operatorID == SETDATE)
    {
      operator_check_argc(1);
      const auto datestr = cdo_operator_argv(0).c_str();
      newval = decode_datestr(datestr);
    }
  else if (operatorID == SETTIME)
    {
      operator_check_argc(1);
      const auto timestr = cdo_operator_argv(0).c_str();
      newval = decode_timestr(timestr);
    }
  else if (operatorID == SHIFTTIME)
    {
      operator_check_argc(1);
      decode_timeunits(cdo_operator_argv(0).c_str(), incperiod, incunit, tunit);
      // increment in seconds
      ijulinc = (int64_t) incperiod * incunit;
    }
  else if (operatorID == SETTUNITS || operatorID == SETTBOUNDS)
    {
      operator_check_argc(1);
      decode_timeunits(cdo_operator_argv(0).c_str(), incperiod, incunit, tunit);

      if (operatorID == SETTBOUNDS
          && !(tunit == TUNIT_HOUR || tunit == TUNIT_3HOURS || tunit == TUNIT_6HOURS || tunit == TUNIT_12HOURS || tunit == TUNIT_DAY
               || tunit == TUNIT_MONTH || tunit == TUNIT_YEAR))
        cdo_abort("Unsupported frequency %s! Use hour, 3hours, 6hours, day, month or year.", cdo_operator_argv(0).c_str());
    }
  else if (operatorID == SETCALENDAR)
    {
      operator_check_argc(1);
      auto cname = cdo_operator_argv(0);
      newcalendar = evaluateCalendarString(operatorID, cname);
    }
  else
    {
      operator_check_argc(1);
      newval = parameter_to_int(cdo_operator_argv(0));
    }

  const auto streamID1 = cdo_open_read(0);

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

  const auto taxisID1 = vlistInqTaxis(vlistID1);
  bool taxis_has_bounds = taxisHasBounds(taxisID1) > 0;
  auto ntsteps = vlistNtsteps(vlistID1);
  const auto nvars = vlistNvars(vlistID1);

  if (ntsteps == 1)
    {
      int varID;
      for (varID = 0; varID < nvars; ++varID)
        if (vlistInqVarTimetype(vlistID1, varID) != TIME_CONSTANT) break;

      if (varID == nvars) ntsteps = 0;
    }

  if (ntsteps == 0)
    {
      for (int varID = 0; varID < nvars; ++varID) vlistDefVarTimetype(vlistID2, varID, TIME_VARYING);
    }

  const auto calendar = taxisInqCalendar(taxisID1);

  if (Options::cdoVerbose) cdo_print("calendar = %d", calendar);

  if (operatorID == SETREFTIME)
    {
      copy_timestep = true;

      if (taxisInqType(taxisID1) == TAXIS_ABSOLUTE)
        {
          cdo_print("Changing absolute to relative time axis!");
          taxisID2 = cdo_taxis_create(TAXIS_RELATIVE);
        }
      else
        taxisID2 = taxisDuplicate(taxisID1);

      if (cdo_operator_argc() != 3) tunit = taxisInqTunit(taxisID1);
      taxisDefTunit(taxisID2, tunit);
    }
  else if (operatorID == SETTUNITS)
    {
      copy_timestep = true;

      if (taxisInqType(taxisID1) == TAXIS_ABSOLUTE)
        {
          cdo_print("Changing absolute to relative time axis!");
          taxisID2 = cdo_taxis_create(TAXIS_RELATIVE);
          taxisDefTunit(taxisID2, tunit);
        }
      else
        taxisID2 = taxisDuplicate(taxisID1);
    }
  else if (operatorID == SETCALENDAR)
    {
      copy_timestep = true;
      // if ( ((char *)argument)[0] == '-' ) cdo_abort("This operator does not work with pipes!");
      if (taxisInqType(taxisID1) == TAXIS_ABSOLUTE)
        {
          // if ( CdoDefault::FileType != CDI_FILETYPE_NC ) cdo_abort("This operator does not work on an absolute time axis!");
          cdo_print("Changing absolute to relative time axis!");
          taxisID2 = cdo_taxis_create(TAXIS_RELATIVE);
        }
      else
        taxisID2 = taxisDuplicate(taxisID1);
    }
  else
    taxisID2 = taxisDuplicate(taxisID1);

  if (operatorID == SETTAXIS)
    {
      taxisDefTunit(taxisID2, tunit);
      taxisDefRdate(taxisID2, sdate);
      taxisDefRtime(taxisID2, stime);
      juldate = julian_date_encode(calendar, sdate, stime);
    }
  else if (operatorID == SETTUNITS)
    {
      taxisDefTunit(taxisID2, tunit);
    }
  else if (operatorID == SETCALENDAR)
    {
      taxisDefCalendar(taxisID2, newcalendar);
    }
  else if (operatorID == SETTBOUNDS)
    {
      taxisWithBounds(taxisID2);
    }

  if (operatorID != SHIFTTIME)
    if (taxis_has_bounds && !copy_timestep)
      {
        cdo_warning("Time bounds unsupported by this operator, removed!");
        taxisDeleteBounds(taxisID2);
        taxis_has_bounds = false;
      }

  vlistDefTaxis(vlistID2, taxisID2);

  CdoStreamID streamID2 = CDO_STREAM_UNDEF;

  auto gridsizemax = vlistGridsizeMax(vlistID1);
  if (vlistNumber(vlistID1) != CDI_REAL) gridsizemax *= 2;
  Varray<double> array(gridsizemax);

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

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

      if (operatorID == SETTAXIS)
        {
          if (tunit == TUNIT_MONTH || tunit == TUNIT_YEAR)
            {
              vtime = stime;
              if (tsID == 0)
                {
                  vdate = sdate;
                  cdiDecodeDate(vdate, &year, &month, &day0);
                }
              else
                {
                  month += (int) ijulinc;
                  adjust_month_and_year(month, year);

                  day = (day0 == 31) ? days_per_month(calendar, year, month) : day0;

                  vdate = cdiEncodeDate(year, month, day);
                }
            }
          else
            {
              julian_date_decode(calendar, juldate, vdate, vtime);
              juldate = julian_date_add_seconds(ijulinc, juldate);
            }
        }
      else if (operatorID == SETTBOUNDS)
        {
          time_gen_bounds(calendar, tunit, incperiod, vdate, vtime, vdateb, vtimeb);

          if (Options::CMOR_Mode)
            {
              const auto juldate1 = julian_date_encode(calendar, vdateb[0], vtimeb[0]);
              const auto juldate2 = julian_date_encode(calendar, vdateb[1], vtimeb[1]);
              const auto seconds = julian_date_to_seconds(julian_date_sub(juldate2, juldate1)) / 2;
              const auto juldatem = julian_date_add_seconds(lround(seconds), juldate1);
              julian_date_decode(calendar, juldatem, vdate, vtime);
            }
        }
      else if (operatorID == SHIFTTIME)
        {
          shifttime(calendar, tunit, ijulinc, vdate, vtime);
          if (taxis_has_bounds)
            {
              taxisInqVdateBounds(taxisID1, &vdateb[0], &vdateb[1]);
              taxisInqVtimeBounds(taxisID1, &vtimeb[0], &vtimeb[1]);
              shifttime(calendar, tunit, ijulinc, vdateb[0], vtimeb[0]);
              shifttime(calendar, tunit, ijulinc, vdateb[1], vtimeb[1]);
            }
        }
      else if (operatorID == SETREFTIME || operatorID == SETCALENDAR || operatorID == SETTUNITS)
        {
        }
      else
        {
          cdiDecodeDate(vdate, &year, &month, &day);

          if (operatorID == SETYEAR) year = newval;
          if (operatorID == SETMON) month = newval;
          if (operatorID == SETMON && (month < 0 || month > 16)) cdo_abort("parameter month=%d out of range!", month);
          if (operatorID == SETDAY) day = newval;
          if (operatorID == SETDAY && (day < 0 || day > 31)) cdo_abort("parameter day=%d out of range!", day);

          vdate = cdiEncodeDate(year, month, day);

          if (operatorID == SETDATE) vdate = newval;
          if (operatorID == SETTIME) vtime = newval;
        }

      if (copy_timestep)
        {
          cdo_taxis_copy_timestep(taxisID2, taxisID1);
          if (operatorID == SETREFTIME)
            {
              taxisDefRdate(taxisID2, sdate);
              taxisDefRtime(taxisID2, stime);
            }
        }
      else
        {
          const auto numavg = taxisInqNumavg(taxisID1);
          taxisDefNumavg(taxisID2, numavg);

          taxisDefVdate(taxisID2, vdate);
          taxisDefVtime(taxisID2, vtime);

          if (taxis_has_bounds || operatorID == SETTBOUNDS)
            {
              taxisDefVdateBounds(taxisID2, vdateb[0], vdateb[1]);
              taxisDefVtimeBounds(taxisID2, vtimeb[0], vtimeb[1]);
            }
        }

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

      cdo_def_timestep(streamID2, tsID);

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

          size_t nmiss;
          cdo_read_record(streamID1, &array[0], &nmiss);
          cdo_write_record(streamID2, &array[0], nmiss);
        }

      tsID++;
    }

  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  cdo_finish();

  return nullptr;
}
