/*
  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.
*/
#include <cdi.h>

#include <utility>

#include "cdo_options.h"
#include "process_int.h"
#include "datetime.h"
#include "util_string.h"

TimeStat CDO_Timestat_Date = TimeStat::UNDEF;
bool CDO_Ignore_Time_Bounds = false;
bool CDO_Use_Time_Bounds = false;
static bool dateTimeInit = false;

void
set_timestat_date(const std::string &optarg)
{
  TimeStat timestatdate = TimeStat::UNDEF;

  // clang-format off
  if      (optarg == "first")   timestatdate = TimeStat::FIRST;
  else if (optarg == "last")    timestatdate = TimeStat::LAST;
  else if (optarg == "middle")  timestatdate = TimeStat::MEAN;
  else if (optarg == "midhigh") timestatdate = TimeStat::MIDHIGH;
  // clang-format on

  if (timestatdate == TimeStat::UNDEF) cdo_abort("option --%s: unsupported argument: %s", "timestat_date", optarg.c_str());

  CDO_Timestat_Date = timestatdate;
}

static void
getTimestatDate(TimeStat *tstat_date)
{
  char *envstr = getenv("CDO_TIMESTAT_DATE");
  if (envstr == nullptr) envstr = getenv("RUNSTAT_DATE");
  if (envstr)
    {
      TimeStat env_date = TimeStat::UNDEF;
      const auto envstrl = string_to_lower(envstr);

      // clang-format off
      if      (envstrl == "first")   env_date = TimeStat::FIRST;
      else if (envstrl == "last")    env_date = TimeStat::LAST;
      else if (envstrl == "middle")  env_date = TimeStat::MEAN;
      else if (envstrl == "midhigh") env_date = TimeStat::MIDHIGH;
      // clang-format on

      if (env_date != TimeStat::UNDEF)
        {
          *tstat_date = env_date;
          if (Options::cdoVerbose) cdo_print("Set CDO_TIMESTAT_DATE to %s", envstr);
        }
    }
}

void
DateTimeList::init()
{
  if (!dateTimeInit) getTimestatDate(&CDO_Timestat_Date);
  dateTimeInit = true;
}

int64_t
DateTimeList::get_vdate(const int tsID)
{
  if (tsID < 0 || (size_t) tsID >= this->size) cdo_abort("Internal error; tsID out of bounds!");

  return this->dtinfo[tsID].c.date;
}

int
DateTimeList::get_vtime(const int tsID)
{
  if (tsID < 0 || (size_t) tsID >= this->size) cdo_abort("Internal error; tsID out of bounds!");

  return this->dtinfo[tsID].c.time;
}

void
DateTimeList::shift()
{
  for (size_t inp = 0; inp < this->size - 1; inp++) this->dtinfo[inp] = this->dtinfo[inp + 1];
}

void
DateTimeList::taxis_inq_timestep(const int taxisID, const int tsID)
{
  constexpr size_t NALLOC = 128;

  if ((size_t) tsID >= this->nalloc)
    {
      this->nalloc += NALLOC;
      this->dtinfo.resize(this->nalloc);
    }

  if ((size_t) tsID >= this->size) this->size = (size_t) tsID + 1;

  this->dtinfo[tsID].v.date = taxisInqVdate(taxisID);
  this->dtinfo[tsID].v.time = taxisInqVtime(taxisID);

  this->dtinfo[tsID].c.date = this->dtinfo[tsID].v.date;
  this->dtinfo[tsID].c.time = this->dtinfo[tsID].v.time;

  if (tsID == 0)
    {
      if (this->has_bounds == -1) this->has_bounds = CDO_Ignore_Time_Bounds ? 0 : taxisHasBounds(taxisID);
      if (this->calendar == -1) this->calendar = taxisInqCalendar(taxisID);
    }

  if (this->has_bounds)
    {
      taxisInqVdateBounds(taxisID, &(this->dtinfo[tsID].b[0].date), &(this->dtinfo[tsID].b[1].date));
      taxisInqVtimeBounds(taxisID, &(this->dtinfo[tsID].b[0].time), &(this->dtinfo[tsID].b[1].time));

      if (CDO_Use_Time_Bounds && this->dtinfo[tsID].b[1].time == 0 && this->dtinfo[tsID].v.date == this->dtinfo[tsID].b[1].date
          && this->dtinfo[tsID].v.time == this->dtinfo[tsID].b[1].time)
        {
          auto vdate = this->dtinfo[tsID].b[0].date;
          auto vtime = this->dtinfo[tsID].b[0].time;
          const auto juldate1 = julian_date_encode(this->calendar, vdate, vtime);

          vdate = this->dtinfo[tsID].b[1].date;
          vtime = this->dtinfo[tsID].b[1].time;
          const auto juldate2 = julian_date_encode(this->calendar, vdate, vtime);

          // int hour, minute, second;
          // cdiDecodeTime(vtime, &hour, &minute, &second);

          if (julian_date_to_seconds(juldate1) < julian_date_to_seconds(juldate2))
            {
              const auto juldate = julian_date_add_seconds(-1, juldate2);
              julian_date_decode(this->calendar, juldate, vdate, vtime);

              this->dtinfo[tsID].c.date = vdate;
              this->dtinfo[tsID].c.time = vtime;
            }
        }
    }
  else
    {
      this->dtinfo[tsID].b[0].date = 0;
      this->dtinfo[tsID].b[1].date = 0;
      this->dtinfo[tsID].b[0].time = 0;
      this->dtinfo[tsID].b[1].time = 0;
    }
}

void
DateTimeList::taxis_set_next_timestep(const int taxisID)
{
  const int tsID = this->size;
  this->taxis_inq_timestep(taxisID, tsID);
}

void
DateTimeList::taxisDefTimestep(const int taxisID, const int tsID)
{
  if (tsID < 0 || (size_t) tsID >= this->size) cdo_abort("Internal error; tsID out of bounds!");

  taxisDefVdate(taxisID, this->dtinfo[tsID].v.date);
  taxisDefVtime(taxisID, this->dtinfo[tsID].v.time);
  if (this->has_bounds)
    {
      taxisDefVdateBounds(taxisID, this->dtinfo[tsID].b[0].date, this->dtinfo[tsID].b[1].date);
      taxisDefVtimeBounds(taxisID, this->dtinfo[tsID].b[0].time, this->dtinfo[tsID].b[1].time);
    }
}

void
DateTimeList::mean(const int nsteps)
{
  int64_t vdate;
  int vtime;

  if (nsteps % 2 == 0)
    {
#ifdef TEST_DTLIST_MEAN
      vdate = this->dtinfo[0].v.date;
      vtime = this->dtinfo[0].v.time;
      const auto juldate0 = julian_date_encode(this->calendar, vdate, vtime);

      JulianDate juldate;
      double seconds = 0;
      for (int i = 1; i < nsteps; ++i)
        {
          vdate = this->dtinfo[i].v.date;
          vtime = this->dtinfo[i].v.time;
          juldate = julian_date_encode(this->calendar, vdate, vtime);

          seconds += julian_date_to_seconds(julian_date_sub(juldate, juldate0));
        }

      juldate = julian_date_add_seconds(lround(seconds / nsteps), juldate0);
      julian_date_decode(this->calendar, juldate, vdate, vtime);
#else
      vdate = this->dtinfo[nsteps / 2 - 1].v.date;
      vtime = this->dtinfo[nsteps / 2 - 1].v.time;
      const auto juldate1 = julian_date_encode(this->calendar, vdate, vtime);

      vdate = this->dtinfo[nsteps / 2].v.date;
      vtime = this->dtinfo[nsteps / 2].v.time;
      const auto juldate2 = julian_date_encode(this->calendar, vdate, vtime);

      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(this->calendar, juldatem, vdate, vtime);
#endif
    }
  else
    {
      vdate = this->dtinfo[nsteps / 2].v.date;
      vtime = this->dtinfo[nsteps / 2].v.time;
    }

  this->timestat.v.date = vdate;
  this->timestat.v.time = vtime;
}

void
DateTimeList::midhigh(const int nsteps)
{
  this->timestat.v.date = this->dtinfo[nsteps / 2].v.date;
  this->timestat.v.time = this->dtinfo[nsteps / 2].v.time;
}

void
DateTimeList::stat_taxis_def_timestep(const int taxisID, const int nsteps)
{
  if ((size_t) nsteps > this->size) cdo_abort("Internal error; unexpected nsteps=%d (limit=%ld)!", nsteps, this->size);

  if (CDO_Timestat_Date != TimeStat::UNDEF) this->stat = CDO_Timestat_Date;

  // clang-format off
  if      (this->stat == TimeStat::MEAN)    this->mean(nsteps);
  else if (this->stat == TimeStat::MIDHIGH) this->midhigh(nsteps);
  else if (this->stat == TimeStat::FIRST)   this->timestat.v = this->dtinfo[0].v;
  else if (this->stat == TimeStat::LAST)    this->timestat.v = this->dtinfo[nsteps - 1].v;
  else cdo_abort("Internal error; implementation missing for timestat=%d", (int)this->stat);
  // clang-format on

  if (this->has_bounds)
    {
      this->timestat.b[0] = this->dtinfo[0].b[0];
      this->timestat.b[1] = this->dtinfo[nsteps - 1].b[1];
    }
  else
    {
      this->timestat.b[0] = this->dtinfo[0].v;
      this->timestat.b[1] = this->dtinfo[nsteps - 1].v;
    }

  taxisDefVdate(taxisID, this->timestat.v.date);
  taxisDefVtime(taxisID, this->timestat.v.time);
  // if ( this->has_bounds )
  {
    taxisDefVdateBounds(taxisID, this->timestat.b[0].date, this->timestat.b[1].date);
    taxisDefVtimeBounds(taxisID, this->timestat.b[0].time, this->timestat.b[1].time);
  }
}

void
DateTimeList::stat_taxis_def_timestep(const int taxisID)
{
  const int nsteps = this->size;
  this->stat_taxis_def_timestep(taxisID, nsteps);
}

void
datetime_avg(const int calendar, const int ndates, CdoDateTime *const datetime)
{
  int64_t vdate;
  int vtime;

  if (ndates % 2 == 0)
    {
      vdate = datetime[ndates / 2 - 1].date;
      vtime = datetime[ndates / 2 - 1].time;
      const auto juldate1 = julian_date_encode(calendar, vdate, vtime);

      vdate = datetime[ndates / 2].date;
      vtime = datetime[ndates / 2].time;
      const auto juldate2 = julian_date_encode(calendar, vdate, vtime);

      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
    {
      vdate = datetime[ndates / 2].date;
      vtime = datetime[ndates / 2].time;
    }

  datetime[ndates].date = vdate;
  datetime[ndates].time = vtime;
}

void
adjust_month_and_year(int &month, int &year)
{
  while (month > 12)
    {
      month -= 12;
      year++;
    }
  while (month < 1)
    {
      month += 12;
      year--;
    }
}

double
delta_time_step_0(const int tsID, const int calendar, const int64_t vdate, const int vtime, JulianDate &juldate0, double &deltat1)
{
  double zj = 0;

  const auto juldate = julian_date_encode(calendar, vdate, vtime);

  if (tsID == 0)
    {
      juldate0 = juldate;
    }
  else
    {
      const auto deltat = julian_date_to_seconds(julian_date_sub(juldate, juldate0));
      if (tsID == 1) deltat1 = deltat;
      zj = deltat / deltat1;
    }

  return zj;
}

TimeIncrement
get_time_increment(const double jdelta, int64_t vdate0, int64_t vdate1)
{
  int64_t lperiod = (jdelta < 0) ? (int64_t)(jdelta - 0.5) : (int64_t)(jdelta + 0.5);

  int sign = 1;
  if (lperiod < 0)
    {
      std::swap(vdate0, vdate1);
      lperiod = -lperiod;
      sign = -1;
    }

  // printf("\n%d %d %d\n",lperiod, vdate0, vdate1);

  int year0, month0, day0;
  cdiDecodeDate(vdate0, &year0, &month0, &day0);
  int year1, month1, day1;
  cdiDecodeDate(vdate1, &year1, &month1, &day1);

  auto deltay = year1 - year0;
  auto deltam = deltay * 12 + (month1 - month0);
  if (deltay == 0) deltay = 1;
  if (deltam == 0) deltam = 1;

  TimeIncrement timeIncr;
  if (lperiod / 60 >= 1 && lperiod / 60 < 60)
    {
      timeIncr = { lperiod / 60, TimeUnit::MINUTES };
    }
  else if (lperiod / 3600 >= 1 && lperiod / 3600 < 24)
    {
      timeIncr = { lperiod / 3600, TimeUnit::HOURS };
    }
  else if (lperiod / (3600 * 24) >= 1 && lperiod / (3600 * 24) < 32)
    {
      timeIncr = { lperiod / (3600 * 24), TimeUnit::DAYS };
      if (timeIncr.period >= 27 && deltam == 1) timeIncr = { 1, TimeUnit::MONTHS };
    }
  else if (lperiod / (3600 * 24 * 30) >= 1 && lperiod / (3600 * 24 * 30) < 12)
    {
      timeIncr = { deltam, TimeUnit::MONTHS };
    }
  else if (lperiod / (3600 * 24 * 30 * 12) >= 1)
    {
      timeIncr = { deltay, TimeUnit::YEARS };
    }
  else
    {
      timeIncr = { lperiod, TimeUnit::SECONDS };
    }

  timeIncr.period *= sign;

  return timeIncr;
}

void
check_time_increment(const int tsID, const int calendar, const int64_t vdate, const int vtime, CheckTimeInc &checkTimeInc)
{
  const auto juldate = julian_date_encode(calendar, vdate, vtime);

  if (tsID)
    {
      const auto jdeltat = julian_date_to_seconds(julian_date_sub(juldate, checkTimeInc.juldate0));
      const auto timeIncr = get_time_increment(jdeltat, checkTimeInc.vdate0, vdate);

      if (tsID == 1) checkTimeInc.timeIncr = timeIncr;

      if (checkTimeInc.lwarn && (timeIncr.period != checkTimeInc.timeIncr.period || timeIncr.unit != checkTimeInc.timeIncr.unit))
        {
          checkTimeInc.lwarn = false;
          const char *tunits[] = { "second", "minute", "hour", "day", "month", "year" };
          cdo_warning("Time increment in step %d (%lld%s) differs from step 1 (%lld%s)!"
                      " Set parameter equal=false for unequal time increments!",
                      tsID + 1, timeIncr.period, tunits[(int) timeIncr.unit], checkTimeInc.timeIncr.period,
                      tunits[(int) checkTimeInc.timeIncr.unit]);
        }

      /*
      if (Options::cdoVerbose)
        fprintf(stdout, "Timestep: %d  increment: %3ld %s%s\n",
                tsID+1, (long) incperiod, tunits[(int)incunit], std::abs(incperiod) != 1 ? "s" : "");
      */
    }

  checkTimeInc.vdate0 = vdate;
  checkTimeInc.juldate0 = juldate;
}

int
decode_month(const int64_t date)
{
  int year, month, day;
  cdiDecodeDate(date, &year, &month, &day);
  return month;
}

int
decode_month_and_day(const int64_t date)
{
  int year, month, day;
  cdiDecodeDate(date, &year, &month, &day);
  return month * 100 + day;
}

int
decode_day_of_year(const int64_t date)
{
  int year, month, day;
  cdiDecodeDate(date, &year, &month, &day);
  return (month >= 1 && month <= 12) ? (month - 1) * 31 + day : 0;
}

int
decode_hour_of_year(const int64_t date, const int time)
{
  int year, month, day;
  int hour, minute, second;
  cdiDecodeDate(date, &year, &month, &day);
  cdiDecodeTime(time, &hour, &minute, &second);

  int houroy = 0;
  if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && hour >= 0 && hour < 24)
    houroy = ((month - 1) * 31 + day - 1) * 25 + hour + 1;

  return houroy;
}

int
decode_hour_of_day(const int64_t date, const int time)
{
  int year, month, day;
  int hour, minute, second;
  cdiDecodeDate(date, &year, &month, &day);
  cdiDecodeTime(time, &hour, &minute, &second);

  int hourod = 0;
  if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && hour >= 0 && hour < 24) hourod = hour + 1;

  return hourod;
}

void
set_date_time(CdoDateTime &datetime, int64_t date, const int time)
{
  int year, month, day;
  cdiDecodeDate(date, &year, &month, &day);
  if (month == 12) date = cdiEncodeDate(year - 1, month, day);

  if (date > datetime.date)
    {
      datetime.date = date;
      datetime.time = time;
    }
}
