#include <cdi.h>

#include "cdo_options.h"
#include "cdo_cdi_wrapper.h"
#include "cdi_uuid.h"
#include "mpmo_color.h"
#include "datetime.h"
#include "compare.h"
#include "printinfo.h"


#define DATE_FORMAT "%5.4d-%2.2d-%2.2d"
#define TIME_FORMAT "%2.2d:%2.2d:%2.2d"

std::string
datetime_to_string(int64_t date, int time)
{
  int year, month, day;
  cdiDecodeDate(date, &year, &month, &day);
  int hour, minute, second;
  cdiDecodeTime(time, &hour, &minute, &second);

  char cstr[64];
  snprintf(cstr, sizeof(cstr), DATE_FORMAT "T" TIME_FORMAT, year, month, day, hour, minute, second);

  return std::string(cstr);
}

std::string
date_to_string(int64_t date)
{
  int year, month, day;
  cdiDecodeDate(date, &year, &month, &day);

  char cstr[32];
  snprintf(cstr, sizeof(cstr), DATE_FORMAT, year, month, day);

  return std::string(cstr);
}

std::string
time_to_string(int time)
{
  int hour, minute, second;
  cdiDecodeTime(time, &hour, &minute, &second);

  char cstr[32];
  snprintf(cstr, sizeof(cstr), TIME_FORMAT, hour, minute, second);

  return std::string(cstr);
}

const char *
comptype_to_name(int comptype)
{
  switch (comptype)
    {
    case CDI_COMPRESS_SZIP: return "szip";
    case CDI_COMPRESS_ZIP: return "zip";
    case CDI_COMPRESS_JPEG: return "jpeg";
    case CDI_COMPRESS_AEC: return "aec";
    }
  return " ";
}

void
print_filetype(CdoStreamID streamID, int vlistID)
{
  const auto filetype = cdo_inq_filetype(streamID);

  const auto filetypestr = cdi_filetype_to_str(filetype);
  if (filetypestr == nullptr || *filetypestr == 0)
    printf("  unsupported filetype %d", filetype);
  else
    printf("%s", filetypestr);

  // clang-format off
  if (filetype == CDI_FILETYPE_SRV || filetype == CDI_FILETYPE_EXT || filetype == CDI_FILETYPE_IEG)
    {
      switch (cdo_inq_byteorder(streamID))
	{
	case CDI_BIGENDIAN:    printf("  BIGENDIAN");  break;
	case CDI_LITTLEENDIAN: printf("  LITTLEENDIAN");  break;
	default: printf("  byteorder: %d undefined", cdo_inq_byteorder(streamID));  break;
	}
    }
  // clang-format on

  const int nvars = vlistNvars(vlistID);
  const int comps[] = { CDI_COMPRESS_ZIP, CDI_COMPRESS_JPEG, CDI_COMPRESS_SZIP, CDI_COMPRESS_AEC };
  unsigned kk = 0;
  for (unsigned k = 0; k < sizeof(comps) / sizeof(int); ++k)
    for (int varID = 0; varID < nvars; varID++)
      {
        const auto comptype = vlistInqVarCompType(vlistID, varID);
        if (comptype == comps[k])
          {
            printf("%c%s", (kk++ == 0) ? ' ' : '/', comptype_to_name(comptype));
            break;
          }
      }

  printf("\n");
}

static void
print_xvals(int gridID, int dig)
{
  const auto xsize = gridInqXsize(gridID);
  if (xsize && gridInqXvals(gridID, NULL))
    {
      char xname[CDI_MAX_NAME], xunits[CDI_MAX_NAME];
      int length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_NAME, xname, &length);
      length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_UNITS, xunits, &length);

      const auto xfirst = gridInqXval(gridID, 0);
      const auto xlast = gridInqXval(gridID, xsize - 1);
      const auto xinc = gridInqXinc(gridID);
      const auto gridtype = gridInqType(gridID);

      if (gridtype == GRID_GAUSSIAN_REDUCED)
        {
          if (xsize == 2) fprintf(stdout, "%33s : %.*g to %.*g %s\n", xname, dig, xfirst, dig, xlast, xunits);
        }
      else
        {
          fprintf(stdout, "%33s : %.*g", xname, dig, xfirst);
          if (xsize > 1)
            {
              fprintf(stdout, " to %.*g", dig, xlast);
              if (IS_NOT_EQUAL(xinc, 0)) fprintf(stdout, " by %.*g", dig, xinc);
            }
          fprintf(stdout, " %s", xunits);
          if (gridIsCircular(gridID)) fprintf(stdout, "  circular");
          fprintf(stdout, "\n");
        }
    }
}

static void
print_yvals(int gridID, int dig)
{
  const auto ysize = gridInqYsize(gridID);
  if (ysize && gridInqYvals(gridID, NULL))
    {
      char yname[CDI_MAX_NAME], yunits[CDI_MAX_NAME];
      int length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_NAME, yname, &length);
      length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_UNITS, yunits, &length);

      const auto yfirst = gridInqYval(gridID, 0);
      const auto ylast = gridInqYval(gridID, ysize - 1);
      const auto yinc = gridInqYinc(gridID);
      fprintf(stdout, "%33s : %.*g", yname, dig, yfirst);
      if (ysize > 1)
        {
          const auto gridtype = gridInqType(gridID);
          fprintf(stdout, " to %.*g", dig, ylast);
          if (IS_NOT_EQUAL(yinc, 0) && gridtype != GRID_GAUSSIAN && gridtype != GRID_GAUSSIAN_REDUCED)
            fprintf(stdout, " by %.*g", dig, yinc);
        }
      fprintf(stdout, " %s", yunits);
      fprintf(stdout, "\n");
    }
}

static double
calc_curvi_xinc(int gridID)
{
  auto xinc = 0.0;
  if (gridInqType(gridID) != GRID_CURVILINEAR) return xinc;
  const auto *xvals2D = gridInqXvalsPtr(gridID);
  const auto xsize = gridInqXsize(gridID);
  const auto ysize = gridInqYsize(gridID);
  if (xsize > 1)
    {
      Varray<double> xvals(xsize);
      for (size_t i = 0; i < xsize; ++i) xvals[i] = xvals2D[i];
      xinc = fabs(xvals[xsize - 1] - xvals[0]) / (xsize - 1);
      for (size_t i = 1; i < xsize; i++)
        if (fabs(fabs(xvals[i - 1] - xvals[i]) - xinc) > 0.005 * xinc)
          {
            xinc = 0.0;
            break;
          }
      if (IS_NOT_EQUAL(xinc, 0.0))
        {
          for (size_t i = 1; i < ysize; i++)
            if (IS_NOT_EQUAL(xvals2D[i * xsize], xvals2D[0])|| IS_NOT_EQUAL(xvals2D[(i + 1) * xsize - 1], xvals2D[xsize - 1]))
              {
                xinc = 0.0;
                break;
              }
        }
    }

  return xinc;
}

static double
calc_curvi_yinc(int gridID)
{
  auto yinc = 0.0;
  if (gridInqType(gridID) != GRID_CURVILINEAR) return yinc;
  const auto *yvals2D = gridInqYvalsPtr(gridID);
  const auto xsize = gridInqXsize(gridID);
  const auto ysize = gridInqYsize(gridID);
  if (ysize > 1)
    {
      Varray<double> yvals(ysize);
      for (size_t i = 0; i < ysize; ++i) yvals[i] = yvals2D[i * xsize];
      yinc = fabs(yvals[ysize - 1] - yvals[0]) / (ysize - 1);
      for (size_t i = 1; i < ysize; i++)
        if (fabs(fabs(yvals[i - 1] - yvals[i]) - yinc) > 0.005 * yinc)
          {
            yinc = 0.0;
            break;
          }
      if (IS_NOT_EQUAL(yinc, 0.0))
        {
          for (size_t i = 1; i < xsize; i++)
            if (IS_NOT_EQUAL(yvals2D[i], yvals2D[0])
                || IS_NOT_EQUAL(yvals2D[(ysize - 1) * xsize + i], yvals2D[(ysize - 1) * xsize]))
              {
                yinc = 0.0;
                break;
              }
        }
    }

  return yinc;
}

static void
print_xyvals2D(int gridID, int dig)
{
  if (gridInqXvals(gridID, NULL) && gridInqYvals(gridID, NULL))
    {
      char xname[CDI_MAX_NAME], yname[CDI_MAX_NAME], xunits[CDI_MAX_NAME], yunits[CDI_MAX_NAME];
      int length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_NAME, xname, &length);
      length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_NAME, yname, &length);
      length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_XAXIS, CDI_KEY_UNITS, xunits, &length);
      length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_YAXIS, CDI_KEY_UNITS, yunits, &length);

      const auto gridsize = gridInqSize(gridID);

      const auto *xvals2D = gridInqXvalsPtr(gridID);
      const auto *yvals2D = gridInqYvalsPtr(gridID);

      const auto xmm = varray_min_max(gridsize, xvals2D);
      const auto ymm = varray_min_max(gridsize, yvals2D);

      const auto gridtype = gridInqType(gridID);
      const auto xinc = (gridtype == GRID_CURVILINEAR) ? calc_curvi_xinc(gridID) : 0.0;
      const auto yinc = (gridtype == GRID_CURVILINEAR) ? calc_curvi_yinc(gridID) : 0.0;

      fprintf(stdout, "%33s : %.*g", xname, dig, xmm.min);
      if (gridsize > 1) fprintf(stdout, " to %.*g", dig, xmm.max);
      if (IS_NOT_EQUAL(xinc, 0.0)) fprintf(stdout, " by %.*g", dig, xinc);
      fprintf(stdout, " %s", xunits);
      if (gridIsCircular(gridID)) fprintf(stdout, "  circular");
      fprintf(stdout, "\n");
      fprintf(stdout, "%33s : %.*g", yname, dig, ymm.min);
      if (gridsize > 1) fprintf(stdout, " to %.*g", dig, ymm.max);
      if (IS_NOT_EQUAL(yinc, 0.0)) fprintf(stdout, " by %.*g", dig, yinc);
      fprintf(stdout, " %s", yunits);
      fprintf(stdout, "\n");
    }
}

static void
printGridNumPoints(int gridtype, int gridID, size_t gridsize, size_t xsize, size_t ysize)
{
  fprintf(stdout, "points=%zu", gridsize);
  if (gridtype == GRID_GAUSSIAN_REDUCED)
    fprintf(stdout, "  nlat=%zu", ysize);
  else if (xsize && ysize)
    fprintf(stdout, " (%zux%zu)", xsize, ysize);

  const auto numLPE = gridInqNP(gridID);
  if (numLPE > 0)
    {
      if (gridtype == GRID_GAUSSIAN) fprintf(stdout, "  F%d", numLPE);
      if (gridtype == GRID_GAUSSIAN_REDUCED) fprintf(stdout, "  N%d", numLPE);
    }
  reset_text_color(stdout);

  fprintf(stdout, "\n");
}

static void
printGridInfoKernel(int gridID, int index, int lproj)
{
  const auto dig = Options::CDO_flt_digits;
  const auto gridtype = gridInqType(gridID);

  if (lproj && gridtype != GRID_PROJECTION)
    fprintf(stderr, "Internal problem (%s): sub grid not equal GRID_PROJECTION!\n", __func__);

  const auto trunc = gridInqTrunc(gridID);
  const auto gridsize = gridInqSize(gridID);
  const auto xsize = gridInqXsize(gridID);
  const auto ysize = gridInqYsize(gridID);

  if (!lproj)
    {
      fprintf(stdout, "  %4d : ", index + 1);
      set_text_color(stdout, BLUE);
      fprintf(stdout, "%-24s", gridNamePtr(gridtype));
      reset_text_color(stdout);
      fprintf(stdout, " : ");
    }

  if (gridtype == GRID_LONLAT || gridtype == GRID_PROJECTION || gridtype == GRID_GENERIC || gridtype == GRID_CHARXY
      || gridtype == GRID_GAUSSIAN || gridtype == GRID_GAUSSIAN_REDUCED)
    {
      if (!lproj)
        {
          set_text_color(stdout, GREEN);
          printGridNumPoints(gridtype, gridID, gridsize, xsize, ysize);
        }

      char gmapname[CDI_MAX_NAME];
      int length = CDI_MAX_NAME;
      cdiInqKeyString(gridID, CDI_GLOBAL, CDI_KEY_GRIDMAP_NAME, gmapname, &length);
      if (gridtype == GRID_PROJECTION || gmapname[0])
        {
          if (gmapname[0] == 0) strcpy(gmapname, "undefined");
          set_text_color(stdout, BLUE);
          fprintf(stdout, "         %24s", "mapping");
          reset_text_color(stdout);
          fprintf(stdout, " : ");
          set_text_color(stdout, GREEN);
          fprintf(stdout, "%s\n", gmapname);
          reset_text_color(stdout);
        }

      print_xvals(gridID, dig);
      print_yvals(gridID, dig);

      if (gridInqXbounds(gridID, NULL) || gridInqYbounds(gridID, NULL))
        {
          fprintf(stdout, "%33s :", "available");
          if (gridtype == GRID_GAUSSIAN_REDUCED && gridInqXvals(gridID, NULL)) fprintf(stdout, " xvals");
          // clang-format off
          if      (gridInqXbounds(gridID, NULL) && gridInqYbounds(gridID, NULL)) fprintf(stdout, " cellbounds");
          else if (gridInqXbounds(gridID, NULL)) fprintf(stdout, " xbounds");
          else if (gridInqYbounds(gridID, NULL)) fprintf(stdout, " ybounds");
          // clang-format on
          if (gridHasArea(gridID)) fprintf(stdout, " area");
          if (gridInqMask(gridID, NULL)) fprintf(stdout, " mask");
          fprintf(stdout, "\n");
        }
    }
  else if (gridtype == GRID_SPECTRAL)
    {
      set_text_color(stdout, GREEN);
      fprintf(stdout, "points=%zu  nsp=%zu  T%d", gridsize, gridsize / 2, trunc);
      if (gridInqComplexPacking(gridID)) fprintf(stdout, "  complexPacking");
      reset_text_color(stdout);
      fprintf(stdout, "\n");
    }
  else if (gridtype == GRID_FOURIER)
    {
      set_text_color(stdout, GREEN);
      fprintf(stdout, "points=%zu  nfc=%zu  T%d\n", gridsize, gridsize / 2, trunc);
      reset_text_color(stdout);
    }
  else if (gridtype == GRID_GME)
    {
      int nd, ni, ni2, ni3;
      gridInqParamGME(gridID, &nd, &ni, &ni2, &ni3);
      set_text_color(stdout, GREEN);
      fprintf(stdout, "points=%zu  nd=%d  ni=%d\n", gridsize, nd, ni);
      reset_text_color(stdout);
    }
  else if (gridtype == GRID_CURVILINEAR || gridtype == GRID_UNSTRUCTURED)
    {
      set_text_color(stdout, GREEN);
      if (gridtype == GRID_CURVILINEAR)
        fprintf(stdout, "points=%zu (%zux%zu)", gridsize, xsize, ysize);
      else
        fprintf(stdout, "points=%zu", gridsize);

      if (gridtype == GRID_UNSTRUCTURED && gridInqNvertex(gridID) > 0) fprintf(stdout, "  nvertex=%d", gridInqNvertex(gridID));
      reset_text_color(stdout);

      fprintf(stdout, "\n");

      if (gridtype == GRID_UNSTRUCTURED)
        {
          int number = 0;
          cdiInqKeyInt(gridID, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDUSED, &number);
          int position = 0;
          cdiInqKeyInt(gridID, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDINREFERENCE, &position);
          if (number > 0) fprintf(stdout, "%33s : number=%d  position=%d\n", "grid", number, 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(stdout, "%33s : %s\n", "uri", reference_link);
            }
        }

      print_xyvals2D(gridID, dig);
    }
  else /* if ( gridtype == GRID_GENERIC ) */
    {
      set_text_color(stdout, GREEN);
      if (ysize == 0)
        fprintf(stdout, "points=%zu\n", gridsize);
      else
        fprintf(stdout, "points=%zu (%zux%zu)\n", gridsize, xsize, ysize);
      reset_text_color(stdout);
    }

  if (gridtype == GRID_CURVILINEAR || gridtype == GRID_UNSTRUCTURED)
    {
      if (gridHasArea(gridID) || gridInqXbounds(gridID, NULL) || gridInqYbounds(gridID, NULL))
        {
          fprintf(stdout, "%33s :", "available");
          if (gridInqXbounds(gridID, NULL) && gridInqYbounds(gridID, NULL)) fprintf(stdout, " cellbounds");
          if (gridHasArea(gridID)) fprintf(stdout, " area");
          if (gridInqMask(gridID, NULL)) fprintf(stdout, " mask");
          fprintf(stdout, "\n");
        }
    }

  unsigned char uuid[CDI_UUID_SIZE] = { 0 };
  int length = CDI_UUID_SIZE;
  const auto status = cdiInqKeyBytes(gridID, CDI_GLOBAL, CDI_KEY_UUID, uuid, &length);
  if (status == CDI_NOERR && !cdiUUIDIsNull(uuid))
    {
      char uuidStr[uuidNumHexChars + 1] = { 0 };
      if (cdiUUID2Str(uuid, uuidStr) == uuidNumHexChars) fprintf(stdout, "%33s : %s\n", "uuid", uuidStr);
    }

  if (Options::cdoVerbose) fprintf(stdout, "%33s : %d\n", "gridID", gridID);
}

void
print_grid_info(int vlistID)
{
  const auto ngrids = vlistNgrids(vlistID);
  for (int index = 0; index < ngrids; index++)
    {
      const auto gridID = vlistGrid(vlistID, index);
      printGridInfoKernel(gridID, index, false);
      const auto projID = gridInqProj(gridID);
      if (projID != CDI_UNDEFID) printGridInfoKernel(projID, index, true);
    }
}

static void
printZaxisBoundsInfo(const int zaxisID, const int dig, const int levelsize, const double zinc, const char *zunits)
{
  auto level1 = zaxisInqLbound(zaxisID, 0);
  auto level2 = zaxisInqUbound(zaxisID, 0);
  if (!(levelsize == 1 && IS_EQUAL(level1, level2) && fabs(level1) <= 0))
    {
      fprintf(stdout, "%33s : ", "bounds");
      fprintf(stdout, "%.*g-%.*g", dig, level1, dig, level2);
      if (levelsize > 1)
        {
          level1 = zaxisInqLbound(zaxisID, levelsize - 1);
          level2 = zaxisInqUbound(zaxisID, levelsize - 1);
          fprintf(stdout, " to %.*g-%.*g", dig, level1, dig, level2);
          if (IS_NOT_EQUAL(zinc, 0)) fprintf(stdout, " by %.*g", dig, zinc);
        }
      fprintf(stdout, " %s", zunits);
      fprintf(stdout, "\n");
    }
}

static bool
zaxisTypeIsSingleLayer(int zaxistype)
{
  switch (zaxistype)
    {
    case ZAXIS_MEANSEA:
    case ZAXIS_TROPOPAUSE:
    case ZAXIS_TOA:
    case ZAXIS_SEA_BOTTOM:
    case ZAXIS_ATMOSPHERE:
    case ZAXIS_CLOUD_BASE:
    case ZAXIS_CLOUD_TOP:
    case ZAXIS_ISOTHERM_ZERO:
    case ZAXIS_LAKE_BOTTOM:
    case ZAXIS_SEDIMENT_BOTTOM:
    case ZAXIS_SEDIMENT_BOTTOM_TA:
    case ZAXIS_SEDIMENT_BOTTOM_TW:
    case ZAXIS_SURFACE: return true;
    }

  return false;
}

static void
printZaxisLevelInfo(const int levelsize, const int zaxisID, const int zaxistype, double &zinc, const int dig, const char *zname,
                    const char *zunits)
{
  Varray<double> levels(levelsize);
  zaxisInqLevels(zaxisID, levels.data());

  if (!(zaxisTypeIsSingleLayer(zaxistype) && levelsize == 1 && fabs(levels[0]) <= 0))
    {
      const auto zfirst = levels[0];
      const auto zlast = levels[levelsize - 1];
      if (levelsize > 2)
        {
          zinc = (levels[levelsize - 1] - levels[0]) / (levelsize - 1);
          for (int levelID = 2; levelID < levelsize; ++levelID)
            if (fabs(fabs(levels[levelID] - levels[levelID - 1]) - zinc) > 0.001 * zinc)
              {
                zinc = 0;
                break;
              }
        }

      fprintf(stdout, "%33s : %.*g", zname, dig, zfirst);
      if (levelsize > 1)
        {
          fprintf(stdout, " to %.*g", dig, zlast);
          if (IS_NOT_EQUAL(zinc, 0)) fprintf(stdout, " by %.*g", dig, zinc);
        }
      fprintf(stdout, " %s", zunits);
      fprintf(stdout, "\n");
    }
}

static void
printZaxisHybridInfo(const int zaxisID)
{
  char psname[CDI_MAX_NAME];
  int length = CDI_MAX_NAME;
  cdiInqKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_PSNAME, psname, &length);
  const int vctsize = zaxisInqVctSize(zaxisID);
  if (vctsize || psname[0])
    {
      fprintf(stdout, "%33s :", "available");
      if (vctsize) fprintf(stdout, " vct");
      if (psname[0]) fprintf(stdout, "  ps: %s", psname);
      fprintf(stdout, "\n");
    }
}

static void
printZaxisGenericInfo(const int ltype, const int zaxistype, const char *zaxisname)
{
  if (zaxistype == ZAXIS_GENERIC && ltype != 0)
    {
      fprintf(stdout, "%-12s (ltype=%3d)", zaxisname, ltype);
    }
  else
    {
      fprintf(stdout, "%-24s", zaxisname);
    }
}

static void
printZaxisReferenceInfo(const int zaxisID)
{
  int number = 0;
  // cdiInqKeyInt(zaxisID, CDI_GLOBAL, CDI_KEY_NUMBEROFVGRIDUSED, &number)
  // if (number > 0)
    {
      fprintf(stdout, "%33s : ", "zaxis");
      fprintf(stdout, "number=%d\n", number);
    }

  unsigned char uuid[CDI_UUID_SIZE] = { 0 };
  int length = CDI_UUID_SIZE;
  const auto status = cdiInqKeyBytes(zaxisID, CDI_GLOBAL, CDI_KEY_UUID, uuid, &length);
  if (status == CDI_NOERR && !cdiUUIDIsNull(uuid))
    {
      char uuidStr[uuidNumHexChars + 1] = { 0 };
      if (cdiUUID2Str(uuid, uuidStr) == uuidNumHexChars) fprintf(stdout, "%33s : %s\n", "uuid", uuidStr);
    }
}

void
print_zaxis_info(int vlistID)
{
  const auto dig = Options::CDO_flt_digits;
  char zaxisname[CDI_MAX_NAME], zname[CDI_MAX_NAME], zunits[CDI_MAX_NAME];

  const auto nzaxis = vlistNzaxis(vlistID);
  for (int index = 0; index < nzaxis; index++)
    {
      const auto zaxisID = vlistZaxis(vlistID, index);
      const auto zaxistype = zaxisInqType(zaxisID);
      const auto levelsize = zaxisInqSize(zaxisID);
      int ltype = 0, ltype2 = -1;
      cdiInqKeyInt(zaxisID, CDI_GLOBAL, CDI_KEY_TYPEOFFIRSTFIXEDSURFACE, &ltype);
      cdiInqKeyInt(zaxisID, CDI_GLOBAL, CDI_KEY_TYPEOFSECONDFIXEDSURFACE, &ltype2);

      zaxisName(zaxistype, zaxisname);
      int length = CDI_MAX_NAME;
      cdiInqKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_NAME, zname, &length);
      length = CDI_MAX_NAME;
      cdiInqKeyString(zaxisID, CDI_GLOBAL, CDI_KEY_UNITS, zunits, &length);
      zunits[12] = 0;

      fprintf(stdout, "  %4d : ", vlistZaxisIndex(vlistID, zaxisID) + 1);
      set_text_color(stdout, BLUE);
      printZaxisGenericInfo(ltype, zaxistype, zaxisname);

      reset_text_color(stdout);

      fprintf(stdout, " :");

      set_text_color(stdout, GREEN);
      fprintf(stdout, " levels=%d", levelsize);
      const int zscalar = (levelsize == 1) ? zaxisInqScalar(zaxisID) : false;
      if (zscalar) fprintf(stdout, "  scalar");
      reset_text_color(stdout);
      fprintf(stdout, "\n");

      double zinc = 0;
      if (zaxisInqLevels(zaxisID, NULL)) printZaxisLevelInfo(levelsize, zaxisID, zaxistype, zinc, dig, zname, zunits);

      if (zaxisInqLbounds(zaxisID, NULL) && zaxisInqUbounds(zaxisID, NULL))
        printZaxisBoundsInfo(zaxisID, dig, levelsize, zinc, zunits);

      if (zaxistype == ZAXIS_HYBRID) printZaxisHybridInfo(zaxisID);

      if (zaxistype == ZAXIS_REFERENCE) printZaxisReferenceInfo(zaxisID);

      if (ltype != ltype2 && ltype2 != -1) fprintf(stdout, "%33s : %d\n", "typeOfSecondFixedSurface", ltype2);

      if (Options::cdoVerbose) fprintf(stdout, "%33s : %d\n", "zaxisID", zaxisID);
    }
}

void
print_subtype_info(int vlistID)
{
  const auto nsubtypes = vlistNsubtypes(vlistID);
  for (int index = 0; index < nsubtypes; index++)
    {
      const auto subtypeID = vlistSubtype(vlistID, index);
      const auto subtypesize = subtypeInqSize(subtypeID);
      // subtypePrint(subtypeID);
      fprintf(stdout, "  %4d : %-24s :", vlistSubtypeIndex(vlistID, subtypeID) + 1, "tiles");
      fprintf(stdout, " ntiles=%d", subtypesize);
      fprintf(stdout, "\n");
    }
}

static int
printDateTime(int ntimeout, const int64_t vdate, const int vtime)
{
  if (ntimeout == 4)
    {
      ntimeout = 0;
      fprintf(stdout, "\n");
    }

  fprintf(stdout, " %s %s", date_to_string(vdate).c_str(), time_to_string(vtime).c_str());

  return ++ntimeout;
}

#define NUM_TIMESTEP 60
#define MAX_DOTS 80

static int
printDot(int ndotout, int *nfact, int *ncout)
{
  // printf("ncout %d %d %d\n",*ncout, (*ncout)%(*nfact), *nfact);
  if ((*ncout) % (*nfact) == 0)
    {
      if (ndotout == MAX_DOTS)
        {
          *ncout = 0;
          ndotout = 0;
          fprintf(stdout, "\n   ");
          (*nfact) *= 10;
        }

      fprintf(stdout, ".");
      fflush(stdout);
      ndotout++;
    }

  (*ncout)++;

  return ndotout;
}

void
print_timesteps(CdoStreamID streamID, int taxisID, int verbose)
{
  struct datetime
  {
    int64_t vdate;
    int vtime;
    struct datetime *next;
  };
  struct datetime vdatetime[NUM_TIMESTEP];
  struct datetime *next_vdatetime = vdatetime;

  for (int i = 0; i < NUM_TIMESTEP - 1; ++i) vdatetime[i].next = &vdatetime[i + 1];
  vdatetime[NUM_TIMESTEP - 1].next = &vdatetime[0];

  int ntimeout = 0;
  int ndotout = 0;
  int nvdatetime = 0;
  int ncout = 0;
  int nfact = 1;
  int tsID = 0;

  DateTimeList dtlist;
  while (true)
    {
      const auto nrecs = cdo_stream_inq_timestep(streamID, tsID);
      if (nrecs == 0) break;

      dtlist.taxis_inq_timestep(taxisID, 0);
      const auto vdate = dtlist.get_vdate(0);
      const auto vtime = dtlist.get_vtime(0);

      if (verbose || tsID < NUM_TIMESTEP)
        {
          ntimeout = printDateTime(ntimeout, vdate, vtime);
        }
      else
        {
          if (tsID == 2 * NUM_TIMESTEP) fprintf(stdout, "\n   ");
          if (tsID >= 2 * NUM_TIMESTEP) ndotout = printDot(ndotout, &nfact, &ncout);

          if (nvdatetime < NUM_TIMESTEP)
            {
              vdatetime[nvdatetime].vdate = vdate;
              vdatetime[nvdatetime].vtime = vtime;
              nvdatetime++;
            }
          else
            {
              next_vdatetime->vdate = vdate;
              next_vdatetime->vtime = vtime;
              next_vdatetime = next_vdatetime->next;
            }
        }

      tsID++;
    }

  if (nvdatetime)
    {
      fprintf(stdout, "\n");

      ntimeout = 0;
      int toff = 0;
      if (tsID > 2 * NUM_TIMESTEP)
        {
          toff = tsID % 4;
          if (toff > 0) toff = 4 - toff;
          for (int i = 0; i < toff; ++i) next_vdatetime = next_vdatetime->next;
        }
      for (int i = toff; i < nvdatetime; ++i)
        {
          const auto vdate = next_vdatetime->vdate;
          const auto vtime = next_vdatetime->vtime;
          ntimeout = printDateTime(ntimeout, vdate, vtime);
          next_vdatetime = next_vdatetime->next;
        }
    }
}
