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

  Author: Uwe Schulzweida

*/

#include <cdi.h>
#include "cdi_uuid.h"

#include "cdo_output.h"
#include "readline.h"
#include "param_conversion.h"
#include "griddes.h"
#include "parse_literals.h"
#include "pmlist.h"

void
cdo_read_field(const char *name, char *pline, int size, double *field, int *lineno, FILE *fp, const char *dname)
{
  constexpr int MAX_LINE_LEN = 65536;
  char line[MAX_LINE_LEN];
  for (int i = 0; i < size; i++)
    {
      auto endptr = pline;
      auto fval = strtod(pline, &endptr);
      if (pline == endptr)
        {
          (*lineno)++;
          if (!cdo::readline(fp, line, MAX_LINE_LEN))
            cdo_abort("Incomplete command: >%s< (line: %d file: %s)", name, *lineno, dname);
          pline = line;
          fval = strtod(pline, &endptr);
        }
      field[i] = fval;
      pline = endptr;
    }
}

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

static void
grid_read_data(size_t ikv, size_t nkv, KVMap *kvmap, GridDesciption &grid, size_t *iproj, size_t *igmap, const char *dname)
{
  char uuidStr[256];

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

      const auto kv = kvmap[ik].kv;
      const auto &key = kv->key;
      const 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 == "gridtype")
        {
          const auto gridtype = parameter_to_word(value);

          if (grid.type != CDI_UNDEFID)
            {
              if (gridtype == "projection") *iproj = ik;
              return;
            }

          if      (gridtype == "lonlat")           grid.type = GRID_LONLAT;
          else if (gridtype == "latlon")           grid.type = GRID_LONLAT;
          else if (gridtype == "gaussian")         grid.type = GRID_GAUSSIAN;
          else if (gridtype == "gaussian_reduced") grid.type = GRID_GAUSSIAN_REDUCED;
          else if (gridtype == "curvilinear")      grid.type = GRID_CURVILINEAR;
          else if (gridtype == "unstructured")     grid.type = GRID_UNSTRUCTURED;
          else if (gridtype == "cell")             grid.type = GRID_UNSTRUCTURED;
          else if (gridtype == "spectral")         grid.type = GRID_SPECTRAL;
          else if (gridtype == "gme")              grid.type = GRID_GME;
          else if (gridtype == "projection")       grid.type = GRID_PROJECTION;
          else if (gridtype == "generic")          grid.type = GRID_GENERIC;
          else cdo_abort("Invalid gridtype: %s (grid description file: %s)", gridtype.c_str(), dname);

          if (grid.type == GRID_LONLAT || grid.type == GRID_GAUSSIAN || grid.type == GRID_GAUSSIAN_REDUCED || grid.type == GRID_PROJECTION)
            grid.nvertex = 2;
          else if (grid.type == GRID_CURVILINEAR)
            grid.nvertex = 4;
        }
      else if (key == "datatype")
        {
          const auto datatype = parameter_to_word(value);
          if      (datatype == "double") grid.datatype = CDI_DATATYPE_FLT64;
          else if (datatype == "float")  grid.datatype = CDI_DATATYPE_FLT32;
          else cdo_abort("Invalid datatype: %s (grid description file: %s)", datatype, dname);
        }
      else if (key == "gridsize")   grid.size = parameter_to_size_t(value);
      else if (key == "xsize")      grid.xsize = parameter_to_size_t(value);
      else if (key == "nlon")       grid.xsize = parameter_to_size_t(value);
      else if (key == "ysize")      grid.ysize = parameter_to_size_t(value);
      else if (key == "nlat")       grid.ysize = parameter_to_size_t(value);
      else if (key == "truncation") grid.ntr = parameter_to_int(value);
      else if (key == "numLPE")     grid.numLPE = parameter_to_int(value);
      else if (key == "np")         grid.numLPE = parameter_to_int(value); // np: obsolete
      else if (key == "nvertex")    grid.nvertex = parameter_to_int(value);
      else if (key == "complexpacking") grid.lcomplex = parameter_to_int(value);
      else if (key == "ni")
        {
          grid.ni = parameter_to_int(value);
          grid.nd = 10;
        }
      else if (key == "position") grid.position = parameter_to_int(value);
      else if (key == "number")   grid.number = parameter_to_int(value);
      else if (key == "scanningMode")
        {
          grid.scanningMode = 64;
          const auto scmode = parameter_to_int(value);
          if (scmode == 0 || scmode == 64 || scmode == 96)
            grid.scanningMode = scmode; // -1: not used; allowed modes: <0, 64, 96>; Default is 64
          else
            cdo_warning("Warning: %d not in allowed modes: <0, 64, 96>; Using default: 64\n", scmode);
        }
      else if (key == "xname")     strcpy(grid.xname, parameter_to_word(value.c_str()));
      else if (key == "yname")     strcpy(grid.yname, parameter_to_word(value.c_str()));
      else if (key == "xdimname")  strcpy(grid.xdimname, parameter_to_word(value.c_str()));
      else if (key == "ydimname")  strcpy(grid.ydimname, parameter_to_word(value.c_str()));
      else if (key == "vdimname")  strcpy(grid.vdimname, parameter_to_word(value.c_str()));
      else if (key == "xlongname") strcpy(grid.xlongname, value.c_str());
      else if (key == "ylongname") strcpy(grid.ylongname, value.c_str());
      else if (key == "xunits")    strcpy(grid.xunits, value.c_str());
      else if (key == "yunits")    strcpy(grid.yunits, value.c_str());
      else if (key == "path")      strcpy(grid.path, value.c_str());
      else if (key == "uuid")
        {
          strcpy(uuidStr, value.c_str());
          cdiStr2UUID(uuidStr, grid.uuid);
        }
      else if (key == "xfirst") grid.xfirst = parameter_to_double(value);
      else if (key == "yfirst") grid.yfirst = parameter_to_double(value);
      else if (key == "xlast")  grid.xlast = parameter_to_double(value);
      else if (key == "ylast")  grid.ylast = parameter_to_double(value);
      else if (key == "xinc")   grid.xinc = parameter_to_double(value);
      else if (key == "yinc")   grid.yinc = parameter_to_double(value);
      else if (key == "a")      grid.a = parameter_to_double(value);
      else if (key == "xvals")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.xsize;
          if (size == 0) cdo_abort("xsize or gridsize undefined (grid description file: %s)!", dname);
          if (size != nvalues)
            cdo_abort("Number of xvals=%zu and size of xvals=%zu differ (grid description file: %s)!", nvalues, size, dname);

          grid.xvals.resize(size);
          for (size_t i = 0; i < size; ++i) grid.xvals[i] = parameter_to_double(values[i]);
        }
      else if (key == "yvals")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.ysize;
          if (size == 0) cdo_abort("ysize or gridsize undefined (grid description file: %s)!", dname);
          if (size != nvalues)
            cdo_abort("Number of yvals=%zu and size of yvals=%zu differ (grid description file: %s)!", nvalues, size, dname);

          grid.yvals.resize(size);
          for (size_t i = 0; i < size; ++i) grid.yvals[i] = parameter_to_double(values[i]);
        }
      else if (key == "xbounds")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.xsize;
          if (size == 0) cdo_abort("xsize or gridsize undefined (grid description file: %s)!", dname);
          if (grid.nvertex == 0) cdo_abort("nvertex undefined (grid description file: %s)!", dname);
          if (grid.nvertex * size != nvalues)
            cdo_abort("Number of xbounds=%zu and size of xbounds=%zu differ (grid description file: %s)!", nvalues,
                     grid.nvertex * size, dname);

          grid.xbounds.resize(grid.nvertex * size);
          for (size_t i = 0; i < grid.nvertex * size; ++i) grid.xbounds[i] = parameter_to_double(values[i]);
        }
      else if (key == "ybounds")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.ysize;
          if (size == 0) cdo_abort("ysize or gridsize undefined (grid description file: %s)!", dname);
          if (grid.nvertex == 0) cdo_abort("nvertex undefined (grid description file: %s)!", dname);
          if (grid.nvertex * size != nvalues)
            cdo_abort("Number of ybounds=%zu and size of ybounds=%zu differ (grid description file: %s)!", nvalues,
                     grid.nvertex * size, dname);

          grid.ybounds.resize(grid.nvertex * size);
          for (size_t i = 0; i < grid.nvertex * size; ++i) grid.ybounds[i] = parameter_to_double(values[i]);
        }
      else if (key == "gridlatlon")
        {
          if (grid.size == 0) grid.size = grid.xsize * grid.ysize;
          if (grid.size == 0) cdo_abort("gridsize undefined (grid description file: %s)!", dname);
          if (grid.size * 2 != nvalues)
            cdo_abort("Number of gridlonlat values=%zu and size of grid=%zu differ (grid description file: %s)!", nvalues,
                     grid.size * 2, dname);
          grid.xvals.resize(grid.size);
          grid.yvals.resize(grid.size);
          for (size_t i = 0; i < grid.size; ++i)
            {
              grid.yvals[i] = parameter_to_double(values[2 * i]);
              grid.xvals[i] = parameter_to_double(values[2 * i + 1]);
            }
        }
      else if (key == "mask")
        {
          auto size = grid.size;
          if (grid.size == 0) cdo_abort("gridsize undefined (grid description file: %s)!", dname);
          if (size != nvalues)
            cdo_abort("Number of mask values=%zu and size of grid=%zu differ (grid description file: %s)!", nvalues, size, dname);
          grid.mask.resize(size);
          size_t count = 0;
          for (size_t i = 0; i < size; ++i)
            {
              grid.mask[i] = parameter_to_int(values[i]);
              if (grid.mask[i] == 1) count++;
            }
          if (count == size)
            {
              grid.mask.clear();
              grid.mask.shrink_to_fit();
            }
        }
      else if (key == "reducedPoints")
        {
          auto size = grid.ysize;
          if (size == 0) cdo_abort("ysize undefined (grid description file: %s)!", dname);
          grid.reducedPoints.resize(size);
          for (size_t i = 0; i < size; ++i) grid.reducedPoints[i] = parameter_to_int(values[i]);
        }
      else if (key == "grid_mapping_name")
        {
          *igmap = ik;
          break;
        }
      else if (key == "grid_mapping")
        {
          *igmap = ik;
          break;
        }
      else
        cdo_abort("Invalid keyword >%s< (grid description file: %s)", key.c_str(), dname);
      // clang-format on
    }
}

static void
grid_read_mapping(size_t igmap, size_t nkv, KVMap *kvmap, int gridID)
{
  auto hasGridmapVarname = false;
  for (size_t ik = igmap; ik < nkv; ++ik)
    {
      if (!kvmap[ik].isValid) continue;

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

      if (key == "grid_mapping")
        {
          hasGridmapVarname = true;
          cdiDefKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_VARNAME, value.c_str());
          continue;
        }

      if (key == "grid_mapping_name")
        {
          if (!hasGridmapVarname)
            {
              if (value == "rotated_latitude_longitude")
                cdiDefKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_VARNAME, "rotated_pole");
              else
                cdiDefKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_VARNAME, "crs");
            }

          cdiDefKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_NAME, value.c_str());
        }

      const auto dtype = literals_find_datatype(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] = literal_to_int(values[i]);
          cdiDefAttInt(gridID, CDI_GLOBAL, key.c_str(), dtype, nvalues, ivals.data());
        }
      else if (dtype == CDI_DATATYPE_FLT32 || dtype == CDI_DATATYPE_FLT64)
        {
          std::vector<double> dvals(nvalues);
          for (size_t i = 0; i < nvalues; ++i) dvals[i] = literal_to_double(values[i]);
          cdiDefAttFlt(gridID, CDI_GLOBAL, key.c_str(), dtype, nvalues, dvals.data());
        }
      else
        {
          const auto len = (int) value.size();
          cdiDefAttTxt(gridID, CDI_GLOBAL, key.c_str(), len, value.c_str());
        }
    }
}

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

  const size_t 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 std::string firstKey = "gridtype";
  for (auto &kv : kvlist)
    {
      if (ik == 0 && kv.key != firstKey)
        cdo_abort("First grid description keyword must be >%s< (found: %s)!", firstKey, kv.key.c_str());

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

  size_t iproj = 0;
  size_t igmap = 0;
  GridDesciption grid;
  grid_read_data(0, nkv, kvmap.data(), grid, &iproj, &igmap, dname);

  const auto gridID = (grid.type == CDI_UNDEFID) ? CDI_UNDEFID : grid_define(grid);

  if (gridID != CDI_UNDEFID)
    {
      auto gridprojID = gridID;

      if (iproj > 0)
        {
          GridDesciption proj;
          grid_read_data(iproj, nkv, kvmap.data(), proj, &iproj, &igmap, dname);

          const auto projID = (proj.type == CDI_UNDEFID) ? CDI_UNDEFID : grid_define(proj);
          if (projID != CDI_UNDEFID)
            {
              gridDefProj(gridID, projID);
              gridprojID = projID;
            }
        }

      if (igmap > 0) grid_read_mapping(igmap, nkv, kvmap.data(), gridprojID);
    }

  return gridID;
}
