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

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

      Change     chcode          Change code number
      Change     chtabnum        Change GRIB1 parameter table number
      Change     chparam         Change parameter identifier
      Change     chname          Change variable or coordinate name
      Change     chlevel         Change level
      Change     chlevelc        Change level of one code
      Change     chlevelv        Change level of one variable
      Change     chltype         Change GRIB level type
*/

#include <cdi.h>

#include "cdo_int.h"
#include "param_conversion.h"
#include "pstream_int.h"

int stringToParam(const char *paramstr);

#define MAXARG 16384

static
void changeName(const int vlistID1, const int vlistID2, const int nch, char *chnames[MAXARG])
{
  char varname[CDI_MAX_NAME], varname2[CDI_MAX_NAME];
  const int npairs = nch/2;
  std::vector<std::pair<char*,char*>> vpairs(npairs);
  for (int i = 0; i < npairs; ++i) vpairs[i].first = chnames[i*2];
  for (int i = 0; i < npairs; ++i) vpairs[i].second = chnames[i*2+1];

  const int nvars = vlistNvars(vlistID2);
  std::vector<bool> namefound(npairs, false);
  for (int varID = 0; varID < nvars; varID++)
    {
      vlistInqVarName(vlistID1, varID, varname);
      for (int i = 0; i < npairs; ++i)
        if (strcmp(varname, vpairs[i].first) == 0)
          {
            namefound[i] = true; 
            vlistDefVarName(vlistID2, varID, vpairs[i].second);
            break;
          }
    }

  bool searchForGridName = false;
  for (int i = 0; i < npairs; ++i)
    if (!namefound[i])
      {
        searchForGridName = true;
        break;
      }
    
  if (searchForGridName)
    {
      const int ngrids = vlistNgrids(vlistID1);
      for (int index = 0; index < ngrids; ++index)
        {
          int gridID2 = -1;
          int gridID1 = vlistGrid(vlistID1, index);
          gridInqXname(gridID1, varname);
          gridInqYname(gridID1, varname2);
          bool xfound = false, yfound = false;
          for (int i = 0; i < npairs; ++i)
            {
              if (!namefound[i])
                {
                  if (strcmp(varname, vpairs[i].first) == 0)
                    {
                      xfound = true;
                      namefound[i] = true;
                      if (gridID2 == -1) gridID2 = gridDuplicate(gridID1);
                      gridDefXname(gridID2, vpairs[i].second);
                    }
                }
              if (!namefound[i])
                {
                  if (strcmp(varname2, vpairs[i].first) == 0)
                    {
                      yfound = true;
                      namefound[i] = true;
                      if (gridID2 == -1) gridID2 = gridDuplicate(gridID1);
                      gridDefYname(gridID2, vpairs[i].second);
                    }
                }

              if (xfound && yfound) break;
            }

          if (gridID2 != -1) vlistChangeGrid(vlistID2, gridID1, gridID2);
        }
    }

  bool searchForZaxisName = false;
  for (int i = 0; i < npairs; ++i)
    if (!namefound[i])
      {
        searchForZaxisName = true;
        break;
      }
    
  if (searchForZaxisName)
    {
      const int nzaxis = vlistNzaxis(vlistID1);
      for (int index = 0; index < nzaxis; ++index)
        {
          const int zaxisID1 = vlistZaxis(vlistID1, index);
          zaxisInqName(zaxisID1, varname);
          for (int i = 0; i < npairs; ++i)
            {
              if (!namefound[i])
                {
                  if (strcmp(varname, vpairs[i].first) == 0)
                    {
                      namefound[i] = true;
                      const int zaxisID2 = zaxisDuplicate(zaxisID1);
                      zaxisDefName(zaxisID2, vpairs[i].second);
                      vlistChangeZaxis(vlistID2, zaxisID1, zaxisID2);
                      break;
                    }
                }
            }
        }
    }

  for (int i = 0; i < npairs; ++i)
    if (!namefound[i]) cdoWarning("Variable name %s not found!", vpairs[i].first);
}


void *
Change(void *process)
{
  int nrecs;
  int varID = 0, levelID;
  int chints[MAXARG];
  char *chnames[MAXARG];
  char varname[CDI_MAX_NAME];
  char *chname = nullptr;
  int chcode = 0;
  int i;
  size_t nmiss;
  int k, index;
  double chlevels[MAXARG];
  int chltypes[MAXARG];

  cdoInitialize(process);

  // clang-format off
  const int CHCODE   = cdoOperatorAdd("chcode",   0, 0, "pairs of old and new code numbers");
  const int CHTABNUM = cdoOperatorAdd("chtabnum", 0, 0, "pairs of old and new GRIB1 table numbers");
  const int CHPARAM  = cdoOperatorAdd("chparam",  0, 0, "pairs of old and new parameter identifiers");
  const int CHNAME   = cdoOperatorAdd("chname",   0, 0, "pairs of old and new variable names");
  const int CHUNIT   = cdoOperatorAdd("chunit",   0, 0, "pairs of old and new variable units");
  const int CHLEVEL  = cdoOperatorAdd("chlevel",  0, 0, "pairs of old and new levels");
  const int CHLEVELC = cdoOperatorAdd("chlevelc", 0, 0, "code number, old and new level");
  const int CHLEVELV = cdoOperatorAdd("chlevelv", 0, 0, "variable name, old and new level");
  const int CHLTYPE  = cdoOperatorAdd("chltype",  0, 0, "pairs of old and new type");
  // clang-format on

  const int operatorID = cdoOperatorID();

  operatorInputArg(cdoOperatorEnter(operatorID));

  const int nch = operatorArgc();

  if (operatorID == CHCODE || operatorID == CHTABNUM)
    {
      if (nch % 2) cdoAbort("Odd number of input arguments!");
      for (i = 0; i < nch; i++) chints[i] = parameter2int(operatorArgv()[i]);
    }
  else if (operatorID == CHPARAM || operatorID == CHNAME || operatorID == CHUNIT)
    {
      if (nch % 2) cdoAbort("Odd number of input arguments!");
      for (i = 0; i < nch; i++) chnames[i] = operatorArgv()[i];
    }
  else if (operatorID == CHLEVEL)
    {
      if (nch % 2) cdoAbort("Odd number of input arguments!");
      for (i = 0; i < nch; i++) chlevels[i] = parameter2double(operatorArgv()[i]);
    }
  else if (operatorID == CHLEVELC)
    {
      operatorCheckArgc(3);

      chcode = parameter2int(operatorArgv()[0]);
      chlevels[0] = parameter2double(operatorArgv()[1]);
      chlevels[1] = parameter2double(operatorArgv()[2]);
    }
  else if (operatorID == CHLEVELV)
    {
      operatorCheckArgc(3);

      chname = operatorArgv()[0];
      chlevels[0] = parameter2double(operatorArgv()[1]);
      chlevels[1] = parameter2double(operatorArgv()[2]);
    }
  else if (operatorID == CHLTYPE)
    {
      if (nch % 2) cdoAbort("Odd number of input arguments!");
      for (i = 0; i < nch; i++) chltypes[i] = parameter2int(operatorArgv()[i]);
    }

  const int streamID1 = cdoStreamOpenRead(cdoStreamName(0));

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

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

  if (operatorID == CHCODE)
    {
      const int nvars = vlistNvars(vlistID2);
      for (varID = 0; varID < nvars; varID++)
        {
          const int code = vlistInqVarCode(vlistID2, varID);
          for (i = 0; i < nch; i += 2)
            if (code == chints[i]) vlistDefVarCode(vlistID2, varID, chints[i + 1]);
        }
    }
  else if (operatorID == CHTABNUM)
    {
      const int nvars = vlistNvars(vlistID2);
      for (varID = 0; varID < nvars; varID++)
        {
          const int tabnum = tableInqNum(vlistInqVarTable(vlistID2, varID));
          for (i = 0; i < nch; i += 2)
            if (tabnum == chints[i])
              {
                const int tableID = tableDef(-1, chints[i + 1], nullptr);
                vlistDefVarTable(vlistID2, varID, tableID);
              }
        }
    }
  else if (operatorID == CHPARAM)
    {
      const int nvars = vlistNvars(vlistID2);
      for (varID = 0; varID < nvars; varID++)
        {
          const int param = vlistInqVarParam(vlistID2, varID);
          if (Options::cdoVerbose)
            {
              int pnum, pcat, pdis;
              cdiDecodeParam(param, &pnum, &pcat, &pdis);
              cdoPrint("pnum, pcat, pdis: %d.%d.%d", pnum, pcat, pdis);
            }
          for (i = 0; i < nch; i += 2)
            if (param == stringToParam(chnames[i])) vlistDefVarParam(vlistID2, varID, stringToParam(chnames[i + 1]));
        }
    }
  else if (operatorID == CHNAME)
    {
      changeName(vlistID1, vlistID2, nch, chnames);
    }
  else if (operatorID == CHUNIT)
    {
      const int nvars = vlistNvars(vlistID2);
      for (varID = 0; varID < nvars; varID++)
        {

          vlistInqVarUnits(vlistID2, varID, varname);
          for (i = 0; i < nch; i += 2)
            if (strcmp(varname, chnames[i]) == 0) vlistDefVarUnits(vlistID2, varID, chnames[i + 1]);
        }
    }
  else if (operatorID == CHLEVEL)
    {
      const int nzaxis = vlistNzaxis(vlistID2);
      for (index = 0; index < nzaxis; index++)
        {
          const int zaxisID1 = vlistZaxis(vlistID2, index);
          if (zaxisInqLevels(zaxisID1, nullptr))
            {
              const int nlevs = zaxisInqSize(zaxisID1);
              std::vector<double> levels(nlevs);
              std::vector<double> newlevels(nlevs);
              zaxisInqLevels(zaxisID1, &levels[0]);

              for (k = 0; k < nlevs; k++) newlevels[k] = levels[k];

              int nfound = 0;
              for (i = 0; i < nch; i += 2)
                for (k = 0; k < nlevs; k++)
                  if (std::fabs(levels[k] - chlevels[i]) < 0.0001) nfound++;

              if (nfound)
                {
                  const int zaxisID2 = zaxisDuplicate(zaxisID1);
                  for (i = 0; i < nch; i += 2)
                    for (k = 0; k < nlevs; k++)
                      if (std::fabs(levels[k] - chlevels[i]) < 0.001) newlevels[k] = chlevels[i + 1];

                  zaxisDefLevels(zaxisID2, &newlevels[0]);
                  vlistChangeZaxis(vlistID2, zaxisID1, zaxisID2);
                }
            }
        }
    }
  else if (operatorID == CHLEVELC || operatorID == CHLEVELV)
    {
      const int nvars = vlistNvars(vlistID2);
      if (operatorID == CHLEVELC)
        {
          for (varID = 0; varID < nvars; varID++)
            {
              int code = vlistInqVarCode(vlistID2, varID);
              if (code == chcode) break;
            }
          if (varID == nvars) cdoAbort("Code %d not found!", chcode);
        }
      else
        {
          for (varID = 0; varID < nvars; varID++)
            {
              vlistInqVarName(vlistID2, varID, varname);
              if (strcmp(varname, chname) == 0) break;
            }
          if (varID == nvars) cdoAbort("Variable name %s not found!", chname);
        }

      const int zaxisID1 = vlistInqVarZaxis(vlistID2, varID);
      if (zaxisInqLevels(zaxisID1, nullptr))
        {
          const int nlevs = zaxisInqSize(zaxisID1);
          std::vector<double> levels(nlevs);
          zaxisInqLevels(zaxisID1, &levels[0]);
          int nfound = 0;
          for (k = 0; k < nlevs; k++)
            if (std::fabs(levels[k] - chlevels[0]) < 0.0001) nfound++;

          if (nfound)
            {
              const int zaxisID2 = zaxisDuplicate(zaxisID1);
              for (k = 0; k < nlevs; k++)
                if (std::fabs(levels[k] - chlevels[0]) < 0.001) levels[k] = chlevels[1];

              zaxisDefLevels(zaxisID2, &levels[0]);
              vlistChangeVarZaxis(vlistID2, varID, zaxisID2);
            }
          else
            cdoAbort("Level %g not found!", chlevels[0]);
        }
    }
  else if (operatorID == CHLTYPE)
    {
      const int nzaxis = vlistNzaxis(vlistID2);
      for (index = 0; index < nzaxis; index++)
        {
          const int zaxisID1 = vlistZaxis(vlistID2, index);
          const int zaxisID2 = zaxisDuplicate(zaxisID1);
          const int ltype = zaxisInqLtype(zaxisID1);

          for (i = 0; i < nch; i += 2)
            {
              const int ltype1 = chltypes[i];
              const int ltype2 = chltypes[i + 1];

              if (ltype1 == ltype)
                {
                  zaxisChangeType(zaxisID2, ZAXIS_GENERIC);
                  zaxisDefLtype(zaxisID2, ltype2);
                  vlistChangeZaxis(vlistID2, zaxisID1, zaxisID2);
                }
            }
        }
    }

  const size_t gridsizemax = vlistGridsizeMax(vlistID2);
  std::vector<double> array(gridsizemax);

  int streamID2 = CDI_UNDEFID;

  int tsID1 = 0;
  while ((nrecs = cdoStreamInqTimestep(streamID1, tsID1)))
    {
      taxisCopyTimestep(taxisID2, taxisID1);
      if (streamID2 == CDI_UNDEFID)
        {
          streamID2 = cdoStreamOpenWrite(cdoStreamName(1));
          pstreamDefVlist(streamID2, vlistID2);
        }
      pstreamDefTimestep(streamID2, tsID1);

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

          pstreamReadRecord(streamID1, &array[0], &nmiss);
          pstreamWriteRecord(streamID2, &array[0], nmiss);
        }
      tsID1++;
    }

  cdoStreamClose(streamID1);
  cdoStreamClose(streamID2);

  cdoFinish();

  return nullptr;
}
