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

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

      Setpartab  setpartab       Set parameter table
*/

#include <cdi.h>

#include "cdo_int.h"
#include "cdo_cmor.h"
#include "pstream_int.h"
#include "pmlist.h"
#include "convert_units.h"
#include "util_files.h"

int stringToParam(const char *paramstr);

void
paramToString(int param, char *paramstr, int maxlen)
{
  int dis, cat, num;
  cdiDecodeParam(param, &num, &cat, &dis);

  size_t umaxlen = maxlen >= 0 ? (unsigned) maxlen : 0U;
  int len;
  if (dis == 255 && (cat == 255 || cat == 0))
    len = snprintf(paramstr, umaxlen, "%d", num);
  else if (dis == 255)
    len = snprintf(paramstr, umaxlen, "%d.%d", num, cat);
  else
    len = snprintf(paramstr, umaxlen, "%d.%d.%d", num, cat, dis);

  if (len >= maxlen || len < 0) fprintf(stderr, "Internal problem (%s): size of input string is too small!\n", __func__);
}

typedef enum { CODE_NUMBER, PARAMETER_ID, VARIABLE_NAME, STANDARD_NAME } pt_mode_t;

static void
apply_parameterlist(pt_mode_t ptmode, list_t *pmlist, int nvars, int vlistID2, std::vector<CmorVar> &vars)
{
  const char *hentry[] = { "Header" };
  const char *ventry[] = { "variable_entry", "parameter" };
  int nventry = (int) sizeof(ventry) / sizeof(ventry[0]);
  int nhentry = (int) sizeof(hentry) / sizeof(hentry[0]);
  char valstr[CDI_MAX_NAME];
  char varname[CDI_MAX_NAME];
  char paramstr[32];
  int codenum;

  // search for global missing value
  bool lmissval = false;
  double missval;
  list_t *kvlist = pmlist_get_kvlist_ventry(pmlist, nhentry, hentry);
  if (kvlist)
    {
      keyValues_t *kv = kvlist_search(kvlist, "missing_value");
      if (kv && kv->nvalues > 0)
        {
          lmissval = true;
          missval = parameter2double(kv->values[0]);
        }
    }

  for (int varID = 0; varID < nvars; varID++)
    {
      CmorVar &var = vars[varID];
      vlistInqVarName(vlistID2, varID, varname);

      strcpy(var.name, varname);
      if (lmissval)
        {
          double missval_old = vlistInqVarMissval(vlistID2, varID);
          if (!DBL_IS_EQUAL(missval, missval_old))
            {
              var.changemissval = true;
              var.missval_old = missval_old;
              vlistDefVarMissval(vlistID2, varID, missval);
            }
        }

      list_t *kvlist = NULL;
      keyValues_t *kv;
      if (ptmode == CODE_NUMBER)
        {
          codenum = vlistInqVarCode(vlistID2, varID);
          snprintf(valstr, sizeof(valstr), "%d", codenum);
          kvlist = pmlist_search_kvlist_ventry(pmlist, "code", valstr, nventry, ventry);
          if (kvlist)
            {
              int tableID = vlistInqVarTable(vlistID2, varID);
              int tabnum = tableInqNum(tableID);
              int levtype = zaxisInqLtype(vlistInqVarZaxis(vlistID2, varID));
              kv = kvlist_search(kvlist, "table");
              int table = (kv && kv->nvalues == 1) ? parameter2int(kv->values[0]) : tabnum;
              kv = kvlist_search(kvlist, "ltype");
              int ltype = (kv && kv->nvalues == 1) ? parameter2int(kv->values[0]) : levtype;
              if (!(tabnum == table && levtype == ltype)) kvlist = NULL;
            }
        }
      else if (ptmode == PARAMETER_ID)
        {
          int param = vlistInqVarParam(vlistID2, varID);
          paramToString(param, paramstr, sizeof(paramstr));
          snprintf(valstr, sizeof(valstr), "%s", paramstr);
          kvlist = pmlist_search_kvlist_ventry(pmlist, "param", valstr, nventry, ventry);
          if (kvlist)
            {
              int levtype = zaxisInqLtype(vlistInqVarZaxis(vlistID2, varID));
              kv = kvlist_search(kvlist, "ltype");
              int ltype = (kv && kv->nvalues == 1) ? parameter2int(kv->values[0]) : levtype;
              if (!(levtype == ltype)) kvlist = NULL;
            }
        }
      else if (ptmode == VARIABLE_NAME)
        {
          kvlist = pmlist_search_kvlist_ventry(pmlist, "name", varname, nventry, ventry);
        }

      if (kvlist)
        {
          int pnum, ptab, pdum;
          cdiDecodeParam(vlistInqVarParam(vlistID2, varID), &pnum, &ptab, &pdum);
          bool lvalid_min = false, lvalid_max = false;

          for (listNode_t *kvnode = kvlist->head; kvnode; kvnode = kvnode->next)
            {
              kv = *(keyValues_t **) kvnode->data;
              const char *key = kv->key;
              const char *value = (kv->nvalues > 0) ? kv->values[0] : NULL;
              bool lv1 = (kv->nvalues == 1);

              // printf("key=%s  value=%s\n", key, value ? value : "");

              if (lv1 && STR_IS_EQ(key, "standard_name"))
                vlistDefVarStdname(vlistID2, varID, value);
              else if (lv1 && STR_IS_EQ(key, "long_name"))
                vlistDefVarLongname(vlistID2, varID, value);
              else if (lv1 && STR_IS_EQ(key, "units"))
                cdo_define_var_units(var, vlistID2, varID, value);
              else if (lv1 && STR_IS_EQ(key, "name"))
                {
                  if (ptmode != VARIABLE_NAME) vlistDefVarName(vlistID2, varID, parameter2word(value));
                }
              else if (lv1 && STR_IS_EQ(key, "out_name"))
                {
                  const char *outname = parameter2word(value);
                  if (!STR_IS_EQ(var.name, outname))
                    {
                      vlistDefVarName(vlistID2, varID, outname);
                      cdiDefAttTxt(vlistID2, varID, "original_name", (int) strlen(var.name), var.name);
                    }
                }
              else if (lv1 && STR_IS_EQ(key, "param"))
                vlistDefVarParam(vlistID2, varID, stringToParam(parameter2word(value)));
              else if (lv1 && STR_IS_EQ(key, "out_param"))
                vlistDefVarParam(vlistID2, varID, stringToParam(parameter2word(value)));
              else if (lv1 && STR_IS_EQ(key, "code"))
                vlistDefVarParam(vlistID2, varID, cdiEncodeParam(parameter2int(value), ptab, 255));
              else if (lv1 && STR_IS_EQ(key, "out_code"))
                vlistDefVarParam(vlistID2, varID, cdiEncodeParam(parameter2int(value), ptab, 255));
              else if (lv1 && STR_IS_EQ(key, "comment"))
                cdiDefAttTxt(vlistID2, varID, key, (int) strlen(value), value);
              else if (lv1 && STR_IS_EQ(key, "chunktype"))
                ;
              else if (lv1 && STR_IS_EQ(key, "cell_methods"))
                cdiDefAttTxt(vlistID2, varID, key, (int) strlen(value), value);
              else if (lv1 && STR_IS_EQ(key, "cell_measures"))
                cdiDefAttTxt(vlistID2, varID, key, (int) strlen(value), value);
              else if (lv1 && STR_IS_EQ(key, "delete"))
                var.remove = parameter2bool(value);
              else if (lv1 && STR_IS_EQ(key, "convert"))
                var.convert = parameter2bool(value);
              else if (lv1 && STR_IS_EQ(key, "factor"))
                {
                  var.lfactor = true;
                  var.factor = parameter2double(value);
                  if (cdoVerbose) cdoPrint("%s - scale factor %g", varname, var.factor);
                }
              else if (lv1 && (STR_IS_EQ(key, "missval") || STR_IS_EQ(key, "missing_value")))
                {
                  double missval = parameter2double(value);
                  double missval_old = vlistInqVarMissval(vlistID2, varID);
                  if (!DBL_IS_EQUAL(missval, missval_old))
                    {
                      if (cdoVerbose) cdoPrint("%s - change missval from %g to %g", varname, missval_old, missval);
                      var.changemissval = true;
                      var.missval_old = missval_old;
                      vlistDefVarMissval(vlistID2, varID, missval);
                    }
                }
              else if (lv1 && STR_IS_EQ(key, "valid_min"))
                {
                  lvalid_min = true;
                  var.valid_min = parameter2double(value);
                }
              else if (lv1 && STR_IS_EQ(key, "valid_max"))
                {
                  lvalid_max = true;
                  var.valid_max = parameter2double(value);
                }
              else if (lv1 && STR_IS_EQ(key, "ok_min_mean_abs"))
                {
                  var.check_min_mean_abs = true;
                  var.ok_min_mean_abs = parameter2double(value);
                }
              else if (lv1 && STR_IS_EQ(key, "ok_max_mean_abs"))
                {
                  var.check_max_mean_abs = true;
                  var.ok_max_mean_abs = parameter2double(value);
                }
              else if (lv1 && (STR_IS_EQ(key, "datatype") || STR_IS_EQ(key, "type")))
                {
                  int datatype = str2datatype(parameter2word(value));
                  if (datatype != -1) vlistDefVarDatatype(vlistID2, varID, datatype);
                }
              else if (lv1 && STR_IS_EQ(key, "dimensions"))
                {
                }
              else
                {
                  int nvalues = kv->nvalues;
                  if (nvalues == 1 && !*value) nvalues = 0;
                  int dtype = literals_find_datatype(nvalues, kv->values);

                  if (dtype == CDI_DATATYPE_INT8 || dtype == CDI_DATATYPE_INT16 || dtype == CDI_DATATYPE_INT32)
                    {
                      std::vector<int> ivals(nvalues);
                      for (int i = 0; i < nvalues; ++i) ivals[i] = literal_to_int(kv->values[i]);
                      cdiDefAttInt(vlistID2, varID, key, dtype, nvalues, ivals.data());
                    }
                  else if (dtype == CDI_DATATYPE_FLT32 || dtype == CDI_DATATYPE_FLT64)
                    {
                      std::vector<double> dvals(nvalues);
                      for (int i = 0; i < nvalues; ++i) dvals[i] = literal_to_double(kv->values[i]);
                      cdiDefAttFlt(vlistID2, varID, key, dtype, nvalues, dvals.data());
                    }
                  else
                    {
                      int len = (value && *value) ? (int) strlen(value) : 0;
                      cdiDefAttTxt(vlistID2, varID, key, len, value);
                    }
                }
            }

          if (lvalid_min && lvalid_max) var.checkvalid = true;
        }
      else
        {
          if (ptmode == CODE_NUMBER)
            cdoPrint("Code number %d not found in parameter table!", codenum);
          else if (ptmode == PARAMETER_ID)
            cdoPrint("Parameter ID %s not found in parameter table!", paramstr);
          else if (ptmode == VARIABLE_NAME)
            cdoPrint("Variable %s not found in parameter table!", varname);
        }
    }
}

void *
Setpartab(void *process)
{
  int nrecs;
  int varID, levelID;
  size_t nmiss;
  int tableID = -1;
  int tableformat = 0;
  bool delvars = false;

  cdoInitialize(process);

  int SETCODETAB = cdoOperatorAdd("setcodetab", 0, 0, "parameter code table name");
  int SETPARTABC = cdoOperatorAdd("setpartabc", 0, 0, "parameter table name");
  int SETPARTABP = cdoOperatorAdd("setpartabp", 0, 0, "parameter table name");
  int SETPARTABN = cdoOperatorAdd("setpartabn", 0, 0, "parameter table name");

  int operatorID = cdoOperatorID();

  operatorInputArg(cdoOperatorEnter(operatorID));

  if (operatorArgc() < 1) cdoAbort("Too few arguments!");

  bool convert_data = false;
  if (operatorArgc() == 2)
    {
      if (strcmp("convert", operatorArgv()[1]) == 0)
        convert_data = true;
      else
        cdoAbort("Unknown parameter: >%s<", operatorArgv()[1]);
    }

  if (operatorArgc() > 2) cdoAbort("Too many arguments!");

  pt_mode_t ptmode = CODE_NUMBER;
  // clang-format off
  if      (operatorID == SETCODETAB) ptmode = CODE_NUMBER;
  else if (operatorID == SETPARTABC) ptmode = CODE_NUMBER;
  else if (operatorID == SETPARTABP) ptmode = PARAMETER_ID;
  else if (operatorID == SETPARTABN) ptmode = VARIABLE_NAME;
  // clang-format on

  if (ptmode == CODE_NUMBER)
    {
      char *partab = operatorArgv()[0];
      FILE *fp = NULL;
      if (fileExists(partab)) fp = fopen(partab, "r");
      if (fp != NULL)
        {
          fseek(fp, 0L, SEEK_END);
          size_t fsize = (size_t) ftell(fp);
          std::vector<char> parbuf(fsize + 1);
          fseek(fp, 0L, SEEK_SET);
          fread(parbuf.data(), fsize, 1, fp);
          parbuf[fsize] = 0;
          fseek(fp, 0L, SEEK_SET);

          if (atoi(parbuf.data()) == 0) tableformat = 1;

          fclose(fp);
        }

      if (tableformat == 0) tableID = defineTable(partab);
    }
  else if (ptmode == PARAMETER_ID)
    {
      tableformat = 1;
    }
  else if (ptmode == VARIABLE_NAME)
    {
      tableformat = 1;
    }

  if (cdoVerbose) cdoPrint("Table format version %d", tableformat);

  int streamID1 = cdoStreamOpenRead(cdoStreamName(0));

  int vlistID1 = cdoStreamInqVlist(streamID1);
  int vlistID2 = vlistDuplicate(vlistID1);
  // vlistPrint(vlistID2);

  int nvars = vlistNvars(vlistID2);
  std::vector<CmorVar> vars(nvars);
  memset(vars.data(), 0, nvars * sizeof(CmorVar));

  if (convert_data)
    for (varID = 0; varID < nvars; ++varID) vars[varID].convert = true;

  if (tableformat == 0)
    {
      /*
        for ( int varID = 0; varID < nvars; varID++ )
          vlistDefVarTable(vlistID2, varID, tableID);
      */
      char name[CDI_MAX_NAME], longname[CDI_MAX_NAME], units[CDI_MAX_NAME];
      for (int varID = 0; varID < nvars; varID++)
        {
          int param = vlistInqVarParam(vlistID2, varID);
          int pdis, pcat, pnum;
          cdiDecodeParam(param, &pnum, &pcat, &pdis);
          if (pdis == 255)
            {
              int code = pnum;
              int ltype = zaxisInqLtype(vlistInqVarZaxis(vlistID2, varID));
              name[0] = 0;
              longname[0] = 0;
              units[0] = 0;
              tableInqEntry(tableID, code, ltype, name, longname, units);
              if (name[0])
                {
                  vlistDefVarName(vlistID2, varID, name);
                  if (longname[0]) vlistDefVarLongname(vlistID2, varID, longname);
                  if (units[0]) vlistDefVarUnits(vlistID2, varID, units);
                }
            }
          vlistDefVarTable(vlistID2, varID, tableID);
        }
    }
  else
    {
      const char *filename = operatorArgv()[0];
      FILE *fp = fopen(filename, "r");
      if (fp == NULL) cdoAbort("Open failed on: %s\n", filename);

      list_t *pmlist = namelist_to_pmlist(fp, filename);
      fclose(fp);

      apply_parameterlist(ptmode, pmlist, nvars, vlistID2, vars);
      list_destroy(pmlist);

      for (int varID = 0; varID < nvars; ++varID)
        if (vars[varID].remove)
          {
            delvars = true;
            break;
          }

      if (delvars)
        {
          vlistClearFlag(vlistID1);
          vlistClearFlag(vlistID2);

          for (int varID = 0; varID < nvars; varID++)
            {
              int zaxisID = vlistInqVarZaxis(vlistID2, varID);
              int nlevs = zaxisInqSize(zaxisID);
              for (int levID = 0; levID < nlevs; levID++)
                {
                  vlistDefFlag(vlistID1, varID, levID, TRUE);
                  vlistDefFlag(vlistID2, varID, levID, TRUE);
                  if (vars[varID].remove)
                    {
                      vlistDefFlag(vlistID1, varID, levID, FALSE);
                      vlistDefFlag(vlistID2, varID, levID, FALSE);
                    }
                }
            }

          int vlistIDx = vlistCreate();
          cdoVlistCopyFlag(vlistIDx, vlistID2);

          vlistDestroy(vlistID2);

          vlistID2 = vlistIDx;
          if (vlistNvars(vlistID2) == 0) cdoAbort("No variable selected!");
        }

      for (int varID = 0; varID < nvars; ++varID)
        {
          CmorVar &var = vars[varID];
          if (var.convert == false) var.changeunits = false;
          if (var.changeunits)
            cdoConvertUnits(&var.ut_converter, &var.changeunits, (char *) &var.units, (char *) &var.units_old, var.name);
        }
    }

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

  // vlistPrint(vlistID2);
  int streamID2 = cdoStreamOpenWrite(cdoStreamName(1), cdoFiletype());
  pstreamDefVlist(streamID2, vlistID2);

  size_t gridsizemax = vlistGridsizeMax(vlistID1);
  if (vlistNumber(vlistID1) != CDI_REAL) gridsizemax *= 2;
  std::vector<double> array(gridsizemax);

  int tsID1 = 0;
  while ((nrecs = cdoStreamInqTimestep(streamID1, tsID1)))
    {
      taxisCopyTimestep(taxisID2, taxisID1);
      pstreamDefTimestep(streamID2, tsID1);

      cmor_check_init(nvars, vars);

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

          CmorVar &var = vars[varID];
          int varID2 = varID;
          int levelID2 = levelID;

          if (delvars)
            {
              if (var.remove) continue;

              if (vlistInqFlag(vlistID1, varID, levelID) == TRUE)
                {
                  varID2 = vlistFindVar(vlistID2, varID);
                  levelID2 = vlistFindLevel(vlistID2, varID, levelID);
                }
            }

          pstreamDefRecord(streamID2, varID2, levelID2);

          pstreamReadRecord(streamID1, array.data(), &nmiss);

          double missval = vlistInqVarMissval(vlistID2, varID2);
          size_t gridsize = gridInqSize(vlistInqVarGrid(vlistID2, varID2));
          if (vlistInqVarNumber(vlistID2, varID2) != CDI_REAL) gridsize *= 2;

          if (nmiss > 0 && var.changemissval)
            {
              for (size_t i = 0; i < gridsize; ++i)
                {
                  if (DBL_IS_EQUAL(array[i], var.missval_old)) array[i] = missval;
                }
            }

          if (var.lfactor)
            {
              for (size_t i = 0; i < gridsize; ++i)
                {
                  if (!DBL_IS_EQUAL(array[i], missval)) array[i] *= var.factor;
                }
            }

#ifdef HAVE_UDUNITS2
          if (var.changeunits)
            {
              int nerr = 0;
              for (size_t i = 0; i < gridsize; ++i)
                {
                  if (!DBL_IS_EQUAL(array[i], missval))
                    {
                      array[i] = cv_convert_double((const cv_converter *) var.ut_converter, array[i]);
                      if (ut_get_status() != UT_SUCCESS) nerr++;
                    }
                }
              if (nerr)
                {
                  cdoWarning("Udunits: Error converting units from [%s] to [%s], parameter: %s", var.units_old, var.units,
                             var.name);
                  var.changeunits = false;
                }
            }
#endif

          pstreamWriteRecord(streamID2, array.data(), nmiss);

          cmor_check_prep(var, gridsize, missval, array.data());
        }

      cmor_check_eval(vlistID2, nvars, vars);

      tsID1++;
    }

  pstreamClose(streamID2);
  pstreamClose(streamID1);

#ifdef HAVE_UDUNITS2
  for (int varID = 0; varID < nvars; varID++)
    if (vars[varID].ut_converter) cdoConvertFree(vars[varID].ut_converter);

  cdoConvertDestroy();
#endif

  cdoFinish();

  return 0;
}
