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

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

      Seltime    seltimestep     Select timesteps
      Seltime    seltime         Select times
      Seltime    selhour         Select hours
      Seltime    selday          Select days
      Seltime    selmonth        Select months
      Seltime    selyear         Select years
      Seltime    selseason       Select seasons
      Seltime    seldate         Select dates
      Seltime    selsmon         Select single month
*/
#include <cassert>
#include <cdi.h>

#include "cdo_options.h"
#include "functs.h"
#include "process_int.h"
#include "param_conversion.h"
#include "util_string.h"

std::vector<int>
seaslist(const std::vector<std::string> &seasonString)
{
  std::vector<int> listArrayInt;
  int imon[13] = { 0 }; /* 1-12 ! */

  const int nsel = seasonString.size();
  if (isdigit(seasonString[0][0]))
    {
      for (int i = 0; i < nsel; i++)
        {
          const auto ival = parameter2int(seasonString[i]);
          // clang-format off
          if      ( ival == 1 ) { imon[12]++; imon[ 1]++; imon[ 2]++; }
          else if ( ival == 2 ) { imon[ 3]++; imon[ 4]++; imon[ 5]++; }
          else if ( ival == 3 ) { imon[ 6]++; imon[ 7]++; imon[ 8]++; }
          else if ( ival == 4 ) { imon[ 9]++; imon[10]++; imon[11]++; }
          else cdoAbort("Season %d not available!", ival);
          // clang-format on
        }
    }
  else
    {
      for (int i = 0; i < nsel; i++) season_to_months(seasonString[i].c_str(), imon);
    }

  for (int i = 1; i < 13; ++i)
    if (imon[i]) listArrayInt.push_back(i);

  return listArrayInt;
}

// last update: 2020-02-17 (Oliver Heidmann)
/*
 * Input:
 *      std::vector<std::string> => vector of length 1 or 2 containing string representing a range of dates or a single date to be selected
 * Output:
 *      std::vector<doube>       => vector of lenght 1 or 2 containing the to double converted date or date range
 *
 * This function turns a date range string into a list of double values which
 * represent the start and the end of the range.
 * when only one argument is given and it contains no time string (marked with
 * T e.g 2006-01-01T23:00:30) we set the second value of the retrun vector to
 * the end of the day from the first argument.  if only one argument is given
 * AND it contains a time string we return a vector of length one which
 * contains only the converted value of the first argument. This function
 * accepts the string "-". This represents the possible maximum start or end
 * date depending on where the string is in the arguments. If the first
 * argument is "-" we set the start date to -999999999 and to 999999999 if the
 * string is in the second.
 */

std::vector<double>
datelist(const std::vector<std::string> &arguments)
{
  std::vector<double> dateList;

  if (arguments.size() < 1) cdoAbort("Too few arguments!");
  if (arguments.size() > 2) cdoAbort("Too many arguments!");

  bool containsTime = false;
  for (size_t i = 0; i < arguments.size(); i++)
    {
      double fval = 0;
      // if "-" set start date to maximum start (i == 0) or end (i == 1)
      if (arguments[i] == "-")
        {
          fval = (i == 0) ? -99999999999. : 99999999999.;
        }
      // get the double value representing the date and check for time string
      else
        {
          fval = datestrToDouble(arguments[i].c_str(), 0);
          if (strchr(arguments[i].c_str(), 'T')) containsTime = true;
        }
      dateList.push_back(fval);
    }
  // if two arguments and no time:
  // set second argument to end of day
  if (arguments.size() == 2 && !containsTime) dateList[1] += 0.999;

  // if time and only one argument:
  // set second date to first and set second to end of day
  if (arguments.size() == 1 && !containsTime) dateList.push_back(dateList[0] + 0.999);

  return dateList;
}

std::string
getArgvString(const std::vector<std::string> &argvVec)
{
  std::string s = argvVec[0];
  for (size_t i = 1; i < argvVec.size(); i++) s += "," + argvVec[i];
  return s;
}

void *Select(void *process);

void *
Seltime(void *process)
{
  CdoStreamID streamID2 = CDO_STREAM_UNDEF;
  int nrecs;
  int varID, levelID;
  int nsel = 0;
  int ncts = 0, nts, it;
  int nts1 = 0, nts2 = 0;
  int its1 = 0, its2 = 0;
  double selfval = 0;
  bool lconstout = false;
  bool process_nts1 = false, process_nts2 = false;
  std::vector<int64_t> vdate_list;
  std::vector<int> vtime_list;
  std::vector<int> intarr;
  std::vector<double> fltarr;

  cdoInitialize(process);
  const bool lcopy = unchangedRecord();

  // clang-format off
  const auto SELTIMESTEP = cdoOperatorAdd("seltimestep", func_step,     0, "timesteps");
  const auto SELDATE     = cdoOperatorAdd("seldate",     func_datetime, 0, "start date and end date (format YYYY-MM-DDThh:mm:ss)");
  const auto SELTIME     = cdoOperatorAdd("seltime",     func_time,     0, "times (format hh:mm:ss)");
  const auto SELHOUR     = cdoOperatorAdd("selhour",     func_time,     0, "hours");
  const auto SELDAY      = cdoOperatorAdd("selday",      func_date,     0, "days");
  const auto SELMONTH    = cdoOperatorAdd("selmonth",    func_date,     0, "months");
  const auto SELYEAR     = cdoOperatorAdd("selyear",     func_date,     0, "years");
  const auto SELSEASON   = cdoOperatorAdd("selseason",   func_date,     0, "seasons");
  const auto SELSMON     = cdoOperatorAdd("selsmon",     func_date,     0, "month[,nts1[,nts2]]");
  // clang-format on

  const auto operatorID = cdoOperatorID();
  const auto operfunc = cdoOperatorF1(operatorID);

  operatorInputArg(cdoOperatorEnter(operatorID));
  const auto argv = getArgvString(cdoGetOperArgv());
  std::string newCommand = "select,";

  bool redirectEnabled = false;  // only for seperating prototype from code (redirects execution to Select.cc)
  bool redirectFound = true;     // for opers that are not redirectable for now
  int ID = operatorID;
  if (redirectEnabled)
    {
      // clang-format off
      if      (ID == SELTIMESTEP ) {newCommand += "timestep=" + argv;}
      else if (ID == SELDATE)      {newCommand += "date="     + argv;}
      //else if(ID == SELTIME)    {newCommand += "time="      + argv;}  // unimplemented
      else if (ID == SELHOUR)      {newCommand += "hour="     + argv;}
      else if (ID == SELDAY)       {newCommand += "day="      + argv;}
      else if (ID == SELMONTH)     {newCommand += "month="    + argv;}
      else if (ID == SELYEAR)      {newCommand += "year="     + argv;}
      else if (ID == SELSEASON)    {newCommand += "season="   + argv;}
      else if (ID == SELSMON)      {newCommand += "month="    + argv;}
      else                         {redirectFound = false;}
      // clang-format on
    }

  if (redirectFound && redirectEnabled)
    {
      Debug(Yellow("Redirecting to %s"), newCommand);
      ((Process *) process)->initProcess("select", newCommand.c_str());
      return Select(process);
      // If a redirect was found the entire process is ended through this return!
    }

  if (operatorID == SELSEASON)
    {
      intarr = seaslist(cdoGetOperArgv());
      nsel = intarr.size();
    }
  else if (operatorID == SELDATE)
    {
      fltarr = datelist(cdoGetOperArgv());
      nsel = fltarr.size();
    }
  else if (operatorID == SELTIME)
    {
      nsel = operatorArgc();
      if (nsel < 1) cdoAbort("Too few arguments!");
      intarr.reserve(nsel);
      for (int i = 0; i < nsel; i++)
        {
          auto currentArgument = cdoOperatorArgv(i).c_str();
          if (strchr(currentArgument, ':'))
            {
              int hour = 0, minute = 0, second = 0;
              sscanf(currentArgument, "%d:%d:%d", &hour, &minute, &second);
              intarr.push_back(cdiEncodeTime(hour, minute, second));
            }
          else
            {
              intarr.push_back(parameter2int(currentArgument));
            }
        }
    }
  else
    {
      intarr = cdoArgvToInt(cdoGetOperArgv());
      nsel = intarr.size();
    }

  if (nsel < 1) cdoAbort("No timestep selected!");

  if (operatorID == SELSMON)
    {
      if (nsel > 1) nts1 = intarr[1];
      nts2 = (nsel > 2) ? intarr[2] : nts1;

      if (nsel > 3) cdoAbort("Too many parameters");

      if (Options::cdoVerbose) cdoPrint("mon=%d  nts1=%d  nts2=%d", intarr[0], nts1, nts2);

      nsel = 1;
    }

  const auto streamID1 = cdoOpenRead(0);

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

  vlistDefNtsteps(vlistID2, (nsel == 1 && operfunc == func_step) ? 1 : -1);

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

  VarList varList1;
  varListInit(varList1, vlistID1);

  Varray<double> array;
  if (!lcopy)
    {
      auto gridsizemax = vlistGridsizeMax(vlistID1);
      if (vlistNumber(vlistID1) != CDI_REAL) gridsizemax *= 2;
      array.resize(gridsizemax);
    }

  auto ntsteps = vlistNtsteps(vlistID1);

  // add support for negative timestep values
  if (operatorID == SELTIMESTEP)
    {
      for (int i = 0; i < nsel; i++)
        {
          if (intarr[i] < 0)
            {
              if (ntsteps < 0 && cdoAssertFilesOnly())
                {
                  int tsID = 0;
                  while (cdoStreamInqTimestep(streamID1, tsID)) tsID++;
                  ntsteps = tsID;
                  if (Options::cdoVerbose) cdoPrint("Found %d timesteps", ntsteps);
                }
              if (ntsteps > 0)
                {
                  if (Options::cdoVerbose) cdoPrint("timestep %d changed to %d", intarr[i], ntsteps + 1 + intarr[i]);
                  intarr[i] = ntsteps + 1 + intarr[i];
                }
            }
        }
    }

  if (Options::cdoVerbose)
    {
      for (int i = 0; i < nsel; i++)
        if (operatorID == SELDATE)
          cdoPrint("fltarr entry: %d %14.4f", i + 1, fltarr[i]);
        else
          cdoPrint("intarr entry: %d %d", i + 1, intarr[i]);
    }

  std::vector<bool> selfound;
  if (nsel)
    {
      selfound.resize(nsel);
      for (int i = 0; i < nsel; i++) selfound[i] = false;
    }

  int iselmax = -1;
  if (operatorID != SELDATE)
    for (int i = 0; i < nsel; i++) iselmax = std::max(iselmax, intarr[i]);

  const auto nvars = vlistNvars(vlistID1);
  int nconst = 0;
  for (varID = 0; varID < nvars; varID++)
    if (varList1[varID].timetype == TIME_CONSTANT) nconst++;

  FieldVector3D vars;

  const bool lnts1 = (operatorID == SELSMON) && (nts1 > 0);

  if (lnts1 || nconst)
    {
      if (lnts1)
        {
          vdate_list.resize(nts1);
          vtime_list.resize(nts1);
        }
      else
        {
          nts1 = 1;
        }

      vars.resize(nts1);

      for (int tsID = 0; tsID < nts1; tsID++)
        {
          fieldsFromVlist(vlistID1, vars[tsID]);

          for (varID = 0; varID < nvars; varID++)
            {
              if (lnts1 || varList1[varID].timetype == TIME_CONSTANT)
                {
                  const auto nlevels = varList1[varID].nlevels;
                  for (levelID = 0; levelID < nlevels; levelID++)
                    vars[tsID][varID][levelID].resize(varList1[varID].gridsize);
                }
            }
        }
    }

  int tsID = 0;
  int tsID2 = 0;
  while ((nrecs = cdoStreamInqTimestep(streamID1, tsID)))
    {
      const auto vdate = taxisInqVdate(taxisID1);
      const auto vtime = taxisInqVtime(taxisID1);

      bool copytimestep = false;
      int selival = -1;

      if (operfunc == func_step)
        {
          selival = tsID + 1;
          if (selival > iselmax) break;
        }
      else if (operfunc == func_date)
        {
          int year, month, day;
          cdiDecodeDate(vdate, &year, &month, &day);
          if (operatorID == SELYEAR)
            selival = year;
          else if (operatorID == SELDAY)
            selival = day;
          else
            selival = month;
        }
      else if (operfunc == func_time)
        {
          int hour, minute, second;
          cdiDecodeTime(vtime, &hour, &minute, &second);
          if (operatorID == SELHOUR)
            selival = hour;
          else if (operatorID == SELTIME)
            selival = vtime;
        }
      else if (operfunc == func_datetime)
        {
          selfval = vdate + vtime / 1000000.;
        }

      if (operatorID == SELDATE)
        {
          if (selfval >= fltarr[0] && selfval <= fltarr[nsel - 1])
            {
              copytimestep = true;
              selfound[0] = true;
              selfound[nsel - 1] = true;
            }
          else if (selfval > fltarr[nsel - 1])
            {
              break;
            }
        }
      else
        {
          for (int i = 0; i < nsel; i++)
            if (selival == intarr[i])
              {
                copytimestep = true;
                selfound[i] = true;
                break;
              }
        }

      bool copy_nts2 = false;
      if (operatorID == SELSMON && !copytimestep)
        {
          copy_nts2 = false;

          if (process_nts1)
            {
              process_nts2 = true;
              its2 = 0;
              process_nts1 = false;
            }

          if (process_nts2)
            {
              if (its2++ < nts2)
                copy_nts2 = true;
              else
                process_nts2 = false;
            }
        }

      if (copytimestep || copy_nts2)
        {
          taxisCopyTimestep(taxisID2, taxisID1);
          if (tsID2 == 0)
            {
              streamID2 = cdoOpenWrite(1);
              cdoDefVlist(streamID2, vlistID2);
            }

          if (lnts1 && ncts == 0)
            {
              nts = nts1;
              if (its1 < nts1)
                {
                  nts = its1;
                  cdoWarning("%d timesteps missing before month %d!", nts1 - its1, intarr[0]);
                }

              for (it = 0; it < nts; it++)
                {
                  taxisDefVdate(taxisID2, vdate_list[it]);
                  taxisDefVtime(taxisID2, vtime_list[it]);
                  cdoDefTimestep(streamID2, tsID2++);

                  for (varID = 0; varID < nvars; varID++)
                    {
                      if (varList1[varID].timetype == TIME_CONSTANT && tsID2 > 1) continue;
                      const auto nlevels = varList1[varID].nlevels;
                      for (levelID = 0; levelID < nlevels; levelID++)
                        {
                          cdoDefRecord(streamID2, varID, levelID);
                          auto single = vars[it][varID][levelID].vec_d.data();
                          const auto nmiss = vars[it][varID][levelID].nmiss;
                          cdoWriteRecord(streamID2, single, nmiss);
                        }
                    }
                }

              its1 = 0;
            }

          ncts++;
          if (!process_nts2)
            {
              its2 = 0;
              process_nts1 = true;
            }

          cdoDefTimestep(streamID2, tsID2++);

          if (tsID > 0 && lconstout)
            {
              lconstout = false;
              nts = nts1 - 1;
              for (varID = 0; varID < nvars; varID++)
                {
                  if (varList1[varID].timetype == TIME_CONSTANT)
                    {
                      const auto nlevel = varList1[varID].nlevels;
                      for (levelID = 0; levelID < nlevel; levelID++)
                        {
                          cdoDefRecord(streamID2, varID, levelID);
                          auto single = vars[nts][varID][levelID].vec_d.data();
                          const auto nmiss = vars[nts][varID][levelID].nmiss;
                          cdoWriteRecord(streamID2, single, nmiss);
                        }
                    }
                }
            }

          for (int recID = 0; recID < nrecs; recID++)
            {
              cdoInqRecord(streamID1, &varID, &levelID);
              cdoDefRecord(streamID2, varID, levelID);
              if (lcopy)
                {
                  cdoCopyRecord(streamID2, streamID1);
                }
              else
                {
                  size_t nmiss;
                  cdoReadRecord(streamID1, array.data(), &nmiss);
                  cdoWriteRecord(streamID2, array.data(), nmiss);
                }
            }
        }
      else
        {
          ncts = 0;

          if (lnts1 || tsID == 0)
            {
              if (tsID == 0 && nconst && (!lnts1)) lconstout = true;

              nts = nts1 - 1;
              if (lnts1)
                {
                  if (its1 <= nts)
                    nts = its1;
                  else
                    for (it = 0; it < nts; it++)
                      {
                        vdate_list[it] = vdate_list[it + 1];
                        vtime_list[it] = vtime_list[it + 1];
                        for (varID = 0; varID < nvars; varID++)
                          {
                            if (varList1[varID].timetype == TIME_CONSTANT) continue;
                            const auto nlevels = varList1[varID].nlevels;
                            for (levelID = 0; levelID < nlevels; levelID++)
                              {
                                vars[it][varID][levelID].vec_d = vars[it + 1][varID][levelID].vec_d;
                                vars[it][varID][levelID].nmiss = vars[it + 1][varID][levelID].nmiss;
                              }
                          }
                      }

                  vdate_list[nts] = taxisInqVdate(taxisID1);
                  vtime_list[nts] = taxisInqVtime(taxisID1);

                  its1++;
                }

              for (int recID = 0; recID < nrecs; recID++)
                {
                  cdoInqRecord(streamID1, &varID, &levelID);
                  if (lnts1 || varList1[varID].timetype == TIME_CONSTANT)
                    {
                      auto single = vars[nts][varID][levelID].vec_d.data();
                      cdoReadRecord(streamID1, single, &vars[nts][varID][levelID].nmiss);
                    }
                }
            }
        }

      tsID++;
    }

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

  if (operatorID == SELSMON)
    if (its2 < nts2) cdoWarning("%d timesteps missing after the last month!", nts2 - its2);

  for (int isel = 0; isel < nsel; isel++)
    {
      if (selfound[isel] == false)
        {

          int isel2;
          bool lcont = false;
          for (isel2 = isel + 1; isel2 < nsel; isel2++)
            if (selfound[isel2]) break;
          if (isel2 == nsel && (nsel - isel) > 1) lcont = true;

          if (operatorID == SELTIMESTEP)
            {
              bool lcont2 = false;
              if (lcont)
                {
                  for (isel2 = isel + 1; isel2 < nsel; isel2++)
                    if (intarr[isel2 - 1] != intarr[isel2] - 1) break;
                  if (isel2 == nsel) lcont2 = true;
                }

              if (lcont2)
                {
                  cdoWarning("Timesteps %d-%d not found!", intarr[isel], intarr[nsel - 1]);
                  break;
                }
              else
                cdoWarning("Timestep %d not found!", intarr[isel]);
            }
          else if (operatorID == SELDATE)
            {
              if (isel == 0) cdoWarning("Date between %14.4f and %14.4f not found!", fltarr[0], fltarr[nsel - 1]);
            }
          else if (operatorID == SELTIME)
            {
              cdoWarning("Time %d not found!", intarr[isel]);
            }
          else if (operatorID == SELHOUR)
            {
              cdoWarning("Hour %d not found!", intarr[isel]);
            }
          else if (operatorID == SELDAY)
            {
              cdoWarning("Day %d not found!", intarr[isel]);
            }
          else if (operatorID == SELMONTH)
            {
              cdoWarning("Month %d not found!", intarr[isel]);
            }
          else if (operatorID == SELYEAR)
            {
              cdoWarning("Year %d not found!", intarr[isel]);
            }
          else if (operatorID == SELSEASON)
            {
              if (isel < 3) cdoWarning("Month %d not found!", intarr[isel]);
            }
        }
    }

  vlistDestroy(vlistID2);

  if (tsID2 == 0) cdoAbort("No timesteps selected!");

  cdoFinish();

  return nullptr;
}
