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

  Copyright (C) 2003-2020 Uwe Schulzweida, <uwe.schulzweida AT mpimet.mpg.de>
  See COPYING file for copying and redistribution conditions.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; version 2 of the License.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <cstdio>

#include "process_int.h"
#include "cdo_math.h"
#include "cdo_options.h"
#include "dmemory.h"
#include "grid_point_search.h"
#include <mpim_grid.h>
#include "kdtreelib/kdtree.h"
#include "nanoflann.hpp"
extern "C"
{
#include "lib/yac/sphere_part.h"
}

#define PI M_PI
#define PI2 (2.0 * PI)

PointSearchMethod pointSearchMethod(PointSearchMethod::nanoflann);

struct gpsFull
{
  size_t n;
  const double *plons;
  const double *plats;
  double **pts;
};

template <typename T>
struct PointCloud
{
  struct Point
  {
    T x, y, z;
  };
  std::vector<Point> pts;
  T min[3], max[3];

  // Must return the number of data points
  inline size_t
  kdtree_get_point_count() const
  {
    return pts.size();
  }

  // Returns the dim'th component of the idx'th point in the class:
  // Since this is inlined and the "dim" argument is typically an immediate
  // value, the
  //  "if/else's" are actually solved at compile time.
  inline T
  kdtree_get_pt(const size_t idx, int dim) const
  {
    if (dim == 0)
      return pts[idx].x;
    else if (dim == 1)
      return pts[idx].y;
    else
      return pts[idx].z;
  }

  // Optional bounding-box computation: return false to default to a standard
  // bbox computation loop.
  //   Return true if the BBOX was already computed by the class and returned in
  //   "bb" so it can be avoided to redo it again. Look at bb.size() to find out
  //   the expected dimensionality (e.g. 2 or 3 for point clouds)
  template <class BBOX>
  bool
  kdtree_get_bbox(BBOX &bb) const
  {
    for (unsigned j = 0; j < 3; ++j)
      {
        bb[j].low = min[j];
        bb[j].high = max[j];
      }
    return true;
  }
  // bool kdtree_get_bbox(BBOX& /* bb */) const { return false; }
};

using nfTree_t = nanoflann::KDTreeSingleIndexAdaptor<nanoflann::L2_Simple_Adaptor<double, PointCloud<double>>, PointCloud<double>, 3>;

static double
cdoDefaultDefaultRadius(void)
{
  extern double pointSearchRadius;

  auto searchRadius = pointSearchRadius;
  searchRadius = std::min(std::max(searchRadius, 0.), 180.);
  searchRadius *= DEG2RAD;

  return searchRadius;
}

void
setPointSearchMethod(const std::string &methodstr)
{
  // clang-format off
  if      (methodstr == "kdtree")     pointSearchMethod = PointSearchMethod::kdtree;
  else if (methodstr == "nanoflann")  pointSearchMethod = PointSearchMethod::nanoflann;
  else if (methodstr == "spherepart") pointSearchMethod = PointSearchMethod::spherepart;
  else if (methodstr == "full")       pointSearchMethod = PointSearchMethod::full;
  else if (methodstr == "latbins")    pointSearchMethod = PointSearchMethod::latbins;
  else cdoAbort("Grid point search method %s not available!", methodstr.c_str());
  // clang-format on
}

void
gridPointSearchExtrapolate(GridPointSearch &gps)
{
  gps.extrapolate = true;
}

void
gridPointSearchCreateReg2d(GridPointSearch &gps, bool xIsCyclic, size_t dims[2], const Varray<double> &lons, const Varray<double> &lats)
{
  gps.is_cyclic = xIsCyclic;
  gps.is_reg2d = true;
  gps.dims[0] = dims[0];
  gps.dims[1] = dims[1];
  const auto nx = dims[0];
  const auto ny = dims[1];

  const auto nxm = xIsCyclic ? nx + 1 : nx;

  gps.reg2d_center_lon.resize(nxm);
  gps.reg2d_center_lat.resize(ny);

  varrayCopy(nxm, lons, gps.reg2d_center_lon);
  varrayCopy(ny, lats, gps.reg2d_center_lat);

  gps.coslon.resize(nx);
  gps.sinlon.resize(nx);
  gps.coslat.resize(ny);
  gps.sinlat.resize(ny);

  auto &coslon = gps.coslon;
  auto &sinlon = gps.sinlon;
  auto &coslat = gps.coslat;
  auto &sinlat = gps.sinlat;

  for (size_t n = 0; n < nx; ++n)
    {
      auto rlon = lons[n];
      if (rlon > PI2) rlon -= PI2;
      if (rlon < 0) rlon += PI2;
      coslon[n] = std::cos(rlon);
      sinlon[n] = std::sin(rlon);
    }

  for (size_t n = 0; n < ny; ++n)
    {
      coslat[n] = std::cos(lats[n]);
      sinlat[n] = std::sin(lats[n]);
    }

  gps.searchRadius = cdoDefaultDefaultRadius();

  gps.in_use = true;
}

template <typename T>
void
adjust_bbox_min(T *min)
{
  for (unsigned j = 0; j < 3; ++j) min[j] = min[j] < 0 ? min[j] * 1.001 : min[j] * 0.999;
}

template <typename T>
void
adjust_bbox_max(T *max)
{
  for (unsigned j = 0; j < 3; ++j) max[j] = max[j] < 0 ? max[j] * 0.999 : max[j] * 1.001;
}

static void *
gps_create_kdtree(size_t n, const Varray<double> &lons, const Varray<double> &lats, GridPointSearch &gps)
{
  std::vector<kd_point> pointlist(n);
  // see  example_cartesian.c

  kdata_t min[3] = { 1.e9, 1.e9, 1.e9 };
  kdata_t max[3] = { -1.e9, -1.e9, -1.e9 };

#ifdef HAVE_OPENMP45
#pragma omp parallel for reduction(min : min[:3]) reduction(max : max[:3])
#endif
  for (size_t i = 0; i < n; i++)
    {
      auto &point = pointlist[i].point;
      gcLLtoXYZ(lons[i], lats[i], point);
      for (unsigned j = 0; j < 3; ++j)
        {
          min[j] = point[j] < min[j] ? point[j] : min[j];
          max[j] = point[j] > max[j] ? point[j] : max[j];
        }
      pointlist[i].index = i;
    }

  adjust_bbox_min(min);
  adjust_bbox_max(max);
  for (unsigned j = 0; j < 3; ++j) gps.min[j] = min[j];
  for (unsigned j = 0; j < 3; ++j) gps.max[j] = max[j];

  if (Options::cdoVerbose) cdoPrint("BBOX: min=%g/%g/%g  max=%g/%g/%g", min[0], min[1], min[2], max[0], max[1], max[2]);

  auto kdt = kd_buildTree(pointlist.data(), n, min, max, 3, Threading::ompNumThreads);
  if (kdt == nullptr) cdoAbort("kd_buildTree failed!");

  return (void *) kdt;
}

static void *
gps_create_nanoflann(size_t n, const Varray<double> &lons, const Varray<double> &lats, GridPointSearch &gps)
{
  PointCloud<double> *pointcloud = new PointCloud<double>();

  double min[3] = { 1.e9, 1.e9, 1.e9 };
  double max[3] = { -1.e9, -1.e9, -1.e9 };

  // Generating  Point Cloud
  pointcloud->pts.resize(n);
#ifdef HAVE_OPENMP45
#pragma omp parallel for reduction(min : min[:3]) reduction(max : max[:3])
#endif
  for (size_t i = 0; i < n; i++)
    {
      double point[3];
      gcLLtoXYZ(lons[i], lats[i], point);
      pointcloud->pts[i].x = point[0];
      pointcloud->pts[i].y = point[1];
      pointcloud->pts[i].z = point[2];
      for (unsigned j = 0; j < 3; ++j)
        {
          min[j] = point[j] < min[j] ? point[j] : min[j];
          max[j] = point[j] > max[j] ? point[j] : max[j];
        }
    }

  gps.pointcloud = (void *) pointcloud;

  adjust_bbox_min(min);
  adjust_bbox_max(max);
  for (unsigned j = 0; j < 3; ++j) gps.min[j] = min[j];
  for (unsigned j = 0; j < 3; ++j) gps.max[j] = max[j];

  for (unsigned j = 0; j < 3; ++j) pointcloud->min[j] = min[j];
  for (unsigned j = 0; j < 3; ++j) pointcloud->max[j] = max[j];

  // construct a kd-tree index:
  nfTree_t *nft = new nfTree_t(3 /*dim*/, *pointcloud, nanoflann::KDTreeSingleIndexAdaptorParams(50 /* max leaf */));
  nft->buildIndex();

  return (void *) nft;
}

static void *
gps_create_spherepart(size_t n, const Varray<double> &lons, const Varray<double> &lats, GridPointSearch &gps)
{
  double(*coordinates_xyz)[3];
  coordinates_xyz = (double(*)[3]) malloc(n * sizeof(*coordinates_xyz));
  gps.coordinates_xyz = coordinates_xyz;

  double min[3] = { 1.e9, 1.e9, 1.e9 };
  double max[3] = { -1.e9, -1.e9, -1.e9 };

#ifdef HAVE_OPENMP45
#pragma omp parallel for reduction(min : min[:3]) reduction(max : max[:3])
#endif
  for (size_t i = 0; i < n; i++)
    {
      double *restrict point = coordinates_xyz[i];
      gcLLtoXYZ(lons[i], lats[i], point);
      for (unsigned j = 0; j < 3; ++j)
        {
          min[j] = point[j] < min[j] ? point[j] : min[j];
          max[j] = point[j] > max[j] ? point[j] : max[j];
        }
    }

  adjust_bbox_min(min);
  adjust_bbox_max(max);
  for (unsigned j = 0; j < 3; ++j) gps.min[j] = min[j];
  for (unsigned j = 0; j < 3; ++j) gps.max[j] = max[j];

  if (Options::cdoVerbose) cdoPrint("BBOX: min=%g/%g/%g  max=%g/%g/%g", min[0], min[1], min[2], max[0], max[1], max[2]);

  return (void *) yac_point_sphere_part_search_new(n, gps.coordinates_xyz);
}

static void
gps_destroy_kdtree(void *search_container)
{
  auto kdt = (kdTree_t *) search_container;
  if (kdt) kd_destroyTree(kdt);
}

static void
gps_destroy_full(void *search_container)
{
  auto full = (gpsFull *) search_container;
  if (full)
    {
      if (full->pts)
        {
          Free(full->pts[0]);
          Free(full->pts);
        }

      Free(full);
    }
}

static void
gps_destroy_spherepart(void *search_container)
{
  yac_delete_point_sphere_part_search((point_sphere_part_search *) search_container);
}

static void *
gps_create_full(size_t n, const Varray<double> &lons, const Varray<double> &lats)
{
  if (Options::cdoVerbose) cdoPrint("Init full grid search: n=%zu", n);

  auto full = (gpsFull *) Calloc(1, sizeof(gpsFull));

  double **p = (double **) Malloc(n * sizeof(double *));
  p[0] = (double *) Malloc(3 * n * sizeof(double));
  for (size_t i = 1; i < n; i++) p[i] = p[0] + i * 3;

#ifdef HAVE_OPENMP4
#pragma omp simd
#endif
  for (size_t i = 0; i < n; i++)
    {
      gcLLtoXYZ(lons[i], lats[i], p[i]);
    }

  full->n = n;
  full->plons = lons.data();
  full->plats = lats.data();
  full->pts = p;

  return (void *) full;
}

void
gridPointSearchCreate(GridPointSearch &gps, bool xIsCyclic, size_t dims[2], size_t n, const Varray<double> &lons, const Varray<double> &lats)
{
  gps.is_cyclic = xIsCyclic;
  gps.is_curve = n != 1 && n == dims[0] * dims[1];
  gps.dims[0] = dims[0];
  gps.dims[1] = dims[1];

  gps.method = pointSearchMethod;
  if (gps.method == PointSearchMethod::latbins) gps.method = PointSearchMethod::nanoflann;

  gps.n = n;
  if (n == 0) return;

  gps.plons = lons.data();
  gps.plats = lats.data();

  // clang-format off
  if (Options::cdoVerbose)
    {
      if      (gps.method == PointSearchMethod::kdtree)     cdoPrint("Point search method: kdtree");
      else if (gps.method == PointSearchMethod::full)       cdoPrint("Point search method: full");
      else if (gps.method == PointSearchMethod::nanoflann)  cdoPrint("Point search method: nanoflann");
      else if (gps.method == PointSearchMethod::spherepart) cdoPrint("Point search method: spherepart");
    }

  if      (gps.method == PointSearchMethod::kdtree)     gps.search_container = gps_create_kdtree(n, lons, lats, gps);
  else if (gps.method == PointSearchMethod::full)       gps.search_container = gps_create_full(n, lons, lats);
  else if (gps.method == PointSearchMethod::nanoflann)  gps.search_container = gps_create_nanoflann(n, lons, lats, gps);
  else if (gps.method == PointSearchMethod::spherepart) gps.search_container = gps_create_spherepart(n, lons, lats, gps);
  else cdoAbort("%s::method undefined!", __func__);
  // clang-format on

  gps.searchRadius = cdoDefaultDefaultRadius();

  gps.in_use = true;
}

void
gridPointSearchDelete(GridPointSearch &gps)
{
  if (gps.in_use)
    {
      varrayFree(gps.reg2d_center_lon);
      varrayFree(gps.reg2d_center_lat);

      varrayFree(gps.coslat);
      varrayFree(gps.coslon);
      varrayFree(gps.sinlat);
      varrayFree(gps.sinlon);

      // clang-format off
      if      (gps.method == PointSearchMethod::kdtree)     gps_destroy_kdtree(gps.search_container);
      else if (gps.method == PointSearchMethod::nanoflann)
        {
          delete ((PointCloud<double> *) gps.pointcloud);
          delete ((nfTree_t *) gps.search_container);
        }
      else if (gps.method == PointSearchMethod::spherepart)
        {
          free(gps.coordinates_xyz);
          gps_destroy_spherepart(gps.search_container);
        }
      else if (gps.method == PointSearchMethod::full)       gps_destroy_full(gps.search_container);
      // clang-format on

      gps.in_use = false;
    }
}

static size_t
gps_nearest_kdtree(void *search_container, double lon, double lat, double searchRadius, size_t *addr, double *dist,
                   const GridPointSearch &gps)
{
  auto kdt = (kdTree_t *) search_container;
  if (kdt == nullptr) return 0;

  const auto sqrDistMax = cdo::sqr(searchRadius);
  auto sqrDist = sqrDistMax;

  double query_pt[3];
  gcLLtoXYZ(lon, lat, query_pt);

  if (!gps.extrapolate)
    for (unsigned j = 0; j < 3; ++j)
      if (query_pt[j] < gps.min[j] || query_pt[j] > gps.max[j]) return 0;

  const auto node = kd_nearest(kdt->node, query_pt, &sqrDist, 3);

  if (node && sqrDist < sqrDistMax)
    {
      *addr = node->index;
      *dist = std::sqrt(sqrDist);
      return 1;
    }

  return 0;
}

static size_t
gps_nearest_nanoflann(void *search_container, double lon, double lat, double searchRadius, size_t *addr, double *dist,
                      const GridPointSearch &gps)
{
  auto nft = (nfTree_t *) search_container;
  if (nft == nullptr) return 0;

  const auto sqrDistMax = cdo::sqr(searchRadius);

  double query_pt[3];
  gcLLtoXYZ(lon, lat, query_pt);

  if (!gps.extrapolate)
    for (unsigned j = 0; j < 3; ++j)
      if (query_pt[j] < gps.min[j] || query_pt[j] > gps.max[j]) return 0;

  const size_t num_results = 1;
  size_t retIndex;
  double sqrDist;
  nanoflann::KNNResultSet<double> resultSet(sqrDistMax, num_results);
  resultSet.init(&retIndex, &sqrDist);
  nft->findNeighbors(resultSet, query_pt, nanoflann::SearchParams(10));

  if (retIndex != GPS_NOT_FOUND)
    {
      *addr = retIndex;
      *dist = std::sqrt(sqrDist);
      return 1;
    }

  return 0;
}

static size_t
gps_nearest_spherepart(void *search_container, double lon, double lat, double searchRadius, size_t *addr, double *dist,
                       const GridPointSearch &gps)
{
  double query_pt[1][3];
  gcLLtoXYZ(lon, lat, query_pt[0]);

  if (!gps.extrapolate)
    for (unsigned j = 0; j < 3; ++j)
      if (query_pt[0][j] < gps.min[j] || query_pt[0][j] > gps.max[j]) return 0;

  size_t local_point_ids_array_size = 0;
  size_t num_local_point_ids;
  size_t *local_point_ids = nullptr;
  double cos_angle;

  yac_point_sphere_part_search_NN((point_sphere_part_search *) search_container, 1, query_pt, &cos_angle, nullptr, nullptr,
                                  &local_point_ids, &local_point_ids_array_size, &num_local_point_ids);

  size_t nadd = 0;
  if (num_local_point_ids > 0)
    {
      *dist = std::acos(cos_angle);
      if (*dist <= searchRadius)
        {
          nadd = 1;
          *addr = local_point_ids[0];
          for (size_t i = 1; i < num_local_point_ids; ++i)
            if (local_point_ids[i] < *addr) *addr = local_point_ids[i];
        }
    }

  if (local_point_ids) free(local_point_ids);

  return nadd;
}

static size_t
gps_nearest_full(void *search_container, double lon, double lat, double searchRadius, size_t *addr, double *dist)
{
  auto full = (gpsFull *) search_container;
  if (full == nullptr) return 0;

  const auto sqrDistMax = cdo::sqr(searchRadius);

  double query_pt[3];
  gcLLtoXYZ(lon, lat, query_pt);

  size_t n = full->n;
  size_t closestpt = n;
  double **pts = full->pts;
  double sqrDist = FLT_MAX;
  for (size_t i = 0; i < n; i++)
    {
      double d = (float) squareDistance(query_pt, pts[i]);
      if (closestpt >= n || d < sqrDist || (d <= sqrDist && i < closestpt))
        {
          sqrDist = d;
          closestpt = i;
        }
    }

  if (closestpt < n && sqrDist < sqrDistMax)
    {
      *addr = closestpt;
      *dist = std::sqrt(sqrDist);
      return 1;
    }

  return 0;
}

bool pointInQuad(bool is_cyclic, size_t nx, size_t ny, size_t i, size_t j, size_t adds[4], double lons[4], double lats[4],
                 double plon, double plat, const double *restrict centerLon, const double *restrict centerLat);

static size_t
llindex_in_quad(const GridPointSearch &gps, size_t index, double lon, double lat)
{
  if (index != GPS_NOT_FOUND)
    {
      const auto nx = gps.dims[0];
      const auto ny = gps.dims[1];
      size_t adds[4];
      double lons[4], lats[4];
      const bool isCyclic = gps.is_cyclic;
      for (unsigned k = 0; k < 4; ++k)
        {
          // Determine neighbor addresses
          auto j = index / nx;
          auto i = index - j * nx;
          if (k == 1 || k == 3) i = (i > 0) ? i - 1 : (isCyclic) ? nx - 1 : 0;
          if (k == 2 || k == 3) j = (j > 0) ? j - 1 : 0;

          if (pointInQuad(isCyclic, nx, ny, i, j, adds, lons, lats, lon, lat, gps.plons, gps.plats)) return index;
        }
    }

  return GPS_NOT_FOUND;
}

size_t
gridPointSearchNearest(GridPointSearch &gps, double lon, double lat, size_t *addr, double *dist)
{
  if (gps.in_use)
    {
      size_t nadds = 0;
      const auto searchRadius = gps.searchRadius;
      void *sc = gps.search_container;
      // clang-format off
      if      (gps.method == PointSearchMethod::kdtree)     nadds = gps_nearest_kdtree(sc, lon, lat, searchRadius, addr, dist, gps);
      else if (gps.method == PointSearchMethod::nanoflann)  nadds = gps_nearest_nanoflann(sc, lon, lat, searchRadius, addr, dist, gps);
      else if (gps.method == PointSearchMethod::spherepart) nadds = gps_nearest_spherepart(sc, lon, lat, searchRadius, addr, dist, gps);
      else if (gps.method == PointSearchMethod::full)       nadds = gps_nearest_full(sc, lon, lat, searchRadius, addr, dist);
      else cdoAbort("%s::method undefined!", __func__);
      // clang-format on

      if (nadds > 0)
        {
          auto index = *addr;
          if (!gps.extrapolate && gps.is_curve) index = llindex_in_quad(gps, *addr, lon, lat);
          if (index != GPS_NOT_FOUND) return 1;
        }
    }

  return 0;
}

static size_t
gps_qnearest_kdtree(GridPointSearch &gps, double lon, double lat, double searchRadius, size_t nnn, size_t *adds, double *dist)
{
  size_t nadds = 0;

  auto kdt = (kdTree_t *) gps.search_container;
  if (kdt == nullptr) return nadds;

  const auto sqrDistMax = cdo::sqr(searchRadius);
  kdata_t sqrDist = sqrDistMax;
  pqueue *result = nullptr;

  kdata_t query_pt[3];
  gcLLtoXYZ(lon, lat, query_pt);

  if (!gps.extrapolate)
    for (unsigned j = 0; j < 3; ++j)
      if (query_pt[j] < gps.min[j] || query_pt[j] > gps.max[j]) return nadds;

  if (gps.in_use)
    {
      result = kd_qnearest(kdt->node, query_pt, &sqrDist, nnn, 3);
      if (result)
        {
          size_t index;
          resItem *p;
          while (pqremove_min(result, &p))
            {
              index = p->node->index;
              sqrDist = p->dist_sq;
              Free(p);  // Free the result node taken from the heap

              if (sqrDist < sqrDistMax)
                {
                  adds[nadds] = index;
                  dist[nadds] = std::sqrt(sqrDist);
                  nadds++;
                }
            }
          Free(result->d);  // free the heap
          Free(result);     // and free the heap information structure
        }
    }

  return nadds;
}

static size_t
gps_qnearest_nanoflann(const GridPointSearch &gps, double lon, double lat, double searchRadius, size_t nnn, size_t *adds, double *dist)
{
  size_t nadds = 0;

  auto nft = (nfTree_t *) gps.search_container;
  if (nft == nullptr) return nadds;

  const auto sqrDistMax = cdo::sqr(searchRadius);

  double query_pt[3];
  gcLLtoXYZ(lon, lat, query_pt);

  if (!gps.extrapolate)
    for (unsigned j = 0; j < 3; ++j)
      if (query_pt[j] < gps.min[j] || query_pt[j] > gps.max[j]) return nadds;

  nadds = nft->knnRangeSearch(&query_pt[0], sqrDistMax, nnn, &adds[0], &dist[0]);
  for (size_t i = 0; i < nadds; ++i) dist[i] = std::sqrt(dist[i]);

  return nadds;
}

static size_t
gps_qnearest_spherepart(GridPointSearch &gps, double lon, double lat, double searchRadius, size_t nnn, size_t *adds, double *dist)
{
  size_t nadds = 0;

  if (gps.in_use)
    {
      double query_pt[3];
      gcLLtoXYZ(lon, lat, query_pt);

      if (!gps.extrapolate)
        for (unsigned j = 0; j < 3; ++j)
          if (query_pt[j] < gps.min[j] || query_pt[j] > gps.max[j]) return nadds;

      size_t local_point_ids_array_size = 0;
      size_t num_local_point_ids;
      size_t *local_point_ids = nullptr;

      size_t cos_angles_array_size = 0;
      double *cos_angles = nullptr;

      yac_point_sphere_part_search_NNN((point_sphere_part_search *) gps.search_container, 1, &query_pt, nnn, &cos_angles,
                                       &cos_angles_array_size, nullptr, nullptr, &local_point_ids, &local_point_ids_array_size,
                                       &num_local_point_ids);

      if (num_local_point_ids > 0)
        {
          const auto maxadds = (num_local_point_ids < nnn) ? num_local_point_ids : nnn;
          nadds = 0;
          for (size_t i = 0; i < maxadds; ++i)
            {
              const auto angle = std::acos(cos_angles[i]);
              if (angle < searchRadius)
                {
                  adds[nadds] = local_point_ids[i];
                  dist[nadds] = angle;
                  nadds++;
                }
            }
        }

      free(cos_angles);
      free(local_point_ids);
    }

  return nadds;
}

size_t
gridPointSearchQnearest(GridPointSearch &gps, double lon, double lat, size_t nnn, size_t *adds, double *dist)
{
  size_t nadds = 0;

  if (gps.in_use)
    {
      const auto searchRadius = gps.searchRadius;
      // clang-format off
      if      ( gps.method == PointSearchMethod::kdtree )     nadds = gps_qnearest_kdtree(gps, lon, lat, searchRadius, nnn, adds, dist);
      else if ( gps.method == PointSearchMethod::nanoflann )  nadds = gps_qnearest_nanoflann(gps, lon, lat, searchRadius, nnn, adds, dist);
      else if ( gps.method == PointSearchMethod::spherepart ) nadds = gps_qnearest_spherepart(gps, lon, lat, searchRadius, nnn, adds, dist);
      else cdoAbort("%s::method undefined!", __func__);
      // clang-format on

      if (!gps.extrapolate && gps.is_curve)
        {
          const auto naddsmax = nadds;
          nadds = 0;
          for (size_t i = 0; i < naddsmax; ++i)
            {
              const auto index = llindex_in_quad(gps, adds[i], lon, lat);
              if (index != GPS_NOT_FOUND)
                {
                  adds[nadds] = adds[i];
                  dist[nadds] = dist[i];
                  nadds++;
                }
            }
        }
    }

  return nadds;
}
