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

  Author: Uwe Schulzweida

*/

#include <atomic>
#include <algorithm> // std::sort

#include "process_int.h"
#include "cdo_wtime.h"
#include "remap.h"
#include "remap_store_link.h"
#include "cdo_options.h"
#include "progress.h"
#include "cimdOmp.h"
#include <mpim_grid.h>

extern "C"
{
#include "lib/yac/clipping.h"
#include "lib/yac/area.h"
#include "lib/yac/geometry.h"
}

struct CellSearch
{
  enum yac_edge_type *edgeType = nullptr;
  size_t numCellCorners = 0;
  size_t maxCells = 0;
  Varray<double> partialAreas;
  Varray<struct grid_cell> gridCells;
  Varray<struct grid_cell> overlapCells;
};

static void
cellsearch_realloc(size_t numCells, CellSearch &search)
{
  if (numCells > search.maxCells)
    {
      search.partialAreas.resize(numCells);
      search.overlapCells.resize(numCells);
      search.gridCells.resize(numCells);

      for (size_t i = search.maxCells; i < numCells; ++i)
        {
          search.overlapCells[i].array_size = 0;
          search.overlapCells[i].num_corners = 0;
          search.overlapCells[i].edge_type = nullptr;
          search.overlapCells[i].coordinates_xyz = nullptr;

          search.gridCells[i].array_size = search.numCellCorners;
          search.gridCells[i].num_corners = search.numCellCorners;
          search.gridCells[i].edge_type = search.edgeType;
          search.gridCells[i].coordinates_xyz = new double[search.numCellCorners][3];
        }

      search.maxCells = numCells;
    }
}

static void
cellsearch_free(CellSearch &search)
{
  for (size_t i = 0; i < search.maxCells; ++i)
    {
      if (search.overlapCells[i].array_size > 0)
        {
          if (search.overlapCells[i].coordinates_xyz) free(search.overlapCells[i].coordinates_xyz);
          if (search.overlapCells[i].edge_type) free(search.overlapCells[i].edge_type);
        }

      delete[] search.gridCells[i].coordinates_xyz;
    }

  varray_free(search.partialAreas);
  varray_free(search.overlapCells);
  varray_free(search.gridCells);
}

static void
boundbox_from_corners1r(size_t ic, size_t nc, const Varray<double> &cornerLon, const Varray<double> &cornerLat, float *boundBox)
{
  const auto inc = ic * nc;

  float clat = cornerLat[inc];
  float clon = cornerLon[inc];

  boundBox[0] = clat;
  boundBox[1] = clat;
  boundBox[2] = clon;
  boundBox[3] = clon;

  for (size_t j = 1; j < nc; ++j)
    {
      clat = cornerLat[inc + j];
      clon = cornerLon[inc + j];

      if (clat < boundBox[0]) boundBox[0] = clat;
      if (clat > boundBox[1]) boundBox[1] = clat;
      if (clon < boundBox[2]) boundBox[2] = clon;
      if (clon > boundBox[3]) boundBox[3] = clon;
    }

  if (std::fabs(boundBox[3] - boundBox[2]) > PI_f)
    {
      boundBox[2] = 0;
      boundBox[3] = PI2_f;
    }
  /*
  if (std::fabs(boundBox[3] - boundBox[2]) > PI_f)
    {
      if (boundBox[3] > boundBox[2] && (boundBox[3]-PI2_f) < 0.0f)
        {
          float tmp = boundBox[2];
          boundBox[2] = boundBox[3] - PI2_f;
          boundBox[3] = tmp;
        }
    }
  */
}

static inline double
gridcell_area(const struct grid_cell &cell)
{
  return yac_huiliers_area(cell);
}

static void
cdo_compute_overlap_areas(size_t numSearchCells, CellSearch &search, const GridCell &gridCell)
{
  auto &overlapCells = search.overlapCells;

  // Do the clipping and get the cell for the overlapping area
  yac_cell_clipping(numSearchCells, search.gridCells.data(), gridCell.yacGridCell, overlapCells.data());

  // Get the partial areas for the overlapping regions
  for (size_t i = 0; i < numSearchCells; ++i) search.partialAreas[i] = gridcell_area(overlapCells[i]);

#ifdef VERBOSE
  for (size_t i = 0; i < numSearchCells; ++i) cdo_print("overlap area : %lf", search.partialAreas[i]);
#endif
}

static double
get_edge_direction(double * ref_corner, double * corner_a, double * corner_b)
{
  double edge_norm[3];
  crossproduct_ld(corner_a, corner_b, edge_norm);
  normalise_vector(edge_norm);

  // sine of the angle between the edge and the reference corner
  double angle =
    edge_norm[0] * ref_corner[0] +
    edge_norm[1] * ref_corner[1] +
    edge_norm[2] * ref_corner[2];

  // if the reference corner is directly on the edge
  // (for small angles sin(x)==x)
  if (fabs(angle) < yac_angle_tol) return 0.0;

  return copysign(1.0, angle);
}

static void
cdo_compute_concave_overlap_areas(size_t numSearchCells, CellSearch &search, const GridCell &gridCell)
{
  const auto targetCell = gridCell.yacGridCell;
  auto &overlapAreas = search.partialAreas;
  auto &overlapCells = search.overlapCells;
  auto &sourceCells = search.gridCells;

  // common node point to all partial target cells
  double *baseCorner = targetCell.coordinates_xyz[0];

  // the triangulation algorithm only works for cells that only have great circle edges
  enum yac_edge_type edgeTypes[3] = { GREAT_CIRCLE_EDGE, GREAT_CIRCLE_EDGE, GREAT_CIRCLE_EDGE };
  double coordinates_xyz[3][3] = { { -1.0, -1.0, -1.0 }, { -1.0, -1.0, -1.0 }, { -1.0, -1.0, -1.0 } };
  coordinates_xyz[0][0] = baseCorner[0];
  coordinates_xyz[0][1] = baseCorner[1];
  coordinates_xyz[0][2] = baseCorner[2];

  // data structure to hold the triangles of the target cell
  struct grid_cell partialCell;
  partialCell.array_size = 3;
  partialCell.num_corners = 3;
  partialCell.coordinates_xyz = coordinates_xyz;
  partialCell.edge_type = edgeTypes;

  // Do the clipping and get the cell for the overlapping area

  for (size_t i = 0; i < numSearchCells; ++i) overlapAreas[i] = 0.0;

  // for all triangles of the target cell
  // (triangles a formed by first corner of the target cells and each edge of
  //  the cell; the first and last edge of the cell already, contain the
  //  first corner, therefore we can skip them)
  for (size_t cornerIdx = 1; cornerIdx < targetCell.num_corners - 1; ++cornerIdx)
    {
      const auto cornerA = targetCell.coordinates_xyz[cornerIdx];
      const auto cornerB = targetCell.coordinates_xyz[(cornerIdx + 1)];

      // if the current edge has a length of zero
      if (points_are_identically(cornerA, cornerB)) continue;

      const auto edgeDirection = get_edge_direction(baseCorner, cornerA, cornerB);

      partialCell.coordinates_xyz[1][0] = cornerA[0];
      partialCell.coordinates_xyz[1][1] = cornerA[1];
      partialCell.coordinates_xyz[1][2] = cornerA[2];
      partialCell.coordinates_xyz[2][0] = cornerB[0];
      partialCell.coordinates_xyz[2][1] = cornerB[1];
      partialCell.coordinates_xyz[2][2] = cornerB[2];

      // clip the current target cell triangle with all source cells
      yac_cell_clipping(numSearchCells, sourceCells.data(), partialCell, overlapCells.data());

      // Get the partial areas for the overlapping regions as sum over the partial target cells.
      for (size_t i = 0; i < numSearchCells; ++i)
        {
          if (overlapCells[i].num_corners == 0) continue;

          overlapAreas[i] += gridcell_area(overlapCells[i]) * edgeDirection;
        }
    }

  for (size_t i = 0; i < numSearchCells; ++i)
    {
      if (overlapAreas[i] < 0.0) overlapAreas[i] = -overlapAreas[i];
    }

#ifdef VERBOSE
  for (size_t i = 0; i < numSearchCells; ++i) cdo_print("overlap area %zu: %lf", i, partialAreas[i]);
#endif
}

static void
set_cell_coordinates_yac(RemapGridType remapGridType, const size_t cellIndex, const size_t numCorners, const RemapGrid *remapGrid,
                         const struct grid_cell &yacGridCell, double *x, double *y)
{
  const auto storeXY = (x && y);
  auto xyz = yacGridCell.coordinates_xyz;

  if (remapGridType == RemapGridType::Reg2D)
    {
      const auto nx = remapGrid->dims[0];
      const auto iy = cellIndex / nx;
      const auto ix = cellIndex - iy * nx;
      const auto reg2d_corner_lon = &remapGrid->reg2d_corner_lon[ix];
      const auto reg2d_corner_lat = &remapGrid->reg2d_corner_lat[iy];
      static const int xi[4] = { 0, 1, 1, 0 };
      static const int yi[4] = { 0, 0, 1, 1 };
      for (int k = 0; k < 4; ++k)
        {
          const auto lon = reg2d_corner_lon[xi[k]];
          const auto lat = reg2d_corner_lat[yi[k]];
          gcLLtoXYZ(lon, lat, xyz[k]);
          if (storeXY)
            {
              x[k] = lon;
              y[k] = lat;
            }
        }
    }
  else
    {
      const auto cell_corner_lon = &remapGrid->cell_corner_lon[cellIndex * numCorners];
      const auto cell_corner_lat = &remapGrid->cell_corner_lat[cellIndex * numCorners];
      for (size_t i = 0; i < numCorners; ++i) gcLLtoXYZ(cell_corner_lon[i], cell_corner_lat[i], xyz[i]);

      if (storeXY)
        {
          for (size_t k = 0; k < numCorners; ++k)
            {
              x[k] = cell_corner_lon[k];
              y[k] = cell_corner_lat[k];
            }
        }
    }
}

static void
set_cell_coordinates(RemapGridType remapGridType, const size_t cellIndex, const size_t numCorners, const RemapGrid *remapGrid,
                     const GridCell &gridCell)
{
  set_cell_coordinates_yac(remapGridType, cellIndex, numCorners, remapGrid, gridCell.yacGridCell,
                           gridCell.coordinates_x, gridCell.coordinates_y);
}

static void
set_coordinates_yac(const size_t numCells, RemapGridType remapGridType, const Varray<size_t> &cellIndices,
                    const size_t numCorners, const RemapGrid *remapGrid, const Varray<grid_cell> &yacGridCell)
{
  for (size_t i = 0; i < numCells; ++i)
    set_cell_coordinates_yac(remapGridType, cellIndices[i], numCorners, remapGrid, yacGridCell[i], nullptr, nullptr);
}

static void
set_coordinates_yac(const double (*xyzCoords)[3], const size_t numCells, const Varray<size_t> &cellIndices,
                    const size_t numCorners, const Varray<grid_cell> &yacGridCell)
{
  for (size_t i = 0; i < numCells; ++i)
    {
      const auto offset = cellIndices[i] * numCorners;
      auto xyz = yacGridCell[i].coordinates_xyz;
      for (size_t k = 0; k < numCorners; ++k)
        for (size_t l = 0; l < 3; ++l)
          xyz[k][l] = xyzCoords[offset + k][l];
    }
}
 
static void
gridcell_init_yac(GridCell &gridCell, size_t numCorners, enum yac_edge_type *edgeType)
{
  gridCell.yacGridCell.array_size = numCorners;
  gridCell.yacGridCell.num_corners = numCorners;
  gridCell.yacGridCell.edge_type = edgeType;
  gridCell.yacGridCell.coordinates_xyz = new double[numCorners][3];
  gridCell.coordinates_x = new double[numCorners];
  gridCell.coordinates_y = new double[numCorners];
}

static void
gridcell_free_yac(const GridCell &gridCell)
{
  delete[] gridCell.yacGridCell.coordinates_xyz;
  delete[] gridCell.coordinates_x;
  delete[] gridCell.coordinates_y;
}

static int
get_lonlat_circle_index(RemapGridType remapGridType, size_t gridsize, size_t numCorners, const Varray<double> &clon,
                        const Varray<double> &clat)
{
  int lonlatCircleIndex = -1;

  if (numCorners == 4)
    {
      if (remapGridType == RemapGridType::Reg2D)
        {
          lonlatCircleIndex = 1;
        }
      else
        {
          const size_t iadd = (gridsize < 100) ? 1 : gridsize / 30 - 1;
          size_t num_i = 0, num_eq0 = 0, num_eq1 = 0;

          for (size_t i = 0; i < gridsize; i += iadd)
            {
              const auto i4 = i * 4;
              num_i++;
              // clang-format off
              if (is_equal(clon[i4 + 1], clon[i4 + 2]) && is_equal(clon[i4 + 3], clon[i4 + 0]) &&
                  is_equal(clat[i4 + 0], clat[i4 + 1]) && is_equal(clat[i4 + 2], clat[i4 + 3]))
                {
                  num_eq1++;
                }
              else if (is_equal(clon[i4 + 0], clon[i4 + 1]) && is_equal(clon[i4 + 2], clon[i4 + 3]) &&
                       is_equal(clat[i4 + 1], clat[i4 + 2]) && is_equal(clat[i4 + 3], clat[i4 + 0]))
                {
                  num_eq0++;
                }
              // clang-format on
            }

          if (num_i == num_eq1) lonlatCircleIndex = 1;
          if (num_i == num_eq0) lonlatCircleIndex = 0;
        }
    }

  // printf("lonlatCircleIndex %d\n", lonlatCircleIndex);

  return lonlatCircleIndex;
}

static int
get_lonlat_circle_index(RemapGrid *remapGrid)
{
  const auto gridsize = remapGrid->size;
  const auto numCorners = remapGrid->num_cell_corners;
  const auto &clon = remapGrid->cell_corner_lon;
  const auto &clat = remapGrid->cell_corner_lat;
  return get_lonlat_circle_index(remapGrid->type, gridsize, numCorners, clon, clat);
}

static void
normalize_weights(RemapGrid *tgtGrid, RemapVars &rv)
{
  // Include centroids in weights and normalize using target cell area if requested
  auto num_links = rv.num_links;
  auto num_wts = rv.num_wts;

  if (rv.normOpt == NormOpt::DESTAREA)
    {
      const auto &cellArea = tgtGrid->cell_area;
#ifdef _OPENMP
#pragma omp parallel for default(shared) schedule(static)
#endif
      for (size_t n = 0; n < num_links; ++n)
        {
          const auto i = rv.tgtCellIndices[n];  // current linear address for target grid cell
          const auto normFactor = is_not_equal(cellArea[i], 0.0) ? 1.0 / cellArea[i] : 0.0;
          rv.wts[n * num_wts] *= normFactor;
        }
    }
  else if (rv.normOpt == NormOpt::FRACAREA)
    {
      const auto &cellFrac = tgtGrid->cell_frac;
#ifdef _OPENMP
#pragma omp parallel for default(shared) schedule(static)
#endif
      for (size_t n = 0; n < num_links; ++n)
        {
          const auto i = rv.tgtCellIndices[n];  // current linear address for target grid cell
          const auto normFactor = is_not_equal(cellFrac[i], 0.0) ? 1.0 / cellFrac[i] : 0.0;
          rv.wts[n * num_wts] *= normFactor;
        }
    }
  else if (rv.normOpt == NormOpt::NONE)
    {
    }
}

static void
normalize_weights(NormOpt normOpt, double cellArea, double cellFrac, size_t numWeights, Varray<double> &weights)
{
  if (normOpt == NormOpt::DESTAREA)
    {
      const auto normFactor = is_not_equal(cellArea, 0.0) ? 1.0 / cellArea : 0.0;
      for (size_t i = 0; i < numWeights; ++i) weights[i] *= normFactor;
    }
  else if (normOpt == NormOpt::FRACAREA)
    {
      const auto normFactor = is_not_equal(cellFrac, 0.0) ? 1.0 / cellFrac : 0.0;
      for (size_t i = 0; i < numWeights; ++i) weights[i] *= normFactor;
    }
  else if (normOpt == NormOpt::NONE)
    {
    }
}

static void
correct_weights(double cellArea, size_t numWeights, Varray<double> &weights)
{
  for (size_t i = 0; i < numWeights; ++i) weights[i] /= cellArea;
  yac_correct_weights(numWeights, weights.data());
  for (size_t i = 0; i < numWeights; ++i) weights[i] *= cellArea;
}

static void
sort_weights(size_t numWeights, Varray<size_t> &searchIndices, Varray<double> &weights)
{
  size_t n;
  for (n = 1; n < numWeights; ++n)
    if (searchIndices[n] < searchIndices[n - 1]) break;
  if (n == numWeights) return;

  if (numWeights > 1)
    {
      struct IndexWeightX
      {
        size_t index;
        double weight;
      };

      Varray<IndexWeightX> indexWeights(numWeights);

      for (n = 0; n < numWeights; ++n)
        {
          indexWeights[n].index = searchIndices[n];
          indexWeights[n].weight = weights[n];
        }

      const auto compareIndex = [](const auto &a, const auto &b) noexcept { return a.index < b.index; };
      std::sort(indexWeights.begin(), indexWeights.end(), compareIndex);

      for (n = 0; n < numWeights; ++n)
        {
          searchIndices[n] = indexWeights[n].index;
          weights[n] = indexWeights[n].weight;
        }
    }
}
/*
static void
reg2d_bound_box(RemapGrid *remapGrid, double *gridBoundBox)
{
  const auto nx = remapGrid->dims[0];
  const auto ny = remapGrid->dims[1];
  const auto &reg2d_corner_lon = remapGrid->reg2d_corner_lon;
  const auto &reg2d_corner_lat = remapGrid->reg2d_corner_lat;

  gridBoundBox[0] = reg2d_corner_lat[0];
  gridBoundBox[1] = reg2d_corner_lat[ny];
  if (gridBoundBox[0] > gridBoundBox[1])
    {
      gridBoundBox[0] = reg2d_corner_lat[ny];
      gridBoundBox[1] = reg2d_corner_lat[0];
    }
  gridBoundBox[2] = reg2d_corner_lon[0];
  gridBoundBox[3] = reg2d_corner_lon[nx];
}
*/

static void
scale_cellfrac(size_t numCells, Varray<double> &cellFrac, const Varray<double> &cellArea)
{
  for (size_t i = 0; i < numCells; ++i)
    if (is_not_equal(cellArea[i], 0)) cellFrac[i] /= cellArea[i];
}

static void
vec_add_weights(Varray<double> &vec, size_t numWeights, const Varray<double> &weights, const Varray<size_t> &searchIndices)
{
  for (size_t i = 0; i < numWeights; ++i)
    {
      const auto weight = weights[i];
      const auto cellIndex = searchIndices[i];
#ifndef __PGI
#ifdef _OPENMP
#pragma omp atomic
#endif
      vec[cellIndex] += weight;
#endif
    }
}

static size_t
remove_invalid_areas(size_t numSearchCells, Varray<double> &areas, Varray<size_t> &searchIndices)
{
  size_t n = 0;
  for (size_t i = 0; i < numSearchCells; ++i)
    {
      if (areas[i] > 0.0)
        {
          areas[n] = areas[i];
          searchIndices[n] = searchIndices[i];
          n++;
        }
    }

  return n;
}

static size_t
remove_invalid_weights(size_t gridSize, size_t numWeights, Varray<double> &weights, Varray<size_t> &searchIndices)
{
  size_t n = 0;
  for (size_t i = 0; i < numWeights; ++i)
    {
      const auto cellIndex = (weights[i] > 0.0) ? searchIndices[i] : gridSize;
      if (cellIndex != gridSize)
        {
          weights[n] = weights[i];
          searchIndices[n] = cellIndex;
          n++;
        }
    }

  return n;
}

static size_t
remove_unmask_weights(const Varray<short> &gridMask, size_t numWeights, Varray<double> &weights,
                      Varray<size_t> &searchIndices)
{
  size_t n = 0;
  for (size_t i = 0; i < numWeights; ++i)
    {
      const auto cellIndex = searchIndices[i];
      /*
        Store the appropriate addresses and weights.
        Also add contributions to cell areas.
        The source grid mask is the master mask.
      */
      if (gridMask[cellIndex])
        {
          weights[n] = weights[i];
          searchIndices[n] = cellIndex;
          n++;
        }
    }

  return n;
}

void
remap_conserv_weights(RemapSearch &remapSearch, RemapVars &rv)
{
  auto srcGrid = remapSearch.srcGrid;
  auto tgtGrid = remapSearch.tgtGrid;

  auto doCheck = true;

  auto srcGridType = srcGrid->type;
  auto tgtGridType = tgtGrid->type;

  if (Options::cdoVerbose) cdo_print("Called %s()", __func__);

  progress::init();

  const auto start = Options::cdoVerbose ? cdo_get_wtime() : 0.0;

  auto srcGridSize = srcGrid->size;
  auto tgtGridSize = tgtGrid->size;

  auto srcNumCorners = srcGrid->num_cell_corners;
  auto tgtNumCorners = tgtGrid->num_cell_corners;

  enum yac_edge_type lonlatCircleType[] = { LON_CIRCLE_EDGE, LAT_CIRCLE_EDGE, LON_CIRCLE_EDGE, LAT_CIRCLE_EDGE, LON_CIRCLE_EDGE };
  Varray<enum yac_edge_type> greatCircleType(std::max(srcNumCorners, tgtNumCorners), GREAT_CIRCLE_EDGE);

  auto srcEdgeType = greatCircleType.data();
  auto tgtEdgeType = greatCircleType.data();

  enum yac_cell_type tgtCellType = MIXED_CELL;

  if (srcNumCorners == 4)
    {
      const auto lonlatCircleIndex = get_lonlat_circle_index(srcGrid);
      if (lonlatCircleIndex >= 0) srcEdgeType = &lonlatCircleType[lonlatCircleIndex];
    }

  if (tgtNumCorners == 4)
    {
      const auto lonlatCircleIndex = get_lonlat_circle_index(tgtGrid);
      if (lonlatCircleIndex >= 0)
        {
          tgtCellType = LON_LAT_CELL;
          tgtEdgeType = &lonlatCircleType[lonlatCircleIndex];
        }
    }

  Varray<GridCell> tgtGridCell2(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i) gridcell_init_yac(tgtGridCell2[i], tgtNumCorners, tgtEdgeType);

  Varray<CellSearch> cellSearch2(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i)
    {
      cellSearch2[i].numCellCorners = srcNumCorners;
      cellSearch2[i].edgeType = srcEdgeType;
    }

  auto numCellCorners = srcNumCorners;  // num of corners of search cells

  // double srcGridBoundBox[4];
  // if (srcGridType == RemapGridType::Reg2D) reg2d_bound_box(srcGrid, srcGridBoundBox);

  std::vector<WeightLinks> weightLinks(tgtGridSize);

  std::atomic<size_t> atomicCount{0};

  size_t numSearchCellsStat[3] = { 0, 100000, 0 };

  extern CellSearchMethod cellSearchMethod;
  auto useCellsearch = (cellSearchMethod == CellSearchMethod::spherepart || srcGridType == RemapGridType::Reg2D);

  Varray<Varray<size_t>> searchIndices2(Threading::ompNumThreads);
  if (!useCellsearch) for (int i = 0; i < Threading::ompNumThreads; ++i) searchIndices2[i].resize(srcGridSize);

  // Loop over target grid

#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic) default(shared)
#endif
  for (size_t tgtCellIndex = 0; tgtCellIndex < tgtGridSize; ++tgtCellIndex)
    {
      const auto ompthID = cdo_omp_get_thread_num();
      auto &cellSearch = cellSearch2[ompthID];
      auto &searchIndices = searchIndices2[ompthID];
      auto &tgtGridCell = tgtGridCell2[ompthID];

      atomicCount++;
      if (ompthID == 0) progress::update(0, 1, (double)atomicCount / tgtGridSize);

      weightLinks[tgtCellIndex].nlinks = 0;

      if (!tgtGrid->mask[tgtCellIndex]) continue;

      set_cell_coordinates(tgtGridType, tgtCellIndex, tgtNumCorners, tgtGrid, tgtGridCell);

      // Get search cells
      size_t numSearchCells;
      if (useCellsearch)
        {
          // numSearchCells = remap_search_cells(remapSearch, tgtCellType == LON_LAT_CELL, tgtGridCell, searchIndices);
          numSearchCells = remap_search_cells(remapSearch, tgtGridType == RemapGridType::Reg2D, tgtGridCell, searchIndices);
        }
      else
        {
          float tgtCellBoundBoxR[4];
          boundbox_from_corners1r(tgtCellIndex, tgtNumCorners, tgtGrid->cell_corner_lon, tgtGrid->cell_corner_lat,
                                  tgtCellBoundBoxR);

          numSearchCells
              = get_srch_cells(tgtCellIndex, remapSearch.tgtBins, remapSearch.srcBins, tgtCellBoundBoxR, searchIndices);
        }

      if (1 && Options::cdoVerbose)
        {
          numSearchCellsStat[0] += numSearchCells;
          numSearchCellsStat[1] = std::min(numSearchCellsStat[1], numSearchCells);
          numSearchCellsStat[2] = std::max(numSearchCellsStat[2], numSearchCells);
        }

      if (0 && Options::cdoVerbose) cdo_print("tgtCellIndex %zu  numSearchCells %zu", tgtCellIndex, numSearchCells);

      if (numSearchCells == 0) continue;

      // Create search arrays

      cellsearch_realloc(numSearchCells, cellSearch);

      if (remapSearch.gcs.xyzCoords)
        set_coordinates_yac(remapSearch.gcs.xyzCoords, numSearchCells, searchIndices, numCellCorners, cellSearch.gridCells);
      else
        set_coordinates_yac(numSearchCells, srcGridType, searchIndices, numCellCorners, srcGrid, cellSearch.gridCells);

      if (tgtNumCorners < 4 || tgtCellType == LON_LAT_CELL)
        cdo_compute_overlap_areas(numSearchCells, cellSearch, tgtGridCell);
      else
        cdo_compute_concave_overlap_areas(numSearchCells, cellSearch, tgtGridCell);

      auto &partialWeights = cellSearch.partialAreas;

      auto numWeights = remove_invalid_areas(numSearchCells, partialWeights, searchIndices);

      const auto tgtCellArea = gridcell_area(tgtGridCell.yacGridCell);
      tgtGrid->cell_area[tgtCellIndex] = tgtCellArea;

      if (rv.normOpt == NormOpt::FRACAREA) correct_weights(tgtCellArea, numWeights, partialWeights);

      numWeights = remove_invalid_weights(srcGridSize, numWeights, partialWeights, searchIndices);

      vec_add_weights(srcGrid->cell_area, numWeights, partialWeights, searchIndices);

      numWeights = remove_unmask_weights(srcGrid->mask, numWeights, partialWeights, searchIndices);

      vec_add_weights(srcGrid->cell_frac, numWeights, partialWeights, searchIndices);

      tgtGrid->cell_frac[tgtCellIndex] = varray_sum(numWeights, partialWeights);

      store_weightlinks(1, numWeights, searchIndices.data(), partialWeights.data(), tgtCellIndex, weightLinks);
    }

  progress::update(0, 1, 1);

  if (1 && Options::cdoVerbose)
    {
      cdo_print("Num search cells min,mean,max :  %zu  %3.1f  %zu", numSearchCellsStat[1],
                numSearchCellsStat[0] / (double) tgtGridSize, numSearchCellsStat[2]);
    }

  // Finished with all cells: deallocate search arrays
  for (auto ompthID = 0; ompthID < Threading::ompNumThreads; ++ompthID)
    {
      cellsearch_free(cellSearch2[ompthID]);
      gridcell_free_yac(tgtGridCell2[ompthID]);
    }

  weight_links_to_remap_links(1, tgtGridSize, weightLinks, rv);

  // Normalize weights using target cell area if requested
  normalize_weights(tgtGrid, rv);

  if (Options::cdoVerbose) cdo_print("Total number of links = %zu", rv.num_links);

  scale_cellfrac(srcGridSize, srcGrid->cell_frac, srcGrid->cell_area);
  scale_cellfrac(tgtGridSize, tgtGrid->cell_frac, tgtGrid->cell_area);

  // Perform some error checking on final weights
  if (doCheck)
    {
      remap_check_area(srcGridSize, &srcGrid->cell_area[0], "Source");
      remap_check_area(tgtGridSize, &tgtGrid->cell_area[0], "Target");

      remap_vars_check_weights(rv);
    }

  if (Options::cdoVerbose) cdo_print("Cells search: %.2f seconds", cdo_get_wtime() - start);
}  // remap_conserv_weights

template <typename T>
static double
conserv_remap(const Varray<T> &srcArray, size_t numWeights, const Varray<double> &weights, const Varray<size_t> &srcIndices)
{
  double tgtPoint = 0.0;
  for (size_t i = 0; i < numWeights; ++i) tgtPoint += srcArray[srcIndices[i]] * weights[i];

  return tgtPoint;
}

template <typename T>
static void
remap_conserv(NormOpt normOpt, RemapSearch &remapSearch, const Varray<T> &srcArray, Varray<T> &tgtArray, T missval)
{
  auto srcGrid = remapSearch.srcGrid;
  auto tgtGrid = remapSearch.tgtGrid;

  auto doCheck = true;

  // Variables necessary if segment manages to hit pole
  auto srcGridType = srcGrid->type;
  auto tgtGridType = tgtGrid->type;

  if (Options::cdoVerbose) cdo_print("Called %s()", __func__);

  progress::init();

  const auto start = Options::cdoVerbose ? cdo_get_wtime() : 0.0;

  auto srcGridSize = srcGrid->size;
  auto tgtGridSize = tgtGrid->size;

  Varray<short> srcGridMask(srcGridSize);
#ifdef _OPENMP
#pragma omp parallel for default(shared) schedule(static)
#endif
  for (size_t i = 0; i < srcGridSize; ++i) srcGridMask[i] = !dbl_is_equal(srcArray[i], missval);

  auto srcNumCorners = srcGrid->num_cell_corners;
  auto tgtNumCorners = tgtGrid->num_cell_corners;

  enum yac_edge_type lonlatCircleType[] = { LON_CIRCLE_EDGE, LAT_CIRCLE_EDGE, LON_CIRCLE_EDGE, LAT_CIRCLE_EDGE, LON_CIRCLE_EDGE };
  Varray<enum yac_edge_type> greatCircleType(std::max(srcNumCorners, tgtNumCorners), GREAT_CIRCLE_EDGE);

  auto srcEdgeType = greatCircleType.data();
  auto tgtEdgeType = greatCircleType.data();

  enum yac_cell_type tgtCellType = MIXED_CELL;

  if (srcNumCorners == 4)
    {
      const auto lonlatCircleIndex = get_lonlat_circle_index(srcGrid);
      if (lonlatCircleIndex >= 0) srcEdgeType = &lonlatCircleType[lonlatCircleIndex];
    }

  if (tgtNumCorners == 4)
    {
      const auto lonlatCircleIndex = get_lonlat_circle_index(tgtGrid);
      if (lonlatCircleIndex >= 0)
        {
          tgtCellType = LON_LAT_CELL;
          tgtEdgeType = &lonlatCircleType[lonlatCircleIndex];
        }
    }

  Varray<GridCell> tgtGridCell2(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i) gridcell_init_yac(tgtGridCell2[i], tgtNumCorners, tgtEdgeType);

  Varray<CellSearch> cellSearch2(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i)
    {
      cellSearch2[i].numCellCorners = srcNumCorners;
      cellSearch2[i].edgeType = srcEdgeType;
    }

  auto numCellCorners = srcNumCorners;  // num of corners of search cells

  // double srcGridBoundBox[4];
  // if (srcGridType == RemapGridType::Reg2D) reg2d_bound_box(srcGrid, srcGridBoundBox);

  std::atomic<size_t> atomicCount{0};

  size_t numSearchCellsStat[3] = { 0, 100000, 0 };

  extern CellSearchMethod cellSearchMethod;
  auto useCellsearch = (cellSearchMethod == CellSearchMethod::spherepart || srcGridType == RemapGridType::Reg2D);

  Varray<Varray<size_t>> searchIndices2(Threading::ompNumThreads);
  if (!useCellsearch) for (int i = 0; i < Threading::ompNumThreads; ++i) searchIndices2[i].resize(srcGridSize);

  // Loop over target grid

#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic) default(shared)
#endif
  for (size_t tgtCellIndex = 0; tgtCellIndex < tgtGridSize; ++tgtCellIndex)
    {
      const auto ompthID = cdo_omp_get_thread_num();
      auto &cellSearch = cellSearch2[ompthID];
      auto &searchIndices = searchIndices2[ompthID];
      auto &tgtGridCell = tgtGridCell2[ompthID];

      atomicCount++;
      if (ompthID == 0) progress::update(0, 1, (double)atomicCount / tgtGridSize);

      tgtArray[tgtCellIndex] = missval;

      if (!tgtGrid->mask[tgtCellIndex]) continue;

      set_cell_coordinates(tgtGridType, tgtCellIndex, tgtNumCorners, tgtGrid, tgtGridCell);

      // Get search cells
      size_t numSearchCells;
      if (useCellsearch)
        {
          // numSearchCells = remap_search_cells(remapSearch, tgtCellType == LON_LAT_CELL, tgtGridCell, searchIndices);
          numSearchCells = remap_search_cells(remapSearch, tgtGridType == RemapGridType::Reg2D, tgtGridCell, searchIndices);
        }
      else
        {
          float tgtCellBoundBoxR[4];
          boundbox_from_corners1r(tgtCellIndex, tgtNumCorners, tgtGrid->cell_corner_lon, tgtGrid->cell_corner_lat,
                                  tgtCellBoundBoxR);

          numSearchCells
              = get_srch_cells(tgtCellIndex, remapSearch.tgtBins, remapSearch.srcBins, tgtCellBoundBoxR, searchIndices);
        }

      if (1 && Options::cdoVerbose)
        {
          numSearchCellsStat[0] += numSearchCells;
          numSearchCellsStat[1] = std::min(numSearchCellsStat[1], numSearchCells);
          numSearchCellsStat[2] = std::max(numSearchCellsStat[2], numSearchCells);
        }

      if (0 && Options::cdoVerbose) cdo_print("tgtCellIndex %zu  numSearchCells %zu", tgtCellIndex, numSearchCells);

      if (numSearchCells == 0) continue;

      // Create search arrays

      cellsearch_realloc(numSearchCells, cellSearch);

      if (remapSearch.gcs.xyzCoords)
        set_coordinates_yac(remapSearch.gcs.xyzCoords, numSearchCells, searchIndices, numCellCorners, cellSearch.gridCells);
      else
        set_coordinates_yac(numSearchCells, srcGridType, searchIndices, numCellCorners, srcGrid, cellSearch.gridCells);

      if (tgtNumCorners < 4 || tgtCellType == LON_LAT_CELL)
        cdo_compute_overlap_areas(numSearchCells, cellSearch, tgtGridCell);
      else
        cdo_compute_concave_overlap_areas(numSearchCells, cellSearch, tgtGridCell);

      auto &partialWeights = cellSearch.partialAreas;

      auto numWeights = remove_invalid_areas(numSearchCells, partialWeights, searchIndices);

      const auto tgtCellArea = gridcell_area(tgtGridCell.yacGridCell);
      tgtGrid->cell_area[tgtCellIndex] = tgtCellArea;

      if (normOpt == NormOpt::FRACAREA) correct_weights(tgtCellArea, numWeights, partialWeights);

      numWeights = remove_invalid_weights(srcGridSize, numWeights, partialWeights, searchIndices);
      numWeights = remove_unmask_weights(srcGridMask, numWeights, partialWeights, searchIndices);

      tgtGrid->cell_frac[tgtCellIndex] = varray_sum(numWeights, partialWeights);

      if (numWeights)
        {
          sort_weights(numWeights, searchIndices, partialWeights);
          // Normalize weights using cell target area if requested
          normalize_weights(normOpt, tgtCellArea, tgtGrid->cell_frac[tgtCellIndex], numWeights, partialWeights);
          tgtArray[tgtCellIndex] = conserv_remap(srcArray, numWeights, partialWeights, searchIndices);
        }
    }

  progress::update(0, 1, 1);

  if (1 && Options::cdoVerbose)
    {
      cdo_print("Num search cells min,mean,max :  %zu  %3.1f  %zu", numSearchCellsStat[1],
                numSearchCellsStat[0] / (double) tgtGridSize, numSearchCellsStat[2]);
    }

  // Finished with all cells: deallocate search arrays

  for (auto ompthID = 0; ompthID < Threading::ompNumThreads; ++ompthID)
    {
      cellsearch_free(cellSearch2[ompthID]);
      gridcell_free_yac(tgtGridCell2[ompthID]);
    }

  scale_cellfrac(tgtGridSize, tgtGrid->cell_frac, tgtGrid->cell_area);

  // Perform some error checking on final weights
  if (doCheck) remap_check_area(tgtGridSize, &tgtGrid->cell_area[0], "Target");

  if (Options::cdoVerbose) cdo_print("Cells search: %.2f seconds", cdo_get_wtime() - start);
}  // remap_conserv

void
remap_conserv(NormOpt normOpt, RemapSearch &remapSearch, const Field &field1, Field &field2)
{
  if (field1.memType != field2.memType) cdo_abort("Interal error, memType of field1 and field2 differ!");

  if (field1.memType == MemType::Float)
    remap_conserv(normOpt, remapSearch, field1.vec_f, field2.vec_f, (float) field1.missval);
  else
    remap_conserv(normOpt, remapSearch, field1.vec_d, field2.vec_d, field1.missval);
}

template <typename T>
static size_t
remove_missing_weights(const Varray<T> &srcArray, T missval, size_t numWeights, Varray<double> &partialWeights,
                       Varray<size_t> &searchIndices)
{
  size_t n = 0;
  for (size_t i = 0; i < numWeights; ++i)
    {
      const auto cellIndex = searchIndices[i];
      if (!dbl_is_equal(srcArray[cellIndex], missval))
        {
          partialWeights[n] = partialWeights[i];
          searchIndices[n] = cellIndex;
          n++;
        }
    }

  return n;
}

static double
sphere_segment_area(double latInRadian)
{
  return 2.0 * M_PI * (1.0 - std::cos(M_PI * 0.5 - latInRadian));
}

static double
latitude_area(double latMin, double latMax)
{
  return sphere_segment_area(latMin) - sphere_segment_area(latMax);
}

void
remap_weights_zonal_mean(const int gridID1, const int gridID2, Varray2D<size_t> &remapIndices, Varray2D<double> &remapWeights)
{
  constexpr double scaleFactor = 1000000000.0;
  auto gridsize1 = gridInqSize(gridID1);
  size_t nv1 = gridInqNvertex(gridID1);
  Varray<double> xbounds1(gridsize1 * nv1), ybounds1(gridsize1 * nv1);
  gridInqXbounds(gridID1, xbounds1.data());
  gridInqYbounds(gridID1, ybounds1.data());

  // Convert lonlat units if required
  cdo_grid_to_radian(gridID1, CDI_XAXIS, gridsize1 * nv1, xbounds1.data(), "source grid longitude bounds");
  cdo_grid_to_radian(gridID1, CDI_YAXIS, gridsize1 * nv1, ybounds1.data(), "source grid latitude bounds");

  Varray<int> yminmax1(gridsize1 * 2);

#ifdef _OPENMP
#pragma omp parallel for schedule(static) default(shared)
#endif
  for (size_t i = 0; i < gridsize1; ++i)
    {
      auto minval = ybounds1[i * nv1];
      auto maxval = ybounds1[i * nv1];
      for (size_t k = 1; k < nv1; ++k)
        {
          const auto val = ybounds1[i * nv1 + k];
          if (val > maxval) maxval = val;
          if (val < minval) minval = val;
        }
      yminmax1[i * 2] = scaleFactor * minval;
      yminmax1[i * 2 + 1] = scaleFactor * maxval;
    }

  auto ysize2 = gridInqYsize(gridID2);
  Varray<double> ybounds2(ysize2 * 2);
  gridInqYbounds(gridID2, ybounds2.data());

  // Convert lat units if required
  cdo_grid_to_radian(gridID2, CDI_YAXIS, ysize2 * 2, ybounds2.data(), "target grid latitude bounds");

  remapIndices.resize(ysize2);
  remapWeights.resize(ysize2);

#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic) default(shared)
#endif
  for (size_t i2 = 0; i2 < ysize2; ++i2)
    {
      int latBounds[2]
          = { static_cast<int>(scaleFactor * ybounds2[2 * i2]), static_cast<int>(scaleFactor * ybounds2[2 * i2 + 1]) };
      if (latBounds[0] > latBounds[1]) std::swap(latBounds[0], latBounds[1]);

      size_t numSearchCells = 0;
      for (size_t i1 = 0; i1 < gridsize1; ++i1)
        {
          if (yminmax1[i1 * 2] < latBounds[1] && yminmax1[i1 * 2 + 1] > latBounds[0]) numSearchCells++;
        }

      remapIndices[i2].resize(numSearchCells);
      size_t n = 0;
      for (size_t i1 = 0; i1 < gridsize1; ++i1)
        {
          if (yminmax1[i1 * 2] < latBounds[1] && yminmax1[i1 * 2 + 1] > latBounds[0]) remapIndices[i2][n++] = i1;
        }

      // printf("lat %zu found %zu of %zu\n", i2 + 1, numSearchCells, gridsize1);
    }

  enum yac_edge_type lonlatCircleType[] = { LON_CIRCLE_EDGE, LAT_CIRCLE_EDGE, LON_CIRCLE_EDGE, LAT_CIRCLE_EDGE, LON_CIRCLE_EDGE };
  Varray<enum yac_edge_type> greatCircleType(nv1, GREAT_CIRCLE_EDGE);

  auto srcEdgeType = greatCircleType.data();

  if (nv1 == 4)
    {
      const auto lonlatCircleIndex = get_lonlat_circle_index(RemapGridType::Undefined, gridsize1, nv1, xbounds1, ybounds1);
      if (lonlatCircleIndex >= 0) srcEdgeType = &lonlatCircleType[lonlatCircleIndex];
    }

#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic) default(shared)
#endif
  for (size_t i2 = 0; i2 < ysize2; ++i2)
    {
      const auto normOpt(NormOpt::FRACAREA);
      double latBounds[2] = { ybounds2[2 * i2], ybounds2[2 * i2 + 1] };
      if (latBounds[0] > latBounds[1]) std::swap(latBounds[0], latBounds[1]);

      const auto tgtCellArea = latitude_area(latBounds[0], latBounds[1]);
      // printf("tgtCellArea %zu %g\n", i2 + 1, tgtCellArea);

      const auto numSearchCells = remapIndices[i2].size();

      CellSearch search;
      search.numCellCorners = nv1;
      search.edgeType = srcEdgeType;

      cellsearch_realloc(numSearchCells, search);

      for (size_t j = 0; j < numSearchCells; ++j)
        {
          const auto cellIndex = remapIndices[i2][j];
          auto xyz = search.gridCells[j].coordinates_xyz;
          const auto cell_corner_lon = &xbounds1[cellIndex * nv1];
          const auto cell_corner_lat = &ybounds1[cellIndex * nv1];
          for (size_t i = 0; i < nv1; ++i) gcLLtoXYZ(cell_corner_lon[i], cell_corner_lat[i], xyz[i]);
        }

      auto &partialWeights = search.partialAreas;
      auto &overlapCells = search.overlapCells;

      // Do the clipping and get the cell for the overlapping area
      yac_cell_lat_clipping(numSearchCells, search.gridCells.data(), latBounds, overlapCells.data());

      // Get the partial areas for the overlapping regions
      for (size_t i = 0; i < numSearchCells; ++i) partialWeights[i] = gridcell_area(overlapCells[i]);

      auto numWeights = remove_invalid_areas(numSearchCells, partialWeights, remapIndices[i2]);
      // printf("numWeights: %zu %zu\n", numSearchCells, numWeights);

      if (normOpt == NormOpt::FRACAREA) correct_weights(tgtCellArea, numWeights, partialWeights);

      numWeights = remove_invalid_weights(gridsize1, numWeights, partialWeights, remapIndices[i2]);

      remapWeights[i2].resize(numWeights);
      for (size_t i = 0; i < numWeights; ++i) remapWeights[i2][i] = partialWeights[i];

      cellsearch_free(search);
    }
}

template <typename T1, typename T2>
static size_t
remap_zonal_mean(const Varray2D<size_t> &remapIndices, const Varray2D<double> &remapWeights, const Varray<T1> &srcArray,
                 Varray<T2> &tgtArray, T1 missval)
{
  auto ysize2 = remapIndices.size();

#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic) default(shared)
#endif
  for (size_t i2 = 0; i2 < ysize2; ++i2)
    {
      auto numWeights = remapWeights[i2].size();
      Varray<size_t> searchIndices(numWeights);
      for (size_t i = 0; i < numWeights; ++i) searchIndices[i] = remapIndices[i2][i];
      Varray<double> partialWeights(numWeights);
      for (size_t i = 0; i < numWeights; ++i) partialWeights[i] = remapWeights[i2][i];

      numWeights = remove_missing_weights(srcArray, missval, numWeights, partialWeights, searchIndices);

      tgtArray[i2] = missval;

      if (numWeights)
        {
          const auto normOpt(NormOpt::FRACAREA);
          const auto tgtCellArea = 0.0;  // not needed for NormOpt::FRACAREA
          const auto tgtCellFrac = varray_sum(numWeights, partialWeights);
          sort_weights(numWeights, searchIndices, partialWeights);
          // Normalize weights using cell target area if requested
          normalize_weights(normOpt, tgtCellArea, tgtCellFrac, numWeights, partialWeights);
          tgtArray[i2] = conserv_remap(srcArray, numWeights, partialWeights, searchIndices);
        }
    }

  size_t nmiss = 0;
  for (size_t i2 = 0; i2 < ysize2; ++i2)
    if (dbl_is_equal(tgtArray[i2], missval)) nmiss++;

  return nmiss;
}

void
remap_zonal_mean(const Varray2D<size_t> &remapIndices, const Varray2D<double> &remapWeights, const Field &field1, Field &field2)
{
  // clang-format off
  if      (field1.memType == MemType::Float && field2.memType == MemType::Float)
    field2.nmiss = remap_zonal_mean(remapIndices, remapWeights, field1.vec_f, field2.vec_f, (float)field1.missval);
  else if (field1.memType == MemType::Float && field2.memType == MemType::Double)
    field2.nmiss = remap_zonal_mean(remapIndices, remapWeights, field1.vec_f, field2.vec_d, (float)field1.missval);
  else if (field1.memType == MemType::Double && field2.memType == MemType::Float)
    field2.nmiss = remap_zonal_mean(remapIndices, remapWeights, field1.vec_d, field2.vec_f, field1.missval);
  else
    field2.nmiss = remap_zonal_mean(remapIndices, remapWeights, field1.vec_d, field2.vec_d, field1.missval);
  // clang-format on
}
