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

  Author: Uwe Schulzweida

*/

#include "remap_utils.h"
#include "param_conversion.h"
#include "cdo_output.h"
#include "util_string.h"
#include "mpim_grid.h"

void remap_sort_addr(RemapVars &rv);

void
remap_print_info(int operfunc, bool remap_genweights, const RemapGrid &srcGrid, const RemapGrid &tgtGrid, size_t nmiss, int numNeighbors)
{
  std::string outStr;
  // clang-format off
  if      (operfunc == REMAPBIL   || operfunc == GENBIL)   outStr = "Bilinear";
  else if (operfunc == REMAPBIC   || operfunc == GENBIC)   outStr = "Bicubic";
  else if (operfunc == REMAPNN    || operfunc == GENNN)    outStr = "Nearest neighbor";
  else if (operfunc == REMAPDIS   || operfunc == GENDIS)   outStr = "Distance-weighted averaged";
  else if (operfunc == REMAPSCON  || operfunc == GENSCON)  outStr = "SCRIP first order conservative";
  else if (operfunc == REMAPCON2  || operfunc == GENCON2)  outStr = "SCRIP second order conservative";
  else if (operfunc == REMAPLAF   || operfunc == GENLAF)   outStr = "YAC largest area fraction";
  else if (operfunc == REMAPCON   || operfunc == GENCON)   outStr = "YAC first order conservative";
  else if (operfunc == REMAPYCON2 || operfunc == GENYCON2) outStr = "YAC second order conservative";
  else if (operfunc == REMAPAVG)                           outStr = "Average";
  else                                                     outStr = "Unknown";
  // clang-format on

  if ((operfunc == REMAPDIS || operfunc == GENDIS) && numNeighbors != 4) outStr += " (n=" + std::to_string(numNeighbors) + ")";

  outStr += remap_genweights ? " weights from " : " remapping from ";
  outStr += srcGrid.name;
  outStr += " (" + std::to_string(srcGrid.dims[0]);
  if (srcGrid.rank == 2) outStr += "x" + std::to_string(srcGrid.dims[1]);
  outStr += ") to ";
  outStr += tgtGrid.name;
  outStr += " (" + std::to_string(tgtGrid.dims[0]);
  if (tgtGrid.rank == 2) outStr += "x" + std::to_string(tgtGrid.dims[1]);
  outStr += ") grid";

  if (nmiss) outStr += ", with source mask (" + std::to_string(gridInqSize(srcGrid.gridID) - nmiss) + ")";

  cdo_print(outStr);
}

void
remap_print_warning(const std::string &remapWeightsFile, int operfunc, const RemapGrid &srcGrid, size_t nmiss)
{
  (void) operfunc;

  std::string outStr = "Remap weights from ";
  outStr += remapWeightsFile;
  outStr += " not used, ";
  outStr += gridNamePtr(gridInqType(srcGrid.gridID));
  outStr += " (" + std::to_string(srcGrid.dims[0]);
  if (srcGrid.rank == 2) outStr += "x" + std::to_string(srcGrid.dims[1]);
  outStr += ") grid";

  if (nmiss) outStr += " with mask (" + std::to_string(gridInqSize(srcGrid.gridID) - nmiss) + ")";

  outStr += " not found!";

  cdo_warning(outStr);
}

RemapParams
remap_get_params()
{
  RemapParams remapParams;

  {
    auto envString = getenv_string("REMAP_NUM_SRCH_BINS");
    if (envString.size())
      {
        auto ival = atoi(envString.c_str());
        if (ival > 0)
          {
            remapParams.numSearchBins = ival;
            remapParams.numSearchBinsIsSet = true;
            if (Options::cdoVerbose) cdo_print("Set REMAP_NUM_SRCH_BINS to %d", remapParams.numSearchBins);
          }
      }
  }

  {
    auto envString = getenv_string("REMAP_EXTRAPOLATE");
    if (envString.size())
      {
        // clang-format off
        if      (envString == "ON"  || envString == "on")  remapParams.extrapolate = true;
        else if (envString == "OFF" || envString == "off") remapParams.extrapolate = false;
        else cdo_warning("Environment variable REMAP_EXTRAPOLATE has wrong value!");
        // clang-format on

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

  {
    auto envString = getenv_string("CDO_REMAP_GENWEIGHTS");
    if (envString.size())
      {
        // clang-format off
        if      (envString == "ON"  || envString == "on")  Options::REMAP_genweights = true;
        else if (envString == "OFF" || envString == "off") Options::REMAP_genweights = false;
        else cdo_warning("Environment variable CDO_REMAP_GENWEIGHTS has wrong value!");
        // clang-format on

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

  {
    auto envString = getenv_string("CDO_REMAP_RADIUS");
    if (envString.size())
      {
        auto fval = radius_str_to_deg(envString);
        if (fval < 0.0 || fval > 180.0) cdo_abort("%s=%g out of bounds (0-180 deg)!", "CDO_REMAP_RADIUS", fval);
        cdo_set_search_radius(fval);
        if (Options::cdoVerbose) cdo_print("Set CDO_REMAP_RADIUS to %g", cdo_get_search_radius());
      }
  }

  {
    auto envString = getenv_string("CDO_GRIDSEARCH_RADIUS");
    if (envString.size())
      {
        auto fval = radius_str_to_deg(envString);
        if (fval < 0.0 || fval > 180.0) cdo_abort("%s=%g out of bounds (0-180 deg)!", "CDO_GRIDSEARCH_RADIUS", fval);
        cdo_set_search_radius(fval);
        if (Options::cdoVerbose) cdo_print("Set CDO_GRIDSEARCH_RADIUS to %g", cdo_get_search_radius());
      }
  }

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

  {
    auto envString = getenv_string("REMAP_MAX_ITER");
    if (envString.size())
      {
        auto ival = atoi(envString.c_str());
        if (ival > 0)
          {
            remap_set_int(REMAP_MAX_ITER, ival);
            if (Options::cdoVerbose) cdo_print("Set REMAP_MAX_ITER to %d", ival);
          }
      }
  }

  {
    auto envString = getenv_string("REMAP_TEST");
    if (envString.size())
      {
        auto ival = atoi(envString.c_str());
        if (ival > 0)
          {
            remapParams.test = ival;
            if (Options::cdoVerbose) cdo_print("Set REMAP_TEST to %d", remapParams.test);
          }
      }
  }

  {
    auto envString = getenv_string("REMAP_AREA_MIN");
    if (envString.size())
      {
        auto fval = atof(envString.c_str());
        if (fval > 0.0)
          {
            remapParams.fracMin = fval;
            if (Options::cdoVerbose) cdo_print("Set REMAP_AREA_MIN to %g", remapParams.fracMin);
          }
      }
  }

  {
    auto envString = getenv_string("REMAP_THRESHHOLD");
    if (envString.size())
      {
        auto fval = atof(envString.c_str());
        if (fval > 0.0)
          {
            remapParams.threshhold = fval;
            if (Options::cdoVerbose) cdo_print("Set REMAP_THRESHHOLD to %g", remapParams.threshhold);
          }
      }
  }

#ifdef _OPENMP
  remapParams.sortMode = (Threading::ompNumThreads == 1) ? HEAP_SORT : MERGE_SORT;
#endif

  {
    auto envString = getenv_string("REMAP_SORT_MODE");
    if (envString.size())
      {
        // clang-format off
        if      (envString == "heap")  remapParams.sortMode = HEAP_SORT;
        else if (envString == "merge") remapParams.sortMode = MERGE_SORT;
        // clang-format on

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

  {
    auto envString = getenv_string("MAX_REMAPS");
    if (envString.size())
      {
        auto ival = atoi(envString.c_str());
        if (ival > 0)
          {
            remapParams.maxRemaps = ival;
            if (Options::cdoVerbose) cdo_print("Set MAX_REMAPS to %d", remapParams.maxRemaps);
          }
      }
  }

  return remapParams;
}

void
remap_set_params(const RemapParams &remapParams)
{
  remap_set_threshhold(remapParams.threshhold);
}

RemapSwitches
remap_operfunc_to_maptype(int operfunc)
{
  RemapSwitches remapSwitches;
  remapSwitches.remapOrder = 1;

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

  return remapSwitches;
}

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

  auto envString = getenv_string("CDO_REMAP_NORM");
  if (envString.size())
    {
      if      (envString == "frac") normOpt = NormOpt::FRACAREA;
      else if (envString == "dest") normOpt = NormOpt::DESTAREA;
      else if (envString == "none") normOpt = NormOpt::NONE;
      else cdo_warning("CDO_REMAP_NORM=%s unsupported!", envString);
    }

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

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

  rv.linksPerValue = lpv;
  return;
  /*
  size_t numLinks = rv.numLinks;

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

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

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

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

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

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

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

  rv.linksPerValue = lpv;
  */
}

static void
remap_sort_addr(int sortMode, RemapVars &rv)
{
  if (sortMode == 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.numLinks, 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.numLinks, rv.num_wts, &rv.tgtCellIndices[0], &rv.srcCellIndices[0], &rv.wts[0]);
    }
}

void
remap_gen_weights(int sortMode, const RemapSwitches &remapSwitches, RemapType &remap)
{
  auto mapType = remapSwitches.mapType;
  auto numNeighbors = remapSwitches.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) remap_sort_addr(sortMode, remap.vars);
  if (remap.vars.linksPerValue == -1) remap_linksPerValue(remap.vars);
}

int
remap_gen_numbins(int ysize)
{
  constexpr int MaxBins = 720;
  int numSearchBins = ysize / 2 + ysize % 2;
  return std::clamp(numSearchBins, 1, MaxBins);
}

std::vector<bool>
remap_set_grids(int vlistID, const VarList &varList)
{
  auto numGrids = vlistNgrids(vlistID);
  std::vector<bool> remapGrids(numGrids, true);
  for (int index = 0; index < numGrids; ++index)
    {
      auto gridID = vlistGrid(vlistID, index);
      auto gridtype = gridInqType(gridID);
      auto hasProjParams = ((gridtype == GRID_PROJECTION) && grid_has_proj_params(gridID));

      if (!gridProjIsSupported(gridID) && !hasProjParams && gridtype != GRID_LONLAT && gridtype != GRID_GAUSSIAN
          && gridtype != GRID_GME && gridtype != GRID_CURVILINEAR && gridtype != GRID_UNSTRUCTURED
          && gridtype != GRID_GAUSSIAN_REDUCED)
        {
          if (gridtype == GRID_GENERIC && gridInqSize(gridID) <= 2) { remapGrids[index] = false; }
          else
            {
              auto numVars = vlistNvars(vlistID);
              for (int varID = 0; varID < numVars; ++varID)
                if (gridID == varList[varID].gridID)
                  {
                    cdo_abort("Unsupported %s coordinates (Variable: %s)!", gridNamePtr(gridtype), varList[varID].name);
                    break;
                  }
            }
        }
    }

  return remapGrids;
}
