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

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

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

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

#include <algorithm>  // sort

#include <cdi.h>

#include "cdo_options.h"
#include "dmemory.h"
#include "process_int.h"
#include "cdi_lockedIO.h"
#include <mpim_grid.h>
#include "grid_point_search.h"
#include "verifygrid.h"

constexpr int MAX_CHILDS = 9;

struct CellIndex
{
  long ncells;
  long *neighbor;  // neighbor cell index
  long *parent;    // parent cell index
  long *child;     // child cell index
  const char *filename;
};

static void
copy_data_to_index(long ncells, const Varray<double> &data, long *cellindex)
{
  for (long i = 0; i < ncells; ++i) cellindex[i] = std::lround(data[i]);
}

static void
free_cellindex(CellIndex *cellindex)
{
  if (cellindex->neighbor) Free(cellindex->neighbor);
  if (cellindex->parent) Free(cellindex->parent);
  if (cellindex->child) Free(cellindex->child);
  Free(cellindex);
}

static CellIndex *
read_cellindex(const std::string &filename)
{
  const auto streamID = stream_open_read_locked(filename.c_str());
  const auto vlistID = streamInqVlist(streamID);
  const auto ngrids = vlistNgrids(vlistID);
  int gridID = -1;
  for (int index = 0; index < ngrids; ++index)
    {
      gridID = vlistGrid(vlistID, index);
      if (gridInqType(gridID) == GRID_UNSTRUCTURED && gridInqNvertex(gridID) == 3) break;
    }

  if (gridID == -1) cdo_abort("No ICON grid found in %s!", filename);

  // int nid = CDI_UNDEFID;
  int pid = CDI_UNDEFID;
  // int cid = CDI_UNDEFID;
  const auto nvars = vlistNvars(vlistID);
  char varname[CDI_MAX_NAME];
  for (int varID = 0; varID < nvars; ++varID)
    {
      vlistInqVarName(vlistID, varID, varname);
      /*
      if (strcmp(varname, "neighbor_cell_index") == 0)
        {
          nid = varID;
          break;
        }
      */
      if (strcmp(varname, "parent_cell_index") == 0)
        {
          pid = varID;
          break;
        }
      /*
      if ( strcmp(varname, "child_cell_index") == 0 )
        {
          cid = varID;
          break;
        }
      */
    }

  // if (nid == CDI_UNDEFID) cdo_abort("neighbor_cell_index not found in %s!", filename);
  // if (pid == CDI_UNDEFID) cdo_abort("parent_cell_index not found in %s!", filename);
  // if (cid == CDI_UNDEFID) cdo_abort("child_cell_index not found in %s!", filename);

  const long ncells = gridInqSize(gridID);

  CellIndex *cellindex = (CellIndex *) Malloc(sizeof(CellIndex));
  cellindex->ncells = ncells;

  cellindex->neighbor = nullptr;
  // cellindex->neighbor = (long*) Malloc(3*ncells*sizeof(long));
  cellindex->parent = (long *) Malloc(ncells * sizeof(long));
  cellindex->child = nullptr;
  // cellindex->child    = (cid != CDI_UNDEFID) ? (int*) Malloc(MAX_CHILDS*ncells*sizeof(int)) : nullptr;
  Varray<double> data(ncells);

  for (long i = 0; i < ncells; ++i) cellindex->parent[i] = 0;

  const auto nrecs = streamInqTimestep(streamID, 0);
  for (int recID = 0; recID < nrecs; recID++)
    {
      int varID, levelID;
      size_t nmiss;
      streamInqRecord(streamID, &varID, &levelID);
      if (varID == pid /* || varID == nid || varID == cid */)
        {
          streamReadRecord(streamID, data.data(), &nmiss);
          if (varID == pid)
            {
              if (Options::cdoVerbose) cdo_print("Read parent_cell_index");
              copy_data_to_index(ncells, data, cellindex->parent);
            }
          // else if ( varID == nid ) copy_data_to_index(ncells, data, cellindex->neighbor+levelID*ncells);
          // else if ( varID == cid ) copy_data_to_index(ncells, data, cellindex->child+levelID*ncells);
        }
    }

  // Fortran to C index
  for (long i = 0; i < ncells; ++i) cellindex->parent[i] -= 1;
  // for ( long i = 0; i < 3*ncells; ++i ) cellindex->neighbor[i] -= 1;

  streamClose(streamID);

  return cellindex;
}

static int
read_grid(const char *filename)
{
  const auto streamID = stream_open_read_locked(filename);
  const auto vlistID = streamInqVlist(streamID);
  const auto ngrids = vlistNgrids(vlistID);
  int gridID = -1;
  for (int index = 0; index < ngrids; ++index)
    {
      gridID = vlistGrid(vlistID, index);
      if (gridInqType(gridID) == GRID_UNSTRUCTURED && gridInqNvertex(gridID) == 3) break;
    }

  if (gridID == -1) cdo_abort("No ICON grid found in %s!", filename);

  const auto gridID2 = gridDuplicate(gridID);

  streamClose(streamID);

  return gridID2;
}

/**
* Return the first index of element x fits.
*
* If no interval can be found return -1.

* @param *array ascending sorted list
* @param n      length of the sorted list
* @param search the element to find a position for
*/
static long
find_index(int search, long n, const long *array)
{
  long first = 0;
  long last = n - 1;
  long middle = (first + last) / 2;

  while (first <= last)
    {
      if (array[middle] < search)
        first = middle + 1;
      else if (array[middle] == search)
        {
          for (long i = middle; i >= 0; i--)
            {
              if (array[i] == search)
                middle = i;
              else
                break;
            }
          return middle;
        }
      else
        last = middle - 1;

      middle = (first + last) / 2;
    }

  return -1;
}

struct SortInfo
{
  int p, i;
};

static bool
cmpsinfo(const SortInfo &a, const SortInfo &b)
{
  return a.p < b.p;
}

static void
compute_child_from_parent(CellIndex *cellindex1, CellIndex *cellindex2)
{
  if (Options::cdoVerbose) cdo_print("%s", __func__);

  const auto ncells1 = cellindex1->ncells;
  long *parent1 = cellindex1->parent;

  std::vector<long> idx1(ncells1);
  for (long i = 0; i < ncells1; ++i) idx1[i] = i;
  for (long i = 1; i < ncells1; ++i)
    if (parent1[i] < parent1[i - 1])
      {
        if (Options::cdoVerbose) cdo_print("Sort parent index of %s!", cellindex1->filename);
        std::vector<SortInfo> sinfo(ncells1);
        for (long j = 0; j < ncells1; ++j)
          {
            sinfo[j].p = parent1[j];
            sinfo[j].i = idx1[j];
          }
        std::sort(sinfo.begin(), sinfo.end(), cmpsinfo);
        for (long j = 0; j < ncells1; ++j)
          {
            parent1[j] = sinfo[j].p;
            idx1[j] = sinfo[j].i;
          }
        break;
      }

  const auto ncells2 = cellindex2->ncells;
  long *child2 = (long *) Malloc(MAX_CHILDS * ncells2 * sizeof(long));
  cellindex2->child = child2;
  for (long i = 0; i < ncells2; ++i)
    {
      for (long k = 0; k < MAX_CHILDS; ++k) child2[i * MAX_CHILDS + k] = -1;
      long j = find_index(i, ncells1, parent1);
      if (j < 0) continue;
      for (long k = 0; k < MAX_CHILDS; ++k)
        {
          if (i != parent1[j + k]) break;
          //  child2[i*MAX_CHILDS+k] = j+k;
          child2[i * MAX_CHILDS + k] = idx1[j + k];
        }
      // if ( i%10000 == 0 ) printf("%d %d %d %d %d %d\n", i, j, parent1[j], parent1[j+1], parent1[j+2], parent1[j+3]);
    }
}

static void
read_coordinates(const char *filename, long n, double *lon, double *lat, int nv, double *lon_bnds, double *lat_bnds)
{
  const auto streamID = streamOpenRead(filename);
  const auto vlistID = streamInqVlist(streamID);
  const auto ngrids = vlistNgrids(vlistID);
  int gridID = -1;
  for (int index = 0; index < ngrids; ++index)
    {
      gridID = vlistGrid(vlistID, index);
      if (gridInqType(gridID) == GRID_UNSTRUCTURED && (long) gridInqSize(gridID) == n && gridInqNvertex(gridID) == 3) break;
    }

  if (gridID == -1) cdo_abort("No ICON grid with %ld cells found in %s!", n, filename);

  gridInqXvals(gridID, lon);
  gridInqYvals(gridID, lat);

  // Convert lat/lon units if required
  cdo_grid_to_radian(gridID, CDI_XAXIS, n, lon, "grid center lon");
  cdo_grid_to_radian(gridID, CDI_YAXIS, n, lat, "grid center lat");

  if (nv == 3 && lon_bnds && lat_bnds)
    {
      gridInqXbounds(gridID, lon_bnds);
      gridInqYbounds(gridID, lat_bnds);

      cdo_grid_to_radian(gridID, CDI_XAXIS, n * 3, lon_bnds, "grid corner lon");
      cdo_grid_to_radian(gridID, CDI_YAXIS, n * 3, lat_bnds, "grid corner lat");
    }

  streamClose(streamID);
}

constexpr int MAX_SEARCH = 128;  // the triangles are distorted!

static void
compute_child_from_bounds(CellIndex *cellindex2, long ncells2, double *grid_center_lon2, double *grid_center_lat2,
                          double *grid_corner_lon2, double *grid_corner_lat2, long ncells1, const Varray<double> &grid_center_lon1,
                          const Varray<double> &grid_center_lat1)
{
  if (Options::cdoVerbose) cdo_print("%s", __func__);

  bool xIsCyclic = false;
  size_t dims[2];
  dims[0] = ncells1;
  dims[1] = 0;
  GridPointSearch gps;
  grid_point_search_create(gps, xIsCyclic, dims, ncells1, grid_center_lon1, grid_center_lat1);
  knnWeightsType knnWeights(MAX_SEARCH);
  size_t *nbr_addr = &knnWeights.m_addr[0];

  int ncorner = 3;
  Point3D center_point_xyz;
  Varray<Point3D> cell_corners_xyz(4);
  Point center_point_plane_projection;
  Varray<Point> cell_corners_plane_projection(4);

  long *child2 = (long *) Malloc(MAX_CHILDS * ncells2 * sizeof(long));
  cellindex2->child = child2;
  for (long cell_no2 = 0; cell_no2 < ncells2; ++cell_no2)
    {
      for (int k = 0; k < MAX_CHILDS; ++k) child2[cell_no2 * MAX_CHILDS + k] = -1;

      set_cell_corners_xyz(ncorner, &grid_corner_lon2[cell_no2 * ncorner], &grid_corner_lat2[cell_no2 * ncorner], cell_corners_xyz);
      cell_corners_xyz[ncorner] = cell_corners_xyz[0];

      const auto coordinate_to_ignore = find_coordinate_to_ignore(cell_corners_xyz);

      bool invert_result = false;
      const auto cval = (coordinate_to_ignore == 1) ? cell_corners_xyz[0].X
                                                    : ((coordinate_to_ignore == 2) ? cell_corners_xyz[0].Y : cell_corners_xyz[0].Z);
      if (cval < 0.0) invert_result = true;

      set_cell_corners_plane_projection(coordinate_to_ignore, ncorner, cell_corners_xyz, cell_corners_plane_projection);

      const auto polygon_area = calculate_the_polygon_area(cell_corners_plane_projection, ncorner + 1);
      auto is_clockwise = are_polygon_vertices_arranged_in_clockwise_order(polygon_area);

      if (invert_result) is_clockwise = !is_clockwise;
      if (is_clockwise) continue;

      grid_search_point(gps, grid_center_lon2[cell_no2], grid_center_lat2[cell_no2], knnWeights);
      int k = 0;

      double center_coordinates[3];
      for (int i = 0; i < MAX_SEARCH; ++i)
        {
          const auto cell_no1 = nbr_addr[i];
          if (cell_no1 < SIZE_MAX)
            {
              gcLLtoXYZ(grid_center_lon1[cell_no1], grid_center_lat1[cell_no1], center_coordinates);
              center_point_xyz.X = center_coordinates[0];
              center_point_xyz.Y = center_coordinates[1];
              center_point_xyz.Z = center_coordinates[2];

              set_center_point_plane_projection(coordinate_to_ignore, center_point_xyz, center_point_plane_projection);

              auto winding_number
                  = winding_numbers_algorithm(cell_corners_plane_projection, ncorner + 1, center_point_plane_projection);
              // printf("%d %g %g %g %g %d\n", cell_no2,
              // RAD2DEG*grid_center_lon2[cell_no2],
              // RAD2DEG*grid_center_lat2[cell_no2],
              // RAD2DEG*grid_center_lon1[cell_no1],
              // RAD2DEG*grid_center_lat1[cell_no1], winding_number);
              if (winding_number != 0)
                {
                  if (k >= MAX_CHILDS) cdo_abort("Internal problem, limit of MAX_CHILDS reached (limit=9).");
                  child2[cell_no2 * MAX_CHILDS + k++] = (long) cell_no1;
                }
            }
        }
      // printf("%d k = %d\n", cell_no2, k);
    }

  grid_point_search_delete(gps);
}

static void
compute_child_from_coordinates(CellIndex *cellindex1, CellIndex *cellindex2)
{
  if (Options::cdoVerbose) cdo_print("%s", __func__);

  const auto ncells1 = cellindex1->ncells;
  const auto ncells2 = cellindex2->ncells;

  Varray<double> lon1(ncells1), lat1(ncells1), lon2(ncells2), lat2(ncells2);
  Varray<double> lon2_bnds(3 * ncells2), lat2_bnds(3 * ncells2);

  read_coordinates(cellindex1->filename, ncells1, lon1.data(), lat1.data(), 0, nullptr, nullptr);
  read_coordinates(cellindex2->filename, ncells2, lon2.data(), lat2.data(), 3, lon2_bnds.data(), lat2_bnds.data());

  compute_child_from_bounds(cellindex2, ncells2, lon2.data(), lat2.data(), lon2_bnds.data(), lat2_bnds.data(), ncells1, lon1, lat1);
}

static void
compute_child(CellIndex *cellindex1, CellIndex *cellindex2)
{
  bool lparent = true;
  const auto ncells1 = cellindex1->ncells;
  long *parent1 = cellindex1->parent;
  {
    long i;
    for (i = 0; i < ncells1; ++i)
      if (parent1[i] >= 0) break;
    if (i == ncells1) lparent = false;
  }

  if (lparent)
    compute_child_from_parent(cellindex1, cellindex2);
  else
    {
      compute_child_from_coordinates(cellindex1, cellindex2);
      // cdo_abort("Missing parent index of %s!", cellindex1->filename);
    }
}

static void
compute_sum(long i, long &n, double &sum, double &sumq, long kci, CellIndex **cellindex, const Varray<double> &array)
{
  // printf("compute: i, kci %d %d\n", i, kci);
  const auto ncells2 = cellindex[kci]->ncells;
  if (i < 0 || i > ncells2) cdo_abort("Child grid cell index %ld out of bounds %ld!", i, ncells2);

  for (int k = 0; k < MAX_CHILDS; ++k)
    {
      long index = cellindex[kci]->child[i * MAX_CHILDS + k];
      if (index == -1) break;
      if (kci == 1)
        {
          sum += array[index];
          sumq += array[index] * array[index];
          n += 1;
        }
      else
        compute_sum(index, n, sum, sumq, kci - 1, cellindex, array);
    }
}

static void
samplegrid(double missval, long nci, CellIndex **cellindex, const Varray<double> &array1, Varray<double> &array2,
           Varray<double> &array3)
{
  static bool lstat = true;
  long kci = nci - 1;
  const auto ncells2 = cellindex[kci]->ncells;
  long nx = 0;
  double x = 0;
#ifdef _OPENMP
//#pragma omp parallel for default(none) shared(missval, ncells2, kci, cellindex, array1, array2, array3)
#endif
  for (long i = 0; i < ncells2; ++i)
    {
      long n = 0;
      double sum = 0, sumq = 0;
      compute_sum(i, n, sum, sumq, kci, cellindex, array1);
      array2[i] = n ? sum / n : missval;  // mean
      double var1 = (n * n > n) ? (sumq * n - sum * sum) / (n * n - n) : missval;
      if (var1 < 0 && var1 > -1.e-5) var1 = 0;
      array3[i] = var_to_std(var1, missval);  // std1
      if (lstat && n)
        {
          nx++;
          x += n;
        }
    }
  if (Options::cdoVerbose && lstat)
    {
      lstat = false;
      cdo_print("Mean number of childs %g", nx ? x / nx : 0);
    }
}

void *
Samplegridicon(void *process)
{
  cdo_initialize(process);

  cdo_operator_add("samplegridicon", 0, 0, "sample grids");

  const auto nsamplegrids = cdo_operator_argc();
  if (nsamplegrids < 2) cdo_abort("Parameter missing!");

  std::vector<CellIndex *> cellindex(nsamplegrids);

  for (int i = 0; i < nsamplegrids; ++i)
    {
      cellindex[i] = read_cellindex(cdo_operator_argv(i));
      cellindex[i]->filename = cdo_operator_argv(i).c_str();
      if (Options::cdoVerbose) cdo_print("Found %ld grid cells in %s", cellindex[i]->ncells, cellindex[i]->filename);
    }

  for (int i = 0; i < nsamplegrids - 1; ++i) compute_child(cellindex[i], cellindex[i + 1]);

  const auto gridID2 = read_grid(cdo_operator_argv(nsamplegrids - 1).c_str());

  const auto streamID1 = cdo_open_read(0);
  const auto vlistID1 = cdo_stream_inq_vlist(streamID1);

  long gridsize = vlistGridsizeMax(vlistID1);
  if (Options::cdoVerbose) cdo_print("Source gridsize = %zu", gridsize);
  if (gridsize != cellindex[0]->ncells)
    cdo_abort("Gridsize (%ld) of input stream and first grid (%ld) differ!", gridsize, cellindex[0]->ncells);
  if (vlistNumber(vlistID1) != CDI_REAL) gridsize *= 2;
  Varray<double> array1(gridsize);

  const auto vlistID2 = vlistDuplicate(vlistID1);
  const auto vlistID3 = vlistDuplicate(vlistID1);

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

  const auto ngrids = vlistNgrids(vlistID1);
  for (int index = 0; index < ngrids; ++index)
    {
      const auto gridID = vlistGrid(vlistID1, index);
      const auto gridtype = gridInqType(gridID);
      if (!(gridtype == GRID_UNSTRUCTURED && gridInqNvertex(gridID) == 3))
        cdo_abort("Unsupported gridtype: %s with %d corners", gridNamePtr(gridtype), gridInqNvertex(gridID));

      vlistChangeGridIndex(vlistID2, index, gridID2);
      vlistChangeGridIndex(vlistID3, index, gridID2);
    }

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

  const auto streamID3 = cdo_open_write(2);
  cdo_def_vlist(streamID3, vlistID3);

  auto gridsize2 = gridInqSize(gridID2);
  if (Options::cdoVerbose) cdo_print("Target gridsize = %ld", gridsize2);
  if (vlistNumber(vlistID2) != CDI_REAL) gridsize2 *= 2;
  Varray<double> array2(gridsize2), array3(gridsize2);

  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_taxis_copy_timestep(taxisID3, taxisID1);
      cdo_def_timestep(streamID2, tsID);
      cdo_def_timestep(streamID3, tsID);

      for (int recID = 0; recID < nrecs; recID++)
        {
          int varID, levelID;
          cdo_inq_record(streamID1, &varID, &levelID);
          size_t nmiss;
          cdo_read_record(streamID1, array1.data(), &nmiss);

          const auto missval = vlistInqVarMissval(vlistID1, varID);

          samplegrid(missval, nsamplegrids, &cellindex[0], array1, array2, array3);

          nmiss = varray_num_mv(gridsize2, array2, missval);
          cdo_def_record(streamID2, varID, levelID);
          cdo_write_record(streamID2, array2.data(), nmiss);

          nmiss = varray_num_mv(gridsize2, array3, missval);
          cdo_def_record(streamID3, varID, levelID);
          cdo_write_record(streamID3, array3.data(), nmiss);
        }

      tsID++;
    }

  cdo_stream_close(streamID3);
  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  vlistDestroy(vlistID2);
  gridDestroy(gridID2);

  for (int i = 0; i < nsamplegrids; ++i) free_cellindex(cellindex[i]);

  cdo_finish();

  return nullptr;
}
