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

  Author: Uwe Schulzweida

*/

#include <cdi.h>

#include "process_int.h"
#include "param_conversion.h"
#include "pmlist.h"
#include "grid_point_search.h"
#include "mpim_grid.h"

int gengridcell(const int gridID1, const size_t gridsize2, const std::vector<long> &cellidx);
void window_cell(const Field &field1, Field &field2, const std::vector<long> &cellidx);
double radiusDegToKm(const double radiusInDeg);

struct CirclePoint
{
  double radius = 1.0;
  double lon = 0.0;
  double lat = 0.0;
  size_t maxpoints = SIZE_MAX;
};

struct RegionInfo
{
  std::vector<long> cellidx;
  long nvals = 0;
  int gridtype = -1;
  int gridID1 = -1, gridID2 = -1;
};

static int
generate_circle_grid(int gridID1, long &gridsize2, std::vector<long> &cellidx, const CirclePoint &cpoint)
{
  const auto gridID0 = gridID1;

  gridID1 = generate_full_point_grid(gridID1);

  if (!gridHasCoordinates(gridID1)) cdo_abort("Cell center coordinates missing!");

  {
    const auto gridsize1 = gridInqSize(gridID1);

    Varray<double> xvals(gridsize1), yvals(gridsize1);
    gridInqXvals(gridID1, xvals.data());
    gridInqYvals(gridID1, yvals.data());

    // Convert lat/lon units if required
    cdo_grid_to_radian(gridID1, CDI_XAXIS, gridsize1, xvals.data(), "grid center lon");
    cdo_grid_to_radian(gridID1, CDI_YAXIS, gridsize1, yvals.data(), "grid center lat");

    GridPointSearch gps;
    // grid_point_search_create(gps, xvals, yvals, PointSearchMethod::spherepart);
    grid_point_search_create(gps, xvals, yvals);

    grid_point_search_set_arc_radius(gps, cpoint.radius);

    auto numNeighbors = cpoint.maxpoints;
    if (numNeighbors > gridsize1) numNeighbors = gridsize1;

    knnWeightsType knnWeights(numNeighbors);
    grid_search_point_smooth(gps, cpoint.lon, cpoint.lat, knnWeights);

    const auto nvals = knnWeights.m_numNeighbors;
    cellidx.resize(nvals);

    for (size_t i = 0; i < nvals; ++i) cellidx[i] = knnWeights.m_addr[i];

    grid_point_search_delete(gps);

    if (nvals == 0) cdo_abort("No grid points found!");

    gridsize2 = nvals;
  }

  const auto gridID2 = gengridcell(gridID1, gridsize2, cellidx);

  if (gridID0 != gridID1) gridDestroy(gridID1);

  return gridID2;
}

static void
selcircleGetParameter(CirclePoint &cpoint)
{
  const auto pargc = cdo_operator_argc();
  if (pargc)
    {
      const auto pargv = cdo_get_oper_argv();

      KVList kvlist;
      kvlist.name = "SELCIRCLE";
      if (kvlist.parse_arguments(pargc, pargv) != 0) cdo_abort("Parse error!");
      if (Options::cdoVerbose) kvlist.print();

      for (const auto &kv : kvlist)
        {
          const auto &key = kv.key;
          if (kv.nvalues > 1) cdo_abort("Too many values for parameter key >%s<!", key.c_str());
          if (kv.nvalues < 1) cdo_abort("Missing value for parameter key >%s<!", key.c_str());
          const auto &value = kv.values[0];

          // clang-format off
          if      (key == "maxpoints") cpoint.maxpoints = parameter_to_size_t(value);
          else if (key == "lon")       cpoint.lon = parameter_to_double(value);
          else if (key == "lat")       cpoint.lat = parameter_to_double(value);
          else if (key == "radius")    cpoint.radius = radius_str_to_deg(value.c_str());
          else cdo_abort("Invalid parameter key >%s<!", key.c_str());
          // clang-format on
        }
    }
}

void *
Selregion(void *process)
{
  int index;

  cdo_initialize(process);

  // clang-format off
  const auto SELCIRCLE  = cdo_operator_add("selcircle",   0,   0, nullptr);
  // clang-format on

  const auto operatorID = cdo_operator_id();

  CirclePoint cpoint;
  if (operatorID == SELCIRCLE) selcircleGetParameter(cpoint);

  if (cpoint.radius < 0.0 || cpoint.radius > 180.0) cdo_abort("%s=%g out of bounds (0-180 deg)!", "radius", cpoint.radius);

  const auto streamID1 = cdo_open_read(0);

  const auto vlistID1 = cdo_stream_inq_vlist(streamID1);
  const auto vlistID2 = vlistDuplicate(vlistID1);

  const auto taxisID1 = vlistInqTaxis(vlistID1);
  const auto taxisID2 = taxisDuplicate(taxisID1);
  vlistDefTaxis(vlistID2, taxisID2);

  const auto nvars = vlistNvars(vlistID1);
  std::vector<bool> varIDs(nvars, false);

  const auto ngrids = vlistNgrids(vlistID1);
  std::vector<RegionInfo> regions(ngrids);

  const auto gridsizemax = vlistGridsizeMax(vlistID1);
  if (gridsizemax < cpoint.maxpoints) cpoint.maxpoints = gridsizemax;
  if (Options::cdoVerbose && operatorID == SELCIRCLE)
    cdo_print("lon = %g, lat = %g, radius = %gdeg(%gkm)", cpoint.lon, cpoint.lat, cpoint.radius, radiusDegToKm(cpoint.radius));

  cpoint.radius *= DEG2RAD;
  cpoint.lon *= DEG2RAD;
  cpoint.lat *= DEG2RAD;

  for (index = 0; index < ngrids; index++)
    {
      auto &region = regions[index];
      const auto gridID1 = vlistGrid(vlistID1, index);
      const auto gridtype = gridInqType(gridID1);
      if (is_point_grid(gridID1))
        {
          int gridID2 = -1;
          if (operatorID == SELCIRCLE)
            {
              const auto gridsize = gridInqSize(gridID1);
              if (gridsize == 1) continue;

              gridID2 = generate_circle_grid(gridID1, region.nvals, region.cellidx, cpoint);
            }

          region.gridtype = gridtype;
          region.gridID1 = gridID1;
          region.gridID2 = gridID2;

          vlistChangeGridIndex(vlistID2, index, gridID2);

          for (int varID = 0; varID < nvars; varID++)
            if (gridID1 == vlistInqVarGrid(vlistID1, varID)) varIDs[varID] = true;
        }
      else if (gridtype == GRID_GENERIC && gridInqXsize(gridID1) <= 1 && gridInqYsize(gridID1) <= 1)
        {
        }
      else
        {
          cdo_print("Unsupported grid type: %s", gridNamePtr(gridtype));
          if (gridtype == GRID_GAUSSIAN_REDUCED) cdo_print("Use option -R to convert Gaussian reduced grid to a regular grid!");
          cdo_abort("Unsupported grid type!");
        }
    }

  VarList varList1, varList2;
  varListInit(varList1, vlistID1);
  varListInit(varList2, vlistID2);

  Field field1, field2;

  const auto streamID2 = cdo_open_write(1);
  cdo_def_vlist(streamID2, vlistID2);

  int tsID = 0;
  while (true)
    {
      const auto nrecs = cdo_stream_inq_timestep(streamID1, tsID);
      if (nrecs == 0) break;

      cdo_taxis_copy_timestep(taxisID2, taxisID1);
      cdo_def_timestep(streamID2, tsID);

      for (int recID = 0; recID < nrecs; recID++)
        {
          int varID, levelID;
          cdo_inq_record(streamID1, &varID, &levelID);
          field1.init(varList1[varID]);
          cdo_read_record(streamID1, field1);

          cdo_def_record(streamID2, varID, levelID);

          if (varIDs[varID])
            {
              const auto gridID1 = varList1[varID].gridID;
              for (index = 0; index < ngrids; index++)
                if (gridID1 == regions[index].gridID1) break;
              if (index == ngrids) cdo_abort("Internal problem, grid not found!");

              field2.init(varList2[varID]);
              window_cell(field1, field2, regions[index].cellidx);

              if (field1.nmiss) field_num_mv(field2);

              cdo_write_record(streamID2, field2);
            }
          else
            {
              cdo_write_record(streamID2, field1);
            }
        }

      tsID++;
    }

  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  cdo_finish();

  return nullptr;
}
