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

#define MAX_LINE_LEN 65536

void
cdo_read_field(const char *name, char *pline, int size, double *field, int *lineno, FILE *fp, const char *dname)
{
  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)) cdoAbort("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;
      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")
        {
          auto gridtype = parameter2word(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 cdoAbort("Invalid gridtype: %s (grid description file: %s)", gridtype.c_str(), dname);

          if (grid.type == GRID_LONLAT || grid.type == GRID_GAUSSIAN)
            grid.nvertex = 2;
          else if (grid.type == GRID_CURVILINEAR)
            grid.nvertex = 4;
        }
      else if (key == "datatype")
        {
          auto datatype = parameter2word(value);

          if      (datatype == "double") grid.datatype = CDI_DATATYPE_FLT64;
          else if (datatype == "float")  grid.datatype = CDI_DATATYPE_FLT32;
          else cdoAbort("Invalid datatype: %s (zaxis description file: %s)", datatype, dname);
        }
      else if (key == "gridsize") grid.size = parameter2sizet(value);
      else if (key == "xsize") grid.xsize = parameter2sizet(value);
      else if (key == "nlon") grid.xsize = parameter2sizet(value);
      else if (key == "ysize") grid.ysize = parameter2sizet(value);
      else if (key == "nlat") grid.ysize = parameter2sizet(value);
      else if (key == "truncation") grid.ntr = parameter2int(value);
      else if (key == "numLPE") grid.numLPE = parameter2int(value);
      else if (key == "complexpacking") grid.lcomplex = parameter2int(value);
      else if (key == "nvertex") grid.nvertex = parameter2int(value);
      else if (key == "ni")
        {
          grid.ni = parameter2int(value);
          grid.nd = 10;
        }
      else if (key == "position") grid.position = parameter2int(value);
      else if (key == "number") grid.number = parameter2int(value);
      else if (key == "scanningMode")
        {
          int scmode = parameter2int(value);
          if ((scmode == 0) || (scmode == 64) || (scmode == 96))
            {
              grid.scanningMode = scmode;  // -1: not used; allowed modes: <0,
                                           // 64, 96>; Default is 64
            }
          else
            {
              cdoWarning("Warning: %d not in allowed modes: <0, 64, 96>; Using default: 64\n", scmode);
              grid.scanningMode = 64;
            }
        }
      else if (key == "xname") strcpy(grid.xname, parameter2word(value.c_str()));
      else if (key == "yname") strcpy(grid.yname, parameter2word(value.c_str()));
      else if (key == "xdimname") strcpy(grid.xdimname, parameter2word(value.c_str()));
      else if (key == "ydimname") strcpy(grid.ydimname, parameter2word(value.c_str()));
      else if (key == "vdimname") strcpy(grid.vdimname, parameter2word(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 = parameter2double(value);
          grid.def_xfirst = true;
        }
      else if (key == "yfirst")
        {
          grid.yfirst = parameter2double(value);
          grid.def_yfirst = true;
        }
      else if (key == "xlast")
        {
          grid.xlast = parameter2double(value);
          grid.def_xlast = true;
        }
      else if (key == "ylast")
        {
          grid.ylast = parameter2double(value);
          grid.def_ylast = true;
        }
      else if (key == "xinc")
        {
          grid.xinc = parameter2double(value);
          grid.def_xinc = true;
        }
      else if (key == "yinc")
        {
          grid.yinc = parameter2double(value);
          grid.def_yinc = true;
        }
      else if (key == "a") grid.a = parameter2double(value);
      else if (key == "xvals")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.xsize;
          if (size == 0) cdoAbort("xsize or gridsize undefined (grid description file: %s)!", dname);
          if (size != nvalues)
            cdoAbort("xsize=%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] = parameter2double(values[i]);
        }
      else if (key == "yvals")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.ysize;
          if (size == 0) cdoAbort("ysize or gridsize undefined (grid description file: %s)!", dname);
          if (size != nvalues)
            cdoAbort("ysize=%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] = parameter2double(values[i]);
        }
      else if (key == "xbounds")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.xsize;
          if (size == 0) cdoAbort("xsize or gridsize undefined (grid description file: %s)!", dname);
          if (grid.nvertex == 0) cdoAbort("nvertex undefined (grid description file: %s)!", dname);
          if (grid.nvertex * size != nvalues)
            cdoAbort("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] = parameter2double(values[i]);
        }
      else if (key == "ybounds")
        {
          auto size = (grid.type == GRID_CURVILINEAR || grid.type == GRID_UNSTRUCTURED) ? grid.size : grid.ysize;
          if (size == 0) cdoAbort("ysize or gridsize undefined (grid description file: %s)!", dname);
          if (grid.nvertex == 0) cdoAbort("nvertex undefined (grid description file: %s)!", dname);
          if (grid.nvertex * size != nvalues)
            cdoAbort("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] = parameter2double(values[i]);
        }
      else if (key == "gridlatlon")
        {
          if (grid.size == 0) grid.size = grid.xsize * grid.ysize;
          if (grid.size == 0) cdoAbort("gridsize undefined (grid description file: %s)!", dname);
          if (grid.size * 2 != nvalues)
            cdoAbort("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] = parameter2double(values[2 * i]);
              grid.xvals[i] = parameter2double(values[2 * i + 1]);
            }
        }
      else if (key == "mask")
        {
          auto size = grid.size;
          if (grid.size == 0) cdoAbort("gridsize undefined (grid description file: %s)!", dname);
          if (size != nvalues)
            cdoAbort("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] = parameter2int(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) cdoAbort("ysize undefined (grid description file: %s)!", dname);
          grid.reducedPoints.resize(size);
          for (size_t i = 0; i < size; ++i) grid.reducedPoints[i] = parameter2int(values[i]);
        }
      else if (key == "grid_mapping_name")
        {
          *igmap = ik;
          break;
        }
      else if (key == "grid_mapping")
        {
          *igmap = ik;
          break;
        }
      else
        cdoAbort("Invalid key word >%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)
{
  for (size_t ik = igmap; 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 (key == "grid_mapping")
        {
          cdiDefKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_VARNAME, value.c_str());
          continue;
        }

      if (key == "grid_mapping_name") cdiDefKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_NAME, value.c_str());

      int 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(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] = literal2double(values[i]);
          cdiDefAttFlt(gridID, CDI_GLOBAL, key.c_str(), dtype, nvalues, dvals.data());
        }
      else
        {
          const int 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.readNamelist(gfp, dname);
  if (pmlist.size() == 0) return -1;
  KVList &kvlist = pmlist.front();

  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)
        cdoAbort("First grid description key word must be >%s< (found: %s)!", firstKey, kv.key.c_str());

      if (kv.nvalues == 0)
        {
          cdoWarning("Grid description key word %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);

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

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

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

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

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

  return gridID;
}
