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

#include <cstring>
#include <cstdlib>

#include <cdi.h>

#include "param_conversion.h"
#include "cdo_options.h"
#include "cdo_zaxis.h"
#include "util_string.h"
#include "parse_literals.h"
#include "pmlist.h"
#include "cdo_output.h"
#include "compare.h"
#include "array.h"

#define MAX_LINE_LEN 65536

struct zaxis_t
{
  Varray<double> vals;
  Varray<double> lbounds;
  Varray<double> ubounds;
  Varray<double> vct;
  size_t vctsize;
  int type;
  int datatype;
  size_t size;
  bool scalar;
  char name[CDI_MAX_NAME];
  char longname[CDI_MAX_NAME];
  char units[CDI_MAX_NAME];
};

void
zaxisInit(zaxis_t &zaxis)
{
  zaxis.type = CDI_UNDEFID;
  zaxis.datatype = CDI_UNDEFID;
  zaxis.vctsize = 0;
  zaxis.size = 0;
  zaxis.scalar = false;
  zaxis.name[0] = 0;
  zaxis.longname[0] = 0;
  zaxis.units[0] = 0;
}

static int
getoptname(char *optname, const char *optstring, int nopt)
{
  int nerr = 0;

  auto pname = optstring;
  auto pend = optstring;

  for (int i = 0; i < nopt; i++)
    {
      pend = strchr(pname, ',');
      if (pend == nullptr) break;
      pname = pend + 1;
    }

  if (pend)
    {
      pend = strchr(pname, ',');
      const auto namelen = (pend == nullptr) ? strlen(pname) : (size_t)(pend - pname);
      memcpy(optname, pname, namelen);
      optname[namelen] = '\0';
    }
  else
    nerr = 1;

  return nerr;
}

int
zaxisDefine(zaxis_t zaxis)
{
  if (zaxis.type == CDI_UNDEFID) cdoAbort("zaxistype undefined!");
  if (zaxis.size == 0) cdoAbort("zaxis size undefined!");

  const auto zaxisID = zaxisCreate(zaxis.type, (int) zaxis.size);

  if (zaxis.size == 1 && zaxis.scalar) zaxisDefScalar(zaxisID);

  if (zaxis.datatype != CDI_UNDEFID) zaxisDefDatatype(zaxisID, zaxis.datatype);

  if (zaxis.vals.size()) zaxisDefLevels(zaxisID, zaxis.vals.data());
  if (zaxis.lbounds.size()) zaxisDefLbounds(zaxisID, zaxis.lbounds.data());
  if (zaxis.ubounds.size()) zaxisDefUbounds(zaxisID, zaxis.ubounds.data());

  if (zaxis.name[0]) cdiDefKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_NAME, zaxis.name);
  if (zaxis.longname[0]) cdiDefKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_LONGNAME, zaxis.longname);
  if (zaxis.units[0]) cdiDefKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_UNITS, zaxis.units);

  if (zaxis.type == ZAXIS_HYBRID || zaxis.type == ZAXIS_HYBRID_HALF)
    {
      if (zaxis.vctsize && zaxis.vct.size())
        zaxisDefVct(zaxisID, (int) zaxis.vctsize, zaxis.vct.data());
      else
        cdoWarning("vct undefined!");
    }

  return zaxisID;
}

struct KVMap
{
  KeyValues *kv;
  bool isValid;
};

static void
zaxis_read_data(size_t nkv, KVMap *kvmap, zaxis_t *zaxis, size_t *iatt, const char *dname)
{
  // char uuidStr[256];

  for (size_t ik = 0; ik < nkv; ++ik)
    {
      if (!kvmap[ik].isValid) continue;

      auto kv = kvmap[ik].kv;
      const auto &key = kv->key;
      size_t nvalues = kv->nvalues;
      if (nvalues == 0) continue;
      const auto &values = kv->values;
      const auto &value = kv->values[0];

      // clang-format off
      if (key == "zaxistype")
        {
          auto zaxistype = parameter2word(value);

          if      (zaxistype == "pressure") zaxis->type = ZAXIS_PRESSURE;
          else if (zaxistype == "hybrid_half") zaxis->type = ZAXIS_HYBRID_HALF;
          else if (zaxistype == "hybrid") zaxis->type = ZAXIS_HYBRID;
          else if (zaxistype == "height") zaxis->type = ZAXIS_HEIGHT;
          else if (zaxistype == "depth_below_sea") zaxis->type = ZAXIS_DEPTH_BELOW_SEA;
          else if (zaxistype == "depth_below_land") zaxis->type = ZAXIS_DEPTH_BELOW_LAND;
          else if (zaxistype == "isentropic") zaxis->type = ZAXIS_ISENTROPIC;
          else if (zaxistype == "surface") zaxis->type = ZAXIS_SURFACE;
          else if (zaxistype == "generic") zaxis->type = ZAXIS_GENERIC;
          else cdoAbort("Invalid zaxis type: %s (zaxis description file: %s)", zaxistype.c_str(), dname);
        }
      else if (key == "datatype")
        {
          auto datatype = parameter2word(value);

          if      (datatype == "double") zaxis->datatype = CDI_DATATYPE_FLT64;
          else if (datatype == "float")  zaxis->datatype = CDI_DATATYPE_FLT32;
          else cdoAbort("Invalid datatype: %s (zaxis description file: %s)", datatype.c_str(), dname);
        }
      else if (key == "size") zaxis->size = parameter2int(value);
      else if (key == "scalar") zaxis->scalar = parameter2bool(value);
      else if (key == "vctsize") zaxis->vctsize = parameter2int(value);
      else if (key == "name") strcpy(zaxis->name, parameter2word(value.c_str()));
      else if (key == "units") strcpy(zaxis->units, parameter2word(value.c_str()));
      else if (key == "longname") strcpy(zaxis->longname, value.c_str());
      else if (key == "levels")
        {
          if (zaxis->size == 0) cdoAbort("size undefined (zaxis description file: %s)!", dname);
          if (zaxis->size != nvalues) cdoAbort("size=%zu and number of levels=%zu differ!", zaxis->size, nvalues);
          zaxis->vals.resize(zaxis->size);
          for (size_t i = 0; i < zaxis->size; ++i) zaxis->vals[i] = parameter2double(values[i]);
        }
      else if (key == "lbounds")
        {
          if (zaxis->size == 0) cdoAbort("size undefined (zaxis description file: %s)!", dname);
          if (zaxis->size != nvalues) cdoAbort("size=%zu and number of lbounds=%zu differ!", zaxis->size, nvalues);
          zaxis->lbounds.resize(zaxis->size);
          for (size_t i = 0; i < zaxis->size; ++i) zaxis->lbounds[i] = parameter2double(values[i]);
        }
      else if (key == "ubounds")
        {
          if (zaxis->size == 0) cdoAbort("size undefined (zaxis description file: %s)!", dname);
          if (zaxis->size != nvalues) cdoAbort("size=%zu and number of ubounds=%zu differ!", zaxis->size, nvalues);
          zaxis->ubounds.resize(zaxis->size);
          for (size_t i = 0; i < zaxis->size; ++i) zaxis->ubounds[i] = parameter2double(values[i]);
        }
      else if (key == "vct")
        {
          if (zaxis->vctsize == 0) cdoAbort("vctsize undefined (zaxis description file: %s)!", dname);
          zaxis->vct.resize(zaxis->vctsize);
          for (size_t i = 0; i < zaxis->vctsize; ++i) zaxis->vct[i] = parameter2double(values[i]);
        }
      else
        {
          *iatt = ik;
          break;
        }
      // clang-format on
    }
}

static void
zaxis_read_attributes(size_t iatt, size_t nkv, KVMap *kvmap, int zaxisID)
{
  const std::vector<std::string> reserved_keys
      = { "zaxistype", "size", "scalar", "vctsize", "name", "units", "longname", "levels", "lbounds", "ubounds", "vct" };
  int num_rkeys = reserved_keys.size();
  const char *attkey0 = nullptr;

  for (size_t ik = iatt; ik < nkv; ++ik)
    {
      if (!kvmap[ik].isValid) continue;

      const auto kv = kvmap[ik].kv;
      const auto &key = kv->key;
      size_t nvalues = kv->nvalues;
      if (nvalues == 0) continue;
      const auto &values = kv->values;
      const auto &value = kv->values[0];

      if (ik == iatt)
        attkey0 = key.c_str();
      else
        {
          for (int n = 0; n < num_rkeys; ++n)
            if (key == reserved_keys[n])
              cdoAbort("Found reserved key word >%s< in attribute names! Check name or position of >%s<.", key.c_str(), attkey0);
        }

      const auto dtype = literalsFindDatatype(nvalues, values);

      if (dtype == CDI_DATATYPE_INT8 || dtype == CDI_DATATYPE_INT16 || dtype == CDI_DATATYPE_INT32)
        {
          std::vector<int> ivals(nvalues);
          for (size_t i = 0; i < nvalues; ++i) ivals[i] = literal2int(values[i]);
          cdiDefAttInt(zaxisID, CDI_GLOBAL, key.c_str(), dtype, nvalues, ivals.data());
        }
      else if (dtype == CDI_DATATYPE_FLT32 || dtype == CDI_DATATYPE_FLT64)
        {
          Varray<double> dvals(nvalues);
          for (size_t i = 0; i < nvalues; ++i) dvals[i] = literal2double(values[i]);
          cdiDefAttFlt(zaxisID, CDI_GLOBAL, key.c_str(), dtype, nvalues, dvals.data());
        }
      else
        {
          const auto len = (int) value.size();
          cdiDefAttTxt(zaxisID, CDI_GLOBAL, key.c_str(), len, value.c_str());
        }
    }
}

int
zaxisFromFile(FILE *gfp, const char *dname)
{
  PMList pmlist;
  pmlist.readNamelist(gfp, dname);
  if (pmlist.size() == 0) return -1;
  KVList &kvlist = pmlist.front();

  const auto nkv = kvlist.size();
  if (nkv == 0) return -1;

  std::vector<KVMap> kvmap(nkv);
  for (size_t i = 0; i < nkv; ++i) kvmap[i].isValid = false;

  size_t ik = 0;
  const char *firstKey = "zaxistype";
  for (auto &kv : kvlist)
    {
      if (ik == 0 && kv.key != firstKey)
        cdoAbort("First zaxis description key word must be >%s< (found: %s)!", firstKey, kv.key.c_str());

      if (kv.nvalues == 0)
        {
          cdoWarning("Z-axis description key word %s has no values, skipped!", kv.key.c_str());
        }
      else
        {
          kvmap[ik].isValid = true;
          kvmap[ik].kv = &kv;
        }
      ik++;
    }

  zaxis_t zaxis;
  zaxisInit(zaxis);

  size_t iatt = 0;
  zaxis_read_data(nkv, kvmap.data(), &zaxis, &iatt, dname);

  int zaxisID = (zaxis.type == CDI_UNDEFID) ? CDI_UNDEFID : zaxisDefine(zaxis);
  if (zaxisID != CDI_UNDEFID && iatt > 0) zaxis_read_attributes(iatt, nkv, kvmap.data(), zaxisID);

  return zaxisID;
}

static void
gen_zaxis_height(zaxis_t *zaxis, const char *pline)
{
  int zaxistype = ZAXIS_HEIGHT;

  if (Options::CMOR_Mode) zaxis->scalar = true;

  if (*pline != 0)
    {
      if (*pline == '_')
        pline++;
      else
        return;

      if (*pline == 0) return;

      if (!isdigit((int) *pline) && !ispunct((int) *pline)) return;

      auto endptr = (char *) pline;
      const auto value = strtod(pline, &endptr);
      if (*endptr != 0)
        {
          pline = endptr;
          if (*pline == '_') pline++;

          if (*pline == 0) return;
          const char *units = pline;

          zaxis->type = zaxistype;
          zaxis->size = 1;
          // zaxis->scalar = true;
          zaxis->vals.resize(1);
          zaxis->vals[0] = value;
          strcpy(zaxis->units, units);

          const auto len = strlen(units);
          if (len > 2 && units[len - 2] == '_' && units[len - 1] == 's')
            {
              zaxis->units[len - 2] = 0;
              zaxis->scalar = true;
            }
        }
    }
}

int
zaxisFromName(const char *zaxisnameptr)
{
  int zaxisID = CDI_UNDEFID;
  size_t len;

  auto zaxisname = strdup(zaxisnameptr);
  cstrToLowerCase(zaxisname);

  zaxis_t zaxis;
  zaxisInit(zaxis);

  auto pline = zaxisname;
  if (cstr2IsEqual(pline, "surface"))  // surface
    {
      zaxis.type = ZAXIS_SURFACE;
      zaxis.size = 1;
      zaxis.vals.resize(zaxis.size);
      zaxis.vals[0] = 0;
    }
  else if (cstr2IsEqualLen(zaxisname, "height", len))
    {
      pline = &zaxisname[len];
      gen_zaxis_height(&zaxis, pline);
    }

  if (zaxis.type != CDI_UNDEFID) zaxisID = zaxisDefine(zaxis);

  free(zaxisname);

  return zaxisID;
}

int
cdoDefineZaxis(const std::string &p_zaxisfile)
{
  return cdoDefineZaxis(p_zaxisfile.c_str());
}

int
cdoDefineZaxis(const char *zaxisfile)
{
  int zaxisID = CDI_UNDEFID;

  auto zfp = fopen(zaxisfile, "r");
  if (zfp == nullptr)
    {
      zaxisID = zaxisFromName(zaxisfile);
      if (zaxisID == CDI_UNDEFID) cdoAbort("Open failed on %s!", zaxisfile);
    }
  else
    {
      zaxisID = zaxisFromFile(zfp, zaxisfile);
      fclose(zfp);
    }

  if (zaxisID == CDI_UNDEFID) cdoAbort("Invalid zaxis description file %s!", zaxisfile);

  return zaxisID;
}

void
defineZaxis(const char *zaxisarg)
{
  char zaxisfile[4096];
  int nfile = 0;

  while (getoptname(zaxisfile, zaxisarg, nfile++) == 0)
    {
      (void) cdoDefineZaxis(zaxisfile);
    }
}

static int
ztype2ltype(int zaxistype)
{
  int ltype = CDI_UNDEFID;

  // clang-format off
  if      ( zaxistype == ZAXIS_SURFACE           )  ltype =   1;
  else if ( zaxistype == ZAXIS_PRESSURE          )  ltype = 100;
  else if ( zaxistype == ZAXIS_ALTITUDE          )  ltype = 103;
  else if ( zaxistype == ZAXIS_HEIGHT            )  ltype = 105;
  else if ( zaxistype == ZAXIS_SIGMA             )  ltype = 107;
  else if ( zaxistype == ZAXIS_HYBRID            )  ltype = 109;
  else if ( zaxistype == ZAXIS_HYBRID_HALF       )  ltype = 109;
  else if ( zaxistype == ZAXIS_DEPTH_BELOW_LAND  )  ltype = 111;
  else if ( zaxistype == ZAXIS_ISENTROPIC        )  ltype = 113;
  else if ( zaxistype == ZAXIS_DEPTH_BELOW_SEA   )  ltype = 160;
  // clang-format on

  return ltype;
}

int
zaxis2ltype(int zaxisID)
{
  int ltype = 0;

  cdiInqKeyInt(zaxisID, CDI_GLOBAL, CDI_KEY_TYPEOFFIRSTFIXEDSURFACE, &ltype);
  if (ltype <= 0) ltype = ztype2ltype(zaxisInqType(zaxisID));

  return ltype;
}
