/*
  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 "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<double> partialWeights;
  Varray<grid_cell> gridCells;
  Varray<grid_cell> overlapCells;
};

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

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

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

      search.maxCells = numSearchCells;
    }
}

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

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

  varray_free(search.partialAreas);
  varray_free(search.partialWeights);
  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 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++) printf("overlap area : %lf\n", search.partialAreas[i]);
#endif
}

static inline int
edge_has_zero_length(double *a, double *b)
{
  return (compare_angles(get_vector_angle_2(a, b), SIN_COS_LOW_TOL) <= 0);
}

static void
cdo_compute_concave_overlap_areas(size_t numSearchCells, CellSearch &search, const GridCell &gridCell, double tgtLon, double tgtLat)
{
  auto &partialAreas = search.partialAreas;
  auto &overlapCells = search.overlapCells;
  auto &sourceCells = search.gridCells;

  double coordinates_xyz[3][3] = { { -1.0, -1.0, -1.0 }, { -1.0, -1.0, -1.0 }, { -1.0, -1.0, -1.0 } };
  enum yac_edge_type edgeTypes[3] = { GREAT_CIRCLE, GREAT_CIRCLE, GREAT_CIRCLE };

  grid_cell targetPartialCell;
  targetPartialCell.array_size = 3;
  targetPartialCell.num_corners = 3;
  targetPartialCell.coordinates_xyz = coordinates_xyz;
  targetPartialCell.edge_type = edgeTypes;

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

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

  // common node point to all partial target cells
  auto partial_cell_xyz = targetPartialCell.coordinates_xyz;
  const auto targetCell = gridCell.yacGridCell;
  auto cell_xyz = targetCell.coordinates_xyz;

  gcLLtoXYZ(tgtLon, tgtLat, partial_cell_xyz[0]);

  for (size_t cornerNum = 0; cornerNum < targetCell.num_corners; ++cornerNum)
    {
      const auto cornerA = cornerNum;
      const auto cornerB = (cornerNum + 1) % targetCell.num_corners;

      // skip clipping and area calculation for degenerated triangles
      //
      // If this is not sufficient, instead we can try something like:
      //
      //     point_list target_list
      //     init_point_list(&target_list);
      //     generate_point_list(&target_list, targetCell);
      //     grid_cell temp_target_cell;
      //     generate_overlap_cell(target_list, temp_target_cell);
      //     free_point_list(&target_list);
      //
      // and use temp_target_cell for triangulation.
      //
      // Compared to the if statement below the alternative seems to be quite costly.

      if (edge_has_zero_length(cell_xyz[cornerA], cell_xyz[cornerB])) continue;

      partial_cell_xyz[1][0] = cell_xyz[cornerA][0];
      partial_cell_xyz[1][1] = cell_xyz[cornerA][1];
      partial_cell_xyz[1][2] = cell_xyz[cornerA][2];
      partial_cell_xyz[2][0] = cell_xyz[cornerB][0];
      partial_cell_xyz[2][1] = cell_xyz[cornerB][1];
      partial_cell_xyz[2][2] = cell_xyz[cornerB][2];

      yac_cell_clipping(numSearchCells, sourceCells.data(), targetPartialCell, 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;

          partialAreas[i] += gridcell_area(overlapCells[i]);
          // we can't use pole_area because it is rather inaccurate for great circle edges that are nearly circles of longitude
          // partialAreas[i] = pole_area(overlapCells[i]);
        }
    }

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

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
        {
          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 destination 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, 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
set_cell_coordinates_yac(RemapGridType remapGridType, const size_t cellIndex, const size_t numCorners, RemapGrid *remapGrid,
                         const grid_cell &yacGridCell, double *x, double *y)
{
  const auto storeXY = (x && y);
  double(*xyz)[3] = 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 i = 0; i < 4; ++i)
        {
          const auto lon = reg2d_corner_lon[xi[i]];
          const auto lat = reg2d_corner_lat[yi[i]];
          gcLLtoXYZ(lon, lat, xyz[i]);
          if (storeXY)
            {
              x[i] = lon;
              y[i] = 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 i = 0; i < numCorners; ++i)
            {
              x[i] = cell_corner_lon[i];
              y[i] = cell_corner_lat[i];
            }
        }
    }
}

static void
set_cell_coordinates(RemapGridType remapGridType, const size_t cellIndex, const size_t numCorners, 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, RemapGrid *remapGrid, const Varray<grid_cell> &yacGridCell)
{
  for (size_t n = 0; n < numCells; ++n)
    set_cell_coordinates_yac(remapGridType, cellIndices[n], numCorners, remapGrid, yacGridCell[n], nullptr, nullptr);
}
/*
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
greatCircleTypeInit(std::vector<enum yac_edge_type> &greatCircleType, size_t srcNumCorners, size_t tgtNumCorners)
{
  auto maxNumCorners = srcNumCorners;
  if (tgtNumCorners > maxNumCorners) maxNumCorners = tgtNumCorners;

  greatCircleType.resize(maxNumCorners);
  for (size_t i = 0; i < maxNumCorners; ++i) greatCircleType[i] = GREAT_CIRCLE;
}

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
yac_gridcell_init(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
yac_gridcell_free(const GridCell &gridCell)
{
  delete[] gridCell.yacGridCell.coordinates_xyz;
  delete[] gridCell.coordinates_x;
  delete[] gridCell.coordinates_y;
}

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 lcheck = 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;

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

  enum yac_edge_type lonlatCircleType[] = { LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE };
  std::vector<enum yac_edge_type> greatCircleType;
  greatCircleTypeInit(greatCircleType, srcNumCorners, tgtNumCorners);

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

  enum yac_cell_type tgtCellType = MIXED_CELL;

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

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

  if (!(tgtNumCorners < 4 || tgtCellType == LON_LAT_CELL))
    {
      if (tgtGrid->cell_center_lon.empty() || tgtGrid->cell_center_lat.empty())
        cdo_abort("Internal problem (%s): missing target point coordinates!", __func__);
    }

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

  std::vector<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 destination 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) printf("tgtCellIndex %zu  numSearchCells %zu\n", tgtCellIndex, numSearchCells);

      if (numSearchCells == 0) continue;

      // Create search arrays

      cellsearch_realloc(numSearchCells, cellSearch);

      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,
                                            tgtGrid->cell_center_lon[tgtCellIndex], tgtGrid->cell_center_lat[tgtCellIndex]);
        }

      auto &partialAreas = cellSearch.partialAreas;
      auto &partialWeights = cellSearch.partialWeights;

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

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

      for (size_t i = 0; i < numWeights; ++i) partialWeights[i] = partialAreas[i] / tgtCellArea;

      if (rv.normOpt == NormOpt::FRACAREA) yac_correct_weights(numWeights, partialWeights.data());

      for (size_t i = 0; i < numWeights; ++i) partialWeights[i] *= tgtCellArea;

      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]);
      yac_gridcell_free(tgtGridCell2[ompthID]);
    }

  weight_links_to_remap_links(1, tgtGridSize, weightLinks, rv);

  // Normalize weights using destination 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 (lcheck)
    {
      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 lcheck = 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, LAT_CIRCLE, LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE };
  std::vector<enum yac_edge_type> greatCircleType;
  greatCircleTypeInit(greatCircleType, srcNumCorners, tgtNumCorners);

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

  if (!(tgtNumCorners < 4 || tgtCellType == LON_LAT_CELL))
    {
      if (tgtGrid->cell_center_lon.empty() || tgtGrid->cell_center_lat.empty())
        cdo_abort("Internal problem (%s): missing target point coordinates!", __func__);
    }

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

  std::vector<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 destination 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) printf("tgtCellIndex %zu  numSearchCells %zu\n", tgtCellIndex, numSearchCells);

      if (numSearchCells == 0) continue;

      // Create search arrays

      cellsearch_realloc(numSearchCells, cellSearch);

      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,
                                            tgtGrid->cell_center_lon[tgtCellIndex], tgtGrid->cell_center_lat[tgtCellIndex]);
        }

      auto &partialAreas = cellSearch.partialAreas;
      auto &partialWeights = cellSearch.partialWeights;

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

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

      for (size_t i = 0; i < numWeights; ++i) partialWeights[i] = partialAreas[i] / tgtCellArea;

      if (normOpt == NormOpt::FRACAREA) yac_correct_weights(numWeights, partialWeights.data());

      for (size_t i = 0; i < numWeights; ++i) partialWeights[i] *= tgtCellArea;

      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.data(), partialWeights.data());
          // Normalize weights using destination area if requested
          normalize_weights(normOpt, tgtCellArea, tgtGrid->cell_frac[tgtCellIndex], numWeights, partialWeights.data());
          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]);
      yac_gridcell_free(tgtGridCell2[ompthID]);
    }

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

  // Perform some error checking on final weights
  if (lcheck) 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 == 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);
  std::vector<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");

  std::vector<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);
  std::vector<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, LAT_CIRCLE, LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE };
  std::vector<enum yac_edge_type> greatCircleType(nv1, GREAT_CIRCLE);

  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 NormOpt 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];
          double(*xyz)[3] = 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 &partialAreas = search.partialAreas;
      auto &partialWeights = search.partialWeights;
      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++) partialAreas[i] = gridcell_area(overlapCells[i]);

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

      for (size_t i = 0; i < numWeights; ++i) partialWeights[i] = partialAreas[i] / tgtCellArea;

      if (normOpt == NormOpt::FRACAREA) yac_correct_weights(numWeights, partialWeights.data());

      for (size_t i = 0; i < numWeights; ++i) partialWeights[i] *= tgtCellArea;

      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)
    {
      const NormOpt normOpt(NormOpt::FRACAREA);

      const auto tgtCellArea = 0.0;  // not needed for NormOpt::FRACAREA

      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 tgtCellFrac = varray_sum(numWeights, partialWeights);
          sort_weights(numWeights, searchIndices.data(), partialWeights.data());
          // Normalize weights using destination area if requested
          normalize_weights(normOpt, tgtCellArea, tgtCellFrac, numWeights, partialWeights.data());
          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::Double && field2.memType == MemType::Float)
    field2.nmiss = remap_zonal_mean(remapIndices, remapWeights, field1.vec_d, field2.vec_f, 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
    field2.nmiss = remap_zonal_mean(remapIndices, remapWeights, field1.vec_d, field2.vec_d, field1.missval);
  // clang-format on
}
