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

      Exprf      expr            Evaluate expressions
      Exprf      exprf           Evaluate expressions from script file
      Exprf      aexpr           Append evaluated expressions
      Exprf      aexprf          Append evaluated expressions from script file
*/
/*
  Operatoren: +, -, *, \, ^, ==, !=, >, <, >=, <=, <=>, &&, ||, ?:
  Functions: sqrt, exp, log, log10, sin, cos, tan, asin, acos, atan
  Functions: min, max, avg, std, var
  Constansts: M_PI, M_E
*/

#include <algorithm>
#include <sys/stat.h> /* stat */
#include <cstdlib>
#include <cassert>

#include <cdi.h>

#include "cdo_options.h"
#include "cdo_vlist.h"
#include "dmemory.h"
#include "process_int.h"
#include <mpim_grid.h>
#include "gridreference.h"
#include "expr.h"
#include "datetime.h"
#include "cdo_zaxis.h"
#include "cdi_lockedIO.h"

void grid_cell_area(int gridID, double *array);
int getSurfaceID(int vlistID);

static char *
exprsFromArgument(const std::vector<std::string> &exprArgv)
{
  char *exprs = nullptr;
  const int exprArgc = exprArgv.size();
  auto firstArg = exprArgv[0].c_str();

  if (exprArgc == 1)
    {
      const size_t slen = strlen(firstArg);
      exprs = (char *) Malloc(slen + 2);
      strcpy(exprs, firstArg);
      if (exprs[slen - 1] != ';')
        {
          exprs[slen] = ';';
          exprs[slen + 1] = 0;
        }
    }
  else if (exprArgc > 1)
    {
      size_t slen = exprArgc + 1;
      for (int i = 0; i < exprArgc; ++i) slen += exprArgv[i].size();
      exprs = (char *) Malloc(slen);
      strcpy(exprs, firstArg);
      char *pexprs = exprs + strlen(firstArg);
      for (int i = 1; i < exprArgc; ++i)
        {
          *pexprs++ = ',';
          strcpy(pexprs, exprArgv[i].c_str());
          pexprs += exprArgv[i].size();
        }
      if (*pexprs != ';')
        {
          *pexprs++ = ';';
          *pexprs = 0;
        }
    }
  else
    {
      operatorCheckArgc(1);
    }

  return exprs;
}

static char *
exprsFromFile(const std::vector<std::string> &exprArgv)
{
  const int exprArgc = exprArgv.size();
  if (exprArgc != 1) operatorCheckArgc(1);
  auto exprf = exprArgv[0].c_str();

  /* Open expr script file for reading */
  auto fp = fopen(exprf, "r");
  if (!fp) cdoAbort("Open failed on %s", exprf);

  struct stat filestat;
  if (stat(exprf, &filestat) != 0) cdoAbort("Stat failed on %s", exprf);

  const size_t fsize = (size_t) filestat.st_size;
  char *exprs = (char *) Malloc(fsize + 1);

  int ichar, ipos = 0;
  while ((ichar = fgetc(fp)) != EOF) exprs[ipos++] = ichar;

  exprs[ipos] = 0;
  if (ipos == 0) cdoAbort("%s is empty!", exprf);

  fclose(fp);

  return exprs;
}

#define MAX_PARAMS 4096

static char *
str_replace(char *target, const char *needle, const char *replacement)
{
  char buffer[1024] = { 0 };
  char *insert_point = &buffer[0];
  auto tmp = target;
  const auto needle_len = strlen(needle);
  const auto repl_len = strlen(replacement);

  while (1)
    {
      char *p = strstr(tmp, needle);

      // walked past last occurrence of needle; copy remaining part
      if (p == nullptr)
        {
          strcpy(insert_point, tmp);
          break;
        }

      // copy part before needle
      memcpy(insert_point, tmp, p - tmp);
      insert_point += p - tmp;

      // copy replacement string
      memcpy(insert_point, replacement, repl_len);
      insert_point += repl_len;

      // adjust pointers, move on
      tmp = p + needle_len;
    }

  // write altered string back to target
  strcpy(target, buffer);

  return target;
}

static char *
exprsExpand(char *exprs, int vlistID)
{
  const auto len = strlen(exprs);
  unsigned nequal = 0, nsemi = 0;
  for (size_t i = 0; i < len; ++i)
    {
      if (exprs[i] == '=') nequal++;
      if (exprs[i] == ';') nsemi++;
    }

  const char *needle = "_T";
  if (nequal == 0 && nsemi == 1 && strstr(exprs, needle))
    {
      char varname[CDI_MAX_NAME];
      const auto nvars = vlistNvars(vlistID);
      const size_t bufsize = 1024;
      char *sbuf = (char *) Malloc(bufsize);
      char *buf = (char *) Malloc(nvars * bufsize);
      buf[0] = 0;
      for (int varID = 0; varID < nvars; ++varID)
        {
          vlistInqVarName(vlistID, varID, varname);
          strcpy(sbuf, exprs);
          str_replace(sbuf, needle, varname);
          strcat(buf, varname);
          strcat(buf, "=");
          strcat(buf, sbuf);
        }
      Free(sbuf);
      Free(exprs);
      exprs = buf;
    }

  return exprs;
}

static void
params_init(std::vector<paramType> &params, int vlistID)
{
  for (int varID = 0; varID < MAX_PARAMS; varID++)
    {
      params[varID].data = nullptr;
      params[varID].name = nullptr;
      params[varID].longname = nullptr;
      params[varID].units = nullptr;
    }

  char name[CDI_MAX_NAME], longname[CDI_MAX_NAME], units[CDI_MAX_NAME];

  const auto nvars1 = vlistNvars(vlistID);
  for (int varID = 0; varID < nvars1; varID++)
    {
      const auto gridID = vlistInqVarGrid(vlistID, varID);
      const auto zaxisID = vlistInqVarZaxis(vlistID, varID);
      const auto datatype = vlistInqVarDatatype(vlistID, varID);
      const auto steptype = vlistInqVarTimetype(vlistID, varID);
      const auto nlat = gridInqYsize(gridID);
      const auto ngp = gridInqSize(gridID);
      const auto nlev = zaxisInqSize(zaxisID);
      const auto missval = vlistInqVarMissval(vlistID, varID);

      vlistInqVarName(vlistID, varID, name);
      vlistInqVarLongname(vlistID, varID, longname);
      vlistInqVarUnits(vlistID, varID, units);

      params[varID].type = PARAM_VAR;
      params[varID].select = false;
      params[varID].remove = false;
      params[varID].lmiss = true;
      params[varID].coord = 0;
      params[varID].gridID = gridID;
      params[varID].zaxisID = zaxisID;
      params[varID].datatype = datatype;
      params[varID].steptype = steptype;
      params[varID].nlat = nlat;
      params[varID].ngp = ngp;
      params[varID].nlev = nlev;
      params[varID].missval = missval;
      params[varID].nmiss = 0;
      params[varID].data = nullptr;
      params[varID].name = strdup(name);
      params[varID].longname = strdup(longname);
      params[varID].units = strdup(units);
    }
}

static void
params_delete(std::vector<paramType> &params)
{
  for (int varID = 0; varID < MAX_PARAMS; varID++)
    {
      if (params[varID].data) Free(params[varID].data);
      if (params[varID].name) Free(params[varID].name);
      if (params[varID].longname) Free(params[varID].longname);
      if (params[varID].units) Free(params[varID].units);
    }
}

static void
params_add_coord(parseParamType &parse_arg, int coord, int cdiID, size_t size, const char *units, const char *longname)
{
  const auto ncoords = parse_arg.ncoords;
  if (ncoords >= parse_arg.maxcoords) cdoAbort("Too many coordinates (limit=%d)", parse_arg.maxcoords);

  parse_arg.coords[ncoords].needed = false;
  parse_arg.coords[ncoords].coord = coord;
  parse_arg.coords[ncoords].cdiID = cdiID;
  parse_arg.coords[ncoords].size = size;
  if (units)
    {
      parse_arg.coords[ncoords].units.resize(strlen(units) + 1);
      strcpy(parse_arg.coords[ncoords].units.data(), units);
    }
  if (longname)
    {
      parse_arg.coords[ncoords].longname.resize(strlen(longname) + 1);
      strcpy(parse_arg.coords[ncoords].longname.data(), longname);
    }

  parse_arg.ncoords++;
}

int
params_get_coordID(parseParamType *parse_arg, int coord, int cdiID)
{
  const auto ncoords = parse_arg->ncoords;
  for (int coordID = 0; coordID < ncoords; ++coordID)
    {
      if (parse_arg->coords[coordID].coord == coord && parse_arg->coords[coordID].cdiID == cdiID) return coordID;
    }

  cdoAbort("%s: coordinate %c not found!", __func__, coord);

  return -1;
}

static void
params_add_coordinates(int vlistID, parseParamType &parse_arg)
{
  char longname[CDI_MAX_NAME], units[CDI_MAX_NAME];

  const auto ngrids = vlistNgrids(vlistID);
  for (int index = 0; index < ngrids; ++index)
    {
      const auto gridID = vlistGrid(vlistID, index);
      const auto size = gridInqSize(gridID);
      int length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_UNITS, units, &length);
      params_add_coord(parse_arg, 'x', gridID, size, units, "longitude");
      length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_UNITS, units, &length);
      params_add_coord(parse_arg, 'y', gridID, size, units, "latitude");

      params_add_coord(parse_arg, 'a', gridID, size, "m^2", "grid cell area");
      params_add_coord(parse_arg, 'w', gridID, size, nullptr, "grid cell area weights");
    }

  const auto nzaxis = vlistNzaxis(vlistID);
  for (int index = 0; index < nzaxis; ++index)
    {
      const auto zaxisID = vlistZaxis(vlistID, index);
      const auto size = zaxisInqSize(zaxisID);
      int length = CDI_MAX_NAME;
      cdiInqKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_UNITS, units, &length);
      length = CDI_MAX_NAME;
      cdiInqKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_LONGNAME, longname, &length);
      params_add_coord(parse_arg, 'z', zaxisID, size, units, longname);
    }
}

static int
params_add_ts(parseParamType &parse_arg)
{
  int varID = -1;
  auto params = parse_arg.params;
  if (params)
    {
      varID = parse_arg.nparams;
      if (varID >= parse_arg.maxparams) cdoAbort("Too many parameter (limit=%d)", parse_arg.maxparams);

      params[varID].name = strdup("_ts");
      params[varID].gridID = parse_arg.pointID;
      params[varID].zaxisID = parse_arg.surfaceID;
      params[varID].steptype = TIME_VARYING;
      params[varID].ngp = CLEN;
      params[varID].nlev = 1;

      parse_arg.nparams++;
      parse_arg.cnparams++;
    }

  return varID;
}

static void
parseParamInit(parseParamType &parse_arg, int vlistID, int pointID, int zonalID, int surfaceID, paramType *params)
{
  const auto nvars = vlistNvars(vlistID);
  const auto ngrids = vlistNgrids(vlistID);
  const auto nzaxis = vlistNzaxis(vlistID);
  const auto maxcoords = ngrids * 4 + nzaxis;

  parse_arg.maxparams = MAX_PARAMS;
  parse_arg.nparams = nvars;
  parse_arg.cnparams = nvars;
  parse_arg.nvars1 = nvars;
  parse_arg.init = true;
  parse_arg.debug = Options::cdoVerbose != 0;
  parse_arg.params = params;
  parse_arg.pointID = pointID;
  parse_arg.zonalID = zonalID;
  parse_arg.surfaceID = surfaceID;
  parse_arg.needed.resize(nvars);
  parse_arg.coords.resize(maxcoords);
  parse_arg.maxcoords = maxcoords;
  parse_arg.ncoords = 0;
}

static int
genZonalID(int vlistID)
{
  int zonalID = -1;

  const auto ngrids = vlistNgrids(vlistID);
  for (int index = 0; index < ngrids; index++)
    {
      const auto gridID = vlistGrid(vlistID, index);
      const auto gridtype = gridInqType(gridID);
      if (gridtype == GRID_LONLAT || gridtype == GRID_GAUSSIAN || gridtype == GRID_GENERIC)
        if (gridInqXsize(gridID) > 1 && gridInqYsize(gridID) >= 1)
          {
            zonalID = gridToZonal(gridID);
            break;
          }
    }

  return zonalID;
}

void *
Expr(void *process)
{
  cdoInitialize(process);

  void *scanner = nullptr;
  int yy_scan_string(const char *str, void *scanner);
  yylex_init(&scanner);

  parseParamType parse_arg = {};
  yyset_extra(&parse_arg, scanner);

  // clang-format off
  cdoOperatorAdd("expr",   1, 1, "expressions");
  cdoOperatorAdd("exprf",  1, 0, "expr script filename");
  cdoOperatorAdd("aexpr",  0, 1, "expressions");
  cdoOperatorAdd("aexprf", 0, 0, "expr script filename");
  // clang-format on

  const auto operatorID = cdoOperatorID();
  const bool replacesVariables = cdoOperatorF1(operatorID);
  const bool readsCommandLine = cdoOperatorF2(operatorID);

  operatorInputArg(cdoOperatorEnter(operatorID));

  auto exprArgv = cdoGetOperArgv();

  auto exprs = readsCommandLine ? exprsFromArgument(exprArgv) : exprsFromFile(exprArgv);

  const auto streamID1 = cdoOpenRead(0);
  const auto vlistID1 = cdoStreamInqVlist(streamID1);

  exprs = exprsExpand(exprs, vlistID1);
  if (Options::cdoVerbose) cdoPrint(exprs);

  const auto nvars1 = vlistNvars(vlistID1);

  const auto pointID = gridCreate(GRID_GENERIC, 1);
  const auto zonalID = genZonalID(vlistID1);
  const auto surfaceID = getSurfaceID(vlistID1);

  std::vector<paramType> params(MAX_PARAMS);
  params_init(params, vlistID1);

  parseParamInit(parse_arg, vlistID1, pointID, zonalID, surfaceID, params.data());

  // Set all input variables to 'needed' if replacing is switched off
  for (int varID = 0; varID < nvars1; varID++) parse_arg.needed[varID] = !replacesVariables;

  // init function rand()
  std::srand(Options::Random_Seed);

  const auto vartsID = params_add_ts(parse_arg);
  parse_arg.tsID = vartsID;
  params_add_coordinates(vlistID1, parse_arg);

  CDO_parser_errorno = 0;
  yy_scan_string(exprs, scanner);
  yyparse(&parse_arg, scanner);
  if (CDO_parser_errorno != 0) cdoAbort("Syntax error!");

  parse_arg.init = false;

  if (Options::cdoVerbose)
    for (int varID = 0; varID < nvars1; varID++)
      if (parse_arg.needed[varID]) cdoPrint("Needed var: %d %s", varID, params[varID].name);

  if (Options::cdoVerbose)
    for (int varID = 0; varID < parse_arg.nparams; varID++)
      cdoPrint("var: %d %s ngp=%zu nlev=%zu coord=%c", varID, params[varID].name, params[varID].ngp, params[varID].nlev,
               params[varID].coord == 0 ? ' ' : params[varID].coord);

  std::vector<int> varIDmap(parse_arg.nparams);

  const auto vlistID2 = vlistCreate();
  vlistDefNtsteps(vlistID2, vlistNtsteps(vlistID1));
  vlistClearFlag(vlistID1);
  if (!replacesVariables)
    {
      int pidx = 0;
      for (int varID = 0; varID < nvars1; varID++)
        {
          params[varID].select = false;
          if (!params[varID].remove)
            {
              varIDmap[pidx++] = varID;
              const auto nlevs = zaxisInqSize(vlistInqVarZaxis(vlistID1, varID));
              // printf("Replace %d nlevs %d\n", varID, nlevs);
              for (int levID = 0; levID < nlevs; levID++) vlistDefFlag(vlistID1, varID, levID, true);
            }
        }
    }
  cdoVlistCopyFlag(vlistID2, vlistID1);  // Copy global attributes

  //  printf("parse_arg.nparams %d\n", parse_arg.nparams);
  for (int pidx = 0; pidx < parse_arg.nparams; pidx++)
    {
      if (pidx < nvars1 && !params[pidx].select) continue;
      if (pidx >= nvars1)
        {
          if (params[pidx].type == PARAM_CONST) continue;
          if (params[pidx].name[0] == '_') continue;
          if (params[pidx].remove) continue;
          if (params[pidx].coord) continue;
        }

      // printf("gridID %d zaxisID %d\n",  params[pidx].gridID, params[pidx].zaxisID);
      const auto varID = vlistDefVar(vlistID2, params[pidx].gridID, params[pidx].zaxisID, params[pidx].steptype);
      cdiDefKeyString(vlistID2, varID, CDI_KEY_NAME, params[pidx].name);
      // printf("add: %d %s %d levs %d\n", pidx,  params[pidx].name, varID, zaxisInqSize(params[pidx].zaxisID));
      if (params[pidx].lmiss) vlistDefVarMissval(vlistID2, varID, params[pidx].missval);
      if (params[pidx].units) cdiDefKeyString(vlistID2, varID, CDI_KEY_UNITS, params[pidx].units);
      if (params[pidx].longname) cdiDefKeyString(vlistID2, varID, CDI_KEY_LONGNAME, params[pidx].longname);
      const auto len = strlen(params[pidx].name);
      if (len > 3 && memcmp(params[pidx].name, "var", 3) == 0)
        {
          if (isdigit(params[pidx].name[3]))
            {
              const auto code = atoi(params[pidx].name + 3);
              vlistDefVarCode(vlistID2, varID, code);
            }
        }
      varIDmap[varID] = pidx;
    }

  if (Options::cdoVerbose)
    {
      for (int varID = 0; varID < nvars1; varID++)
        if (parse_arg.needed[varID]) printf("needed: %d %s\n", varID, parse_arg.params[varID].name);
      cdoPrint("vlistNvars(vlistID1)=%d, vlistNvars(vlistID2)=%d", vlistNvars(vlistID1), vlistNvars(vlistID2));
    }

  const auto nvars2 = vlistNvars(vlistID2);
  if (nvars2 == 0) cdoAbort("No output variable found!");

  for (int varID = 0; varID < nvars1; varID++)
    {
      if (parse_arg.needed[varID])
        {
          const auto nItems = std::max((size_t) 4, params[varID].ngp * params[varID].nlev);
          params[varID].data = (double *) Malloc(nItems * sizeof(double));
        }
    }

  for (int varID = parse_arg.nvars1; varID < parse_arg.nparams; varID++)
    {
      const auto nItems = std::max((size_t) 4, params[varID].ngp * params[varID].nlev);
      params[varID].data = (double *) Malloc(nItems * sizeof(double));
    }

  for (int i = 0; i < parse_arg.ncoords; i++)
    {
      if (parse_arg.coords[i].needed)
        {
          const int coord = parse_arg.coords[i].coord;
          if (coord == 'x' || coord == 'y' || coord == 'a' || coord == 'w')
            {
              auto gridID = parse_arg.coords[i].cdiID;
              const auto ngp = parse_arg.coords[i].size;
              parse_arg.coords[i].data.resize(ngp);
              auto &cdata = parse_arg.coords[i].data;
              if (coord == 'x' || coord == 'y')
                {
                  const auto gridtype = gridInqType(gridID);
                  if (gridtype == GRID_GME) gridID = gridToUnstructured(gridID, 0);
                  if (gridtype != GRID_UNSTRUCTURED && gridtype != GRID_CURVILINEAR) gridID = gridToCurvilinear(gridID, 0);

                  if (gridtype == GRID_UNSTRUCTURED && !gridHasCoordinates(gridID))
                    {
                      const auto reference = dereferenceGrid(gridID);
                      if (reference.isValid) gridID = reference.gridID;
                      if (reference.notFound) cdoAbort("Reference to source grid not found!");
                    }

                  if (!gridHasCoordinates(gridID)) cdoAbort("Cell center coordinates missing!");

                  if (coord == 'x') gridInqXvals(gridID, cdata.data());
                  if (coord == 'y') gridInqYvals(gridID, cdata.data());

                  if (gridID != parse_arg.coords[i].cdiID) gridDestroy(gridID);
                }
              else if (coord == 'a')
                {
                  grid_cell_area(gridID, cdata.data());
                }
              else if (coord == 'w')
                {
                  cdata[0] = 1;
                  if (ngp > 1)
                    {
                      int wstatus = gridWeights(gridID, cdata.data());
                      if (wstatus) cdoWarning("Grid cell bounds not available, using constant grid cell area weights!");
                    }
                }
            }
          else if (coord == 'z')
            {
              const auto zaxisID = parse_arg.coords[i].cdiID;
              const auto nlev = parse_arg.coords[i].size;
              parse_arg.coords[i].data.resize(nlev);
              auto &cdata = parse_arg.coords[i].data;
              cdoZaxisInqLevels(zaxisID, cdata.data());
            }
          else
            cdoAbort("Computation of coordinate %c not implemented!", coord);
        }
    }

  for (int varID = parse_arg.nvars1; varID < parse_arg.nparams; varID++)
    {
      const int coord = params[varID].coord;
      if (coord)
        {
          if (coord == 'x' || coord == 'y' || coord == 'a' || coord == 'w')
            {
              const auto coordID = params_get_coordID(&parse_arg, coord, params[varID].gridID);
              const auto gridID = parse_arg.coords[coordID].cdiID;
              const auto ngp = parse_arg.coords[coordID].size;
              auto &cdata = parse_arg.coords[coordID].data;
              assert(gridID == params[varID].gridID);
              assert(!cdata.empty());

              arrayCopy(ngp, cdata.data(), params[varID].data);
            }
          else if (coord == 'z')
            {
              const auto coordID = params_get_coordID(&parse_arg, coord, params[varID].zaxisID);
              const auto zaxisID = parse_arg.coords[coordID].cdiID;
              const auto nlev = parse_arg.coords[coordID].size;
              auto &cdata = parse_arg.coords[coordID].data;
              assert(zaxisID == params[varID].zaxisID);
              assert(!cdata.empty());

              arrayCopy(nlev, cdata.data(), params[varID].data);
            }
          else
            cdoAbort("Computation of coordinate %c not implemented!", coord);
        }
    }

  if (Options::cdoVerbose) vlistPrint(vlistID2);

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

  const auto streamID2 = cdoOpenWrite(1);
  cdoDefVlist(streamID2, vlistID2);

  int64_t vdate0 = 0;
  int vtime0 = 0;
  const auto calendar = taxisInqCalendar(taxisID1);

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

      double jdelta = 0;

      if (tsID)
        {
          const auto juldate0 = julianDateEncode(calendar, vdate0, vtime0);
          const auto juldate = julianDateEncode(calendar, vdate, vtime);
          jdelta = julianDateToSeconds(julianDateSub(juldate, juldate0));
        }

      vdate0 = vdate;
      vtime0 = vtime;

      params[vartsID].data[CTIMESTEP] = tsID + 1;
      params[vartsID].data[CDATE] = vdate;
      params[vartsID].data[CTIME] = vtime;
      params[vartsID].data[CDELTAT] = jdelta;

      int year, mon, day;
      int hour, minute, second;
      cdiDecodeDate(vdate, &year, &mon, &day);
      cdiDecodeTime(vtime, &hour, &minute, &second);

      params[vartsID].data[CDAY] = day;
      params[vartsID].data[CMONTH] = mon;
      params[vartsID].data[CYEAR] = year;
      params[vartsID].data[CSECOND] = second;
      params[vartsID].data[CMINUTE] = minute;
      params[vartsID].data[CHOUR] = hour;

      taxisCopyTimestep(taxisID2, taxisID1);

      cdoDefTimestep(streamID2, tsID);

      for (int varID = 0; varID < nvars1; varID++)
        if (tsID == 0 || params[varID].steptype != TIME_CONSTANT) params[varID].nmiss = 0;

      for (int recID = 0; recID < nrecs; recID++)
        {
          int varID, levelID;
          cdoInqRecord(streamID1, &varID, &levelID);
          if (parse_arg.needed[varID])
            {
              const auto offset = params[varID].ngp * levelID;
              auto vardata = params[varID].data + offset;
              size_t nmiss;
              cdoReadRecord(streamID1, vardata, &nmiss);
              params[varID].nmiss += nmiss;

              if (nmiss > 0) cdo_check_missval(params[varID].missval, params[varID].name);
            }
        }

      for (int varID = 0; varID < nvars2; varID++)
        {
          const auto pidx = varIDmap[varID];
          if (pidx < nvars1) continue;
          const auto ngp = params[pidx].ngp;
          const auto nlev = params[pidx].nlev;

          params[pidx].nmiss = 0;
          varrayFill(ngp * nlev, params[pidx].data, 0.0);
        }

      parse_arg.cnparams = vartsID + 1;
      yy_scan_string(exprs, scanner);
      yyparse(&parse_arg, scanner);

      for (int varID = 0; varID < nvars2; varID++)
        {
          const auto pidx = varIDmap[varID];

          if (tsID > 0 && params[pidx].steptype == TIME_CONSTANT) continue;

          const auto missval = vlistInqVarMissval(vlistID2, varID);

          const auto ngp = params[pidx].ngp;
          const int nlev = (int) params[pidx].nlev;
          for (int levelID = 0; levelID < nlev; levelID++)
            {
              const auto offset = ngp * levelID;
              double *vardata = params[pidx].data + offset;
              const auto nmiss = arrayNumMV(ngp, vardata, missval);
              cdoDefRecord(streamID2, varID, levelID);
              cdoWriteRecord(streamID2, vardata, nmiss);
            }
        }

      tsID++;
    }

  cdoStreamClose(streamID2);
  cdoStreamClose(streamID1);

  vlistDestroy(vlistID2);

  yylex_destroy(scanner);

  if (exprs) Free(exprs);

  params_delete(params);

  gridDestroy(pointID);
  if (zonalID != -1) gridDestroy(zonalID);

  cdoFinish();

  return nullptr;
}
