/*
  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:

      Interpolate remapcon        First order conservative remapping
      Interpolate remapcon2       Second order conservative remapping
      Interpolate remapbil        Bilinear interpolation
      Interpolate remapbic        Bicubic interpolation
      Interpolate remapdis        Distance-weighted averaging
      Interpolate remapnn         Nearest neighbor remapping
      Interpolate remaplaf        Largest area fraction remapping
      Genweights  gencon          Generate first order conservative remap
      Genweights  gencon2         Generate second order conservative remap
      Genweights  genbil          Generate bilinear interpolation weights
      Genweights  genbic          Generate bicubic interpolation weights
      Genweights  gendis          Generate distance-weighted averaging weights
      Genweights  gennn           Generate nearest neighbor weights
      Genweights  genlaf          Generate largest area fraction weights
      Remap       remap           SCRIP grid remapping
*/

#include <algorithm>

#include <cdi.h>

#include "process_int.h"
#include "param_conversion.h"
#include "remap.h"
#include <mpim_grid.h>
#include "griddes.h"
#include "cdo_options.h"
#include "timer.h"

enum
{
  REMAPSCON,
  REMAPCON2,
  REMAPBIL,
  REMAPBIC,
  REMAPDIS,
  REMAPNN,
  REMAPLAF,
  REMAPAVG,
  GENSCON,
  GENCON2,
  GENBIL,
  GENBIC,
  GENDIS,
  GENNN,
  GENLAF,
  REMAP,
  REMAPCON,
  REMAPYCON2,
  GENCON,
  GENYCON2
};

enum
{
  HEAP_SORT,
  MERGE_SORT
};

static void
getMaptype(int operfunc, RemapMethod &mapType, SubmapType &submapType, int &numNeighbors, int &remapOrder)
{
  remapOrder = 1;
  switch (operfunc)
    {
    case REMAPCON:
    case GENCON: mapType = RemapMethod::CONSERV; break;
    case REMAPYCON2:
    case GENYCON2:
      mapType = RemapMethod::CONSERV;
      remapOrder = 2;
      break;
    case REMAPSCON:
    case GENSCON: mapType = RemapMethod::CONSERV_SCRIP; break;
    case REMAPCON2:
    case GENCON2:
      mapType = RemapMethod::CONSERV_SCRIP;
      remapOrder = 2;
      break;
    case REMAPLAF:
    case GENLAF:
      mapType = RemapMethod::CONSERV;
      submapType = SubmapType::LAF;
      break;
    case REMAPAVG:
      mapType = RemapMethod::CONSERV;
      submapType = SubmapType::AVG;
      break;
    case REMAPBIL:
    case GENBIL: mapType = RemapMethod::BILINEAR; break;
    case REMAPBIC:
    case GENBIC: mapType = RemapMethod::BICUBIC; break;
    case REMAPDIS:
    case GENDIS:
      mapType = RemapMethod::DISTWGT;
      if (numNeighbors == 0) numNeighbors = 4;
      break;
    case REMAPNN:
    case GENNN:
      mapType = RemapMethod::DISTWGT;
      numNeighbors = 1;
      break;
    default: cdo_abort("Unknown mapping method"); break;
    }
}

static int
maptype2operfunc(RemapMethod mapType, SubmapType submapType, int numNeighbors, int remapOrder)
{
  int operfunc = -1;

  if (mapType == RemapMethod::CONSERV_SCRIP)
    operfunc = (remapOrder == 2) ? REMAPCON2 : REMAPSCON;
  else if (mapType == RemapMethod::CONSERV)
    operfunc = (submapType == SubmapType::LAF) ? REMAPLAF : ((remapOrder == 2) ? REMAPYCON2 : REMAPCON);
  else if (mapType == RemapMethod::BILINEAR)
    operfunc = REMAPBIL;
  else if (mapType == RemapMethod::BICUBIC)
    operfunc = REMAPBIC;
  else if (mapType == RemapMethod::DISTWGT)
    operfunc = (numNeighbors == 1) ? REMAPNN : REMAPDIS;
  else
    cdo_abort("Unsupported mapping method (mapType = %d)", mapType);

  return operfunc;
}

static void
remap_print_info(int operfunc, bool remap_genweights, RemapGrid &src_grid, RemapGrid &tgt_grid, size_t nmiss)
{
  char line[256], tmpstr[256];
  line[0] = 0;

  // clang-format off
  if      (operfunc == REMAPBIL   || operfunc == GENBIL)   strcpy(line, "Bilinear");
  else if (operfunc == REMAPBIC   || operfunc == GENBIC)   strcpy(line, "Bicubic");
  else if (operfunc == REMAPNN    || operfunc == GENNN)    strcpy(line, "Nearest neighbor");
  else if (operfunc == REMAPDIS   || operfunc == GENDIS)   strcpy(line, "Distance-weighted average");
  else if (operfunc == REMAPSCON  || operfunc == GENSCON)  strcpy(line, "SCRIP first order conservative");
  else if (operfunc == REMAPCON2  || operfunc == GENCON2)  strcpy(line, "SCRIP second order conservative");
  else if (operfunc == REMAPLAF   || operfunc == GENLAF)   strcpy(line, "YAC largest area fraction");
  else if (operfunc == REMAPCON   || operfunc == GENCON)   strcpy(line, "YAC first order conservative");
  else if (operfunc == REMAPYCON2 || operfunc == GENYCON2) strcpy(line, "YAC second order conservative");
  else if (operfunc == REMAPAVG)   strcpy(line, "Average");
  else strcpy(line, "Unknown");
  // clang-format on

  strcat(line, remap_genweights ? " weights from " : " remapping from ");

  strcat(line, gridNamePtr(gridInqType(src_grid.gridID)));
  if (src_grid.rank == 2)
    snprintf(tmpstr, sizeof(tmpstr), " (%zux%zu)", src_grid.dims[0], src_grid.dims[1]);
  else
    snprintf(tmpstr, sizeof(tmpstr), " (%zu)", src_grid.dims[0]);
  strcat(line, tmpstr);
  strcat(line, " to ");
  strcat(line, gridNamePtr(gridInqType(tgt_grid.gridID)));
  if (tgt_grid.rank == 2)
    snprintf(tmpstr, sizeof(tmpstr), " (%zux%zu)", tgt_grid.dims[0], tgt_grid.dims[1]);
  else
    snprintf(tmpstr, sizeof(tmpstr), " (%zu)", tgt_grid.dims[0]);
  strcat(line, tmpstr);
  strcat(line, " grid");

  if (nmiss)
    {
      snprintf(tmpstr, sizeof(tmpstr), ", with source mask (%zu)", gridInqSize(src_grid.gridID) - nmiss);
      strcat(line, tmpstr);
    }

  cdo_print(line);
}

static void
remap_print_warning(const char *remap_file, int operfunc, RemapGrid &src_grid, size_t nmiss)
{
  char line[256], tmpstr[256];
  line[0] = 0;

  (void) operfunc;

  strcat(line, "Remap weights from ");
  strcat(line, remap_file);
  strcat(line, " not used, ");
  strcat(line, gridNamePtr(gridInqType(src_grid.gridID)));
  if (src_grid.rank == 2)
    snprintf(tmpstr, sizeof(tmpstr), " (%zux%zu)", src_grid.dims[0], src_grid.dims[1]);
  else
    snprintf(tmpstr, sizeof(tmpstr), " (%zu)", src_grid.dims[0]);
  strcat(line, tmpstr);
  strcat(line, " grid");

  if (nmiss)
    {
      snprintf(tmpstr, sizeof(tmpstr), " with mask (%zu)", gridInqSize(src_grid.gridID) - nmiss);
      strcat(line, tmpstr);
    }

  strcat(line, " not found!");

  cdo_warning(line);
}

double remap_threshhold = 2.0;
double pointSearchRadius = 180.0;
int remap_test = 0;
int remap_non_global = false;
int remap_num_srch_bins = 180;
static bool lremap_num_srch_bins = false;
static bool remap_extrapolate = false;
static bool lextrapolate = false;
static int MaxRemaps = -1;
int sort_mode = HEAP_SORT;
double remap_frac_min = 0.0;

static void
remapGetenv(void)
{
  char *envstr;

  envstr = getenv("MAX_REMAPS");
  if (envstr && *envstr)
    {
      const auto ival = atoi(envstr);
      if (ival > 0)
        {
          MaxRemaps = ival;
          if (Options::cdoVerbose) cdo_print("Set MAX_REMAPS to %d", MaxRemaps);
        }
    }

  envstr = getenv("REMAP_MAX_ITER");
  if (envstr && *envstr)
    {
      const auto ival = atoi(envstr);
      if (ival > 0)
        {
          remap_set_int(REMAP_MAX_ITER, ival);
          if (Options::cdoVerbose) cdo_print("Set REMAP_MAX_ITER to %d", ival);
        }
    }

  envstr = getenv("REMAP_TEST");
  if (envstr && *envstr)
    {
      const auto ival = atoi(envstr);
      if (ival > 0)
        {
          remap_test = ival;
          if (Options::cdoVerbose) cdo_print("Set REMAP_TEST to %d", remap_test);
        }
    }

#ifdef _OPENMP
  sort_mode = (Threading::ompNumThreads == 1) ? HEAP_SORT : MERGE_SORT;
#endif

  envstr = getenv("REMAP_SORT_MODE");
  if (envstr && *envstr)
    {
      if (strcmp(envstr, "heap") == 0)
        sort_mode = HEAP_SORT;
      else if (strcmp(envstr, "merge") == 0)
        sort_mode = MERGE_SORT;

      if (Options::cdoVerbose) cdo_print("Set sort_mode to %s", (sort_mode == HEAP_SORT) ? "HEAP_SORT" : "MERGE_SORT");
    }

  envstr = getenv("REMAP_THRESHHOLD");
  if (envstr && *envstr)
    {
      const auto fval = atof(envstr);
      if (fval > 0.0)
        {
          remap_threshhold = fval;
          if (Options::cdoVerbose) cdo_print("Set REMAP_THRESHHOLD to %g", remap_threshhold);
        }
    }

  remap_set_threshhold(remap_threshhold);

  envstr = getenv("CDO_REMAP_RADIUS");
  if (envstr && *envstr)
    {
      const auto fval = radius_str_to_deg(envstr);
      if (fval < 0.0 || fval > 180.0) cdo_abort("%s=%g out of bounds (0-180 deg)!", "CDO_REMAP_RADIUS", fval);
      pointSearchRadius = fval;
      if (Options::cdoVerbose) cdo_print("Set CDO_REMAP_RADIUS to %g", pointSearchRadius);
    }

  envstr = getenv("CDO_GRIDSEARCH_RADIUS");
  if (envstr && *envstr)
    {
      const auto fval = radius_str_to_deg(envstr);
      if (fval < 0.0 || fval > 180.0) cdo_abort("%s=%g out of bounds (0-180 deg)!", "CDO_GRIDSEARCH_RADIUS", fval);
      pointSearchRadius = fval;
      if (Options::cdoVerbose) cdo_print("Set CDO_GRIDSEARCH_RADIUS to %g", pointSearchRadius);
    }

  if (Options::cdoVerbose) cdo_print("Point search radius = %g deg", pointSearchRadius);

  envstr = getenv("REMAP_AREA_MIN");
  if (envstr && *envstr)
    {
      const auto fval = atof(envstr);
      if (fval > 0.0)
        {
          remap_frac_min = fval;
          if (Options::cdoVerbose) cdo_print("Set REMAP_AREA_MIN to %g", remap_frac_min);
        }
    }

  envstr = getenv("REMAP_NUM_SRCH_BINS");
  if (envstr && *envstr)
    {
      const auto ival = atoi(envstr);
      if (ival > 0)
        {
          remap_num_srch_bins = ival;
          lremap_num_srch_bins = true;
          if (Options::cdoVerbose) cdo_print("Set REMAP_NUM_SRCH_BINS to %d", remap_num_srch_bins);
        }
    }

  envstr = getenv("REMAP_NON_GLOBAL");
  if (envstr && *envstr)
    {
      const auto ival = atoi(envstr);
      if (ival >= 0)
        {
          remap_non_global = ival;
          if (Options::cdoVerbose) cdo_print("Set REMAP_NON_GLOBAL to %d", remap_non_global);
        }
    }

  envstr = getenv("REMAP_EXTRAPOLATE");
  if (envstr && *envstr)
    {
      if (memcmp(envstr, "ON", 2) == 0 || memcmp(envstr, "on", 2) == 0)
        {
          lextrapolate = true;
          remap_extrapolate = true;
        }
      else if (memcmp(envstr, "OFF", 3) == 0 || memcmp(envstr, "off", 3) == 0)
        {
          lextrapolate = true;
          remap_extrapolate = false;
        }
      else
        cdo_warning("Environment variable REMAP_EXTRAPOLATE has wrong value!");

      if (Options::cdoVerbose) cdo_print("Extrapolation %s!", remap_extrapolate ? "enabled" : "disabled");
    }

  envstr = getenv("CDO_REMAP_GENWEIGHTS");
  if (envstr && *envstr)
    {
      if (memcmp(envstr, "ON", 2) == 0 || memcmp(envstr, "on", 2) == 0)
        Options::REMAP_genweights = true;
      else if (memcmp(envstr, "OFF", 3) == 0 || memcmp(envstr, "off", 3) == 0)
        Options::REMAP_genweights = false;
      else
        cdo_warning("Environment variable CDO_REMAP_GENWEIGHTS has wrong value!");

      if (Options::cdoVerbose) cdo_print("Generation of weights %s!", Options::REMAP_genweights ? "enabled" : "disabled");
    }
}

static bool
gridIsGlobal(int gridID)
{
  auto global_grid = true;
  const auto non_global = (remap_non_global || !gridIsCircular(gridID));

  const auto gridtype = gridInqType(gridID);
  if (gridProjIsSupported(gridID) || (gridtype == GRID_LONLAT && non_global) || (gridtype == GRID_CURVILINEAR && non_global))
    global_grid = false;

  return global_grid;
}

template <typename T>
static void
scaleGridboxArea(size_t gridsize1, const Varray<T> &array1, size_t gridsize2, Varray<T> &array2, const Varray<double> &grid2_area)
{
  double array1sum = 0.0;
  for (size_t i = 0; i < gridsize1; i++) array1sum += array1[i];

  double array2sum = 0.0;
  for (size_t i = 0; i < gridsize2; i++) array2sum += grid2_area[i];
  for (size_t i = 0; i < gridsize2; i++) array2[i] = grid2_area[i] / array2sum * array1sum;

  static auto lgridboxinfo = true;
  if (lgridboxinfo)
    {
      cdo_print("gridbox_area replaced and scaled to %g", array1sum);
      lgridboxinfo = false;
    }
}

static void
scaleGridboxArea(const Field &field1, Field &field2, const Varray<double> &grid2_area)
{
  scaleGridboxArea(field1.gridsize, field1.vec_f, field2.gridsize, field2.vec_f, grid2_area);
  scaleGridboxArea(field1.gridsize, field1.vec_d, field2.gridsize, field2.vec_d, grid2_area);
}

template <typename T>
void gme_grid_restore(T *p, int ni, int nd);

static void
restore_gme_grid(const Field &field1, Field &field2, const RemapGrid &tgt_grid)
{
  const auto gridID2 = field2.grid;
  const auto gridsize2 = field2.gridsize;

  int nd, ni, ni2, ni3;
  gridInqParamGME(gridID2, &nd, &ni, &ni2, &ni3);

  if (field1.memType == MemType::Float)
    {
      auto j = tgt_grid.size;
      for (size_t i = gridsize2; i > 0; i--)
        if (tgt_grid.vgpm[i - 1]) field2.vec_f[i - 1] = field2.vec_f[--j];

      gme_grid_restore(field2.vec_f.data(), ni, nd);
    }
  else
    {
      auto j = tgt_grid.size;
      for (size_t i = gridsize2; i > 0; i--)
        if (tgt_grid.vgpm[i - 1]) field2.vec_d[i - 1] = field2.vec_d[--j];

      gme_grid_restore(field2.vec_d.data(), ni, nd);
    }
}

static void
store_gme_grid(Field &field, const Varray<int> &vgpm)
{
  const auto gridsize = field.gridsize;
  if (field.memType == MemType::Float)
    {
      for (size_t i = 0, j = 0; i < gridsize; i++)
        if (vgpm[i]) field.vec_f[j++] = field.vec_f[i];
    }
  else
    {
      for (size_t i = 0, j = 0; i < gridsize; i++)
        if (vgpm[i]) field.vec_d[j++] = field.vec_d[i];
    }
}

static int
set_remapgrids(int filetype, int vlistID, int ngrids, std::vector<bool> &remapgrids)
{
  (void) filetype;
  int index;
  for (index = 0; index < ngrids; index++)
    {
      remapgrids[index] = true;

      const auto gridID = vlistGrid(vlistID, index);
      const auto gridtype = gridInqType(gridID);
      const auto lprojparams = (gridtype == GRID_PROJECTION) && grid_has_proj_params(gridID);

      if (!gridProjIsSupported(gridID) && !lprojparams && gridtype != GRID_LONLAT && gridtype != GRID_GAUSSIAN
          && gridtype != GRID_GME && gridtype != GRID_CURVILINEAR && gridtype != GRID_UNSTRUCTURED
          && gridtype != GRID_GAUSSIAN_REDUCED)
        {
          /*
          if (gridtype == GRID_GAUSSIAN_REDUCED)
            {
              if (!Options::cdoRegulargrid && filetype == CDI_FILETYPE_GRB)
                cdo_abort("Unsupported grid type: %s, use CDO option -R to convert reduced to regular Gaussian grid!",
                         gridNamePtr(gridtype));
              else
                cdo_abort(
                    "Unsupported grid type: %s, use CDO operator -setgridtype,regular to convert reduced to regular Gaussian grid!",
                    gridNamePtr(gridtype));
            }
          else */
          if (gridtype == GRID_GENERIC && gridInqSize(gridID) <= 2)
            {
              remapgrids[index] = false;
            }
          else
            {
              char varname[CDI_MAX_NAME];
              const auto nvars = vlistNvars(vlistID);
              for (int varID = 0; varID < nvars; ++varID)
                if (gridID == vlistInqVarGrid(vlistID, varID))
                  {
                    vlistInqVarName(vlistID, varID, varname);
                    break;
                  }
              cdo_abort("Unsupported %s coordinates (Variable: %s)!", gridNamePtr(gridtype), varname);
            }
        }
    }

  for (index = 0; index < ngrids; index++)
    if (remapgrids[index]) break;
  if (index == ngrids) cdo_abort("No remappable grid found!");

  return index;
}

static int
set_max_remaps(int vlistID)
{
  int max_remaps = 0;

  const auto nzaxis = vlistNzaxis(vlistID);
  for (int index = 0; index < nzaxis; index++)
    {
      const auto zaxisID = vlistZaxis(vlistID, index);
      const auto zaxisSize = zaxisInqSize(zaxisID);
      if (zaxisSize > max_remaps) max_remaps = zaxisSize;
    }

  const auto nvars = vlistNvars(vlistID);
  if (nvars > max_remaps) max_remaps = nvars;

  max_remaps++;

  if (Options::cdoVerbose) cdo_print("Set max_remaps to %d", max_remaps);

  return max_remaps;
}

static NormOpt
get_normOpt(void)
{
  // clang-format off
  NormOpt normOpt(NormOpt::FRACAREA);

  char *envstr = getenv("CDO_REMAP_NORMALIZE_OPT");  // obsolete
  if (envstr && *envstr)
    {
      if      (memcmp(envstr, "frac", 4) == 0) normOpt = NormOpt::FRACAREA;
      else if (memcmp(envstr, "dest", 4) == 0) normOpt = NormOpt::DESTAREA;
      else if (memcmp(envstr, "none", 4) == 0) normOpt = NormOpt::NONE;
      else cdo_warning("CDO_REMAP_NORMALIZE_OPT=%s unsupported!", envstr);
    }

  envstr = getenv("CDO_REMAP_NORM");
  if (envstr && *envstr)
    {
      if      (memcmp(envstr, "frac", 4) == 0) normOpt = NormOpt::FRACAREA;
      else if (memcmp(envstr, "dest", 4) == 0) normOpt = NormOpt::DESTAREA;
      else if (memcmp(envstr, "none", 4) == 0) normOpt = NormOpt::NONE;
      else cdo_warning("CDO_REMAP_NORM=%s unsupported!", envstr);
    }

  if (Options::cdoVerbose)
    {
      const char *outStr = "none";
      if      (normOpt == NormOpt::FRACAREA) outStr = "frac";
      else if (normOpt == NormOpt::DESTAREA) outStr = "dest";
      cdo_print("Normalization option: %s", outStr);
    }
  // clang-format on

  return normOpt;
}

template <typename T>
static void
remap_normalize_field(NormOpt normOpt, size_t gridsize, Varray<T> &array, T missval, const RemapGrid &tgt_grid)
{
  // used to check the result of remapcon

  if (normOpt == NormOpt::NONE)
    {
      for (size_t i = 0; i < gridsize; i++)
        {
          if (!DBL_IS_EQUAL(array[i], missval))
            {
              const auto grid_err = tgt_grid.cell_frac[i] * tgt_grid.cell_area[i];

              if (std::fabs(grid_err) > 0)
                array[i] /= grid_err;
              else
                array[i] = missval;
            }
        }
    }
  else if (normOpt == NormOpt::DESTAREA)
    {
      for (size_t i = 0; i < gridsize; i++)
        {
          if (!DBL_IS_EQUAL(array[i], missval))
            {
              if (std::fabs(tgt_grid.cell_frac[i]) > 0)
                array[i] /= tgt_grid.cell_frac[i];
              else
                array[i] = missval;
            }
        }
    }
}

static void
remap_normalize_field(NormOpt normOpt, Field &field, const RemapGrid &tgt_grid)
{
  if (field.memType == MemType::Float)
    remap_normalize_field(normOpt, field.gridsize, field.vec_f, (float) field.missval, tgt_grid);
  else
    remap_normalize_field(normOpt, field.gridsize, field.vec_d, field.missval, tgt_grid);
}

template <typename T>
static void
remap_set_frac_min(size_t gridsize, Varray<T> &array, T missval, RemapGrid *tgt_grid)
{
  if (remap_frac_min > 0.0)
    {
      for (size_t i = 0; i < gridsize; i++)
        if (tgt_grid->cell_frac[i] < remap_frac_min) array[i] = missval;
    }
}

static void
remap_set_frac_min(Field &field, RemapGrid *tgt_grid)
{
  if (field.memType == MemType::Float)
    remap_set_frac_min(field.gridsize, field.vec_f, (float) field.missval, tgt_grid);
  else
    remap_set_frac_min(field.gridsize, field.vec_d, field.missval, tgt_grid);
}

int timer_remap, timer_remap_init, timer_remap_sort;

static void
remapTimerInit(void)
{
  timer_remap = timer_new("remap");
  timer_remap_init = timer_new("remap init");
  timer_remap_sort = timer_new("remap sort");
}

static void
remapLinksPerValue(RemapVars &rv)
{
  long lpv = -1;

  rv.links_per_value = lpv;
  return;
  /*
  size_t num_links = rv.num_links;

  if ( num_links > 0 )
    {
      lpv = 1;
      const auto &dst_add = rv.tgtCellIndices;
      size_t n = 0;
      size_t ival = dst_add[n];
      for ( n = 1; n < num_links; ++n )
        if ( dst_add[n] == ival ) lpv++;
        else break;

    printf("lpv %zu\n", lpv);

      if ( num_links%lpv != 0 ) lpv = 0;

      n++;
      if ( n < num_links )
        {
          size_t lpv2 = 0;
          size_t ival2 = dst_add[n];
          for ( ; n < num_links; ++n )
            if ( dst_add[n] == ival2 ) lpv2++;
            else if ( lpv == lpv2 )
              {
                lpv2 = 0;
                ival2 = dst_add[n];
              }
            else
              {
                lpv = 0;
                break;
              }
        }

  printf("lpv %zu\n", lpv);

      if ( lpv == 1 )
        {
          for ( size_t n = 1; n < num_links; ++n )
            {
              if ( dst_add[n] == dst_add[n-1] )
                {
                  lpv = 0;
                  break;
                }
            }
        }
      else if ( lpv > 1 )
        {
          for ( size_t n = 1; n < num_links/lpv; ++n )
            {
              ival = dst_add[n*lpv];
              for ( size_t k = 1; k < lpv; ++k )
                {
                  if ( dst_add[n*lpv+k] != ival )
                    {
                      lpv = 0;
                      break;
                    }
                }
              if ( lpv == 0 ) break;
            }
        }
    }

  printf("lpv %zu\n", lpv);

  rv.links_per_value = lpv;
  */
}

static void
remapSortAddr(RemapVars &rv)
{
  if (Options::Timer) timer_start(timer_remap_sort);
  if (sort_mode == MERGE_SORT)
    { /*
      ** use a combination of the old sort_add and a split and merge approach.
      ** The chunk size is determined by MERGE_SORT_LIMIT_SIZE in remaplib.c.
      ** OpenMP parallelism is supported
      */
      sort_iter(rv.num_links, rv.num_wts, &rv.tgtCellIndices[0], &rv.srcCellIndices[0], &rv.wts[0], Threading::ompNumThreads);
    }
  else
    { /* use a pure heap sort without any support of parallelism */
      sort_add(rv.num_links, rv.num_wts, &rv.tgtCellIndices[0], &rv.srcCellIndices[0], &rv.wts[0]);
    }
  if (Options::Timer) timer_stop(timer_remap_sort);
}

static int
remapGenNumBins(int ysize)
{
  constexpr int maxbins = 720;
  int num_srch_bins = ysize / 2 + ysize % 2;
  if (num_srch_bins > maxbins) num_srch_bins = maxbins;
  if (num_srch_bins < 1) num_srch_bins = 1;
  return num_srch_bins;
}

void
remapInit(RemapType &remap)
{
  remap.nused = 0;
  remap.gridID = -1;
  remap.gridsize = 0;
  remap.nmiss = 0;
}

static void
remapGenWeights(RemapMethod mapType, RemapType &remap, int numNeighbors)
{
  // clang-format off
  if      (mapType == RemapMethod::CONSERV_SCRIP) remap_conserv_weights_scrip(remap.search, remap.vars);
  else if (mapType == RemapMethod::BILINEAR)      remap_bilinear_weights(remap.search, remap.vars);
  else if (mapType == RemapMethod::BICUBIC)       remap_bicubic_weights(remap.search, remap.vars);
  else if (mapType == RemapMethod::DISTWGT)       remap_distwgt_weights(numNeighbors, remap.search, remap.vars);
  else if (mapType == RemapMethod::CONSERV)       remap_conserv_weights(remap.search, remap.vars);
  // clang-format on

  if (remap.vars.sort_add) remapSortAddr(remap.vars);
  if (remap.vars.links_per_value == -1) remapLinksPerValue(remap.vars);
}

static void
remapField(RemapMethod mapType, RemapType &remap, int numNeighbors, const Field &field1, Field &field2)
{
  // clang-format off
  if      (mapType == RemapMethod::BILINEAR) remap_bilinear(remap.search, field1, field2);
  else if (mapType == RemapMethod::BICUBIC)  remap_bicubic(remap.search, field1, field2);
  else if (mapType == RemapMethod::DISTWGT)  remap_dist_wgt(numNeighbors, remap.search, field1, field2);
  else if (mapType == RemapMethod::CONSERV)  remap_conserv(remap.vars.normOpt, remap.search, field1, field2);
  // clang-format on
}

static void
addOperators(void)
{
  // clang-format off
  cdo_operator_add("remap",        REMAP,        0, nullptr);
  cdo_operator_add("remapcon",     REMAPCON,     0, nullptr);
  cdo_operator_add("remapycon2test",   REMAPYCON2,   0, nullptr);
  cdo_operator_add("remapscon",    REMAPSCON,    0, nullptr);
  cdo_operator_add("remapcon2",    REMAPCON2,    0, nullptr);
  cdo_operator_add("remapbil",     REMAPBIL,     0, nullptr);
  cdo_operator_add("remapbic",     REMAPBIC,     0, nullptr);
  cdo_operator_add("remapdis",     REMAPDIS,     0, nullptr);
  cdo_operator_add("remapnn",      REMAPNN,      0, nullptr);
  cdo_operator_add("remaplaf",     REMAPLAF,     0, nullptr);
  cdo_operator_add("remapavgtest", REMAPAVG,     0, nullptr);
  cdo_operator_add("gencon",       GENCON,       1, nullptr);
  cdo_operator_add("genycon2test",     GENYCON2,     1, nullptr);
  cdo_operator_add("genscon",      GENSCON,      1, nullptr);
  cdo_operator_add("gencon2",      GENCON2,      1, nullptr);
  cdo_operator_add("genbil",       GENBIL,       1, nullptr);
  cdo_operator_add("genbic",       GENBIC,       1, nullptr);
  cdo_operator_add("gendis",       GENDIS,       1, nullptr);
  cdo_operator_add("gennn",        GENNN,        1, nullptr);
  cdo_operator_add("genlaf",       GENLAF,       1, nullptr);
  // clang-format on
}

void *
Remap(void *argument)
{
  RemapMethod mapType(RemapMethod::UNDEF);
  SubmapType submapType(SubmapType::NONE);
  bool remap_genweights = Options::REMAP_genweights;
  CdoStreamID streamID2 = CDO_STREAM_UNDEF;
  int varID, levelID;
  size_t nmiss2 = 0;
  int remapIndex = -1;
  int nremaps = 0;
  int numNeighbors = 0;
  const char *remap_file = nullptr;

  if (Options::Timer) remapTimerInit();

  cdo_initialize(argument);

  addOperators();

  const auto operatorID = cdo_operator_id();
  auto operfunc = cdo_operator_f1(operatorID);
  const bool writeRemapWeightsOnly = cdo_operator_f2(operatorID);
  const auto doRemap = (operfunc == REMAP);

  remap_set_int(REMAP_WRITE_REMAP, writeRemapWeightsOnly);

  if (operfunc == REMAPDIS || operfunc == GENDIS || operfunc == REMAPNN || operfunc == GENNN) remap_extrapolate = true;

  remapGetenv();

  if (Options::cdoVerbose) cdo_print("Extrapolation %s!", remap_extrapolate ? "enabled" : "disabled");

  if (doRemap)
    {
      operator_input_arg("grid description file or name, remap weights file (SCRIP NetCDF)");
      operator_check_argc(2);
      remap_file = cdo_operator_argv(1).c_str();
    }
  else
    {
      operator_input_arg("grid description file or name");
      if (operfunc == REMAPDIS && cdo_operator_argc() == 2)
        {
          const auto inum = parameter_to_int(cdo_operator_argv(1));
          if (inum < 1) cdo_abort("Number of nearest neighbors out of range (>0)!");
          numNeighbors = inum;
        }
      else
        {
          operator_check_argc(1);
        }
    }

  const auto gridID2 = cdo_define_grid(cdo_operator_argv(0));

  if (gridInqType(gridID2) == GRID_GENERIC) cdo_abort("Unsupported target grid type (generic)!");

  const auto streamID1 = cdo_open_read(0);

  const auto filetype = cdo_inq_filetype(streamID1);

  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);

  const auto ngrids = vlistNgrids(vlistID1);
  std::vector<bool> remapgrids(ngrids);
  const auto gridID1 = vlistGrid(vlistID1, set_remapgrids(filetype, vlistID1, ngrids, remapgrids));

  for (int index = 0; index < ngrids; index++)
    if (remapgrids[index]) vlistChangeGridIndex(vlistID2, index, gridID2);

  if (MaxRemaps == -1) MaxRemaps = set_max_remaps(vlistID1);
  if (MaxRemaps < 1) cdo_abort("max_remaps out of range (>0)!");

  std::vector<RemapType> remaps(MaxRemaps);
  // for (int r = 0; r < MaxRemaps; r++) remapInit(remaps[r]);
  for (auto &rmap : remaps) remapInit(rmap);

  if (writeRemapWeightsOnly || doRemap) remap_genweights = true;

  int remapOrder = 0;
  if (doRemap)
    {
      remap_read_data_scrip(remap_file, gridID1, gridID2, mapType, submapType, numNeighbors, remapOrder, remaps[0].src_grid,
                            remaps[0].tgt_grid, remaps[0].vars);

      if (remaps[0].vars.links_per_value == 0) remapLinksPerValue(remaps[0].vars);

      nremaps = 1;
      auto gridsize = remaps[0].src_grid.size;
      remaps[0].gridID = gridID1;
      remaps[0].gridsize = gridInqSize(gridID1);

      if (mapType == RemapMethod::DISTWGT && !lextrapolate) remap_extrapolate = true;
      if (gridIsCircular(gridID1) && !lextrapolate) remap_extrapolate = true;

      if (gridInqType(gridID1) == GRID_GME) gridsize = remaps[0].src_grid.nvgp;

      if (gridsize != remaps[0].gridsize) cdo_abort("Size of source grid and weights from %s differ!", remap_file);

      if (gridInqType(gridID1) == GRID_GME) gridsize = remaps[0].src_grid.size;

      for (size_t i = 0; i < gridsize; i++)
        if (remaps[0].src_grid.mask[i] == false) remaps[0].nmiss++;

      auto gridsize2 = gridInqSize(gridID2);
      if (gridInqType(gridID2) == GRID_GME)
        {
          remaps[0].tgt_grid.nvgp = gridInqSize(gridID2);
          remaps[0].tgt_grid.vgpm.resize(gridInqSize(gridID2));
          const auto gridID2_gme = gridToUnstructured(gridID2, 1);
          gridInqMaskGME(gridID2_gme, &remaps[0].tgt_grid.vgpm[0]);
          gridDestroy(gridID2_gme);
          size_t isize = 0;
          for (size_t i = 0; i < gridsize2; ++i)
            if (remaps[0].tgt_grid.vgpm[i]) isize++;
          gridsize2 = isize;
        }
      // printf("grid2 %zu %d %zu\n", gridsize2, remaps[0].tgt_grid.nvgp, remaps[0].tgt_grid.size);
      if (remaps[0].tgt_grid.size != gridsize2) cdo_abort("Size of target grid and weights from %s differ!", remap_file);

      operfunc = maptype2operfunc(mapType, submapType, numNeighbors, remapOrder);

      if (remap_test) remap_vars_reorder(remaps[0].vars);
    }
  else
    {
      getMaptype(operfunc, mapType, submapType, numNeighbors, remapOrder);
    }

  // if (!remap_genweights && (mapType == RemapMethod::CONSERV_SCRIP || mapType == RemapMethod::CONSERV)) remap_genweights = true;
  if (!remap_genweights && mapType == RemapMethod::CONSERV_SCRIP) remap_genweights = true;

  bool useMask = !(!remap_genweights
                   && (mapType == RemapMethod::BILINEAR || mapType == RemapMethod::BICUBIC || mapType == RemapMethod::DISTWGT
                       || mapType == RemapMethod::CONSERV));

  remap_set_int(REMAP_GENWEIGHTS, (int) remap_genweights);

  NormOpt normOpt(NormOpt::NONE);
  if (mapType == RemapMethod::CONSERV_SCRIP || mapType == RemapMethod::CONSERV) normOpt = get_normOpt();

  const auto grid1sizemax = vlistGridsizeMax(vlistID1);

  auto needGradients = (mapType == RemapMethod::BICUBIC);
  if ((mapType == RemapMethod::CONSERV_SCRIP || mapType == RemapMethod::CONSERV) && remapOrder == 2)
    {
      if (Options::cdoVerbose) cdo_print("Second order remapping");
      needGradients = true;
    }

  RemapGradients gradients;
  if (needGradients) gradients.init(grid1sizemax);

  VarList varList1;
  varListInit(varList1, vlistID1);

  VarList varList2;
  varListInit(varList2, vlistID2);

  Varray<short> imask;

  Field field1, field2;

  if (!writeRemapWeightsOnly)
    {
      streamID2 = cdo_open_write(1);
      cdo_def_vlist(streamID2, vlistID2);
    }

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

      cdo_taxis_copy_timestep(taxisID2, taxisID1);

      if (!writeRemapWeightsOnly) cdo_def_timestep(streamID2, tsID);

      for (int recID = 0; recID < nrecs; recID++)
        {
          cdo_inq_record(streamID1, &varID, &levelID);
          field1.init(varList1[varID]);
          cdo_read_record(streamID1, field1);
          const auto nmiss1 = useMask ? field1.nmiss : 0;

          field2.init(varList2[varID]);

          const auto gridID = varList1[varID].gridID;
          const auto missval = varList1[varID].missval;
          auto gridsize = varList1[varID].gridsize;

          auto skipVar = false;
          if (!remapgrids[vlistGridIndex(vlistID1, gridID)])
            {
              if (writeRemapWeightsOnly)
                continue;
              else
                {
                  nmiss2 = nmiss1;
                  field_copy(field1, field2);
                  skipVar = true;
                }
            }

          if (!skipVar)
            {
              if (mapType != RemapMethod::CONSERV_SCRIP && mapType != RemapMethod::CONSERV && gridInqType(gridID) == GRID_GME)
                cdo_abort("Only conservative remapping is available to remap between GME grids!");

              if (gridIsCircular(gridID) && !lextrapolate) remap_extrapolate = true;

              imask.resize(gridsize);

              if (field1.memType == MemType::Float)
                for (size_t i = 0; i < gridsize; i++) imask[i] = useMask ? !DBL_IS_EQUAL(field1.vec_f[i], (float) missval) : 1;
              else
                for (size_t i = 0; i < gridsize; i++) imask[i] = useMask ? !DBL_IS_EQUAL(field1.vec_d[i], missval) : 1;

              for (remapIndex = nremaps - 1; remapIndex >= 0; remapIndex--)
                {
                  if (gridID == remaps[remapIndex].gridID)
                    {
                      if ((useMask && nmiss1 == remaps[remapIndex].nmiss && imask == remaps[remapIndex].src_grid.mask) || !useMask)
                        {
                          remaps[remapIndex].nused++;
                          break;
                        }
                    }
                }

              if (Options::cdoVerbose && remapIndex >= 0) cdo_print("Using remap %d", remapIndex);

              if (remapIndex < 0)
                {
                  if (nremaps < MaxRemaps)
                    {
                      remapIndex = nremaps;
                      nremaps++;
                    }
                  else
                    {
                      const int remapIndex0 = (MaxRemaps > 1 && remaps[0].nused > remaps[1].nused);
                      remap_vars_free(remaps[remapIndex0].vars);
                      remap_grid_free(remaps[remapIndex0].src_grid);
                      remap_grid_free(remaps[remapIndex0].tgt_grid);
                      remap_search_free(remaps[remapIndex0].search);
                      remapIndex = remapIndex0;
                      remapInit(remaps[remapIndex]);
                    }

                  if (remaps[remapIndex].gridID != gridID)
                    {
                      if (gridIsCircular(gridID) && !lextrapolate) remap_extrapolate = true;
                      remaps[remapIndex].src_grid.non_global = false;
                      if (mapType == RemapMethod::DISTWGT && !remap_extrapolate && gridInqSize(gridID) > 1 && !gridIsGlobal(gridID))
                        remaps[remapIndex].src_grid.non_global = true;

                      //  remaps[remapIndex].src_grid.luse_cell_area = false;
                      //  remaps[remapIndex].tgt_grid.luse_cell_area = false;

                      if (gridInqType(gridID) != GRID_UNSTRUCTURED && !lremap_num_srch_bins)
                        {
                          remap_num_srch_bins
                              = (!remap_extrapolate && mapType == RemapMethod::DISTWGT) ? 1 : remapGenNumBins(gridInqYsize(gridID));
                        }

                      remap_set_int(REMAP_NUM_SRCH_BINS, remap_num_srch_bins);

                      remaps[remapIndex].vars.normOpt = normOpt;
                      remaps[remapIndex].vars.pinit = false;

                      if ((mapType == RemapMethod::BILINEAR || mapType == RemapMethod::BICUBIC)
                          && (gridInqType(gridID) == GRID_GME || gridInqType(gridID) == GRID_UNSTRUCTURED))
                        cdo_abort("Bilinear/bicubic interpolation doesn't support unstructured source grids!");

                      // Initialize grid information for both grids
                      if (Options::Timer) timer_start(timer_remap_init);
                      remap_init_grids(mapType, remap_extrapolate, gridID, remaps[remapIndex].src_grid, gridID2, remaps[remapIndex].tgt_grid);
                      remap_search_init(mapType, remaps[remapIndex].search, remaps[remapIndex].src_grid, remaps[remapIndex].tgt_grid);
                      if (Options::Timer) timer_stop(timer_remap_init);
                    }

                  remaps[remapIndex].gridID = gridID;
                  remaps[remapIndex].nmiss = nmiss1;

                  if (gridInqType(gridID) == GRID_GME)
                    {
                      for (size_t i = 0, j = 0; i < gridsize; i++)
                        if (remaps[remapIndex].src_grid.vgpm[i]) imask[j++] = imask[i];
                    }

                  varray_copy(remaps[remapIndex].src_grid.size, imask, remaps[remapIndex].src_grid.mask);

                  if (mapType == RemapMethod::CONSERV_SCRIP || mapType == RemapMethod::CONSERV)
                    {
                      varray_fill(remaps[remapIndex].src_grid.cell_area, 0.0);
                      varray_fill(remaps[remapIndex].src_grid.cell_frac, 0.0);
                      varray_fill(remaps[remapIndex].tgt_grid.cell_area, 0.0);
                    }
                  varray_fill(remaps[remapIndex].tgt_grid.cell_frac, 0.0);

                  // initialize some remapping variables
                  remap_vars_init(mapType, remapOrder, remaps[remapIndex].vars);

                  remap_print_info(operfunc, remap_genweights, remaps[remapIndex].src_grid, remaps[remapIndex].tgt_grid, nmiss1);

                  if (remap_genweights)
                    {
                      remapGenWeights(mapType, remaps[remapIndex], numNeighbors);

                      if (writeRemapWeightsOnly) goto WRITE_REMAP;

                      if (remap_test) remap_vars_reorder(remaps[remapIndex].vars);
                    }
                }

              if (gridInqType(gridID) == GRID_GME) store_gme_grid(field1, remaps[remapIndex].src_grid.vgpm);

              const auto gridsize2 = gridInqSize(gridID2);

              if (remap_genweights)
                {
                  remaps[remapIndex].nused++;

                  if (needGradients)
                    {
                      if (remaps[remapIndex].src_grid.rank != 2 && remapOrder == 2)
                        cdo_abort("Second order remapping is not available for unstructured grids!");

                      remap_gradients(remaps[remapIndex].src_grid, field1, gradients);
                    }

                  if (operfunc == REMAPLAF)
                    remap_laf(field2, missval, gridsize2, remaps[remapIndex].vars, field1);
                  else if (operfunc == REMAPAVG)
                    remap_avg(field2, missval, gridsize2, remaps[remapIndex].vars, field1);
                  else
                    remap(field2, missval, gridsize2, remaps[remapIndex].vars, field1, gradients);
                }
              else
                {
                  remapField(mapType, remaps[remapIndex], numNeighbors, field1, field2);
                }

              if (operfunc == REMAPSCON || operfunc == REMAPCON2 || operfunc == REMAPCON || operfunc == REMAPYCON2)
                {
                  // used only to check the result of remapcon
                  if (0) remap_normalize_field(remaps[remapIndex].vars.normOpt, field2, remaps[remapIndex].tgt_grid);

                  remap_set_frac_min(field2, &remaps[remapIndex].tgt_grid);
                }

              if (operfunc == REMAPSCON || operfunc == REMAPCON2 || operfunc == REMAPCON || operfunc == REMAPYCON2)
                {
                  if (strcmp(varList1[varID].name, "gridbox_area") == 0)
                    scaleGridboxArea(field1, field2, remaps[remapIndex].tgt_grid.cell_area);
                }

              // calculate some statistics
              if (Options::cdoVerbose)
                remap_stat(remapOrder, remaps[remapIndex].src_grid, remaps[remapIndex].tgt_grid, remaps[remapIndex].vars, field1, field2);

              if (gridInqType(gridID2) == GRID_GME) restore_gme_grid(field1, field2, remaps[remapIndex].tgt_grid);

              nmiss2 = field_num_mv(field2);
            }

          cdo_def_record(streamID2, varID, levelID);
          field2.nmiss = nmiss2;
          cdo_write_record(streamID2, field2);
        }

      tsID++;
    }

  cdo_stream_close(streamID2);

WRITE_REMAP:

  if (writeRemapWeightsOnly)
    remap_write_data_scrip(cdo_get_stream_name(1), mapType, submapType, numNeighbors, remapOrder, remaps[remapIndex].src_grid,
                           remaps[remapIndex].tgt_grid, remaps[remapIndex].vars);

  cdo_stream_close(streamID1);

  if (doRemap && remap_genweights && remaps[0].nused == 0)
    remap_print_warning(remap_file, operfunc, remaps[0].src_grid, remaps[0].nmiss);

  for (remapIndex = 0; remapIndex < nremaps; remapIndex++)
    {
      remap_vars_free(remaps[remapIndex].vars);
      remap_grid_free(remaps[remapIndex].src_grid);
      remap_grid_free(remaps[remapIndex].tgt_grid);
      remap_search_free(remaps[remapIndex].search);
    }

  cdo_finish();

  return nullptr;
}
