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

      Tinfo      tinfo           Time information
*/

#include <cdi.h>

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

#define MAX_GAPS 64
#define MAX_NTSM 128
#define LIM_NTSM 1024

const char *tunits[] = { "second", "minute", "hour", "day", "month", "year" };
int iunits[] = { 1, 60, 3600, 86400, 1, 12 };

static void
printBounds(int taxisID, int calendar)
{
  int64_t vdate0, vdate1;
  taxisInqVdateBounds(taxisID, &vdate0, &vdate1);
  int vtime0, vtime1;
  taxisInqVtimeBounds(taxisID, &vtime0, &vtime1);

  fprintf(stdout, " %s %s", date_to_string(vdate0).c_str(), time_to_string(vtime0).c_str());
  fprintf(stdout, " %s %s", date_to_string(vdate1).c_str(), time_to_string(vtime1).c_str());

  const auto juldate0 = julian_date_encode(calendar, vdate0, vtime0);
  const auto juldate1 = julian_date_encode(calendar, vdate1, vtime1);
  const auto jdelta = julian_date_to_seconds(julian_date_sub(juldate1, juldate0));

  const auto timeIncr = get_time_increment(jdelta, vdate0, vdate1);

  // fprintf(stdout, "  %g  %g  %g  %d", jdelta, jdelta/3600, std::fmod(jdelta,3600), timeIncr.period%3600);
  int len = fprintf(stdout, " %3ld %s%s", (long) timeIncr.period, tunits[(int) timeIncr.unit],
                    std::abs(timeIncr.period) != 1 ? "s" : "");
  for (int i = 0; i < 11 - len; ++i) fprintf(stdout, " ");
}

static int
fill_gap(int ngaps, int ntsm[MAX_NTSM], int rangetsm[MAX_GAPS][2], int64_t vdatem[MAX_GAPS][MAX_NTSM],
         int vtimem[MAX_GAPS][MAX_NTSM], int tsID, int64_t incperiod0, TimeUnit incunit0, int64_t vdate, int vdate0, int vtime0,
         int calendar, int day0, JulianDate juldate, JulianDate juldate0)
{
  int its = 0;
  int year, month, day;
  int64_t ndate;
  int ntime;
  int64_t ijulinc = incperiod0 * iunits[(int) incunit0];

  if (ijulinc > 0 && ngaps < MAX_GAPS)
    {
      rangetsm[ngaps][0] = tsID;
      rangetsm[ngaps][1] = tsID + 1;

      if (incunit0 == TimeUnit::MONTHS || incunit0 == TimeUnit::YEARS)
        {
          its = 0;
          ndate = vdate0;
          // printf("fill_gap %d\n", ndate);
          while (true)
            {
              cdiDecodeDate(ndate, &year, &month, &day);

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

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

              ndate = cdiEncodeDate(year, month, day);
              ntime = vtime0;
              if (ndate >= vdate) break;
              /* printf("\n1 %d %d\n", ndate, ntime); */
              if (its < MAX_NTSM)
                {
                  vdatem[ngaps][its] = ndate;
                  vtimem[ngaps][its] = ntime;
                }
              else if (its >= LIM_NTSM)
                break;

              its++;
            }
        }
      else
        {
          its = 0;
          juldate0 = julian_date_add_seconds(ijulinc, juldate0);
          while (julian_date_to_seconds(juldate0) < julian_date_to_seconds(juldate))
            {
              julian_date_decode(calendar, juldate0, ndate, ntime);
              juldate0 = julian_date_add_seconds(ijulinc, juldate0);
              if (its < MAX_NTSM)
                {
                  vdatem[ngaps][its] = ndate;
                  vtimem[ngaps][its] = ntime;
                }
              else if (its >= LIM_NTSM)
                break;

              its++;
            }
        }
      ntsm[ngaps] = its;
    }

  return its;
}

void *
Tinfo(void *process)
{
  int64_t vdate_first = 0, vdate0 = 0, vdate = 0;
  int vtime_first = 0, vtime0 = 0, vtime = 0;
  int tsID = 0, ntimeout;
  int year0, month0, day0 = 0;
  int year, month, day;
  bool lforecast = false;
  int64_t incperiod0 = 0, incperiod = 0;
  TimeUnit incunit0 = TimeUnit::SECONDS, incunit = TimeUnit::SECONDS;
  int its = 0, igap;
  int ngaps = 0;
  int ntsm[MAX_GAPS];
  int rangetsm[MAX_GAPS][2];
  int64_t vdatem[MAX_GAPS][MAX_NTSM];
  int vtimem[MAX_GAPS][MAX_NTSM];
  JulianDate juldate, juldate0;
  double jdelta = 0, jdelta0 = 0;
  int arrow = 0;
  int i, len;

  cdo_initialize(process);

  operator_check_argc(0);

  const auto streamID = cdo_open_read(0);
  const auto vlistID = cdo_stream_inq_vlist(streamID);

  fprintf(stdout, "\n");

  const auto taxisID = vlistInqTaxis(vlistID);
  const auto ntsteps = vlistNtsteps(vlistID);

  if (ntsteps != 0)
    {
      if (ntsteps == CDI_UNDEFID)
        fprintf(stdout, "   Time axis :  unlimited steps\n");
      else
        fprintf(stdout, "   Time axis :  %d step%s\n", ntsteps, ntsteps == 1 ? "" : "s");

      if (taxisID != CDI_UNDEFID)
        {
          if (taxisInqType(taxisID) != TAXIS_ABSOLUTE)
            {
              vdate = taxisInqRdate(taxisID);
              vtime = taxisInqRtime(taxisID);
              fprintf(stdout, "     RefTime = %s %s", date_to_string(vdate).c_str(), time_to_string(vtime).c_str());

              auto unit = taxisInqTunit(taxisID);
              if (unit != CDI_UNDEFID) fprintf(stdout, "  Units = %s", tunit_to_cstr(unit));

              const auto calendar = taxisInqCalendar(taxisID);
              if (calendar != CDI_UNDEFID) fprintf(stdout, "  Calendar = %s", calendar_to_cstr(calendar));

              if (taxisHasBounds(taxisID)) fprintf(stdout, "  Bounds = true");

              fprintf(stdout, "\n");

              if (taxisInqType(taxisID) == TAXIS_FORECAST)
                {
                  const auto fdate = taxisInqFdate(taxisID);
                  const auto ftime = taxisInqFtime(taxisID);
                  fprintf(stdout, "     Forecast RefTime = %s %s", date_to_string(fdate).c_str(), time_to_string(ftime).c_str());

                  unit = taxisInqForecastTunit(taxisID);
                  if (unit != CDI_UNDEFID) fprintf(stdout, "  Units = %s", tunit_to_cstr(unit));

                  fprintf(stdout, "\n");

                  lforecast = true;
                }
            }
        }

      const auto calendar = taxisInqCalendar(taxisID);

      fprintf(stdout, "\n");
      fprintf(stdout, "         Verification Time              ");
      if (lforecast) fprintf(stdout, " Forecast Reference Time     ");
      if (taxisHasBounds(taxisID)) fprintf(stdout, " lower bound          upper bound");
      fprintf(stdout, "\n");

      fprintf(stdout, "Timestep YYYY-MM-DD hh:mm:ss   Increment");
      if (lforecast) fprintf(stdout, " YYYY-MM-DD hh:mm:ss   Period");
      if (taxisHasBounds(taxisID)) fprintf(stdout, " YYYY-MM-DD hh:mm:ss  YYYY-MM-DD hh:mm:ss  Difference");
      fprintf(stdout, "\n");

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

          vdate = taxisInqVdate(taxisID);
          vtime = taxisInqVtime(taxisID);

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

          fprintf(stdout, "%6d  %s %s", tsID + 1, date_to_string(vdate).c_str(), time_to_string(vtime).c_str());

          if (tsID)
            {
              cdiDecodeDate(vdate0, &year0, &month0, &day0);

              juldate0 = julian_date_encode(calendar, vdate0, vtime0);
              juldate = julian_date_encode(calendar, vdate, vtime);
              jdelta = julian_date_to_seconds(julian_date_sub(juldate, juldate0));

              const auto timeIncr = get_time_increment(jdelta, vdate0, vdate);
              incperiod = timeIncr.period;
              incunit = timeIncr.unit;

              // fprintf(stdout, "  %g  %g  %g  %d", jdelta, jdelta/3600, std::fmod(jdelta,3600), incperiod%3600);
              len = fprintf(stdout, " %3ld %s%s", (long) incperiod, tunits[(int) incunit], std::abs(incperiod) != 1 ? "s" : "");
              for (i = 0; i < 11 - len; ++i) fprintf(stdout, " ");
            }
          else
            {
              vdate_first = vdate;
              vtime_first = vtime;
              fprintf(stdout, "   --------");
            }

          if (lforecast)
            {
              const auto fdate = taxisInqFdate(taxisID);
              const auto ftime = taxisInqFtime(taxisID);
              fprintf(stdout, " %s %s", date_to_string(fdate).c_str(), time_to_string(ftime).c_str());

              const auto fc_period = taxisInqForecastPeriod(taxisID);
              fprintf(stdout, " %7g", fc_period);
            }

          if (taxisHasBounds(taxisID)) printBounds(taxisID, calendar);

          if (tsID > 1 && (incperiod != incperiod0 || incunit != incunit0))
            {
              if (tsID == 2 && (jdelta0 > jdelta))
                {
                  jdelta0 = jdelta;
                  incperiod0 = incperiod;
                  incunit0 = incunit;

                  its = fill_gap(ngaps, ntsm, rangetsm, vdatem, vtimem, 1, incperiod0, incunit0, vdate_first, vdate, vtime,
                                 calendar, day, juldate0, julian_date_encode(calendar, vdate_first, vtime_first));

                  arrow = '^';
                }
              else
                {
                  its = fill_gap(ngaps, ntsm, rangetsm, vdatem, vtimem, tsID, incperiod0, incunit0, vdate, vdate0, vtime0, calendar,
                                 day0, juldate, juldate0);

                  arrow = '<';

                  if (its == 0 && incperiod < 0)
                    {
                      its = -1;
                      vdate = vdate0;
                      vtime = vtime0;
                    }
                }

              if (its > 0)
                {
                  ngaps++;
                  if (Options::cdoVerbose)
                    fprintf(stdout, "  %c--- Gap %d, missing %s%d timestep%s", arrow, ngaps, its >= LIM_NTSM ? "more than " : "",
                            its, its != 1 ? "s" : "");
                }
              else if (its < 0)
                {
                  if (Options::cdoVerbose) fprintf(stdout, "  %c--- Wrong date/time information, negative increment!", arrow);
                }
            }

          if (tsID == 1)
            {
              jdelta0 = jdelta;
              incperiod0 = incperiod;
              incunit0 = incunit;
            }

          fprintf(stdout, "\n");

          vdate0 = vdate;
          vtime0 = vtime;

          tsID++;
        }
    }

  cdo_stream_close(streamID);

  fprintf(stdout, "\n");

  fprintf(stdout, " Start date          : %s %s\n", date_to_string(vdate_first).c_str(), time_to_string(vtime_first).c_str());
  fprintf(stdout, " End date            : %s %s\n", date_to_string(vdate).c_str(), time_to_string(vtime).c_str());

  fprintf(stdout, " Increment           : %3ld %s%s\n", (long) incperiod0, tunits[(int) incunit0], incperiod0 != 1 ? "s" : "");
  fprintf(stdout, " Number of timesteps : %d\n", tsID);
  fprintf(stdout, " Gaps identified     : %d\n", ngaps);

  if (Options::cdoVerbose && ngaps)
    {
      fprintf(stdout, "\nFound potentially %d gap%s in the time series", ngaps, ngaps != 1 ? "s" : "");
      if (ngaps >= MAX_GAPS)
        {
          ngaps = MAX_GAPS;
          fprintf(stdout, ", here are the first %d", ngaps);
        }
      fprintf(stdout, ":\n");
      for (igap = 0; igap < ngaps; ++igap)
        {
          fprintf(stdout, "  Gap %d between timestep %d and %d, missing %d timestep%s", igap + 1, rangetsm[igap][0],
                  rangetsm[igap][1], ntsm[igap], ntsm[igap] != 1 ? "s" : "");
          if (ntsm[igap] >= MAX_NTSM)
            {
              ntsm[igap] = MAX_NTSM;
              fprintf(stdout, ", here are the first %d", ntsm[igap]);
            }
          fprintf(stdout, ":\n");

          ntimeout = 0;
          for (its = 0; its < ntsm[igap]; ++its)
            {
              if (ntimeout == 4)
                {
                  ntimeout = 0;
                  fprintf(stdout, "\n");
                }

              vdate = vdatem[igap][its];
              vtime = vtimem[igap][its];
              fprintf(stdout, "  %s %s", date_to_string(vdate).c_str(), time_to_string(vtime).c_str());

              ntimeout++;
              tsID++;
            }
          fprintf(stdout, "\n");
        }
    }

  cdo_finish();

  return nullptr;
}
