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

#ifdef HAVE_LIBGRIB

#ifdef HAVE_LIBFDB5
#include "cdi_fdb.h"
#endif

#include "dmemory.h"
#include "cdi.h"
#include "cdi_int.h"
#include "gribapi.h"
#include "stream_cgribex.h"
#include "stream_grb.h"
#include "stream_gribapi.h"
#include "file.h"
#include "cgribex.h"  /* gribZip gribGetZip gribGinfo */
#include "namespace.h"


static
size_t grbEncode(int filetype, int memtype, int varID, int levelID, int vlistID, int gridID, int zaxisID,
		 int date, int time, int tsteptype, int numavg,
		 size_t datasize, const void *data, size_t nmiss, void **gribbuffer,
		 int comptype, void *gribContainers)
{
  size_t nbytes = 0;

#ifdef HAVE_LIBCGRIBEX
  if (filetype == CDI_FILETYPE_GRB && !CDI_gribapi_grib1)
    {
      size_t gribbuffersize = datasize*4+3000;
      *gribbuffer = Malloc(gribbuffersize);

      nbytes = cgribexEncode(memtype, varID, levelID, vlistID, gridID, zaxisID,
			     date, time, tsteptype, numavg,
			     datasize, data, nmiss, *gribbuffer, gribbuffersize);
    }
  else
#endif
#ifdef HAVE_LIBGRIB_API
    {
#ifdef GRIBCONTAINER2D
      void *gribContainer = (void *) &((gribContainer_t **)gribContainers)[varID][levelID];
#else
      void *gribContainer = (void *) &((gribContainer_t *)gribContainers)[varID];
#endif
      const void *datap = data;
      if ( memtype == MEMTYPE_FLOAT )
        {
          const float *dataf = (const float*) data;
          double *datad = (double*) Malloc(datasize*sizeof(double));
          for ( size_t i = 0; i < datasize; ++i ) datad[i] = (double) dataf[i];
          datap = (const void*) datad;
        }

      size_t gribbuffersize;
      nbytes = gribapiEncode(varID, levelID, vlistID, gridID, zaxisID,
			     date, time, tsteptype, numavg,
			     (long) datasize, datap, nmiss, gribbuffer, &gribbuffersize,
			     comptype, gribContainer);
      
      if ( memtype == MEMTYPE_FLOAT ) Free((void*)datap);
    }
#else
    {
      Error("ecCodes support not compiled in!");
      (void)gribContainers;
      (void)comptype;
    }
#endif

  return nbytes;
}

static
size_t grbSzip(int filetype, void *gribbuffer, size_t gribbuffersize)
{
  size_t buffersize = gribbuffersize + 1000; /* compressed record can be greater than source record */
  void *buffer = Malloc(buffersize);

  // memcpy(buffer, gribbuffer, gribbuffersize);

  size_t nbytes = 0;
  if ( filetype == CDI_FILETYPE_GRB )
    {
      nbytes = (size_t)gribZip((unsigned char *)gribbuffer, (long) gribbuffersize, (unsigned char *)buffer, (long) buffersize);
    }
  else
    {
      static int lszip_warn = 1;
      if ( lszip_warn ) Warning("Szip compression of GRIB2 records not implemented!");
      lszip_warn = 0;
      nbytes = gribbuffersize;
    }

  Free(buffer);

  return nbytes;
}


typedef struct
{
  char date[16];
  char time[8];
  char param[8];
  char levtype[8];
  char levelist[8];
}
FDB_Keys;


void cdi_fdb_store(void *fdbHandle, const char *filename, void *gribbuffer, size_t nbytes, const FDB_Keys *fdbKeys)
{
#ifdef HAVE_LIBFDB5
  size_t len = strlen(filename);
  if (len == 4) Error("FDB keys missing!");

  KeyValueEntry keyValue;
  keyValue.item = NULL;
  decode_fdbitem(filename + 4, &keyValue);

  if (keyValue.numKeys == 0) Error("FDB keys missing!");

  const char *class = NULL;
  const char *stream = NULL;
  const char *expver = NULL;
  for (int i = 0; i < keyValue.numKeys; i++)
    {
      if      (strcmp(keyValue.keys[i], "class") == 0) class = keyValue.values[i];
      else if (strcmp(keyValue.keys[i], "stream") == 0) stream = keyValue.values[i];
      else if (strcmp(keyValue.keys[i], "expver") == 0) expver = keyValue.values[i];
      else Error("Unsupported FDB parameter: %s=%s", keyValue.keys[i], keyValue.values[i]);
    }

  if (!class) Error("FDB parameter <class> undefined!");
  if (!stream) Error("FDB parameter <stream> undefined!");
  if (!expver) Error("FDB parameter <expver> undefined!");

  if (CDI_Debug)
    {
      printf("{class=%s,expver=%s,stream=%s,date=%s,time=%s,domain=g}", class, expver, stream, fdbKeys->date, fdbKeys->time);
      printf("{type=an,levtype=%s}{step=0,", fdbKeys->levtype);
      if (fdbKeys->levelist[0]) printf("levelist=%s,", fdbKeys->levelist);
      printf("param=%s},length=%zu\n", fdbKeys->param, nbytes);
    }

  fdb_key_t *key;
  fdb_new_key(&key);
  fdb_key_add(key, "class", class);
  fdb_key_add(key, "expver", expver);
  fdb_key_add(key, "stream", stream);
  fdb_key_add(key, "domain", "g");
  fdb_key_add(key, "date", fdbKeys->date);
  fdb_key_add(key, "time", fdbKeys->time);
  fdb_key_add(key, "type", "an");
  fdb_key_add(key, "levtype", fdbKeys->levtype);
  fdb_key_add(key, "step", "0");
  fdb_key_add(key, "param", fdbKeys->param);
  if (fdbKeys->levelist[0]) fdb_key_add(key, "levelist", fdbKeys->levelist);

  fdb_archive(fdbHandle, key, gribbuffer, nbytes);
  // alternative: write to tmpfile and use C++ code from fdb_write

  fdb_delete_key(key);
#endif
}

static
int map_grib2_param(int pnum, int pcat, int pdis)
{
  if      (pnum ==  8 && pcat == 2 && pdis == 0) return 135;
  else if (pnum ==  0 && pcat == 0 && pdis == 0) return 130;
  else if (pnum ==  0 && pcat == 1 && pdis == 0) return 133;
  else if (pnum == 83 && pcat == 1 && pdis == 0) return 246;
  else if (pnum == 84 && pcat == 1 && pdis == 0) return 247;
  else if (pnum == 85 && pcat == 1 && pdis == 0) return  75;
  else if (pnum == 86 && pcat == 1 && pdis == 0) return  76;
  else if (pnum ==  2 && pcat == 2 && pdis == 0) return 131;
  else if (pnum ==  3 && pcat == 2 && pdis == 0) return 132;
  else if (pnum == 25 && pcat == 3 && pdis == 0) return 152;
  else if (pnum ==  4 && pcat == 3 && pdis == 0) return 129;

  return -1;
}

static
int get_fdb_param(int cdiParam)
{
  int pnum, pcat, pdis;
  cdiDecodeParam(cdiParam, &pnum, &pcat, &pdis);
  if (pnum < 0) pnum = -pnum;
  if (pnum > 255) pnum = pnum%256;

  int fdbParam = (pdis == 255) ? pnum : map_grib2_param(pnum, pcat, pdis);

  return fdbParam;
}

static
void fillup_gribbuffer(size_t nbytes, unsigned char *gribbuffer)
{
  while (nbytes & 7) gribbuffer[nbytes++] = 0;
}

void grbCopyRecord(stream_t *streamptr2, stream_t *streamptr1)
{
  const int filetype = streamptr1->filetype;
  const int fileID1 = streamptr1->fileID;
  const int fileID2 = streamptr2->fileID;
  const int tsID = streamptr1->curTsID;
  const int vrecID = streamptr1->tsteps[tsID].curRecID;
  const int recID = streamptr1->tsteps[tsID].recIDs[vrecID];
  const record_t *record = &streamptr1->tsteps[tsID].records[recID];
  const off_t recpos = record->position;
  size_t recsize = record->size;

  void *gribbuffer = NULL;

  if (streamptr1->fdbRetrieve)
    {
#ifdef HAVE_LIBFDB5
      void *fdbItem = streamptr1->tsteps[tsID].records[recID].fdbItem;
      if (!fdbItem) Error("fdbItem not available!");

      size_t buffersize = 0;
      recsize = fdb_read_record(streamptr1->fdbHandle, fdbItem, &buffersize, &gribbuffer);

      // round up recsize to next multiple of 8
      const size_t gribbuffersize = ((recsize + 7U) & ~7U);

      gribbuffer = (unsigned char *) Realloc(gribbuffer, gribbuffersize);
#endif
    }
  else
    {
      fileSetPos(fileID1, recpos, SEEK_SET);

      // round up recsize to next multiple of 8
      const size_t gribbuffersize = ((recsize + 7U) & ~7U);

      gribbuffer = (unsigned char *) Malloc(gribbuffersize);

      if (fileRead(fileID1, gribbuffer, recsize) != recsize)
        Error("Could not read GRIB record for copying!");
    }

  size_t nbytes = recsize;

#ifdef HAVE_LIBCGRIBEX
  if ( filetype == CDI_FILETYPE_GRB && !CDI_gribapi_grib1 )
    {
      if ( cdiGribChangeParameterID.active )
        {
          // Even if you are stream-copy records you might need to change a bit of grib-header !
          void *gh = cgribex_handle_new_from_meassage((void*) gribbuffer, recsize);
          cgribexChangeParameterIdentification(gh, cdiGribChangeParameterID.code, cdiGribChangeParameterID.ltype, cdiGribChangeParameterID.lev);
          cgribex_handle_delete(gh);
          cdiGribChangeParameterID.active = false; // after grib attributes have been changed turn it off again
        }
    }
  else
#endif
#ifdef HAVE_LIBGRIB_API
    {
      if ( cdiGribChangeParameterID.active )
        {
          // Even if you are stream-copy records you might need to change a bit of grib-header !
          grib_handle *gh = grib_handle_new_from_message(NULL, (void*) gribbuffer, recsize);
          gribapiChangeParameterIdentification(gh, cdiGribChangeParameterID.code, cdiGribChangeParameterID.ltype, cdiGribChangeParameterID.lev);
          grib_handle_delete(gh);
          cdiGribChangeParameterID.active = false; // after grib attributes have been changed turn it off again
        }
    }
#else
    {
      Error("ecCodes support not compiled in!");
    }
#endif

#ifdef HAVE_LIBGRIB_API
#ifdef HIRLAM_EXTENSIONS
  // Even if you are stream-copy records you might need to change a bit of grib-header !

  if ( cdiGribDataScanningMode.active )
    // allowed modes: <0, 64, 96>; Default is 64
    // This will overrule the old scanning mode of the given grid
    {
      grib_handle *gh = grib_handle_new_from_message(NULL, (void *) gribbuffer, recsize);

      const int scanModeIN = gribapiGetScanningMode(gh);

      grib_handle_delete(gh);

      if (cdiDebugExt>=20) Message("Change GribDataScanningMode => %d (scanModeIN = %d)", cdiGribDataScanningMode.value, scanModeIN);

      if (scanModeIN != cdiGribDataScanningMode.value)
        {
          size_t nmiss = 0;

          const int vlistID = streamptr1->vlistID;
          const int varID = record->varID;
          const int levelID = record->levelID;
          const int gridID = vlistInqVarGrid(vlistID, varID);

          size_t gridsize = gridInqSize(gridID);
          if ( vlistNumber(vlistID) != CDI_REAL ) gridsize *= 2;
          double *data = (double *) malloc(gridsize*sizeof(double));

          if (cdiDebugExt>=20) Message(" processing varID %d; levelID %d", varID, levelID);

          grb_write_var_slice(streamptr2, varID, levelID, MEMTYPE_DOUBLE, (const void *) data, nmiss);

          free(data);
        }
    }
#endif // HIRLAM_EXTENSIONS
#endif

  if (filetype == CDI_FILETYPE_GRB)
    {
      size_t unzipsize;
      const int izip = gribGetZip(recsize, gribbuffer, &unzipsize);

      if (izip == 0 && (streamptr2->comptype == CDI_COMPRESS_SZIP || streamptr2->comptype == CDI_COMPRESS_AEC))
        nbytes = grbSzip(filetype, gribbuffer, nbytes);
    }

  fillup_gribbuffer(nbytes, gribbuffer);

  if (streamptr2->fdbStore)
    {
      const int vlistID = streamptr1->vlistID;
      const int varID = record->varID;
      const int zaxisID = vlistInqVarZaxis(vlistID, varID);
      const int zaxisType = zaxisInqType(zaxisID);

      FDB_Keys fdbKeys;
      snprintf(fdbKeys.date, sizeof(fdbKeys.date), "%d", (int)streamptr1->tsteps[tsID].taxis.vdate);
      snprintf(fdbKeys.time, sizeof(fdbKeys.time), "%04d", streamptr1->tsteps[tsID].taxis.vtime / 100);
      snprintf(fdbKeys.param, sizeof(fdbKeys.param), "%d", get_fdb_param(record->param));
      const bool isML = (zaxisType == ZAXIS_HYBRID || zaxisType == ZAXIS_HYBRID_HALF);
      snprintf(fdbKeys.levtype, sizeof(fdbKeys.levtype), "%s", isML ? "ml" : "sfc");
      fdbKeys.levelist[0] = 0;
      if (isML) snprintf(fdbKeys.levelist, sizeof(fdbKeys.levelist), "%d", isML ? record->ilevel : 0);

#ifdef HAVE_LIBFDB5
      cdi_fdb_store(streamptr2->fdbHandle, streamptr2->filename, gribbuffer, nbytes, &fdbKeys);
#endif
    }
  else
    {
      const size_t nwrite = fileWrite(fileID2, gribbuffer, nbytes);
      if (nwrite != nbytes)
        {
          perror(__func__);
          Error("Could not write record for copying!");
        }
    }

  Free(gribbuffer);
}


void grb_write_var_slice(stream_t *streamptr, int varID, int levelID, int memtype, const void *data, size_t nmiss)
{
  const int filetype = streamptr->filetype;
  const int fileID = streamptr->fileID;
  const int vlistID = streamptr->vlistID;
  const int gridID = vlistInqVarGrid(vlistID, varID);
  const int zaxisID = vlistInqVarZaxis(vlistID, varID);
  const int tsteptype = vlistInqVarTsteptype(vlistID, varID);
  const int tsID = streamptr->curTsID;
  const int date = streamptr->tsteps[tsID].taxis.vdate;
  const int time = streamptr->tsteps[tsID].taxis.vtime;
  const int numavg = (tsteptype == TSTEP_AVG) ? streamptr->tsteps[tsID].taxis.numavg : 0;
  int comptype = streamptr->comptype;

  if (CDI_Debug) Message("gridID = %d zaxisID = %d", gridID, zaxisID);

  const size_t datasize = gridInqSize(gridID);

  if (comptype != CDI_COMPRESS_JPEG && comptype != CDI_COMPRESS_SZIP && comptype != CDI_COMPRESS_AEC) comptype = CDI_COMPRESS_NONE;

  if (filetype == CDI_FILETYPE_GRB && !CDI_gribapi_grib1 && comptype == CDI_COMPRESS_JPEG)
    {
      static bool ljpeg_warn = true;
      if (ljpeg_warn) Warning("JPEG compression of GRIB1 records not available!");
      ljpeg_warn = false;
    }

  void *gribbuffer = NULL;
  size_t nbytes = grbEncode(filetype, memtype, varID, levelID, vlistID, gridID, zaxisID, date, time, tsteptype, numavg,
                            datasize, data, nmiss, &gribbuffer, comptype, streamptr->gribContainers);

  if (filetype == CDI_FILETYPE_GRB && (comptype == CDI_COMPRESS_SZIP || comptype == CDI_COMPRESS_AEC))
    nbytes = grbSzip(filetype, gribbuffer, nbytes);

  if (streamptr->fdbStore)
    {
      const int zaxisType = zaxisInqType(zaxisID);
      const double level = zaxisInqLevel(zaxisID, levelID);

      FDB_Keys fdbKeys;
      snprintf(fdbKeys.date, sizeof(fdbKeys.date), "%d", (int)streamptr->tsteps[tsID].taxis.vdate);
      snprintf(fdbKeys.time, sizeof(fdbKeys.time), "%04d", streamptr->tsteps[tsID].taxis.vtime / 100);
      snprintf(fdbKeys.param, sizeof(fdbKeys.param), "%d", get_fdb_param(vlistInqVarParam(vlistID, varID)));
      const bool isML = (zaxisType == ZAXIS_HYBRID || zaxisType == ZAXIS_HYBRID_HALF);
      snprintf(fdbKeys.levtype, sizeof(fdbKeys.levtype), "%s", isML ? "ml" : "sfc");
      fdbKeys.levelist[0] = 0;
      int ilevel = (isML) ? (int)level : 0;
      if (isML) snprintf(fdbKeys.levelist, sizeof(fdbKeys.levelist), "%d", isML ? ilevel : 0);

#ifdef HAVE_LIBFDB5
     cdi_fdb_store(streamptr->fdbHandle, streamptr->filename, gribbuffer, nbytes, &fdbKeys);
#endif
    }
  else
    {
      size_t (*myFileWrite)(int fileID, const void *restrict buffer, size_t len)
        = (size_t (*)(int, const void *restrict, size_t))
        namespaceSwitchGet(NSSWITCH_FILE_WRITE).func;

      const size_t nwrite = myFileWrite(fileID, gribbuffer, nbytes);
      if (nwrite != nbytes)
        {
          perror(__func__);
          Error("Failed to write GRIB slice!");
        }
    }

  if (gribbuffer) Free(gribbuffer);
}


void grb_write_var(stream_t *streamptr, int varID, int memtype, const void *data, size_t nmiss)
{
  const int vlistID  = streamptr->vlistID,
    gridID   = vlistInqVarGrid(vlistID, varID),
    zaxisID  = vlistInqVarZaxis(vlistID, varID),
    nlevs    = zaxisInqSize(zaxisID);
  const size_t gridsize = gridInqSize(gridID);
  const double missval = vlistInqVarMissval(vlistID, varID);

  const size_t chunkLen = gridsize;
  if ( memtype == MEMTYPE_FLOAT )
    for ( int levelID = 0; levelID < nlevs; levelID++ )
      {
        const float *restrict fdata = ((const float *)data)+levelID*gridsize;
        
        size_t nmiss_slice = 0;
        if ( nmiss )
          for ( size_t i = 0; i < chunkLen; ++i )
            nmiss_slice += DBL_IS_EQUAL(fdata[i], missval);

        grb_write_var_slice(streamptr, varID, levelID, memtype, fdata, nmiss_slice);
      }
  else
    for ( int levelID = 0; levelID < nlevs; levelID++ )
      {
        const double *restrict ddata = ((const double *)data)+levelID*gridsize;
        
        size_t nmiss_slice = 0;
        if ( nmiss )
          for ( size_t i = 0; i < chunkLen; ++i )
            nmiss_slice += DBL_IS_EQUAL(ddata[i], missval);

        grb_write_var_slice(streamptr, varID, levelID, memtype, ddata, nmiss_slice);
      }
}


void grb_write_record(stream_t *streamptr, int memtype, const void *data, size_t nmiss)
{
  const int varID   = streamptr->record->varID;
  const int levelID = streamptr->record->levelID;
  grb_write_var_slice(streamptr, varID, levelID, memtype, data, nmiss);
}


#endif
