/*
  This file is part of CDO. CDO is a collection of Operators to manipulate and analyse Climate model Data.

  Author: Uwe Schulzweida

*/

/*
   This module contains the following operators:

      Pack    pack         Pack
*/

#ifdef _OPENMP
#include <omp.h>
#endif

#include <climits>

#include <cdi.h>

#include "process_int.h"
#include "cdo_vlist.h"
#include "datetime.h"
#include "cdo_default_values.h"

static int
get_type_values(const int datatype, double &tmin, double &tmax, double &tmv)
{
  int status = 0;

  // clang-format off
  switch (datatype)
    {
    case CDI_DATATYPE_INT8:   tmv = -SCHAR_MAX; tmin = -SCHAR_MAX + 1;  tmax = SCHAR_MAX;     break;
    case CDI_DATATYPE_UINT8:  tmv =  UCHAR_MAX; tmin = 0;               tmax = UCHAR_MAX - 1; break;
    case CDI_DATATYPE_INT16:  tmv =  -SHRT_MAX; tmin = -SHRT_MAX + 1;   tmax = SHRT_MAX;      break;
    case CDI_DATATYPE_UINT16: tmv =  USHRT_MAX; tmin = 0;               tmax = USHRT_MAX - 1; break;
    case CDI_DATATYPE_INT32:  tmv =   -INT_MAX; tmin = -INT_MAX + 1;    tmax = INT_MAX;       break;
    case CDI_DATATYPE_UINT32: tmv =   UINT_MAX; tmin = 0;               tmax = UINT_MAX - 1;  break;
    default: status = 1; break;
    }
  // clang-format on

  return status;
}

static int
compute_scale_and_offset(const int datatype, const double fmin, const double fmax, double &scale_factor, double &add_offset)
{
  scale_factor = 1.0;
  add_offset = 0.0;

  double tmin, tmax, tmv;
  if (get_type_values(datatype, tmin, tmax, tmv)) return 1;

  if (IS_NOT_EQUAL(fmin, fmax))
    {
      scale_factor = (fmax - fmin) / (tmax - tmin);
      add_offset = ((fmax + fmin) - scale_factor * (tmin + tmax)) / 2;
    }

  return 0;
}

static MinMax
field_min_max(Field &field)
{
  auto nmiss = field.nmiss;
  auto missval = field.missval;
  auto len = field.size;

  if (field.memType == MemType::Float)
    return nmiss ? varray_min_max_mv(len, field.vec_f, (float) missval) : varray_min_max(len, field.vec_f);
  else
    return nmiss ? varray_min_max_mv(len, field.vec_d, missval) : varray_min_max(len, field.vec_d);
}

static void
fieldChangeMissval(Field &field, double missval1, double missval2)
{
  auto len = field.size;

  if (field.memType == MemType::Float)
    {
      auto &v = field.vec_f;
      for (size_t i = 0; i < len; ++i)
        if (DBL_IS_EQUAL(v[i], (float) missval1)) v[i] = missval2;
    }
  else
    {
      auto &v = field.vec_d;
      for (size_t i = 0; i < len; ++i)
        if (DBL_IS_EQUAL(v[i], missval1)) v[i] = missval2;
    }
}

void *
Pack(void *process)
{
  int datatype = CDI_DATATYPE_INT16;
  DateTimeList dtlist;

  cdo_initialize(process);

  operator_check_argc(0);

  const auto streamID1 = cdo_open_read(0);

  const auto vlistID1 = cdo_stream_inq_vlist(streamID1);
  const auto vlistID2 = vlistDuplicate(vlistID1);

  const auto taxisID1 = vlistInqTaxis(vlistID1);
  const auto taxisID2 = taxisDuplicate(taxisID1);
  vlistDefTaxis(vlistID2, taxisID2);

  VarList varList;
  varListInit(varList, vlistID1);

  const auto nvars = vlistNvars(vlistID1);

  FieldVector3D vars;

  int tsID = 0;
  while (true)
    {
      const auto nrecs = cdo_stream_inq_timestep(streamID1, tsID);
      if (nrecs == 0) break;

      constexpr size_t NALLOC_INC = 1024;
      if ((size_t) tsID >= vars.size()) vars.resize(vars.size() + NALLOC_INC);

      dtlist.taxis_inq_timestep(taxisID1, tsID);

      fields_from_vlist(vlistID1, vars[tsID]);

      for (int recID = 0; recID < nrecs; recID++)
        {
          int varID, levelID;
          cdo_inq_record(streamID1, &varID, &levelID);
          auto &field = vars[tsID][varID][levelID];
          field.init(varList[varID]);
          cdo_read_record(streamID1, field);
        }

      tsID++;
    }

  const auto nts = tsID;

  if (CdoDefault::DataType != CDI_UNDEFID)
    {
      if (CdoDefault::DataType == CDI_DATATYPE_FLT64 || CdoDefault::DataType == CDI_DATATYPE_FLT32)
        {
          cdo_warning("Changed default output datatype to int16");
          CdoDefault::DataType = datatype;
        }
      else
        {
          datatype = CdoDefault::DataType;
        }
    }

  CdoDefault::DataType = datatype;
  constexpr double undefValue = 1.0e300;

  for (int varID = 0; varID < nvars; varID++)
    {
      double fmin = undefValue, fmax = -undefValue;
      size_t nmisspv = 0;

      const auto timetype = varList[varID].timetype;
      const auto missval1 = varList[varID].missval;
      const auto gridsize = varList[varID].gridsize;
      const auto nlevels = varList[varID].nlevels;
      for (int levelID = 0; levelID < nlevels; levelID++)
        {
          for (int t = 0; t < nts; t++)
            {
              if (t > 0 && timetype == TIME_CONSTANT) continue;

              auto &field = vars[t][varID][levelID];
              const auto nmiss = field.nmiss;

              if (nmiss) nmisspv += nmiss;

              if (nmiss < gridsize)
                {
                  const auto mm = field_min_max(field);
                  fmin = std::min(fmin, mm.min);
                  fmax = std::max(fmax, mm.max);
                }
            }
        }

      vlistDefVarDatatype(vlistID2, varID, datatype);
      // const auto missval2 = vlistInqVarMissval(vlistID2, varID);

      const auto hasValidData = (is_not_equal(fmin, undefValue) && is_not_equal(fmax, -undefValue));

      if (nmisspv)
        {
          double tmin, tmax, missval2;
          if (!get_type_values(datatype, tmin, tmax, missval2))
            {
              vlistDefVarMissval(vlistID2, varID, missval2);

              if (!(missval2 < tmin || missval2 > tmax))
                cdo_warning("new missing value %g is inside data range (%g - %g)!", missval2, tmin, tmax);

              for (int levelID = 0; levelID < nlevels; levelID++)
                {
                  for (int t = 0; t < nts; t++)
                    {
                      if (t > 0 && timetype == TIME_CONSTANT) continue;
 
                      auto &field = vars[t][varID][levelID];
                      if (field.nmiss) fieldChangeMissval(field, missval1, missval2);
                    }
                }
            }
        }

      if (hasValidData)
        {
          double sf, ao;
          if (!compute_scale_and_offset(datatype, fmin, fmax, sf, ao))
            {
              if (varList[varID].memType == MemType::Float)
                {
                  sf = (float) sf;
                  ao = (float) ao;
                }
              vlistDefVarScalefactor(vlistID2, varID, sf);
              vlistDefVarAddoffset(vlistID2, varID, ao);
            }
        }
    }

  const auto streamID2 = cdo_open_write(1);
  cdo_def_vlist(streamID2, vlistID2);

  for (tsID = 0; tsID < nts; tsID++)
    {
      dtlist.taxisDefTimestep(taxisID2, tsID);
      cdo_def_timestep(streamID2, tsID);

      for (int varID = 0; varID < nvars; varID++)
        {
          if (tsID > 0 && varList[varID].timetype == TIME_CONSTANT) continue;
          for (int levelID = 0; levelID < varList[varID].nlevels; levelID++)
            {
              auto &field = vars[tsID][varID][levelID];
              if (field.hasData())
                {
                  cdo_def_record(streamID2, varID, levelID);
                  cdo_write_record(streamID2, field);
                }
            }
        }
    }

  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  cdo_finish();

  return nullptr;
}
