/*
  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_options.h"
#include "dmemory.h"
#include "process_int.h"
#include "util_string.h"

static void
printDblsPrefixAutoBrk(FILE *fp, int dig, const char prefix[], int nbyte0, size_t n, const double vals[])
{
  fputs(prefix, fp);
  auto nbyte = nbyte0;
  for (size_t i = 0; i < n; i++)
    {
      if (nbyte > 80)
        {
          fprintf(fp, "\n%*s", nbyte0, "");
          nbyte = nbyte0;
        }
      nbyte += fprintf(fp, "%.*g ", dig, vals[i]);
    }
  fputs("\n", fp);
}

static void
printIntsPrefixAutoBrk(FILE *fp, const char prefix[], int nbyte0, size_t n, const int vals[])
{
  fputs(prefix, fp);
  auto nbyte = nbyte0;
  for (size_t i = 0; i < n; i++)
    {
      if (nbyte > 80)
        {
          fprintf(fp, "\n%*s", nbyte0, "");
          nbyte = nbyte0;
        }
      nbyte += fprintf(fp, "%d ", vals[i]);
    }
  fputs("\n", fp);
}

static void
printBounds(FILE *fp, int dig, const char prefix[], size_t nbyte0, size_t n, size_t nvertex, const double bounds[])
{
  fputs(prefix, fp);
  for (size_t i = 0; i < n; i++)
    {
      if (i > 0) fprintf(fp, "\n%*s", (int) nbyte0, "");
      for (size_t iv = 0; iv < nvertex; iv++) fprintf(fp, "%.*g ", dig, bounds[i * nvertex + iv]);
    }
  fputs("\n", fp);
}

static void
printMask(FILE *fp, const char prefix[], size_t nbyte0, size_t n, const int mask[])
{
  fputs(prefix, fp);
  auto nbyte = nbyte0;
  for (size_t i = 0; i < n; i++)
    {
      if (nbyte > 80)
        {
          fprintf(fp, "\n%*s", (int) nbyte0, "");
          nbyte = nbyte0;
        }
      nbyte += (size_t) fprintf(fp, "%d ", mask[i]);
    }
  fputs("\n", fp);
}

static inline void *
resizeBuffer(void **buf, size_t *bufSize, size_t reqSize)
{
  if (reqSize > *bufSize)
    {
      *buf = Realloc(*buf, reqSize);
      *bufSize = reqSize;
    }
  return *buf;
}

static void
grid_print_attributes(FILE *fp, int gridID)
{
  int cdiID = gridID;
  int varID = CDI_GLOBAL;
  int atttype, attlen;
  char attname[CDI_MAX_NAME + 1];
  void *attBuf = nullptr;
  size_t attBufSize = 0;
  char fltstr[128];

  int natts;
  cdiInqNatts(cdiID, varID, &natts);

  for (int iatt = 0; iatt < natts; ++iatt)
    {
      cdiInqAtt(cdiID, varID, iatt, attname, &atttype, &attlen);

      if (attlen == 0) continue;

      if (cstrIsEqual(attname, "grid_mapping_name")) continue;

      if (atttype == CDI_DATATYPE_TXT)
        {
          const auto attSize = (size_t)(attlen + 1) * sizeof(char);
          auto atttxt = (char *) resizeBuffer(&attBuf, &attBufSize, attSize);
          cdiInqAttTxt(cdiID, varID, attname, attlen, atttxt);
          atttxt[attlen] = 0;
          if (strchr(atttxt, '"'))
            fprintf(fp, "%s = '%s'\n", attname, atttxt);
          else
            fprintf(fp, "%s = \"%s\"\n", attname, atttxt);
        }
      else if (atttype == CDI_DATATYPE_INT8 || atttype == CDI_DATATYPE_UINT8 || atttype == CDI_DATATYPE_INT16
               || atttype == CDI_DATATYPE_UINT16 || atttype == CDI_DATATYPE_INT32 || atttype == CDI_DATATYPE_UINT32)
        {
          const auto attSize = (size_t) attlen * sizeof(int);
          auto attint = (int *) resizeBuffer(&attBuf, &attBufSize, attSize);
          cdiInqAttInt(cdiID, varID, attname, attlen, &attint[0]);
          fprintf(fp, "%s =", attname);
          for (int i = 0; i < attlen; ++i) fprintf(fp, " %d", attint[i]);
          fprintf(fp, "\n");
        }
      else if (atttype == CDI_DATATYPE_FLT32 || atttype == CDI_DATATYPE_FLT64)
        {
          const auto attSize = (size_t) attlen * sizeof(double);
          auto attflt = (double *) resizeBuffer(&attBuf, &attBufSize, attSize);
          cdiInqAttFlt(cdiID, varID, attname, attlen, attflt);
          fprintf(fp, "%s =", attname);
          if (atttype == CDI_DATATYPE_FLT32)
            for (int i = 0; i < attlen; ++i)
              fprintf(fp, " %sf", doubleToAttstr(Options::CDO_flt_digits, fltstr, sizeof(fltstr), attflt[i]));
          else
            for (int i = 0; i < attlen; ++i)
              fprintf(fp, " %s", doubleToAttstr(Options::CDO_dbl_digits, fltstr, sizeof(fltstr), attflt[i]));
          fprintf(fp, "\n");
        }
    }

  Free(attBuf);
}

static void
grid_print_kernel(int gridID, int opt, FILE *fp)
{
  size_t xdim, ydim;
  char attstr[CDI_MAX_NAME], attstr2[CDI_MAX_NAME];
  const auto nxvals = gridInqXvals(gridID, nullptr);
  const auto nyvals = gridInqYvals(gridID, nullptr);
  const auto nxbounds = gridInqXbounds(gridID, nullptr);
  const auto nybounds = gridInqYbounds(gridID, nullptr);

  const auto gridsize = gridInqSize(gridID);
  const auto xsize = gridInqXsize(gridID);
  const auto ysize = gridInqYsize(gridID);
  const auto type = gridInqType(gridID);
  const auto nvertex = gridInqNvertex(gridID);
  const auto datatype = gridInqDatatype(gridID);
  const auto xstrlen = gridInqXIsc(gridID);
  const auto ystrlen = gridInqYIsc(gridID);

  char **xcvals = nullptr;
  if (xstrlen)
    {
      xcvals = (char **) Malloc(xsize * sizeof(char *));
      for (size_t i = 0; i < xsize; i++) xcvals[i] = (char *) Malloc((xstrlen + 1) * sizeof(char));
      gridInqXCvals(gridID, xcvals);
      for (size_t i = 0; i < xsize; i++) xcvals[i][xstrlen] = 0;
      for (size_t i = 0; i < xsize; i++)
        for (int k = xstrlen - 1; k; k--)
          {
            if (xcvals[i][k] == ' ')
              xcvals[i][k] = 0;
            else
              break;
          }
    }

  char **ycvals = nullptr;
  if (ystrlen)
    {
      ycvals = (char **) Malloc(ysize * sizeof(char *));
      for (size_t i = 0; i < ysize; i++) ycvals[i] = (char *) Malloc((ystrlen + 1) * sizeof(char));
      gridInqYCvals(gridID, ycvals);
      for (size_t i = 0; i < ysize; i++) ycvals[i][ystrlen] = 0;
      for (size_t i = 0; i < ysize; i++)
        for (int k = ystrlen - 1; k; k--)
          {
            if (ycvals[i][k] == ' ')
              ycvals[i][k] = 0;
            else
              break;
          }
    }

  const int dig = (datatype == CDI_DATATYPE_FLT64) ? Options::CDO_dbl_digits : Options::CDO_flt_digits;

  fprintf(fp, "gridtype  = %s\n", gridNamePtr(type));
  fprintf(fp, "gridsize  = %zu\n", gridsize);
  if (datatype == CDI_DATATYPE_FLT32) fprintf(fp, "datatype  = float\n");

  if (type != GRID_GME)
    {
      if (type != GRID_UNSTRUCTURED && type != GRID_SPECTRAL && type != GRID_FOURIER)
        {
          if (xsize > 0) fprintf(fp, "xsize     = %zu\n", xsize);
          if (ysize > 0) fprintf(fp, "ysize     = %zu\n", ysize);
        }

      if (nxvals > 0 || xcvals)
        {
          int length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_NAME, attstr, &length);
          if (attstr[0]) fprintf(fp, "xname     = %s\n", attstr);
          length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_DIMNAME, attstr2, &length);
          if (attstr2[0] && !cstrIsEqual(attstr, attstr2)) fprintf(fp, "xdimname  = %s\n", attstr2);
          length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_LONGNAME, attstr, &length);
          if (attstr[0]) fprintf(fp, "xlongname = \"%s\"\n", attstr);
          length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_UNITS, attstr, &length);
          if (attstr[0]) fprintf(fp, "xunits    = \"%s\"\n", attstr);
        }

      if (nyvals > 0 || ycvals)
        {
          int length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_NAME, attstr, &length);
          if (attstr[0]) fprintf(fp, "yname     = %s\n", attstr);
          length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_DIMNAME, attstr2, &length);
          if (attstr2[0] && !cstrIsEqual(attstr, attstr2)) fprintf(fp, "ydimname  = %s\n", attstr2);
          length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_LONGNAME, attstr, &length);
          if (attstr[0]) fprintf(fp, "ylongname = \"%s\"\n", attstr);
          length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_UNITS, attstr, &length);
          if (attstr[0]) fprintf(fp, "yunits    = \"%s\"\n", attstr);
        }

      if (type == GRID_UNSTRUCTURED || type == GRID_CURVILINEAR)
        {
          int length = CDI_MAX_NAME;
          cdiInqKeyString(gridID, CDI_GLOBAL, CDI_KEY_VDIMNAME, attstr, &length);
          if (attstr[0]) fprintf(fp, "vdimname  = %s\n", attstr);
        }
      if (type == GRID_UNSTRUCTURED && nvertex > 0) fprintf(fp, "nvertex   = %d\n", nvertex);
    }

  switch (type)
    {
    case GRID_LONLAT:
    case GRID_GAUSSIAN:
    case GRID_GAUSSIAN_REDUCED:
    case GRID_GENERIC:
    case GRID_PROJECTION:
    case GRID_CURVILINEAR:
    case GRID_UNSTRUCTURED:
    case GRID_TRAJECTORY:
    case GRID_CHARXY:
      {
        if (type == GRID_GAUSSIAN || type == GRID_GAUSSIAN_REDUCED) fprintf(fp, "numLPE    = %d\n", gridInqNP(gridID));

        if (type == GRID_CURVILINEAR || type == GRID_UNSTRUCTURED)
          {
            xdim = gridsize;
            ydim = gridsize;
          }
        else if (type == GRID_GAUSSIAN_REDUCED)
          {
            xdim = 2;
            ydim = ysize;
          }
        else
          {
            xdim = xsize;
            ydim = ysize;
          }

        if (type == GRID_UNSTRUCTURED)
          {
            int number = 0;
            cdiInqKeyInt(gridID, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDUSED, &number);
            if (number > 0)
              {
                fprintf(fp, "number    = %d\n", number);
                int position = 0;
                cdiInqKeyInt(gridID, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDINREFERENCE, &position);
                if (position >= 0) fprintf(fp, "position  = %d\n", position);
              }

            int length = 0;
            if (CDI_NOERR == cdiInqKeyLen(gridID, CDI_GLOBAL, CDI_KEY_REFERENCEURI, &length))
              {
                char reference_link[8192];
                cdiInqKeyString(gridID, CDI_GLOBAL, CDI_KEY_REFERENCEURI, reference_link, &length);
                fprintf(fp, "uri       = %s\n", reference_link);
              }
          }

        if (nxvals > 0)
          {
            double xfirst = 0.0, xinc = 0.0;

            if (type == GRID_LONLAT || type == GRID_GAUSSIAN || type == GRID_PROJECTION || type == GRID_GENERIC)
              {
                xfirst = gridInqXval(gridID, 0);
                xinc = gridInqXinc(gridID);
              }

            if (IS_NOT_EQUAL(xinc, 0) && opt)
              {
                fprintf(fp, "xfirst    = %.*g\n", dig, xfirst);
                fprintf(fp, "xinc      = %.*g\n", dig, xinc);
              }
            else
              {
                std::vector<double> xvals(nxvals);
                gridInqXvals(gridID, xvals.data());
                static const char prefix[] = "xvals     = ";
                printDblsPrefixAutoBrk(fp, dig, prefix, (int) sizeof(prefix) - 1, nxvals, xvals.data());
              }
          }

        if (nxbounds)
          {
            std::vector<double> xbounds(nxbounds);
            gridInqXbounds(gridID, xbounds.data());
            static const char prefix[] = "xbounds   = ";
            printBounds(fp, dig, prefix, sizeof(prefix) - 1, xdim, nvertex, xbounds.data());
          }

        if (xcvals)
          {
            int length = CDI_MAX_NAME;
            cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_NAME, attstr, &length);
            if (attstr[0])
              fprintf(fp, "x%ss  = \"%.*s\"", attstr, xstrlen, xcvals[0]);
            else
              fprintf(fp, "xstrings  = \"%.*s\"", xstrlen, xcvals[0]);
            for (size_t i = 1; i < xsize; i++) fprintf(fp, ", \"%.*s\"", xstrlen, xcvals[i]);
            fprintf(fp, "\n");

            for (size_t i = 0; i < xsize; i++) Free(xcvals[i]);
            Free(xcvals);
          }

        if (nyvals > 0)
          {
            double yfirst = 0.0, yinc = 0.0;

            if (type == GRID_LONLAT || type == GRID_GENERIC || type == GRID_PROJECTION || type == GRID_GENERIC)
              {
                yfirst = gridInqYval(gridID, 0);
                yinc = gridInqYinc(gridID);
              }

            if (IS_NOT_EQUAL(yinc, 0) && opt)
              {
                fprintf(fp, "yfirst    = %.*g\n", dig, yfirst);
                fprintf(fp, "yinc      = %.*g\n", dig, yinc);
              }
            else
              {
                std::vector<double> yvals(nyvals);
                gridInqYvals(gridID, yvals.data());
                static const char prefix[] = "yvals     = ";
                printDblsPrefixAutoBrk(fp, dig, prefix, (int) sizeof(prefix) - 1, nyvals, yvals.data());
              }
          }

        if (nybounds)
          {
            std::vector<double> ybounds(nybounds);
            gridInqYbounds(gridID, ybounds.data());
            static const char prefix[] = "ybounds   = ";
            printBounds(fp, dig, prefix, sizeof(prefix) - 1, ydim, nvertex, ybounds.data());
          }

        if (ycvals)
          {
            int length = CDI_MAX_NAME;
            cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_NAME, attstr, &length);
            if (attstr[0])
              fprintf(fp, "x%ss  = \"%.*s\"", attstr, ystrlen, ycvals[0]);
            else
              fprintf(fp, "ystrings  = \"%.*s\"", ystrlen, ycvals[0]);
            for (size_t i = 1; i < ysize; i++) fprintf(fp, ", \"%.*s\"", ystrlen, ycvals[i]);
            fprintf(fp, "\n");

            for (size_t i = 0; i < ysize; i++) Free(ycvals[i]);
            Free(ycvals);
          }

        if (gridHasArea(gridID))
          {
            std::vector<double> area(gridsize);
            gridInqArea(gridID, area.data());
            static const char prefix[] = "area      = ";
            printDblsPrefixAutoBrk(fp, dig, prefix, (int) sizeof(prefix) - 1, gridsize, area.data());
          }

        if (type == GRID_GAUSSIAN_REDUCED)
          {
            std::vector<int> reducedPoints(ysize);
            gridInqReducedPoints(gridID, reducedPoints.data());
            static const char prefix[] = "reducedPoints = ";
            printIntsPrefixAutoBrk(fp, prefix, (int) sizeof(prefix) - 1, (size_t)(ysize > 0 ? ysize : 0), reducedPoints.data());
          }

#ifdef HIRLAM_EXTENSIONS
        {
          int scanningMode = 0;
          cdiInqKeyInt(gridID, CDI_GLOBAL, CDI_KEY_SCANNINGMODE, &scanningMode);
          fprintf(fp, "scanningMode = %d\n", scanningMode);
        }
#endif  // HIRLAM_EXTENSIONS

        if (type == GRID_PROJECTION)
          {
            int length = CDI_MAX_NAME;
            cdiInqKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_VARNAME, attstr, &length);
            if (attstr[0]) fprintf(fp, "grid_mapping = %s\n", attstr);
            length = CDI_MAX_NAME;
            cdiInqKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_NAME, attstr, &length);
            if (attstr[0]) fprintf(fp, "grid_mapping_name = %s\n", attstr);
            grid_print_attributes(fp, gridID);
          }

        break;
      }
    case GRID_SPECTRAL:
      {
        fprintf(fp, "truncation = %d\n", gridInqTrunc(gridID));
        fprintf(fp, "complexpacking = %d\n", gridInqComplexPacking(gridID));
        break;
      }
    case GRID_FOURIER:
      {
        fprintf(fp, "truncation = %d\n", gridInqTrunc(gridID));
        break;
      }
    case GRID_GME:
      {
        int nd, ni, ni2, ni3;
        gridInqParamGME(gridID, &nd, &ni, &ni2, &ni3);
        fprintf(fp, "ni        = %d\n", ni);
        break;
      }
    default:
      {
        fprintf(stderr, "Unsupported grid type: %s\n", gridNamePtr(type));
        break;
      }
    }

  unsigned char uuid[CDI_UUID_SIZE] = { 0 };
  int length = CDI_UUID_SIZE;
  cdiInqKeyBytes(gridID, CDI_GLOBAL, CDI_KEY_UUID, uuid, &length);
  if (!cdiUUIDIsNull(uuid))
    {
      char uuidStr[37];
      cdiUUID2Str(uuid, uuidStr);
      if (uuidStr[0] != 0 && strlen(uuidStr) == 36) fprintf(fp, "uuid      = %s\n", uuidStr);
    }

  if (gridInqMask(gridID, nullptr))
    {
      std::vector<int> mask(gridsize);
      gridInqMask(gridID, mask.data());
      static const char prefix[] = "mask      = ";
      printMask(fp, prefix, sizeof(prefix) - 1, (size_t)(gridsize > 0 ? gridsize : 0), mask.data());
    }

  const auto projID = gridInqProj(gridID);
  if (projID != CDI_UNDEFID && gridInqType(projID) == GRID_PROJECTION) grid_print_kernel(projID, opt, fp);
}

void
cdoPrintGrid(int gridID, int opt)
{
  grid_print_kernel(gridID, opt, stdout);
}
