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

  Copyright (C) 2003-2020 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 double *restrict data, long *restrict 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 = streamOpenReadLocked(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) cdoAbort("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) cdoAbort("neighbor_cell_index not found in %s!", filename);
  // if (pid == CDI_UNDEFID) cdoAbort("parent_cell_index not found in %s!", filename);
  // if (cid == CDI_UNDEFID) cdoAbort("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) cdoPrint("Read parent_cell_index");
              copy_data_to_index(ncells, data.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 = streamOpenReadLocked(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) cdoAbort("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 *restrict 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) cdoPrint("%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) cdoPrint("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) cdoAbort("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);
}

#define 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) cdoPrint("%s", __func__);

  bool xIsCyclic = false;
  size_t dims[2];
  dims[0] = ncells1;
  dims[1] = 0;
  GridPointSearch gps;
  gridPointSearchCreate(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;
  double corner_coordinates[3];
  double center_point_xyz[3];
  double cell_corners_xyz[12];
  double cell_corners_plane_projection[8];
  double center_point_plane_projection[2];

  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;

      for (int corner_no = 0; corner_no < ncorner; corner_no++)
        {
          // Conversion of corner spherical coordinates to Cartesian coordinates.
          gcLLtoXYZ(grid_corner_lon2[cell_no2 * ncorner + corner_no], grid_corner_lat2[cell_no2 * ncorner + corner_no],
                    corner_coordinates);

          // The components of the result vector are appended to the list of cell corner coordinates.
          const auto off = corner_no * 3;
          cell_corners_xyz[off + 0] = corner_coordinates[0];
          cell_corners_xyz[off + 1] = corner_coordinates[1];
          cell_corners_xyz[off + 2] = corner_coordinates[2];
        }
      cell_corners_xyz[ncorner * 3 + 0] = cell_corners_xyz[0];
      cell_corners_xyz[ncorner * 3 + 1] = cell_corners_xyz[1];
      cell_corners_xyz[ncorner * 3 + 2] = cell_corners_xyz[2];

      const auto coordinate_to_ignore = find_coordinate_to_ignore(cell_corners_xyz);

      bool invert_result = false;
      if (cell_corners_xyz[coordinate_to_ignore - 1] < 0) invert_result = true;

      switch (coordinate_to_ignore)
        {
        case 1:
          for (int corner_no = 0; corner_no <= ncorner; corner_no++)
            {
              cell_corners_plane_projection[corner_no * 2 + 0] = cell_corners_xyz[corner_no * 3 + 1];
              cell_corners_plane_projection[corner_no * 2 + 1] = cell_corners_xyz[corner_no * 3 + 2];
            }
          break;
        case 2:
          for (int corner_no = 0; corner_no <= ncorner; corner_no++)
            {
              cell_corners_plane_projection[corner_no * 2 + 0] = cell_corners_xyz[corner_no * 3 + 2];
              cell_corners_plane_projection[corner_no * 2 + 1] = cell_corners_xyz[corner_no * 3 + 0];
            }
          break;
        case 3:
          for (int corner_no = 0; corner_no <= ncorner; corner_no++)
            {
              cell_corners_plane_projection[corner_no * 2 + 0] = cell_corners_xyz[corner_no * 3 + 0];
              cell_corners_plane_projection[corner_no * 2 + 1] = cell_corners_xyz[corner_no * 3 + 1];
            }
          break;
        }

      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;

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

      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_point_xyz);

              switch (coordinate_to_ignore)
                {
                case 1:
                  center_point_plane_projection[0] = center_point_xyz[1];
                  center_point_plane_projection[1] = center_point_xyz[2];
                  break;
                case 2:
                  center_point_plane_projection[0] = center_point_xyz[2];
                  center_point_plane_projection[1] = center_point_xyz[0];
                  break;
                case 3:
                  center_point_plane_projection[0] = center_point_xyz[0];
                  center_point_plane_projection[1] = center_point_xyz[1];
                  break;
                }

              int 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) cdoAbort("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);
    }

  gridPointSearchDelete(gps);
}

static void
compute_child_from_coordinates(CellIndex *cellindex1, CellIndex *cellindex2)
{
  if (Options::cdoVerbose) cdoPrint("%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);
      // cdoAbort("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) cdoAbort("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] = varToStd(var1, missval);  // std1
      if (lstat && n)
        {
          nx++;
          x += n;
        }
    }
  if (Options::cdoVerbose && lstat)
    {
      lstat = false;
      cdoPrint("Mean number of childs %g", nx ? x / nx : 0);
    }
}

void *
Samplegridicon(void *process)
{
  int nrecs;
  int varID, levelID;

  cdoInitialize(process);

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

  const auto nsamplegrids = operatorArgc();
  if (nsamplegrids < 2) cdoAbort("Parameter missing!");

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

  for (int i = 0; i < nsamplegrids; ++i)
    {
      cellindex[i] = read_cellindex(cdoOperatorArgv(i));
      cellindex[i]->filename = cdoOperatorArgv(i).c_str();
      if (Options::cdoVerbose) cdoPrint("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(cdoOperatorArgv(nsamplegrids - 1).c_str());

  const auto streamID1 = cdoOpenRead(0);
  const auto vlistID1 = cdoStreamInqVlist(streamID1);

  long gridsize = vlistGridsizeMax(vlistID1);
  if (Options::cdoVerbose) cdoPrint("Source gridsize = %zu", gridsize);
  if (gridsize != cellindex[0]->ncells)
    cdoAbort("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))
        cdoAbort("Unsupported gridtype: %s with %d corners", gridNamePtr(gridtype), gridInqNvertex(gridID));

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

  const auto streamID2 = cdoOpenWrite(1);
  cdoDefVlist(streamID2, vlistID2);

  const auto streamID3 = cdoOpenWrite(2);
  cdoDefVlist(streamID3, vlistID3);

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

  int tsID = 0;
  while ((nrecs = cdoStreamInqTimestep(streamID1, tsID)))
    {
      taxisCopyTimestep(taxisID2, taxisID1);
      taxisCopyTimestep(taxisID3, taxisID1);
      cdoDefTimestep(streamID2, tsID);
      cdoDefTimestep(streamID3, tsID);

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

          const auto missval = vlistInqVarMissval(vlistID1, varID);

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

          nmiss = varrayNumMV(gridsize2, array2, missval);
          cdoDefRecord(streamID2, varID, levelID);
          cdoWriteRecord(streamID2, array2.data(), nmiss);

          nmiss = varrayNumMV(gridsize2, array3, missval);
          cdoDefRecord(streamID3, varID, levelID);
          cdoWriteRecord(streamID3, array3.data(), nmiss);
        }

      tsID++;
    }

  cdoStreamClose(streamID3);
  cdoStreamClose(streamID2);
  cdoStreamClose(streamID1);

  vlistDestroy(vlistID2);
  gridDestroy(gridID2);

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

  cdoFinish();

  return nullptr;
}
