/*
  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"
}

struct search_t
{
  enum yac_edge_type *src_edge_type;
  size_t srch_corners;
  size_t max_srch_cells;
  Varray<double> partial_areas;
  Varray<double> partial_weights;
  Varray<grid_cell> src_grid_cells;
  Varray<grid_cell> overlap_buffer;
};

static void
cellSearchRealloc(size_t num_srch_cells, search_t &search)
{
  if (num_srch_cells > search.max_srch_cells)
    {
      search.partial_areas.resize(num_srch_cells);
      search.partial_weights.resize(num_srch_cells);
      search.overlap_buffer.resize(num_srch_cells);
      search.src_grid_cells.resize(num_srch_cells);

      for (size_t n = search.max_srch_cells; n < num_srch_cells; ++n)
        {
          search.overlap_buffer[n].array_size = 0;
          search.overlap_buffer[n].num_corners = 0;
          search.overlap_buffer[n].edge_type = nullptr;
          search.overlap_buffer[n].coordinates_xyz = nullptr;

          search.src_grid_cells[n].array_size = search.srch_corners;
          search.src_grid_cells[n].num_corners = search.srch_corners;
          search.src_grid_cells[n].edge_type = search.src_edge_type;
          search.src_grid_cells[n].coordinates_xyz = new double[search.srch_corners][3];
        }

      search.max_srch_cells = num_srch_cells;
    }
}

static void
cellSearchFree(search_t &search)
{
  const auto max_srch_cells = search.max_srch_cells;
  for (size_t n = 0; n < max_srch_cells; n++)
    {
      if (search.overlap_buffer[n].array_size > 0)
        {
          if (search.overlap_buffer[n].coordinates_xyz) free(search.overlap_buffer[n].coordinates_xyz);
          if (search.overlap_buffer[n].edge_type) free(search.overlap_buffer[n].edge_type);
        }

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

  varray_free(search.partial_areas);
  varray_free(search.partial_weights);
  varray_free(search.overlap_buffer);
  varray_free(search.src_grid_cells);
}

static void
boundbox_from_corners1r(size_t ic, size_t nc, const Varray<double> &corner_lon, const Varray<double> &corner_lat, float *bound_box)
{
  const auto inc = ic * nc;

  float clat = corner_lat[inc];
  float clon = corner_lon[inc];

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

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

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

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

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

static void
cdo_compute_overlap_areas(size_t n, search_t &search, const grid_cell &target_cell)
{
  auto overlap_cells = search.overlap_buffer.data();

  // Do the clipping and get the cell for the overlapping area
  yac_cell_clipping(n, search.src_grid_cells.data(), target_cell, overlap_cells);

  // Get the partial areas for the overlapping regions
  for (size_t i = 0; i < n; i++) search.partial_areas[i] = gridcell_area(overlap_cells[i]);

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

static constexpr double tol = 1.0e-12;

enum cell_type
{
  UNDEF_CELL,
  LON_LAT_CELL,
  LAT_CELL,
  GREAT_CIRCLE_CELL,
  MIXED_CELL
};

static void
cdo_compute_concave_overlap_areas(size_t N, search_t &search, const grid_cell &target_cell, double target_node_x,
                                  double target_node_y)
{
  auto &partial_areas = search.partial_areas;
  auto overlap_cells = search.overlap_buffer.data();
  auto source_cell = search.src_grid_cells.data();

  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 edge_types[3] = { GREAT_CIRCLE, GREAT_CIRCLE, GREAT_CIRCLE };

  grid_cell target_partial_cell;
  target_partial_cell.array_size = 3;
  target_partial_cell.num_corners = 3;
  target_partial_cell.coordinates_x = nullptr;
  target_partial_cell.coordinates_y = nullptr;
  target_partial_cell.coordinates_xyz = coordinates_xyz;
  target_partial_cell.edge_type = edge_types;

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

  for (size_t n = 0; n < N; n++) partial_areas[n] = 0.0;

  auto partial_cell_xyz = target_partial_cell.coordinates_xyz;
  auto cell_xyz = target_cell.coordinates_xyz;

  gcLLtoXYZ(target_node_x, target_node_y, partial_cell_xyz[0]);

  for (size_t num_corners = 0; num_corners < target_cell.num_corners; ++num_corners)
    {
      const auto corner_a = num_corners;
      const auto corner_b = (num_corners + 1) % target_cell.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, target_cell);
      //     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.

      // clang-format off
      if (((std::fabs(cell_xyz[corner_a][0] - cell_xyz[corner_b][0]) < tol)
        && (std::fabs(cell_xyz[corner_a][1] - cell_xyz[corner_b][1]) < tol)
        && (std::fabs(cell_xyz[corner_a][2] - cell_xyz[corner_b][2]) < tol))
       || ((std::fabs(cell_xyz[corner_a][0] - partial_cell_xyz[0][0]) < tol)
        && (std::fabs(cell_xyz[corner_a][1] - partial_cell_xyz[0][1]) < tol)
        && (std::fabs(cell_xyz[corner_a][2] - partial_cell_xyz[0][2]) < tol))
       || ((std::fabs(cell_xyz[corner_b][0] - partial_cell_xyz[0][0]) < tol)
        && (std::fabs(cell_xyz[corner_b][1] - partial_cell_xyz[0][1]) < tol)
        && (std::fabs(cell_xyz[corner_b][2] - partial_cell_xyz[0][2]) < tol)))
        continue;
      // clang-format on

      partial_cell_xyz[1][0] = cell_xyz[corner_a][0];
      partial_cell_xyz[1][1] = cell_xyz[corner_a][1];
      partial_cell_xyz[1][2] = cell_xyz[corner_a][2];
      partial_cell_xyz[2][0] = cell_xyz[corner_b][0];
      partial_cell_xyz[2][1] = cell_xyz[corner_b][1];
      partial_cell_xyz[2][2] = cell_xyz[corner_b][2];

      yac_cell_clipping(N, source_cell, target_partial_cell, overlap_cells);

      // Get the partial areas for the overlapping regions as sum over the partial target cells.
      for (size_t n = 0; n < N; n++)
        {
          partial_areas[n] += gridcell_area(overlap_cells[n]);
          // we cannot use pole_area because it is rather inaccurate for great
          // circle edges that are nearly circles of longitude
          // partial_areas[n] = pole_area (overlap_cells[n]);
        }
    }

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

static int
get_lonlat_circle_index(RemapGridType remap_grid_type, size_t gridsize, size_t num_cell_corners, const Varray<double> &clon,
                        const Varray<double> &clat)
{
  int lonlat_circle_index = -1;

  if (num_cell_corners == 4)
    {
      if (remap_grid_type == RemapGridType::Reg2D)
        {
          lonlat_circle_index = 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) lonlat_circle_index = 1;
          if (num_i == num_eq0) lonlat_circle_index = 0;
        }
    }

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

  return lonlat_circle_index;
}

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

static void
remapNormalizeWeights(RemapGrid *tgt_grid, 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 &cell_area = tgt_grid->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.tgt_cell_add[n];  // current linear address for target grid cell
          const auto norm_factor = IS_NOT_EQUAL(cell_area[i], 0.0) ? 1.0 / cell_area[i] : 0.0;
          rv.wts[n * num_wts] *= norm_factor;
        }
    }
  else if (rv.normOpt == NormOpt::FRACAREA)
    {
      const auto &cell_frac = tgt_grid->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.tgt_cell_add[n];  // current linear address for target grid cell
          const auto norm_factor = IS_NOT_EQUAL(cell_frac[i], 0.0) ? 1.0 / cell_frac[i] : 0.0;
          rv.wts[n * num_wts] *= norm_factor;
        }
    }
  else if (rv.normOpt == NormOpt::NONE)
    {
    }
}

static void
remapNormalizeWeights(NormOpt normOpt, double cell_area, double cell_frac, size_t num_weights, double *weights)
{
  if (normOpt == NormOpt::DESTAREA)
    {
      const auto norm_factor = IS_NOT_EQUAL(cell_area, 0.0) ? 1.0 / cell_area : 0.0;
      for (size_t i = 0; i < num_weights; ++i) weights[i] *= norm_factor;
    }
  else if (normOpt == NormOpt::FRACAREA)
    {
      const auto norm_factor = IS_NOT_EQUAL(cell_frac, 0.0) ? 1.0 / cell_frac : 0.0;
      for (size_t i = 0; i < num_weights; ++i) weights[i] *= norm_factor;
    }
  else if (normOpt == NormOpt::NONE)
    {
    }
}

static void
setCellCoordinatesYac(RemapGridType remapGridType, const size_t cellAddr, const size_t numCellCorners, RemapGrid *remap_grid,
                      const grid_cell &yac_grid_cell, bool storeXY = true)
{
  auto x = yac_grid_cell.coordinates_x;
  auto y = yac_grid_cell.coordinates_y;
  double(*xyz)[3] = yac_grid_cell.coordinates_xyz;

  if (remapGridType == RemapGridType::Reg2D)
    {
      const auto nx = remap_grid->dims[0];
      const auto iy = cellAddr / nx;
      const auto ix = cellAddr - iy * nx;
      const auto reg2d_corner_lon = &remap_grid->reg2d_corner_lon[ix];
      const auto reg2d_corner_lat = &remap_grid->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 = &remap_grid->cell_corner_lon[cellAddr * numCellCorners];
      const auto cell_corner_lat = &remap_grid->cell_corner_lat[cellAddr * numCellCorners];
      for (size_t i = 0; i < numCellCorners; ++i) gcLLtoXYZ(cell_corner_lon[i], cell_corner_lat[i], xyz[i]);

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

static void
setCoordinatesYac(const size_t numCells, RemapGridType remapGridType, const Varray<size_t> &cellIndices,
                  const size_t numCellCorners, RemapGrid *remap_grid, const Varray<grid_cell> &yac_grid_cell)
{
  for (size_t n = 0; n < numCells; ++n)
    setCellCoordinatesYac(remapGridType, cellIndices[n], numCellCorners, remap_grid, yac_grid_cell[n], false);
}

static void
reg2d_bound_box(RemapGrid *remap_grid, double *grid_bound_box)
{
  const auto nx = remap_grid->dims[0];
  const auto ny = remap_grid->dims[1];
  const auto &reg2d_corner_lon = remap_grid->reg2d_corner_lon;
  const auto &reg2d_corner_lat = remap_grid->reg2d_corner_lat;

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

static void
greatCircleTypeInit(std::vector<enum yac_edge_type> &great_circle_type, size_t src_num_cell_corners, size_t tgt_num_cell_corners)
{
  auto max_num_cell_corners = src_num_cell_corners;
  if (tgt_num_cell_corners > max_num_cell_corners) max_num_cell_corners = tgt_num_cell_corners;

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

static void
scaleCellFrac(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
yacGridCellInit(grid_cell &cell, size_t numCellCorners, enum yac_edge_type *edgeType)
{
  cell.array_size = numCellCorners;
  cell.num_corners = numCellCorners;
  cell.edge_type = edgeType;
  cell.coordinates_x = new double[numCellCorners];
  cell.coordinates_y = new double[numCellCorners];
  cell.coordinates_xyz = new double[numCellCorners][3];
}

static void
yacGridCellFree(const grid_cell &cell)
{
  delete[] cell.coordinates_x;
  delete[] cell.coordinates_y;
  delete[] cell.coordinates_xyz;
}

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

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

  return n;
}

static size_t
remove_invalid_weights(size_t grid_size, size_t num_weights, Varray<double> &partial_weights, Varray<size_t> &srch_add)
{
  size_t n = 0;
  for (size_t i = 0; i < num_weights; ++i)
    {
      const auto cell_add = (partial_weights[i] > 0.0) ? srch_add[i] : grid_size;
      if (cell_add != grid_size)
        {
          partial_weights[n] = partial_weights[i];
          srch_add[n] = cell_add;
          n++;
        }
    }

  return n;
}

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

  return n;
}

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

  auto lcheck = true;

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

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

  progress::init();

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

  auto src_grid_size = src_grid->size;
  auto tgt_grid_size = tgt_grid->size;

  auto src_num_cell_corners = src_grid->num_cell_corners;
  auto tgt_num_cell_corners = tgt_grid->num_cell_corners;

  enum yac_edge_type lonlat_circle_type[] = { LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE };
  std::vector<enum yac_edge_type> great_circle_type;
  greatCircleTypeInit(great_circle_type, src_num_cell_corners, tgt_num_cell_corners);

  auto src_edge_type = great_circle_type.data();
  auto tgt_edge_type = great_circle_type.data();

  enum cell_type target_cell_type = UNDEF_CELL;

  if (src_num_cell_corners == 4)
    {
      const auto lonlat_circle_index = get_lonlat_circle_index(src_grid);
      if (lonlat_circle_index >= 0) src_edge_type = &lonlat_circle_type[lonlat_circle_index];
    }

  if (tgt_num_cell_corners == 4)
    {
      const auto lonlat_circle_index = get_lonlat_circle_index(tgt_grid);
      if (lonlat_circle_index >= 0)
        {
          target_cell_type = LON_LAT_CELL;
          tgt_edge_type = &lonlat_circle_type[lonlat_circle_index];
        }
    }

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

  std::vector<grid_cell> tgt_grid_cell(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i) yacGridCellInit(tgt_grid_cell[i], tgt_num_cell_corners, tgt_edge_type);

  std::vector<search_t> search(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i)
    {
      search[i].srch_corners = src_num_cell_corners;
      search[i].src_edge_type = src_edge_type;
      search[i].max_srch_cells = 0;
    }

  auto srch_corners = src_num_cell_corners;  // num of corners of srch cells

  double src_grid_bound_box[4];
  if (srcGridType == RemapGridType::Reg2D) reg2d_bound_box(src_grid, src_grid_bound_box);

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

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

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

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

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

  // Loop over destination grid

#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic) default(shared)
#endif
  for (size_t tgt_cell_add = 0; tgt_cell_add < tgt_grid_size; ++tgt_cell_add)
    {
      const auto ompthID = cdo_omp_get_thread_num();

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

      weightLinks[tgt_cell_add].nlinks = 0;

      if (!tgt_grid->mask[tgt_cell_add]) continue;

      setCellCoordinatesYac(tgtGridType, tgt_cell_add, tgt_num_cell_corners, tgt_grid, tgt_grid_cell[ompthID]);

      // Get search cells
      size_t num_srch_cells;
      if (useCellsearch)
        {
          // num_srch_cells = remap_search_cells(rsearch, target_cell_type == LON_LAT_CELL, tgt_grid_cell[ompthID],
          // srch_add[ompthID].data());
          num_srch_cells
              = remap_search_cells(rsearch, tgtGridType == RemapGridType::Reg2D, tgt_grid_cell[ompthID], srch_add[ompthID]);
        }
      else
        {
          float tgt_cell_bound_box_r[4];
          boundbox_from_corners1r(tgt_cell_add, tgt_num_cell_corners, tgt_grid->cell_corner_lon, tgt_grid->cell_corner_lat,
                                  tgt_cell_bound_box_r);

          num_srch_cells
              = get_srch_cells(tgt_cell_add, rsearch.tgtBins, rsearch.srcBins, tgt_cell_bound_box_r, srch_add[ompthID]);
        }

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

      if (0 && Options::cdoVerbose) printf("tgt_cell_add %zu  num_srch_cells %zu\n", tgt_cell_add, num_srch_cells);

      if (num_srch_cells == 0) continue;

      // Create search arrays

      cellSearchRealloc(num_srch_cells, search[ompthID]);

      setCoordinatesYac(num_srch_cells, srcGridType, srch_add[ompthID], srch_corners, src_grid, search[ompthID].src_grid_cells);

      if (tgt_num_cell_corners < 4 || target_cell_type == LON_LAT_CELL)
        {
          cdo_compute_overlap_areas(num_srch_cells, search[ompthID], tgt_grid_cell[ompthID]);
        }
      else
        {
          cdo_compute_concave_overlap_areas(num_srch_cells, search[ompthID], tgt_grid_cell[ompthID],
                                            tgt_grid->cell_center_lon[tgt_cell_add], tgt_grid->cell_center_lat[tgt_cell_add]);
        }

      auto &partial_areas = search[ompthID].partial_areas;
      auto &partial_weights = search[ompthID].partial_weights;

      auto num_weights = remove_invalid_areas(num_srch_cells, partial_areas, srch_add[ompthID]);

      const auto tgt_area = gridcell_area(tgt_grid_cell[ompthID]);
      tgt_grid->cell_area[tgt_cell_add] = tgt_area;

      for (size_t i = 0; i < num_weights; ++i) partial_weights[i] = partial_areas[i] / tgt_area;

      if (rv.normOpt == NormOpt::FRACAREA) yac_correct_weights(num_weights, partial_weights.data());

      for (size_t i = 0; i < num_weights; ++i) partial_weights[i] *= tgt_area;

      num_weights = remove_invalid_weights(src_grid_size, num_weights, partial_weights, srch_add[ompthID]);

      vec_add_weights(src_grid->cell_area, num_weights, partial_weights, srch_add[ompthID]);

      num_weights = remove_unmask_weights(src_grid->mask, num_weights, partial_weights, srch_add[ompthID]);

      vec_add_weights(src_grid->cell_frac, num_weights, partial_weights, srch_add[ompthID]);

      tgt_grid->cell_frac[tgt_cell_add] = varray_sum(num_weights, partial_weights);

      store_weightlinks(1, num_weights, srch_add[ompthID].data(), partial_weights.data(), tgt_cell_add, weightLinks);
    }

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

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

  // Finished with all cells: deallocate search arrays
  for (auto ompthID = 0; ompthID < Threading::ompNumThreads; ++ompthID)
    {
      cellSearchFree(search[ompthID]);
      yacGridCellFree(tgt_grid_cell[ompthID]);
    }

  weight_links_to_remap_links(1, tgt_grid_size, weightLinks, rv);

  // Normalize weights using destination area if requested
  remapNormalizeWeights(tgt_grid, rv);

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

  scaleCellFrac(src_grid_size, src_grid->cell_frac, src_grid->cell_area);
  scaleCellFrac(tgt_grid_size, tgt_grid->cell_frac, tgt_grid->cell_area);

  // Perform some error checking on final weights
  if (lcheck)
    {
      remap_check_area(src_grid_size, &src_grid->cell_area[0], "Source");
      remap_check_area(tgt_grid_size, &tgt_grid->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
conservRemap(const Varray<T> &src_array, size_t num_weights, const Varray<double> &weights, const Varray<size_t> &src_add)
{
  double tgt_point = 0.0;
  for (size_t i = 0; i < num_weights; ++i) tgt_point += src_array[src_add[i]] * weights[i];

  return tgt_point;
}

template <typename T>
static void
remap_conserv(NormOpt normOpt, RemapSearch &rsearch, const Varray<T> &src_array, Varray<T> &tgt_array, T missval)
{
  auto src_grid = rsearch.srcGrid;
  auto tgt_grid = rsearch.tgtGrid;

  auto lcheck = true;

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

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

  progress::init();

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

  auto src_grid_size = src_grid->size;
  auto tgt_grid_size = tgt_grid->size;

  Varray<short> src_grid_mask(src_grid_size);
#ifdef _OPENMP
#pragma omp parallel for default(shared) schedule(static)
#endif
  for (size_t i = 0; i < src_grid_size; ++i) src_grid_mask[i] = !DBL_IS_EQUAL(src_array[i], missval);

  auto src_num_cell_corners = src_grid->num_cell_corners;
  auto tgt_num_cell_corners = tgt_grid->num_cell_corners;

  enum yac_edge_type lonlat_circle_type[] = { LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE };
  std::vector<enum yac_edge_type> great_circle_type;
  greatCircleTypeInit(great_circle_type, src_num_cell_corners, tgt_num_cell_corners);

  auto src_edge_type = great_circle_type.data();
  auto tgt_edge_type = great_circle_type.data();

  enum cell_type target_cell_type = UNDEF_CELL;

  if (src_num_cell_corners == 4)
    {
      const auto lonlat_circle_index = get_lonlat_circle_index(src_grid);
      if (lonlat_circle_index >= 0) src_edge_type = &lonlat_circle_type[lonlat_circle_index];
    }

  if (tgt_num_cell_corners == 4)
    {
      const auto lonlat_circle_index = get_lonlat_circle_index(tgt_grid);
      if (lonlat_circle_index >= 0)
        {
          target_cell_type = LON_LAT_CELL;
          tgt_edge_type = &lonlat_circle_type[lonlat_circle_index];
        }
    }

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

  std::vector<grid_cell> tgt_grid_cell(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i) yacGridCellInit(tgt_grid_cell[i], tgt_num_cell_corners, tgt_edge_type);

  std::vector<search_t> search(Threading::ompNumThreads);
  for (int i = 0; i < Threading::ompNumThreads; ++i)
    {
      search[i].srch_corners = src_num_cell_corners;
      search[i].src_edge_type = src_edge_type;
      search[i].max_srch_cells = 0;
    }

  auto srch_corners = src_num_cell_corners;  // num of corners of srch cells

  double src_grid_bound_box[4];
  if (srcGridType == RemapGridType::Reg2D) reg2d_bound_box(src_grid, src_grid_bound_box);

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

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

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

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

  // Loop over destination grid

#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic) default(shared)
#endif
  for (size_t tgt_cell_add = 0; tgt_cell_add < tgt_grid_size; ++tgt_cell_add)
    {
      const auto ompthID = cdo_omp_get_thread_num();

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

      tgt_array[tgt_cell_add] = missval;

      if (!tgt_grid->mask[tgt_cell_add]) continue;

      setCellCoordinatesYac(tgtGridType, tgt_cell_add, tgt_num_cell_corners, tgt_grid, tgt_grid_cell[ompthID]);

      // Get search cells
      size_t num_srch_cells;
      if (useCellsearch)
        {
          // num_srch_cells = remap_search_cells(rsearch, target_cell_type == LON_LAT_CELL, tgt_grid_cell[ompthID],
          // srch_add[ompthID].data());
          num_srch_cells
              = remap_search_cells(rsearch, tgtGridType == RemapGridType::Reg2D, tgt_grid_cell[ompthID], srch_add[ompthID]);
        }
      else
        {
          float tgt_cell_bound_box_r[4];
          boundbox_from_corners1r(tgt_cell_add, tgt_num_cell_corners, tgt_grid->cell_corner_lon, tgt_grid->cell_corner_lat,
                                  tgt_cell_bound_box_r);

          num_srch_cells
              = get_srch_cells(tgt_cell_add, rsearch.tgtBins, rsearch.srcBins, tgt_cell_bound_box_r, srch_add[ompthID]);
        }

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

      if (0 && Options::cdoVerbose) printf("tgt_cell_add %zu  num_srch_cells %zu\n", tgt_cell_add, num_srch_cells);

      if (num_srch_cells == 0) continue;

      // Create search arrays

      cellSearchRealloc(num_srch_cells, search[ompthID]);

      setCoordinatesYac(num_srch_cells, srcGridType, srch_add[ompthID], srch_corners, src_grid, search[ompthID].src_grid_cells);

      if (tgt_num_cell_corners < 4 || target_cell_type == LON_LAT_CELL)
        {
          cdo_compute_overlap_areas(num_srch_cells, search[ompthID], tgt_grid_cell[ompthID]);
        }
      else
        {
          cdo_compute_concave_overlap_areas(num_srch_cells, search[ompthID], tgt_grid_cell[ompthID],
                                            tgt_grid->cell_center_lon[tgt_cell_add], tgt_grid->cell_center_lat[tgt_cell_add]);
        }

      auto &partial_areas = search[ompthID].partial_areas;
      auto &partial_weights = search[ompthID].partial_weights;

      auto num_weights = remove_invalid_areas(num_srch_cells, partial_areas, srch_add[ompthID]);

      const auto tgt_area = gridcell_area(tgt_grid_cell[ompthID]);
      tgt_grid->cell_area[tgt_cell_add] = tgt_area;

      for (size_t i = 0; i < num_weights; ++i) partial_weights[i] = partial_areas[i] / tgt_area;

      if (normOpt == NormOpt::FRACAREA) yac_correct_weights(num_weights, partial_weights.data());

      for (size_t i = 0; i < num_weights; ++i) partial_weights[i] *= tgt_area;

      num_weights = remove_invalid_weights(src_grid_size, num_weights, partial_weights, srch_add[ompthID]);

      num_weights = remove_unmask_weights(src_grid_mask, num_weights, partial_weights, srch_add[ompthID]);

      tgt_grid->cell_frac[tgt_cell_add] = varray_sum(num_weights, partial_weights);

      if (num_weights)
        {
          sort_weights(num_weights, srch_add[ompthID].data(), partial_weights.data());
          // Normalize weights using destination area if requested
          remapNormalizeWeights(normOpt, tgt_area, tgt_grid->cell_frac[tgt_cell_add], num_weights, partial_weights.data());
          tgt_array[tgt_cell_add] = conservRemap(src_array, num_weights, partial_weights, srch_add[ompthID]);
        }
    }

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

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

  // Finished with all cells: deallocate search arrays

  for (auto ompthID = 0; ompthID < Threading::ompNumThreads; ++ompthID)
    {
      cellSearchFree(search[ompthID]);
      yacGridCellFree(tgt_grid_cell[ompthID]);
    }

  scaleCellFrac(tgt_grid_size, tgt_grid->cell_frac, tgt_grid->cell_area);

  // Perform some error checking on final weights
  if (lcheck) remap_check_area(tgt_grid_size, &tgt_grid->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 &rsearch, const Field &field1, Field &field2)
{
  if (field1.memType == MemType::Float)
    remap_conserv(normOpt, rsearch, field1.vec_f, field2.vec_f, (float) field1.missval);
  else
    remap_conserv(normOpt, rsearch, field1.vec_d, field2.vec_d, field1.missval);
}

template <typename T>
static size_t
remove_missing_weights(const Varray<T> &src_array, T missval, size_t num_weights, Varray<double> &partial_weights,
                       Varray<size_t> &srch_add)
{
  size_t n = 0;
  for (size_t i = 0; i < num_weights; ++i)
    {
      const auto cell_add = srch_add[i];
      if (!DBL_IS_EQUAL(src_array[cell_add], missval))
        {
          partial_weights[n] = partial_weights[i];
          srch_add[n] = cell_add;
          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
calc_lat_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 lat_bounds[2]
          = { static_cast<int>(scaleFactor * ybounds2[2 * i2]), static_cast<int>(scaleFactor * ybounds2[2 * i2 + 1]) };
      if (lat_bounds[0] > lat_bounds[1]) std::swap(lat_bounds[0], lat_bounds[1]);

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

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

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

  enum yac_edge_type lonlat_circle_type[] = { LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE, LAT_CIRCLE, LON_CIRCLE };
  std::vector<enum yac_edge_type> great_circle_type(nv1, GREAT_CIRCLE);

  auto src_edge_type = great_circle_type.data();

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

#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 lat_bounds[2] = { ybounds2[2 * i2], ybounds2[2 * i2 + 1] };
      if (lat_bounds[0] > lat_bounds[1]) std::swap(lat_bounds[0], lat_bounds[1]);

      const auto tgt_area = calc_lat_area(lat_bounds[0], lat_bounds[1]);
      // printf("tgt_area %zu %g\n", i2 + 1, tgt_area);

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

      search_t search;
      search.srch_corners = nv1;
      search.src_edge_type = src_edge_type;
      search.max_srch_cells = 0;

      cellSearchRealloc(num_srch_cells, search);

      for (size_t j = 0; j < num_srch_cells; ++j)
        {
          const auto cellIndex = remapIndices[i2][j];
          double(*xyz)[3] = search.src_grid_cells[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 &partial_areas = search.partial_areas;
      auto &partial_weights = search.partial_weights;
      auto overlap_cells = search.overlap_buffer.data();

      // Do the clipping and get the cell for the overlapping area
      yac_cell_lat_clipping(num_srch_cells, search.src_grid_cells.data(), lat_bounds, overlap_cells);

      // Get the partial areas for the overlapping regions
      for (size_t i = 0; i < num_srch_cells; i++) partial_areas[i] = gridcell_area(overlap_cells[i]);

      auto num_weights = remove_invalid_areas(num_srch_cells, partial_areas, remapIndices[i2]);
      // printf("num_weights: %zu %zu\n", num_srch_cells, num_weights);

      for (size_t i = 0; i < num_weights; ++i) partial_weights[i] = partial_areas[i] / tgt_area;

      if (normOpt == NormOpt::FRACAREA) yac_correct_weights(num_weights, partial_weights.data());

      for (size_t i = 0; i < num_weights; ++i) partial_weights[i] *= tgt_area;

      num_weights = remove_invalid_weights(gridsize1, num_weights, partial_weights, remapIndices[i2]);

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

      cellSearchFree(search);
    }
}

template <typename T1, typename T2>
static size_t
remap_zonal_mean(const Varray2D<size_t> &remapIndices, const Varray2D<double> &remapWeights, const Varray<T1> &src_array,
                 Varray<T2> &tgt_array, 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 tgt_area = 0.0;  // not needed for NormOpt::FRACAREA

      auto num_weights = remapWeights[i2].size();
      Varray<size_t> search_indices(num_weights);
      for (size_t i = 0; i < num_weights; ++i) search_indices[i] = remapIndices[i2][i];
      Varray<double> partial_weights(num_weights);
      for (size_t i = 0; i < num_weights; ++i) partial_weights[i] = remapWeights[i2][i];

      num_weights = remove_missing_weights(src_array, missval, num_weights, partial_weights, search_indices);

      tgt_array[i2] = missval;

      if (num_weights)
        {
          const auto tgt_cell_frac = varray_sum(num_weights, partial_weights);
          sort_weights(num_weights, search_indices.data(), partial_weights.data());
          // Normalize weights using destination area if requested
          remapNormalizeWeights(normOpt, tgt_area, tgt_cell_frac, num_weights, partial_weights.data());
          tgt_array[i2] = conservRemap(src_array, num_weights, partial_weights, search_indices);
        }
    }

  size_t nmiss = 0;
  for (size_t i2 = 0; i2 < ysize2; ++i2)
    if (DBL_IS_EQUAL(tgt_array[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
}
