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

  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.
*/

/*
   This module contains the following operators:

      Zonstat    zonmin          Zonal minimum
      Zonstat    zonmax          Zonal maximum
      Zonstat    zonrange        Zonal range
      Zonstat    zonsum          Zonal sum
      Zonstat    zonmean         Zonal mean
      Zonstat    zonavg          Zonal average
      Zonstat    zonstd          Zonal standard deviation
      Zonstat    zonstd1         Zonal standard deviation [Normalize by (n-1)]
      Zonstat    zonvar          Zonal variance
      Zonstat    zonvar1         Zonal variance [Normalize by (n-1)]
      Zonstat    zonpctl         Zonal percentiles
*/

#include <cdi.h>

#include <utility>

#include "cdo_options.h"
#include "cdo_vlist.h"
#include "process_int.h"
#include "param_conversion.h"
#include <mpim_grid.h>
#include "griddes.h"

void remap_weights_zonal_mean(const int gridID1, const int gridID2, Varray2D<size_t> &remapIndices, Varray2D<double> &remapWeights);
void remap_zonal_mean(const Varray2D<size_t> &remapIndices, const Varray2D<double> &remapWeights, const Field &field1,
                      Field &field2);

static void
add_operators(void)
{
  // clang-format off
  cdo_operator_add("zonmin",    FieldFunc_Min,    0, nullptr);
  cdo_operator_add("zonmax",    FieldFunc_Max,    0, nullptr);
  cdo_operator_add("zonrange",  FieldFunc_Range,  0, nullptr);
  cdo_operator_add("zonsum",    FieldFunc_Sum,    0, nullptr);
  cdo_operator_add("zonmean",   FieldFunc_Mean,   0, nullptr);
  cdo_operator_add("zonavg",    FieldFunc_Avg,    0, nullptr);
  cdo_operator_add("zonvar",    FieldFunc_Var,    0, nullptr);
  cdo_operator_add("zonvar1",   FieldFunc_Var1,   0, nullptr);
  cdo_operator_add("zonstd",    FieldFunc_Std,    0, nullptr);
  cdo_operator_add("zonstd1",   FieldFunc_Std1,   0, nullptr);
  cdo_operator_add("zonskew",   FieldFunc_Skew,   0, nullptr);
  cdo_operator_add("zonkurt",   FieldFunc_Kurt,   0, nullptr);
  cdo_operator_add("zonmedian", FieldFunc_Median, 0, nullptr);
  cdo_operator_add("zonpctl",   FieldFunc_Pctl,   0, nullptr);
  // clang-format on
}

void *
Zonstat(void *process)
{
  int gridIDdestroy = -1, gridID1 = -1, gridID2 = -1;
  int zongridID = -1;
  int sourceGridIsRegular = true;

  cdo_initialize(process);

  add_operators();

  const auto operatorID = cdo_operator_id();
  const auto operfunc = cdo_operator_f1(operatorID);

  double pn = 0.0;
  if (operfunc == FieldFunc_Pctl)
    {
      operator_input_arg("percentile number");
      pn = parameter_to_double(cdo_operator_argv(0));
    }
  else if (cdo_operator_argc() == 1 && operfunc == FieldFunc_Mean)
    {
      sourceGridIsRegular = false;
      gridID2 = cdo_define_grid(cdo_operator_argv(0));
      const auto gridtype = gridInqType(gridID2);
      if (gridtype != GRID_GAUSSIAN && gridtype != GRID_LONLAT) cdo_abort("Target grid type must be Gaussian or LonLat!");
      if (!gridInqYbounds(gridID2, NULL)) cdo_abort("Target grid cell bounds missing!");
      if (gridInqXsize(gridID2) > 1) cdo_abort("Target grid must be zonal!");
    }
  else
    {
      operator_check_argc(0);
    }

  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 ngrids = vlistNgrids(vlistID1);
  for (int index = 0; index < ngrids; index++)
    {
      const auto gridID = vlistGrid(vlistID1, index);
      const auto xsize = gridInqXsize(gridID);
      const auto ysize = gridInqYsize(gridID);
      const auto gridtype = gridInqType(gridID);
      if (xsize > 1 || gridtype == GRID_GAUSSIAN_REDUCED)
        {
          if (gridID1 == -1) gridID1 = gridID;
        }
      else
        {
          if (ysize > 1 && zongridID == -1) zongridID = gridID;
        }
    }

  int ndiffgrids = 0;
  for (int index = 0; index < ngrids; index++)
    {
      const auto gridID = vlistGrid(vlistID1, index);
      if (zongridID != -1 && zongridID == gridID) continue;
      if (gridID1 != gridID) ndiffgrids++;
    }
  if (ndiffgrids) cdo_abort("Too many different grids!");

  if (gridID1 != -1)
    {
      const auto gridtype = gridInqType(gridID1);
      if (sourceGridIsRegular)
        {
          if (gridtype == GRID_LONLAT || gridtype == GRID_GAUSSIAN || gridtype == GRID_GAUSSIAN_REDUCED || gridtype == GRID_GENERIC)
            {
              gridID2 = (zongridID != -1 && gridInqYsize(zongridID) == gridInqYsize(gridID1)) ? zongridID : gridToZonal(gridID1);
            }
          else
            {
              cdo_abort("Unsupported gridtype: %s", gridNamePtr(gridtype));
            }
        }
      else
        {
          const auto gridID = generate_full_cell_grid(gridID1);
          if (gridID != gridID1) gridIDdestroy = gridID1 = gridID;
        }
    }
  else
    {
      gridID2 = zongridID;
      cdo_warning("Input stream already contains zonal data!");
    }

  for (int index = 0; index < ngrids; index++) vlistChangeGridIndex(vlistID2, index, gridID2);

  if (Options::cdoChunkType == CDI_UNDEFID)
    {
      const auto nvars = vlistNvars(vlistID2);
      for (int varID = 0; varID < nvars; ++varID) vlistDefVarChunkType(vlistID2, varID, CDI_CHUNK_AUTO);
    }

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

  VarList varList1;
  varListInit(varList1, vlistID1);

  const auto nlatmax = gridInqYsize(gridID2);

  Field field1, field2;
  field2.resize(nlatmax, 0.0);
  field2.grid = gridID2;
  field2.memType = MemType::Double;

  Varray2D<size_t> remapIndices;
  Varray2D<double> remapWeights;

  if (!sourceGridIsRegular) remap_weights_zonal_mean(gridID1, gridID2, remapIndices, remapWeights);

  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);
          field1.grid = gridID1;

          field2.missval = field1.missval;

          if (zongridID != -1 && zongridID == field1.grid)
            {
              field2.nmiss = field1.nmiss;
              if (field1.memType == MemType::Float)
                varray_copy(nlatmax, field1.vec_f, field2.vec_d);
              else
                varray_copy(nlatmax, field1.vec_d, field2.vec_d);
            }
          else if (sourceGridIsRegular)
            {
              (operfunc == FieldFunc_Pctl) ? zonal_pctl(field1, field2, pn) : zonal_function(field1, field2, operfunc);
            }
          else
            {
              remap_zonal_mean(remapIndices, remapWeights, field1, field2);
            }

          cdo_def_record(streamID2, varID, levelID);
          cdo_write_record(streamID2, field2);
        }

      tsID++;
    }

  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  if (gridIDdestroy != -1) gridDestroy(gridIDdestroy);

  cdo_finish();

  return nullptr;
}
