#ifdef HAVE_CONFIG_H
#include "../src/config.h"
#endif

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <math.h>

#include <cdi.h>

int vlistInqVarMissvalUsed(int vlistID, int varID);
#ifndef DBL_IS_NAN
#if defined(HAVE_DECL_ISNAN)
#define DBL_IS_NAN(x) (isnan(x))
#elif defined(FP_NAN)
#define DBL_IS_NAN(x) (fpclassify(x) == FP_NAN)
#else
#define DBL_IS_NAN(x) ((x) != (x))
#endif
#endif

#ifndef DBL_IS_EQUAL
/*#define DBL_IS_EQUAL(x,y) (!(x < y || y < x)) */
#define DBL_IS_EQUAL(x, y) (DBL_IS_NAN(x) || DBL_IS_NAN(y) ? (DBL_IS_NAN(x) && DBL_IS_NAN(y) ? 1 : 0) : !(x < y || y < x))
#endif

#ifndef IS_NOT_EQUAL
#define IS_NOT_EQUAL(x, y) (x < y || y < x)
#endif

#ifndef HOST_ENDIANNESS
#ifdef __cplusplus
static const uint32_t HOST_ENDIANNESS_temp[1] = { UINT32_C(0x00030201) };
#define HOST_ENDIANNESS (((const unsigned char *) HOST_ENDIANNESS_temp)[0])
#else
#define HOST_ENDIANNESS (((const unsigned char *) &(const uint32_t[1]){ UINT32_C(0x00030201) })[0])
#endif
#endif

#include "printinfo.h"

#ifdef __cplusplus
extern "C"
{
#endif
  void cdiDefTableID(int tableID);
#ifdef __cplusplus
}
#endif

int getopt(int argc, char *const argv[], const char *optstring);

extern char *optarg;
extern int optind, opterr, optopt;

static char *Progname;

static int DefaultFileType = CDI_UNDEFID;
static int DefaultDataType = CDI_UNDEFID;
static int DefaultByteorder = CDI_UNDEFID;

static int comptype = CDI_COMPRESS_NONE;  // Compression type
static int complevel = 0;                 // Compression level

enum datamode
{
  SP_MODE,
  DP_MODE
};
static int datamode = DP_MODE;

static void
version(void)
{
  int filetypes[] = { CDI_FILETYPE_SRV, CDI_FILETYPE_EXT, CDI_FILETYPE_IEG, CDI_FILETYPE_GRB,  CDI_FILETYPE_GRB2,
                      CDI_FILETYPE_NC,  CDI_FILETYPE_NC2, CDI_FILETYPE_NC4, CDI_FILETYPE_NC4C, CDI_FILETYPE_NC5 };
  const char *typenames[] = { "srv", "ext", "ieg", "grb", "grb2", "nc", "nc2", "nc4", "nc4c", "nc5" };

  fprintf(stderr, "CDI version 1.9\n");
#ifdef COMPILER
  fprintf(stderr, "C Compiler: %s\n", COMPILER);
#endif
#ifdef COMP_VERSION
  fprintf(stderr, "C version: %s\n", COMP_VERSION);
#endif
#ifdef SYSTEM_TYPE
  fprintf(stderr, "System: %s\n", SYSTEM_TYPE);
#endif

  fprintf(stderr, "Features:");
#ifdef HAVE_CF_INTERFACE
  fprintf(stderr, " Fortran");
#endif
#ifdef HAVE_LIBPTHREAD
  fprintf(stderr, " PTHREADS");
#endif
#ifdef _OPENMP
  fprintf(stderr, " OpenMP");
#endif
#ifdef HAVE_NETCDF4
  fprintf(stderr, " NC4");
#ifdef HAVE_NC4HDF5
  fprintf(stderr, "/HDF5");
#ifdef HAVE_NC4HDF5_THREADSAFE
  fprintf(stderr, "/threadsafe");
#endif
#endif
#endif
#ifdef HAVE_LIBNC_DAP
  fprintf(stderr, " OPeNDAP");
#endif
#ifdef HAVE_LIBSZ
  fprintf(stderr, " SZ");
#endif
#ifdef HAVE_LIBJASPER
  fprintf(stderr, " JASPER");
#endif
#ifdef HAVE_LIBDRMAA
  fprintf(stderr, " DRMAA");
#endif
#ifdef HAVE_LIBCURL
  fprintf(stderr, " CURL");
#endif
  fprintf(stderr, "\n");

  fprintf(stderr, "Filetypes: ");
  for (size_t i = 0; i < sizeof(filetypes) / sizeof(int); ++i)
    if (cdiHaveFiletype(filetypes[i])) fprintf(stderr, "%s ", typenames[i]);
  fprintf(stderr, "\n");

  cdiPrintVersion();
  fprintf(stderr, "\n");
  /*
    1.0.0   6 Feb 2001 : initial version
    1.1.0  30 Jul 2003 : missing values implemented
    1.2.0   8 Aug 2003 : changes for CDI library version 0.7.0
    1.3.0  10 Feb 2004 : changes for CDI library version 0.7.9
    1.4.0   5 May 2004 : changes for CDI library version 0.8.1 (error handling)
    1.4.1  18 Sep 2004 : netCDF 2 support
    1.4.2  22 Mar 2005 : change level from int to double
    1.4.3  11 Apr 2005 : change date and time format to ISO
    1.5.0  22 Nov 2005 : IEG support
    1.5.1  21 Feb 2006 : added option -s for short info
    1.6.0   1 Aug 2006 : added option -z szip for SZIP compression of GRIB records
    1.6.1  27 Feb 2007 : short info with ltype for GENERIC zaxis
    1.6.2   3 Jan 2008 : changes for CDI library version 1.1.0 (compress)
    1.6.3  26 Mar 2008 : call streamDefTimestep also if ntsteps = 0 (buf fix)
    1.7.0  11 Apr 2008 : added option -z zip for deflate compression of netCDF4 variables
    1.7.1   1 Nov 2009 : added option -z jpeg for JPEG compression of GRIB2 records
    1.7.2  14 Nov 2012 : added optional compression level -z zip[_1-9]
    1.9.0  29 May 2019 : added option -i to set number of input worker
  */
}

static void
usage(void)
{
  const char *name;

  fprintf(stderr, "usage : %s  [Option]  [ifile]  [ofile]\n", Progname);

  fprintf(stderr, "\n");
  fprintf(stderr, "  Options:\n");
  fprintf(stderr, "    -d             Print debugging information\n");
  fprintf(stderr, "    -f <format>    Format of the output file. (grb, grb2, nc, nc2, nc4, nc4c, nc5, srv, ext or ieg)\n");
  fprintf(stderr, "    -i <num>       Number of worker to decode/decompress GRIB records\n");
  fprintf(stderr, "    -m             Move records\n");
  fprintf(stderr, "    -r             Use CDI record API\n");
  fprintf(stderr, "    -s             give short information if ofile is missing\n");
  fprintf(stderr, "    -t <table>     Parameter table name/file\n");
  fprintf(stderr, "                   Predefined tables: ");
  for (int id = 0; id < tableInqNumber(); id++)
    if ((name = tableInqNamePtr(id))) fprintf(stderr, " %s", name);
  fprintf(stderr, "\n");

  fprintf(stderr, "    -V             Print version number\n");
  fprintf(stderr, "    -z szip        SZIP compression of GRIB1 records\n");
  fprintf(stderr, "       jpeg        JPEG compression of GRIB2 records\n");
  fprintf(stderr, "        zip[_1-9]  Deflate compression of netCDF4 variables\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "  Report bugs to <https://code.mpimet.mpg.de/projects/cdi>\n");
}

static void
printInfo(int64_t vdate, int vtime, char *varname, double level, size_t datasize, int number, size_t nmiss, double missval,
          const double *data, int vardis)
{
  static int rec = 0;
  size_t ivals = 0, imiss = 0;
  double arrmean, arrmin, arrmax;
  char vdatestr[32], vtimestr[32];

  if (!rec)
    {
      if (vardis)
        fprintf(stdout,
                "   Rec :       Date     Time   Level Gridsize    Miss :     Minimum        Mean     Maximum : Parameter name\n");
      //         ----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0----+
      else
        fprintf(stdout,
                "   Rec :       Date     Time   Level Gridsize    Miss :     Minimum        Mean     Maximum : Parameter ID\n");
      //         ----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0----+
    }

  date2str(vdate, vdatestr, sizeof(vdatestr));
  time2str(vtime, vtimestr, sizeof(vtimestr));

  fprintf(stdout, "%6d :%s %s %7g ", ++rec, vdatestr, vtimestr, level);

  fprintf(stdout, "%8zu ", datasize);

  fprintf(stdout, "%7zu :", nmiss);

  if (number == CDI_REAL)
    {
      if (nmiss > 0)
        {
          arrmean = 0;
          arrmin = 1.e300;
          arrmax = -1.e300;
          for (size_t i = 0; i < datasize; i++)
            {
              if (!DBL_IS_EQUAL(data[i], missval))
                {
                  if (data[i] < arrmin) arrmin = data[i];
                  if (data[i] > arrmax) arrmax = data[i];
                  arrmean += data[i];
                  ivals++;
                }
            }
          imiss = datasize - ivals;
          datasize = ivals;
        }
      else
        {
          arrmean = data[0];
          arrmin = data[0];
          arrmax = data[0];
          for (size_t i = 1; i < datasize; i++)
            {
              if (data[i] < arrmin) arrmin = data[i];
              if (data[i] > arrmax) arrmax = data[i];
              arrmean += data[i];
            }
        }

      if (datasize > 0) arrmean /= datasize;

      fprintf(stdout, "%#12.5g%#12.5g%#12.5g", arrmin, arrmean, arrmax);
    }
  else
    {
      size_t nvals_r = 0, nvals_i = 0;
      double arrsum_r = 0, arrsum_i = 0, arrmean_r = 0, arrmean_i = 0;

      for (size_t i = 0; i < datasize; i++)
        {
          if (!DBL_IS_EQUAL(data[i * 2], missval))
            {
              arrsum_r += data[i * 2];
              nvals_r++;
            }
          if (!DBL_IS_EQUAL(data[i * 2 + 1], missval))
            {
              arrsum_i += data[i * 2 + 1];
              nvals_i++;
            }
        }

      imiss = datasize - nvals_r;

      if (nvals_r > 0) arrmean_r = arrsum_r / nvals_r;
      if (nvals_i > 0) arrmean_i = arrsum_i / nvals_i;
      fprintf(stdout, "  -  (%#12.5g,%#12.5g)  -", arrmean_r, arrmean_i);
    }

  fprintf(stdout, " : %-14s\n", varname);

  if (imiss != nmiss && nmiss > 0) fprintf(stdout, "Found %zu of %zu missing values!\n", imiss, nmiss);
}

static const char *
tunit2str(int tunits)
{
  // clang-format off
  if      ( tunits == TUNIT_YEAR )       return ("years");
  else if ( tunits == TUNIT_MONTH )      return ("months");
  else if ( tunits == TUNIT_DAY )        return ("days");
  else if ( tunits == TUNIT_12HOURS )    return ("12hours");
  else if ( tunits == TUNIT_6HOURS )     return ("6hours");
  else if ( tunits == TUNIT_3HOURS )     return ("3hours");
  else if ( tunits == TUNIT_HOUR )       return ("hours");
  else if ( tunits == TUNIT_30MINUTES )  return ("30minutes");
  else if ( tunits == TUNIT_QUARTER )    return ("15minutes");
  else if ( tunits == TUNIT_MINUTE )     return ("minutes");
  else if ( tunits == TUNIT_SECOND )     return ("seconds");
  else                                   return ("unknown");
  // clang-format on
}

static const char *
calendar2str(int calendar)
{
  // clang-format off
  if      ( calendar == CALENDAR_STANDARD )  return ("standard");
  else if ( calendar == CALENDAR_PROLEPTIC ) return ("proleptic_gregorian");
  else if ( calendar == CALENDAR_360DAYS )   return ("360_day");
  else if ( calendar == CALENDAR_365DAYS )   return ("365_day");
  else if ( calendar == CALENDAR_366DAYS )   return ("366_day");
  else                                       return ("unknown");
  // clang-format on
}

static void
limit_string_length(char *string, size_t maxlen)
{
  string[maxlen - 1] = 0;
  const size_t len = strlen(string);

  if (len > 10)
    {
      for (size_t i = 3; i < len; ++i)
        if (string[i] == ' ' || string[i] == ',' || (i > 10 && string[i] == '.'))
          {
            string[i] = 0;
            break;
          }
    }
}

static void
printShortinfo(int streamID, int vlistID, int vardis)
{
  int varID;
  char tmpname[CDI_MAX_NAME];
  char varname[CDI_MAX_NAME];
  int year, month, day, hour, minute, second;
  char pstr[4];
  char paramstr[32];

  fprintf(stdout, "   File format");
  fprintf(stdout, " : ");
  printFiletype(streamID, vlistID);

  // vlistPrint(vlistID);
  int nvars = vlistNvars(vlistID);
  int nsubtypes = vlistNsubtypes(vlistID);

  if (nsubtypes > 0)
    fprintf(stdout, "   Var : Institut Source   T Steptype Subtypes Levels Num    Points Num Dtype : ");
  else
    fprintf(stdout, "   Var : Institut Source   T Steptype Levels Num    Points Num Dtype : ");

  if (vardis)
    fprintf(stdout, "Parameter name\n");
  else
    fprintf(stdout, "Parameter ID\n");

  for (varID = 0; varID < nvars; varID++)
    {
      int param = vlistInqVarParam(vlistID, varID);
      int gridID = vlistInqVarGrid(vlistID, varID);
      int zaxisID = vlistInqVarZaxis(vlistID, varID);

      fprintf(stdout, "%6d : ", varID + 1);

      // institute info
      const char *instptr = institutInqNamePtr(vlistInqVarInstitut(vlistID, varID));
      strcpy(tmpname, "unknown");
      if (instptr) strncpy(tmpname, instptr, CDI_MAX_NAME - 1);
      limit_string_length(tmpname, CDI_MAX_NAME);
      fprintf(stdout, "%-8s ", tmpname);

      // source info
      const char *modelptr = modelInqNamePtr(vlistInqVarModel(vlistID, varID));
      strcpy(tmpname, "unknown");
      if (modelptr) strncpy(tmpname, modelptr, CDI_MAX_NAME - 1);
      limit_string_length(tmpname, CDI_MAX_NAME);
      fprintf(stdout, "%-8s ", tmpname);

      // timetype
      int timetype = vlistInqVarTimetype(vlistID, varID);
      fprintf(stdout, "%c ", timetype == TIME_CONSTANT ? 'c' : 'v');

      // tsteptype
      int tsteptype = vlistInqVarTsteptype(vlistID, varID);
      // clang-format off
	  if      (tsteptype == TSTEP_INSTANT ) fprintf(stdout, "%-8s ", "instant");
	  else if (tsteptype == TSTEP_INSTANT2) fprintf(stdout, "%-8s ", "instant");
	  else if (tsteptype == TSTEP_INSTANT3) fprintf(stdout, "%-8s ", "instant");
	  else if (tsteptype == TSTEP_MIN     ) fprintf(stdout, "%-8s ", "min");
	  else if (tsteptype == TSTEP_MAX     ) fprintf(stdout, "%-8s ", "max");
	  else if (tsteptype == TSTEP_AVG     ) fprintf(stdout, "%-8s ", "avg");
	  else if (tsteptype == TSTEP_ACCUM   ) fprintf(stdout, "%-8s ", "accum");
	  else if (tsteptype == TSTEP_RANGE   ) fprintf(stdout, "%-8s ", "range");
	  else if (tsteptype == TSTEP_DIFF    ) fprintf(stdout, "%-8s ", "diff");
	  else if (tsteptype == TSTEP_SUM     ) fprintf(stdout, "%-8s ", "sum");
	  else                                  fprintf(stdout, "%-8s ", "unknown");
      // clang-format on

      if (nsubtypes > 0)
        {
          int subtypeID = vlistInqVarSubtype(vlistID, varID);
          int subtypesize = subtypeInqSize(subtypeID);
          fprintf(stdout, " %6d  ", subtypesize);
          fprintf(stdout, "%3d ", vlistSubtypeIndex(vlistID, subtypeID) + 1);
        }

      /* layer info */
      int levelsize = zaxisInqSize(zaxisID);
      fprintf(stdout, "%6d ", levelsize);
      fprintf(stdout, "%3d ", vlistZaxisIndex(vlistID, zaxisID) + 1);

      /* grid info */
      size_t gridsize = gridInqSize(gridID);
      fprintf(stdout, "%9zu ", gridsize);
      fprintf(stdout, "%3d ", vlistGridIndex(vlistID, gridID) + 1);

      /* datatype */
      int datatype = vlistInqVarDatatype(vlistID, varID);
      // clang-format off
	  if      (datatype == CDI_DATATYPE_PACK  ) strcpy(pstr, "P0");
	  else if (datatype > 0 && datatype <= 32 ) sprintf(pstr, "P%d", datatype);
	  else if (datatype == CDI_DATATYPE_CPX32 ) strcpy(pstr, "C32");
	  else if (datatype == CDI_DATATYPE_CPX64 ) strcpy(pstr, "C64");
	  else if (datatype == CDI_DATATYPE_FLT32 ) strcpy(pstr, "F32");
	  else if (datatype == CDI_DATATYPE_FLT64 ) strcpy(pstr, "F64");
	  else if (datatype == CDI_DATATYPE_INT8  ) strcpy(pstr, "I8");
	  else if (datatype == CDI_DATATYPE_INT16 ) strcpy(pstr, "I16");
	  else if (datatype == CDI_DATATYPE_INT32 ) strcpy(pstr, "I32");
	  else if (datatype == CDI_DATATYPE_UINT8 ) strcpy(pstr, "U8");
	  else if (datatype == CDI_DATATYPE_UINT16) strcpy(pstr, "U16");
	  else if (datatype == CDI_DATATYPE_UINT32) strcpy(pstr, "U32");
	  else                                      strcpy(pstr, "-1");
      // clang-format on

      fprintf(stdout, " %-3s", pstr);

      if (vlistInqVarCompType(vlistID, varID) == CDI_COMPRESS_NONE)
        fprintf(stdout, "  ");
      else
        fprintf(stdout, "z ");

      /* parameter info */
      fprintf(stdout, ": ");

      cdiParamToString(param, paramstr, sizeof(paramstr));

      if (vardis)
        {
          vlistInqVarName(vlistID, varID, varname);
          fprintf(stdout, "%-14s", varname);
        }
      else
        fprintf(stdout, "%-14s", paramstr);

      fprintf(stdout, "\n");
    }

  fprintf(stdout, "   Grid coordinates");
  fprintf(stdout, " :\n");

  printGridInfo(vlistID);

  fprintf(stdout, "   Vertical coordinates");
  fprintf(stdout, " :\n");

  printZaxisInfo(vlistID);

  if (nsubtypes > 0)
    {
      fprintf(stdout, "   Subtypes");
      fprintf(stdout, " :\n");

      printSubtypeInfo(vlistID);
    }

  int taxisID = vlistInqTaxis(vlistID);
  int ntsteps = vlistNtsteps(vlistID);

  if (ntsteps != 0)
    {
      if (ntsteps == CDI_UNDEFID)
        fprintf(stdout, "   Time coordinate :  unlimited steps\n");
      else
        fprintf(stdout, "   Time coordinate :  %d step%s\n", ntsteps, ntsteps == 1 ? "" : "s");

      if (taxisID != CDI_UNDEFID)
        {
          if (taxisInqType(taxisID) == TAXIS_RELATIVE)
            {
              int64_t vdate = taxisInqRdate(taxisID);
              int vtime = taxisInqRtime(taxisID);

              cdiDecodeDate(vdate, &year, &month, &day);
              cdiDecodeTime(vtime, &hour, &minute, &second);

              fprintf(stdout, "     RefTime = %4.4d-%2.2d-%2.2d %2.2d:%2.2d:%2.2d", year, month, day, hour, minute, second);

              int tunits = taxisInqTunit(taxisID);
              if (tunits != CDI_UNDEFID) fprintf(stdout, "  Units = %s", tunit2str(tunits));

              int calendar = taxisInqCalendar(taxisID);
              if (calendar != CDI_UNDEFID) fprintf(stdout, "  Calendar = %s", calendar2str(calendar));

              if (taxisHasBounds(taxisID)) fprintf(stdout, "  Bounds = true");

              fprintf(stdout, "\n");
            }
        }

      fprintf(stdout, "  YYYY-MM-DD hh:mm:ss  YYYY-MM-DD hh:mm:ss  YYYY-MM-DD hh:mm:ss  YYYY-MM-DD hh:mm:ss\n");

      printTimesteps(streamID, taxisID, 0);

      fprintf(stdout, "\n");
    }
}

static void
setDefaultDataType(char *datatypestr)
{
  int nbits = -1;
  enum
  {
    D_UINT,
    D_INT,
    D_FLT,
    D_CPX
  };
  int dtype = -1;

  const int datatype = tolower(*datatypestr);
  // clang-format off
  if      (datatype == 'i') { dtype = D_INT;  datatypestr++; }
  else if (datatype == 'u') { dtype = D_UINT; datatypestr++; }
  else if (datatype == 'f') { dtype = D_FLT;  datatypestr++; }
  else if (datatype == 'c') { dtype = D_CPX;  datatypestr++; }
  else if (datatype == 'p') {                 datatypestr++; }
  // clang-format on

  if (isdigit((int) *datatypestr))
    {
      nbits = atoi(datatypestr);
      datatypestr += 1;
      if (nbits >= 10) datatypestr += 1;

      if (dtype == -1)
        {
          if (nbits > 0 && nbits < 32)
            DefaultDataType = nbits;
          else if (nbits == 32)
            {
              if (DefaultFileType == CDI_FILETYPE_GRB)
                DefaultDataType = CDI_DATATYPE_PACK32;
              else
                DefaultDataType = CDI_DATATYPE_FLT32;
            }
          else if (nbits == 64)
            DefaultDataType = CDI_DATATYPE_FLT64;
          else
            {
              fprintf(stderr, "Unsupported number of bits %d!\n", nbits);
              fprintf(stderr,
                      "Use I8/I16/I32/F32/F64 for nc/nc2/nc4/nc4c/nc5; F32/F64 for grb2/srv/ext/ieg; P1 - P24 for grb/grb2.\n");
              exit(EXIT_FAILURE);
            }
        }
      else
        {
          if (dtype == D_INT)
            {
              if (nbits == 8)
                DefaultDataType = CDI_DATATYPE_INT8;
              else if (nbits == 16)
                DefaultDataType = CDI_DATATYPE_INT16;
              else if (nbits == 32)
                DefaultDataType = CDI_DATATYPE_INT32;
              else
                {
                  fprintf(stderr, "Unsupported number of bits = %d for datatype INT!\n", nbits);
                  exit(EXIT_FAILURE);
                }
            }
          /*
          else if ( dtype == D_UINT )
            {
              if      ( nbits ==  8 ) DefaultDataType = CDI_DATATYPE_UINT8;
              else
                {
                  fprintf(stderr, "Unsupported number of bits = %d for datatype UINT!\n", nbits);
                  exit(EXIT_FAILURE);
                }
            }
          */
          else if (dtype == D_FLT)
            {
              if (nbits == 32)
                DefaultDataType = CDI_DATATYPE_FLT32;
              else if (nbits == 64)
                DefaultDataType = CDI_DATATYPE_FLT64;
              else
                {
                  fprintf(stderr, "Unsupported number of bits = %d for datatype FLT!\n", nbits);
                  exit(EXIT_FAILURE);
                }
            }
          else if (dtype == D_CPX)
            {
              if (nbits == 32)
                DefaultDataType = CDI_DATATYPE_CPX32;
              else if (nbits == 64)
                DefaultDataType = CDI_DATATYPE_CPX64;
              else
                {
                  fprintf(stderr, "Unsupported number of bits = %d for datatype CPX!\n", nbits);
                  exit(EXIT_FAILURE);
                }
            }
        }
    }

  if (*datatypestr != 0)
    {
      if (*datatypestr == 'l' || *datatypestr == 'L')
        {
          if (HOST_ENDIANNESS == CDI_BIGENDIAN) DefaultByteorder = CDI_LITTLEENDIAN;
          datatypestr++;
        }
      else if (*datatypestr == 'b' || *datatypestr == 'B')
        {
          if (HOST_ENDIANNESS == CDI_LITTLEENDIAN) DefaultByteorder = CDI_BIGENDIAN;
          datatypestr++;
        }
      else
        {
          fprintf(stderr, "Unsupported character in number of bytes: >%s< !\n", datatypestr);
          exit(EXIT_FAILURE);
        }
    }
}

static void
setDefaultFileType(char *filetypestr)
{
  if (filetypestr)
    {
      char *ftstr = filetypestr;

      // clang-format off
      if      ( memcmp(filetypestr, "grb2", 4)  == 0 ) { ftstr += 4; DefaultFileType = CDI_FILETYPE_GRB2;}
      else if ( memcmp(filetypestr, "grb1", 4)  == 0 ) { ftstr += 4; DefaultFileType = CDI_FILETYPE_GRB; }
      else if ( memcmp(filetypestr, "grb",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_GRB; }
      else if ( memcmp(filetypestr, "nc2",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_NC2; }
      else if ( memcmp(filetypestr, "nc4c", 4)  == 0 ) { ftstr += 4; DefaultFileType = CDI_FILETYPE_NC4C;}
      else if ( memcmp(filetypestr, "nc4",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_NC4; }
      else if ( memcmp(filetypestr, "nc5",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_NC5; }
      else if ( memcmp(filetypestr, "nc1",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_NC;  }
      else if ( memcmp(filetypestr, "nc",   2)  == 0 ) { ftstr += 2; DefaultFileType = CDI_FILETYPE_NC2; }
      else if ( memcmp(filetypestr, "srv",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_SRV; }
      else if ( memcmp(filetypestr, "ext",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_EXT; }
      else if ( memcmp(filetypestr, "ieg",  3)  == 0 ) { ftstr += 3; DefaultFileType = CDI_FILETYPE_IEG; }
      else
	{
	  fprintf(stderr, "Unsupported filetype %s!\n", filetypestr);
	  fprintf(stderr, "Available filetypes: grb, grb2, nc, nc2, nc4, nc4c, nc5, srv, ext and ieg\n");
	  exit(EXIT_FAILURE);
	}
      // clang-format on

      if (DefaultFileType != CDI_UNDEFID && *ftstr != 0)
        {
          if (*ftstr == '_')
            {
              ftstr++;

              setDefaultDataType(ftstr);
            }
          else
            {
              fprintf(stderr, "Unexpected character >%c< in file type >%s<!\n", *ftstr, filetypestr);
              fprintf(stderr, "Use format[_nbits] with:\n");
              fprintf(stderr, "    format = grb, grb2, nc, nc2, nc4, nc4c, nc5, srv, ext or ieg\n");
              fprintf(stderr, "    nbits  = 32/64 for grb2/nc/nc2/nc4/nc4c/nc5/srv/ext/ieg; 1 - 24 for grb/grb2\n");
              exit(EXIT_FAILURE);
            }
        }
    }
}

static int
handle_error(int cdiErrno, const char *fmt, ...)
{
  va_list args;

  va_start(args, fmt);

  printf("\n");
  vfprintf(stderr, fmt, args);
  fprintf(stderr, "\n");

  va_end(args);

  fprintf(stderr, "%s\n", cdiStringError(cdiErrno));

  return cdiErrno;
}

static void
defineCompress(const char *arg)
{
  const size_t len = strlen(arg);

  if (strncmp(arg, "szip", len) == 0)
    {
      comptype = CDI_COMPRESS_SZIP;
    }
  else if (strncmp(arg, "jpeg", len) == 0)
    {
      comptype = CDI_COMPRESS_JPEG;
    }
  else if (strncmp(arg, "zip", 3) == 0)
    {
      comptype = CDI_COMPRESS_ZIP;
      if (len == 5 && arg[3] == '_' && isdigit(arg[4]))
        complevel = atoi(&arg[4]);
      else
        complevel = 1;
    }
  else
    fprintf(stderr, "%s compression unsupported!\n", arg);
}

int
main(int argc, char *argv[])
{
  int c;
  char *fname1 = NULL;
  char *fname2 = NULL;
  char *rTable = NULL;
  char *wTable = NULL;
  int Move = 0;
  int Record = 0;
  int Variable = 0;
  int Debug = 0;
  /* int Quiet  = 0; */
  int Vardis = 0;
  int Version = 0;
  int Longinfo = 0;
  int Shortinfo = 0;
  int varID;
  int itableID = CDI_UNDEFID, otableID = CDI_UNDEFID;
  int Info = 1;
  int NoInfo = 0;
  int numWorkerIn = 0;
  int numWorkerOut = 0;
  char varname[CDI_MAX_NAME];
  char paramstr[32];

  Progname = strrchr(argv[0], '/');
  if (Progname == 0)
    Progname = argv[0];
  else
    Progname++;

  // clang-format off
  while ((c = getopt(argc, argv, "b:f:i:o:t:w:z:cdhlMmnqRrsvVxXZ")) != EOF)
    {
      switch (c)
	{
	case 'b': setDefaultDataType(optarg); break;
	case 'd': Debug = 1; break;
	case 'f': setDefaultFileType(optarg); break;
	case 'h': usage(); exit (0);
	case 'i': numWorkerIn = atoi(optarg);  break;
	case 'o': numWorkerOut = atoi(optarg); break;
	case 'l': Longinfo = 1; break;
	case 'M': cdiDefGlobal("HAVE_MISSVAL", 1); break;
	case 'm': Move = 1; break;
	case 'n': Info = 0;  NoInfo = 1;  break;
	case 'q': /* Quiet = 1; */  break;
	case 'R': cdiDefGlobal("REGULARGRID", 1); break;
	case 'r': Record = 1; break;
	case 'X': Variable = 1; break;
	case 's': Shortinfo = 1; break;
	case 't': rTable = optarg;  break;
	case 'v': Vardis = 1; break;
	case 'V': Version = 1; break;
	case 'w': wTable = optarg; break;
	case 'x': datamode = SP_MODE; break;
	case 'z': defineCompress(optarg); break;
	}
    }
  // clang-format on

  if (optind < argc) fname1 = argv[optind++];
  if (optind < argc) fname2 = argv[optind++];
  if (optind < argc) fprintf(stderr, "optind: %d argc: %d\n", optind, argc);

  if (Debug || Version) version();

  if (Debug) cdiDebug(Debug);

  if (rTable)
    {
      itableID = tableInq(-1, 0, rTable);
      if (itableID != CDI_UNDEFID) cdiDefTableID(itableID);
      otableID = itableID;
    }

  if (fname1 == NULL && !(Debug || Version))
    {
      usage();
      exit(0);
    }

  if (fname1)
    {
      SizeType nmiss;
      size_t datasize = 0;
      size_t maxlev = 0;
      int streamID2 = CDI_UNDEFID;
      int filetype;
      int nrecs;
      int levelID;
      int nts = 0;
      int recID;
      int taxisID2 = CDI_UNDEFID;
      int vlistID2 = CDI_UNDEFID;

      const int streamID1 = streamOpenRead(fname1);
      if (streamID1 < 0) return handle_error(streamID1, "Open failed on %s", fname1);

      if (numWorkerIn > 0) streamDefNumWorker(streamID1, numWorkerIn);

      const int vlistID1 = streamInqVlist(streamID1);

      if (Longinfo) vlistPrint(vlistID1);

      const int nvars = vlistNvars(vlistID1);
      const int taxisID1 = vlistInqTaxis(vlistID1);
      const int ntsteps = vlistNtsteps(vlistID1);

      if (Debug) fprintf(stderr, "nvars   = %d\nntsteps = %d\n", nvars, ntsteps);

      if (fname2)
        {
          vlistID2 = vlistDuplicate(vlistID1);
          taxisID2 = taxisDuplicate(taxisID1);
          vlistDefTaxis(vlistID2, taxisID2);
        }

      for (varID = 0; varID < nvars; varID++)
        {
          const int gridID = vlistInqVarGrid(vlistID1, varID);
          const size_t gridsize = gridInqSize(gridID);
          if (gridsize > datasize) datasize = gridsize;
          const int zaxisID = vlistInqVarZaxis(vlistID1, varID);
          const size_t nlev = zaxisInqSize(zaxisID);
          if (nlev > maxlev) maxlev = nlev;
          if (fname2)
            {
              if (DefaultDataType != CDI_UNDEFID) vlistDefVarDatatype(vlistID2, varID, DefaultDataType);
            }
        }

      if (fname2)
        {
          Info = 0;
          filetype = streamInqFiletype(streamID1);

          if (DefaultFileType != CDI_UNDEFID) filetype = DefaultFileType;

          streamID2 = streamOpenWrite(fname2, filetype);
          if (streamID2 < 0) return handle_error(streamID2, "Open failed on %s", fname2);

          if (numWorkerOut > 0) streamDefNumWorker(streamID2, numWorkerOut);

          if (DefaultByteorder != CDI_UNDEFID) streamDefByteorder(streamID2, DefaultByteorder);

          if (comptype != CDI_COMPRESS_NONE)
            {
              streamDefCompType(streamID2, comptype);
              streamDefCompLevel(streamID2, complevel);
            }

          streamDefVlist(streamID2, vlistID2);

          if (otableID == CDI_UNDEFID) otableID = itableID;
        }

      if (vlistNumber(vlistID1) != CDI_REAL) datasize *= 2;
      if (Variable) datasize *= maxlev;
      double *data = (double *) malloc(datasize * sizeof(double));

      // nts = cdiInqTimeSize(streamID1);
      if (Debug) printf("nts = %d streamID1 = %d, streamID2 = %d\n", nts, streamID1, streamID2);

      if (Shortinfo)
        {
          Info = 0;
          printShortinfo(streamID1, vlistID1, Vardis);
        }

      int tsID = 0;
      if (Info || fname2 || NoInfo)
        while ((nrecs = streamInqTimestep(streamID1, tsID)) > 0)
          {
            if (fname2 /* && ntsteps != 0*/)
              {
                taxisCopyTimestep(taxisID2, taxisID1);
                streamDefTimestep(streamID2, tsID);
              }
            const int64_t vdate = taxisInqVdate(taxisID1);
            const int vtime = taxisInqVtime(taxisID1);

            if (Debug) fprintf(stdout, "tsID = %d nrecs = %d date = %ld time = %d\n", tsID, nrecs, (long) vdate, vtime);

            if (Record)
              {
                for (recID = 0; recID < nrecs; recID++)
                  {
                    streamInqRecord(streamID1, &varID, &levelID);
                    streamReadRecord(streamID1, data, &nmiss);

                    const int number = vlistInqVarNumber(vlistID1, varID);
                    const int gridID = vlistInqVarGrid(vlistID1, varID);
                    const int zaxisID = vlistInqVarZaxis(vlistID1, varID);
                    const int param = vlistInqVarParam(vlistID1, varID);

                    cdiParamToString(param, paramstr, sizeof(paramstr));

                    if (Vardis)
                      vlistInqVarName(vlistID1, varID, varname);
                    else
                      strcpy(varname, paramstr);

                    // printf("varID=%d, param=%d, gridID=%d, zaxisID=%d levelID=%d\n", varID, param, gridID, zaxisID, levelID);

                    const SizeType gridsize = gridInqSize(gridID);
                    const double level = zaxisInqLevels(zaxisID, NULL) ? zaxisInqLevel(zaxisID, levelID) : levelID + 1;
                    const double missval = vlistInqVarMissval(vlistID1, varID);

                    if (Info)
                      printInfo(vdate, vtime, varname, level, (size_t) gridsize, number, (size_t) nmiss, missval, data, Vardis);

                    if (fname2)
                      {
                        streamDefRecord(streamID2, varID, levelID);
                        if (Move)
                          streamCopyRecord(streamID2, streamID1);
                        else
                          streamWriteRecord(streamID2, data, nmiss);
                      }
                  }
              }
            else if (Variable)
              {
                for (varID = 0; varID < nvars; varID++)
                  {
                    if (vlistInqVarTimetype(vlistID1, varID) == TIME_CONSTANT && tsID > 0) continue;

                    const int number = vlistInqVarNumber(vlistID1, varID);
                    const int gridID = vlistInqVarGrid(vlistID1, varID);
                    const int zaxisID = vlistInqVarZaxis(vlistID1, varID);
                    const int param = vlistInqVarParam(vlistID1, varID);

                    cdiParamToString(param, paramstr, sizeof(paramstr));

                    if (Vardis)
                      vlistInqVarName(vlistID1, varID, varname);
                    else
                      strcpy(varname, paramstr);

                    if (Debug) fprintf(stdout, "varID = %d param = %d gridID = %d zaxisID = %d\n", varID, param, gridID, zaxisID);

                    const size_t gridsize = gridInqSize(gridID);
                    const double missval = vlistInqVarMissval(vlistID1, varID);

                    streamReadVar(streamID1, varID, data, &nmiss);

                    const int nlevs = zaxisInqSize(zaxisID);
                    for (levelID = 0; levelID < nlevs; levelID++)
                      {
                        const double level = zaxisInqLevels(zaxisID, NULL) ? zaxisInqLevel(zaxisID, levelID) : levelID + 1;
                        const size_t offset = levelID * gridsize;
                        if (Info) printInfo(vdate, vtime, varname, level, gridsize, number, nmiss, missval, data + offset, Vardis);
                      }

                    if (fname2) streamWriteVar(streamID2, varID, data, nmiss);
                  }
              }
            else
              {
                for (varID = 0; varID < nvars; varID++)
                  {
                    if (vlistInqVarTimetype(vlistID1, varID) == TIME_CONSTANT && tsID > 0) continue;

                    const int number = vlistInqVarNumber(vlistID1, varID);
                    const int gridID = vlistInqVarGrid(vlistID1, varID);
                    const int zaxisID = vlistInqVarZaxis(vlistID1, varID);
                    const int param = vlistInqVarParam(vlistID1, varID);

                    cdiParamToString(param, paramstr, sizeof(paramstr));

                    if (Vardis)
                      vlistInqVarName(vlistID1, varID, varname);
                    else
                      strcpy(varname, paramstr);

                    if (Debug) fprintf(stdout, "varID = %d param = %d gridID = %d zaxisID = %d\n", varID, param, gridID, zaxisID);

                    const size_t gridsize = gridInqSize(gridID);
                    const double missval = vlistInqVarMissval(vlistID1, varID);

                    const int nlevs = zaxisInqSize(zaxisID);
                    for (levelID = 0; levelID < nlevs; levelID++)
                      {
                        const double level = zaxisInqLevels(zaxisID, NULL) ? zaxisInqLevel(zaxisID, levelID) : levelID + 1;
                        streamReadVarSlice(streamID1, varID, levelID, data, &nmiss);

                        if (Info) printInfo(vdate, vtime, varname, level, gridsize, number, nmiss, missval, data, Vardis);

                        if (fname2) streamWriteVarSlice(streamID2, varID, levelID, data, nmiss);
                      }
                  }
              }

            tsID++;
          }

      free(data);

      // fprintf(stderr, "%ld\n", (long) streamNvals(streamID1));

      if (fname2)
        {
          streamClose(streamID2);
          vlistDestroy(vlistID2);
          taxisDestroy(taxisID2);
        }
      streamClose(streamID1);
    }

  if (wTable) tableWrite(wTable, itableID);

  return 0;
}
