/*
  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>
#include <climits>

#include <cdi.h>

#include "cdo_options.h"
#include "process_int.h"
#include "param_conversion.h"
#include "libncl.h"
#include "pmlist.h"

static void
uv2dv_cfd_W(double missval, double *u, double *v, double *lon, double *lat, size_t nlon, size_t nlat, size_t nlev, int boundOpt,
            double *div)
{
  int ierror;

  // Test dimension sizes.
  if ((nlon > INT_MAX) || (nlat > INT_MAX)) cdoAbort("nlat and/or nlon is greater than INT_MAX!");

  int inlon = (int) nlon;
  int inlat = (int) nlat;

  size_t gridsize_uv = nlat * nlon;

  for (size_t k = 0; k < nlev; ++k)
    {
      double *tmp_u = u + k * gridsize_uv;
      double *tmp_v = v + k * gridsize_uv;
      double *tmp_div = div + k * gridsize_uv;
      // Init output array.
      varrayFill(gridsize_uv, tmp_div, 0.0);
      // Call the Fortran routine.
#ifdef HAVE_CF_INTERFACE
      DDVFIDF(tmp_u, tmp_v, lat, lon, inlon, inlat, missval, boundOpt, tmp_div, ierror);
#else
      cdoAbort("Fortran support not compiled in!");
#endif
    }
}

static void
uv2vr_cfd_W(double missval, double *u, double *v, double *lon, double *lat, size_t nlon, size_t nlat, size_t nlev, int boundOpt,
            double *vort)
{
  int ierror;

  // Test dimension sizes.
  if ((nlon > INT_MAX) || (nlat > INT_MAX)) cdoAbort("nlat and/or nlon is greater than INT_MAX!");

  int inlon = (int) nlon;
  int inlat = (int) nlat;

  size_t gridsize_uv = nlat * nlon;

  for (size_t k = 0; k < nlev; ++k)
    {
      double *tmp_u = u + k * gridsize_uv;
      double *tmp_v = v + k * gridsize_uv;
      double *tmp_vort = vort + k * gridsize_uv;
      // Init output array.
      varrayFill(gridsize_uv, tmp_vort, 0.0);
      // Call the Fortran routine.
#ifdef HAVE_CF_INTERFACE
      DVRFIDF(tmp_u, tmp_v, lat, lon, inlon, inlat, missval, boundOpt, tmp_vort, ierror);
#else
      cdoAbort("Fortran support not compiled in!");
#endif
    }
}

static int
find_name(int vlistID, char *name)
{
  char varname[CDI_MAX_NAME];

  const auto nvars = vlistNvars(vlistID);
  for (int varID = 0; varID < nvars; ++varID)
    {
      vlistInqVarName(vlistID, varID, varname);
      if (cstrIsEqual(name, varname)) return varID;
    }

  return CDI_UNDEFID;
}

enum struct OutMode
{
  NEW,
  APPEND,
  REPLACE
};

// Parameter
OutMode outMode(OutMode::NEW);
int boundOpt = -1;
char name_u[CDI_MAX_NAME], name_v[CDI_MAX_NAME];

static void
print_parameter(void)
{
  cdoPrint("u=%s, v=%s, boundOpt=%d, outMode=%s", name_u, name_v, boundOpt,
           outMode == OutMode::NEW ? "new" : outMode == OutMode::APPEND ? "append" : "replace");
}

static void
set_parameter(void)
{
  strcpy(name_u, "u");
  strcpy(name_v, "v");

  const auto pargc = operatorArgc();
  if (pargc)
    {
      auto pargv = cdoGetOperArgv();

      KVList kvlist;
      kvlist.name = "PARAMETER";
      if (kvlist.parseArguments(pargc, pargv) != 0) cdoAbort("Parse error!");
      if (Options::cdoVerbose) kvlist.print();

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

          // clang-format off
          if      (key == "u") strcpy(name_u, value.c_str());
          else if (key == "v") strcpy(name_v, value.c_str());
          else if (key == "boundOpt") boundOpt = parameter2int(value);
          else if (key == "outMode")
            {
              if      (value == "new")     outMode = OutMode::NEW;
              else if (value == "append")  outMode = OutMode::APPEND;
              else if (value == "replace") outMode = OutMode::REPLACE;
              else cdoAbort("Invalid parameter key value: outMode=%s (valid are: new/append/replace)", value.c_str());
            }
          else cdoAbort("Invalid parameter key >%s<!", key.c_str());
          // clang-format on
        }
    }

  if (Options::cdoVerbose) print_parameter();
}

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

  cdoInitialize(process);

  const auto UV2DV_CFD = cdoOperatorAdd("uv2dv_cfd", 0, 0, "[u, v, boundsOpt, outMode]");
  const auto UV2VR_CFD = cdoOperatorAdd("uv2vr_cfd", 0, 0, "[u, v, boundsOpt, outMode]");

  const auto operatorID = cdoOperatorID();

  set_parameter();

  const auto streamID1 = cdoOpenRead(0);

  const auto vlistID1 = cdoStreamInqVlist(streamID1);
  int vlistID2 = CDI_UNDEFID;
  if (outMode == OutMode::NEW)
    vlistID2 = vlistCreate();
  else if (outMode == OutMode::APPEND)
    vlistID2 = vlistDuplicate(vlistID1);
  else
    cdoAbort("outMode=%d unsupported!", outMode);

  const auto varIDu = find_name(vlistID1, name_u);
  const auto varIDv = find_name(vlistID1, name_v);

  if (varIDu == CDI_UNDEFID) cdoAbort("%s not found!", name_u);
  if (varIDv == CDI_UNDEFID) cdoAbort("%s not found!", name_v);

  const auto gridIDu = vlistInqVarGrid(vlistID1, varIDu);
  const auto gridIDv = vlistInqVarGrid(vlistID1, varIDv);
  const auto gridtype = gridInqType(gridIDu);
  const auto gridsizeuv = gridInqSize(gridIDu);

  if (!((gridtype == GRID_LONLAT || gridtype == GRID_GAUSSIAN) && gridtype == gridInqType(gridIDv)))
    cdoAbort("u and v must be on a regular lonlat or Gaussian grid!");

  if (gridsizeuv != gridInqSize(gridIDv)) cdoAbort("u and v must have the same grid size!");

  if (boundOpt == -1) boundOpt = gridIsCircular(gridIDu) ? 1 : 0;
  if (Options::cdoVerbose) print_parameter();
  if (boundOpt < 0 || boundOpt > 3) cdoAbort("Parameter boundOpt=%d out of bounds (0-3)!", boundOpt);

  const auto nlon = gridInqXsize(gridIDu);
  const auto nlat = gridInqYsize(gridIDu);

  const auto zaxisIDu = vlistInqVarZaxis(vlistID1, varIDu);
  const auto nlev = zaxisInqSize(zaxisIDu);

  if (nlev != zaxisInqSize(vlistInqVarZaxis(vlistID1, varIDv))) cdoAbort("u and v must have the same number of level!");

  const auto missvalu = vlistInqVarMissval(vlistID1, varIDu);
  const auto missvalv = vlistInqVarMissval(vlistID1, varIDv);

  const auto timetype = vlistInqVarTimetype(vlistID1, varIDu);
  const auto varIDo = vlistDefVar(vlistID2, gridIDu, zaxisIDu, timetype);
  if (operatorID == UV2DV_CFD)
    {
      cdiDefKeyString(vlistID2, varIDo, CDI_KEY_NAME, "d");
      cdiDefKeyString(vlistID2, varIDo, CDI_KEY_LONGNAME, "divergence");
      cdiDefKeyString(vlistID2, varIDo, CDI_KEY_UNITS, "1/s");
    }
  else if (operatorID == UV2VR_CFD)
    {
      cdiDefKeyString(vlistID2, varIDo, CDI_KEY_NAME, "vo");
      cdiDefKeyString(vlistID2, varIDo, CDI_KEY_LONGNAME, "vorticity");
      cdiDefKeyString(vlistID2, varIDo, CDI_KEY_UNITS, "1/s");
    }

  vlistDefVarMissval(vlistID2, varIDo, missvalu);

  Varray<double> lon(nlon), lat(nlat);
  gridInqXvals(gridIDu, &lon[0]);
  gridInqYvals(gridIDu, &lat[0]);

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

  const auto streamID2 = cdoOpenWrite(1);

  cdoDefVlist(streamID2, vlistID2);

  const auto gridsizemax = vlistGridsizeMax(vlistID1);
  Varray<double> array(gridsizemax);
  Varray<double> arrayu(nlev * gridsizeuv);
  Varray<double> arrayv(nlev * gridsizeuv);
  Varray<double> arrayo(nlev * gridsizeuv);

  int tsID = 0;
  while ((nrecs = cdoStreamInqTimestep(streamID1, tsID)))
    {
      size_t nmissu = 0, nmissv = 0;

      taxisCopyTimestep(taxisID2, taxisID1);
      cdoDefTimestep(streamID2, tsID);

      for (int recID = 0; recID < nrecs; recID++)
        {
          size_t nmiss;
          cdoInqRecord(streamID1, &varID, &levelID);
          cdoReadRecord(streamID1, &array[0], &nmiss);

          if (varID == varIDu || varID == varIDv)
            {
              if (varID == varIDu)
                {
                  std::copy_n(&array[0], gridsizeuv, &arrayu[levelID * gridsizeuv]);
                  nmissu += nmiss;
                }
              if (varID == varIDv)
                {
                  std::copy_n(&array[0], gridsizeuv, &arrayv[levelID * gridsizeuv]);
                  nmissv += nmiss;
                }
            }

          if (outMode == OutMode::APPEND)
            {
              cdoDefRecord(streamID2, varID, levelID);
              cdoWriteRecord(streamID2, &array[0], nmiss);
            }
        }

      if (nmissu != nmissv)
        {
          cdoAbort("u and v have different number of missing values!");
          if (nmissu && !DBL_IS_EQUAL(missvalu, missvalv))
            {
              for (levelID = 0; levelID < nlev; ++levelID)
                {
                  auto parray = &arrayv[levelID * gridsizeuv];
                  for (size_t i = 0; i < gridsizeuv; ++i)
                    if (DBL_IS_EQUAL(parray[i], missvalv)) parray[i] = missvalu;
                }
            }
        }

      if (operatorID == UV2DV_CFD)
        uv2dv_cfd_W(missvalu, &arrayu[0], &arrayv[0], &lon[0], &lat[0], nlon, nlat, nlev, boundOpt, &arrayo[0]);
      else if (operatorID == UV2VR_CFD)
        uv2vr_cfd_W(missvalu, &arrayu[0], &arrayv[0], &lon[0], &lat[0], nlon, nlat, nlev, boundOpt, &arrayo[0]);

      for (levelID = 0; levelID < nlev; ++levelID)
        {
          auto parray = &arrayo[levelID * gridsizeuv];
          auto nmiss = arrayNumMV(gridsizeuv, parray, missvalu);
          cdoDefRecord(streamID2, varIDo, levelID);
          cdoWriteRecord(streamID2, parray, nmiss);
        }

      tsID++;
    }

  cdoStreamClose(streamID1);
  cdoStreamClose(streamID2);

  vlistDestroy(vlistID2);

  cdoFinish();

  return nullptr;
}
