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

  Author: Uwe Schulzweida

*/

/*
   This module contains the following operators:

      Derivepar     gheight           Geopotential height on full-levels
      Derivepar     gheight_half      Geopotential height on half-levels
      Derivepar     sealevelpressure  Sea level pressure
*/

#include <cdi.h>

#include "process_int.h"
#include "cdo_vlist.h"
#include "vertical_interp.h"
#include "stdnametable.h"
#include "const.h"
#include "cdo_zaxis.h"
#include "cdo_options.h"

void geopot_height_half(double *gheight, const double *ta_fl, const double *hus_fl, const double *p_hl, long ngp, long nlev);
void geopot_height_full(double *gheight, const double *ta_fl, const double *hus_fl, const double *p_hl, long ngp, long nlev);

static void
check_range_var2d(int stepNum, const Varray<double> &var2d, double rMin, double rMax, const char *varname)
{
  auto mm = varray_min_max(var2d);
  if (mm.min < rMin || mm.max > rMax)
    cdo_warning("%s out of range (min=%g max=%g) [timestep:%d]!", varname, mm.min, mm.max, stepNum);
}

static void
check_range_var3d(int stepNum, int nlevels, size_t gridsize, const Varray<double> &var3d, double rMin, double rMax,
                  const char *varname)
{
  static auto printWarning = true;
  if (printWarning)
    {
      double minVal = 1.e33, maxVal = -1.e33;
      for (int levelID = 0; levelID < nlevels; ++levelID)
        {
          auto mm = varray_min_max(gridsize, &var3d[gridsize * levelID]);
          minVal = std::min(minVal, mm.min);
          maxVal = std::max(maxVal, mm.max);
        }

      if (minVal < rMin || maxVal > rMax)
        {
          printWarning = false;
          cdo_warning("%s out of range (min=%g max=%g) [timestep:%d]!", varname, minVal, maxVal, stepNum);
        }
    }
}

class Derivepar : public Process
{
public:
  using Process::Process;
  inline static CdoModule module = {
    .name = "Derivepar",
    .operators = { { "gheight", DeriveparHelp }, { "gheight_half", DeriveparHelp }, { "sealevelpressure", DeriveparHelp } },
    .aliases = { { "gheight_full", "gheight" }, { "gheighthalf", "gheight_half" } },
    .mode = EXPOSED,     // Module mode: 0:intern 1:extern
    .number = CDI_REAL,  // Allowed number type
    .constraints = { 1, 1, NoRestriction },
  };
  inline static auto registration = RegisterEntry<Derivepar>(module);

  int GHEIGHT_FULL, GHEIGHT_HALF, SEALEVELPRESSURE;
  int surfaceID = -1;
  int presID = -1;

  CdoStreamID streamID1;
  int taxisID1;

  CdoStreamID streamID2;
  int taxisID2;
  int vlistID2;

  size_t gridsize;
  VarIDs varIDs;
  int zaxisID_ML = -1;
  int numHybridLevels = 0;
  int numFullLevels = 0;
  int numHalfLevels = 0;

  Varray<double> array;
  Varray<double> sgeopot;
  Varray<double> ps;
  Varray<double> temp;

  Varray<double> hum;
  Varray<double> gheight;
  Varray<double> halfPress;
  Varray<double> fullPress;
  Varray<double> sealevelpressure;
  Varray<double> vct;

  VarList varList1;

  int operatorID;

public:
  void
  init() override
  {
    GHEIGHT_FULL = module.get_id("gheight");
    GHEIGHT_HALF = module.get_id("gheight_half");
    SEALEVELPRESSURE = module.get_id("sealevelpressure");

    operatorID = cdo_operator_id();

    operator_check_argc(0);

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

    auto gridID0 = vlistGrid(vlistID1, 0);
    if (gridInqType(gridID0) == GRID_SPECTRAL) cdo_abort("Spectral data unsupported!");

    gridsize = vlist_check_gridsize(vlistID1);

    vct = vlist_read_vct(vlistID1, zaxisID_ML, numHybridLevels, numFullLevels, numHalfLevels);
    int vctSize = vct.size();

    if (Options::cdoVerbose)
      {
        auto numAB = vctSize / 2;
        for (int i = 0; i < 10; ++i) cdo_print("vct: %5d %25.17f %25.17f", i, vct[i], vct[numAB + i]);
        cdo_print("vct:   ...");
        for (int i = numAB - 10; i < numAB; ++i) cdo_print("vct: %5d %25.17f %25.17f", i, vct[i], vct[numAB + i]);
      }

    if (zaxisID_ML == -1) cdo_abort("No 3D variable with hybrid sigma pressure coordinate found!");

    varList1 = VarList(vlistID1);
    varList_set_unique_memtype(varList1);

    auto numVars = varList1.numVars();

    varIDs = varList_search_varIDs(varList1, numFullLevels);

    if (Options::cdoVerbose)
      {
        cdo_print("Found:");
        // clang-format off
        if (-1 != varIDs.humID)     cdo_print("  %s -> %s", var_stdname(specific_humidity), varList1.vars[varIDs.humID].name);
        if (-1 != varIDs.tempID)    cdo_print("  %s -> %s", var_stdname(air_temperature), varList1.vars[varIDs.tempID].name);
        if (-1 != varIDs.psID)      cdo_print("  %s -> %s", var_stdname(surface_air_pressure), varList1.vars[varIDs.psID].name);
        if (-1 != varIDs.lnpsID)    cdo_print("  LOG(%s) -> %s", var_stdname(surface_air_pressure), varList1.vars[varIDs.lnpsID].name);
        if (-1 != varIDs.sgeopotID) cdo_print("  %s -> %s", var_stdname(surface_geopotential), varList1.vars[varIDs.sgeopotID].name);
        if (-1 != varIDs.geopotID)  cdo_print("  %s -> %s", var_stdname(geopotential), varList1.vars[varIDs.geopotID].name);
        if (-1 != varIDs.gheightID) cdo_print("  %s -> %s", var_stdname(geopotential_height), varList1.vars[varIDs.gheightID].name);
        // clang-format on
      }

    if (varIDs.lnpsID != -1 && varIDs.lnpsID2 != -1)
      cdo_abort("Found LOG(%s) twice: lsp and lnps!", var_stdname(surface_air_pressure));

    if (varIDs.tempID == -1) cdo_abort("%s not found!", var_stdname(air_temperature));

    for (int varID = 0; varID < numVars; ++varID)
      {
        if (operatorID == SEALEVELPRESSURE) varIDs.humID = -1;

        const auto &var = varList1.vars[varID];
        if (var.gridType == GRID_SPECTRAL && var.zaxisType == ZAXIS_HYBRID) cdo_abort("Spectral data on model level unsupported!");

        if (var.gridType == GRID_SPECTRAL) cdo_abort("Spectral data unsupported!");
      }

    array = Varray<double>(gridsize);
    sgeopot = Varray<double>(gridsize);
    ps = Varray<double>(gridsize);
    temp = Varray<double>(gridsize * numFullLevels);
    halfPress = Varray<double>(gridsize * (numFullLevels + 1));

    if (operatorID == GHEIGHT_FULL || operatorID == GHEIGHT_HALF)
      {
        if (varIDs.humID == -1)
          cdo_warning("%s not found - using algorithm without %s!", var_stdname(specific_humidity), var_stdname(specific_humidity));
        else
          hum.resize(gridsize * numFullLevels);

        gheight.resize(gridsize * (numFullLevels + 1));
      }

    if (operatorID == SEALEVELPRESSURE)
      {
        fullPress.resize(gridsize * numFullLevels);

        surfaceID = zaxis_from_name("surface");
        sealevelpressure.resize(gridsize);
      }

    if (zaxisID_ML != -1 && varIDs.sgeopotID == -1)
      {
        if (varIDs.geopotID == -1)
          cdo_warning("%s not found - set to zero!", var_stdname(surface_geopotential));
        else
          cdo_print("%s not found - using bottom layer of %s!", var_stdname(surface_geopotential), var_stdname(geopotential));

        ranges::fill(sgeopot, 0.0);
      }

    presID = varIDs.lnpsID;
    if (zaxisID_ML != -1 && varIDs.lnpsID == -1)
      {
        if (varIDs.psID == -1)
          cdo_abort("%s not found!", var_stdname(surface_air_pressure));
        else
          presID = varIDs.psID;
      }

    if (Options::cdoVerbose)
      {
        if (presID == varIDs.lnpsID)
          cdo_print("using LOG(%s)", var_stdname(surface_air_pressure));
        else
          cdo_print("using %s", var_stdname(surface_air_pressure));
      }

    vlistID2 = vlistCreate();
    vlistDefNtsteps(vlistID2, vlistNtsteps(vlistID1));

    {
      int var_id = -1;
      int varID = -1;

      if (operatorID == GHEIGHT_FULL)
        {
          var_id = geopotential_height;
          varID = vlistDefVar(vlistID2, gridID0, zaxisID_ML, TIME_VARYING);
        }
      else if (operatorID == GHEIGHT_HALF)
        {
          auto zaxisID_ML_Half = zaxisCreate(ZAXIS_HYBRID_HALF, numHalfLevels);
          zaxisDefVct(zaxisID_ML_Half, 2 * numHalfLevels, vct.data());
          Varray<double> levs(numHalfLevels);
          for (int i = 0; i < numHalfLevels; ++i) levs[i] = i + 1;
          zaxisDefLevels(zaxisID_ML_Half, levs.data());
          var_id = geopotential_height;
          varID = vlistDefVar(vlistID2, gridID0, zaxisID_ML_Half, TIME_VARYING);
        }
      else if (operatorID == SEALEVELPRESSURE)
        {
          var_id = air_pressure_at_sea_level;
          varID = vlistDefVar(vlistID2, gridID0, surfaceID, TIME_VARYING);
        }
      else
        cdo_abort("Internal problem, invalid operatorID: %d!", operatorID);

      vlistDefVarParam(vlistID2, varID, cdiEncodeParam(var_echamcode(var_id), 128, 255));
      cdiDefKeyString(vlistID2, varID, CDI_KEY_NAME, var_name(var_id));
      cdiDefKeyString(vlistID2, varID, CDI_KEY_STDNAME, var_stdname(var_id));
      cdiDefKeyString(vlistID2, varID, CDI_KEY_UNITS, var_units(var_id));
    }

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

    streamID2 = cdo_open_write(1);
    cdo_def_vlist(streamID2, vlistID2);
  }

  void
  run() override
  {
    int tsID = 0;
    while (true)
      {
        auto numFields = cdo_stream_inq_timestep(streamID1, tsID);
        if (numFields == 0) break;

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

        for (int fieldID = 0; fieldID < numFields; ++fieldID)
          {
            auto [varID, levelID] = cdo_inq_field(streamID1);
            size_t numMissVals;
            cdo_read_field(streamID1, array.data(), &numMissVals);

            auto offset = gridsize * levelID;

            if (zaxisID_ML != -1)
              {
                if (varID == varIDs.sgeopotID) { varray_copy(gridsize, array, sgeopot); }
                else if (varID == varIDs.geopotID && varIDs.sgeopotID == -1 && (levelID + 1) == numFullLevels)
                  {
                    varray_copy(gridsize, array, sgeopot);
                  }
                else if (varID == presID)
                  {
                    if (varIDs.lnpsID != -1)
                      for (size_t i = 0; i < gridsize; ++i) ps[i] = std::exp(array[i]);
                    else if (varIDs.psID != -1)
                      varray_copy(gridsize, array, ps);
                  }
                else if (varID == varIDs.tempID)
                  array_copy(gridsize, array.data(), &temp[offset]);
                else if (varID == varIDs.humID)
                  array_copy(gridsize, array.data(), &hum[offset]);
              }
          }

        if (zaxisID_ML != -1)
          {
            // check range of psProg
            check_range_var2d(tsID + 1, ps, MIN_PS, MAX_PS, "Surface pressure");

            // check range of surface geopot
            check_range_var2d(tsID + 1, sgeopot, MIN_FIS, MAX_FIS, "Orography");
          }

        check_range_var3d(tsID + 1, varList1.vars[varIDs.tempID].nlevels, gridsize, temp, MIN_T, MAX_T, "Temperature");

        if (varIDs.humID != -1)
          check_range_var3d(tsID + 1, varList1.vars[varIDs.humID].nlevels, gridsize, hum, -0.1, MAX_Q, "Humidity");

        if (operatorID == GHEIGHT_FULL || operatorID == GHEIGHT_HALF)
          {
            vct_to_hybrid_pressure((double *) nullptr, halfPress.data(), vct, ps.data(), numFullLevels, gridsize);
            array_copy(gridsize, sgeopot.data(), gheight.data() + gridsize * numFullLevels);
            if (operatorID == GHEIGHT_FULL)
              geopot_height_full(gheight.data(), temp.data(), hum.data(), halfPress.data(), gridsize, numFullLevels);
            else
              geopot_height_half(gheight.data(), temp.data(), hum.data(), halfPress.data(), gridsize, numFullLevels);

            int varID = 0;
            auto nlevels = (operatorID == GHEIGHT_FULL) ? numFullLevels : numHalfLevels;
            for (int levelID = 0; levelID < nlevels; ++levelID)
              {
                cdo_def_field(streamID2, varID, levelID);
                cdo_write_field(streamID2, gheight.data() + levelID * gridsize, 0);
              }
          }
        else if (operatorID == SEALEVELPRESSURE)
          {
            vct_to_hybrid_pressure(fullPress.data(), halfPress.data(), vct, ps.data(), numFullLevels, gridsize);

            extrapolate_P(sealevelpressure.data(), &halfPress[gridsize * numFullLevels], &fullPress[gridsize * (numFullLevels - 1)],
                          sgeopot.data(), &temp[gridsize * (numFullLevels - 1)], gridsize);

            cdo_def_field(streamID2, 0, 0);
            cdo_write_field(streamID2, sealevelpressure.data(), 0);
          }
        else
          cdo_abort("Internal error");

        tsID++;
      }
  }

  void
  close() override
  {
    cdo_stream_close(streamID2);
    cdo_stream_close(streamID1);

    vlistDestroy(vlistID2);
  }
};
