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

      Adisit      adisit          compute insitu from potential temperature
      Adisit      adipot          compute potential from insitu temperature
*/

#include <cdi.h>

#include "cdo_options.h"
#include "cdo_zaxis.h"
#include "process_int.h"
#include "cdo_vlist.h"
#include "param_conversion.h"
#include "util_string.h"
#include "field_functions.h"

/*
  Transformation from potential to in situ temperature according to Bryden, 1973,
  "New polynomials for thermal expansion, adiabatic temperature gradient and potential temperature of sea water".
  Deep Sea Research and Oceanographic Abstracts. 20, 401-408 (GILL P.602), which gives the inverse
  transformation for an approximate value, all terms linear in t are taken after that one newton step.
  For the check value 8.4678516 the accuracy is 0.2 mikrokelvin.
*/

// compute insitu temperature from potential temperature
static inline double
adisit(const double tpot, const double sal, const double p)
{
  constexpr double a_a1 = 3.6504E-4, a_a2 = 8.3198E-5, a_a3 = 5.4065E-7, a_a4 = 4.0274E-9, a_b1 = 1.7439E-5, a_b2 = 2.9778E-7,
                   a_c1 = 8.9309E-7, a_c2 = 3.1628E-8, a_c3 = 2.1987E-10, a_d = 4.1057E-9, a_e1 = 1.6056E-10, a_e2 = 5.0484E-12;

  const double qc = p * (a_a1 + p * (a_c1 - a_e1 * p));
  const double qv = p * (a_b1 - a_d * p);
  const double dc = 1.0 + p * (-a_a2 + p * (a_c2 - a_e2 * p));
  const double dv = a_b2 * p;
  const double qnq = -p * (-a_a3 + p * a_c3);
  const double qn3 = -p * a_a4;

  const double tpo = tpot;
  const double qvs = qv * (sal - 35.0) + qc;
  const double dvs = dv * (sal - 35.0) + dc;
  double t = (tpo + qvs) / dvs;
  const double fne = -qvs + t * (dvs + t * (qnq + t * qn3)) - tpo;
  const double fst = dvs + t * (2.0 * qnq + 3.0 * qn3 * t);
  t = t - fne / fst;

  return t;
}

// compute potential temperature from insitu temperature
// Ref: Gill, p. 602, Section A3.5:Potential Temperature
static inline double
adipot(const double t, const double s, const double p)
{
  constexpr double a_a1 = 3.6504E-4, a_a2 = 8.3198E-5, a_a3 = 5.4065E-7, a_a4 = 4.0274E-9, a_b1 = 1.7439E-5, a_b2 = 2.9778E-7,
                   a_c1 = 8.9309E-7, a_c2 = 3.1628E-8, a_c3 = 2.1987E-10, a_d = 4.1057E-9, a_e1 = 1.6056E-10, a_e2 = 5.0484E-12;

  const double s_rel = s - 35.0;

  const double aa = (a_a1 + t * (a_a2 - t * (a_a3 - a_a4 * t)));
  const double bb = s_rel * (a_b1 - a_b2 * t);
  const double cc = (a_c1 + t * (-a_c2 + a_c3 * t));
  const double cc1 = a_d * s_rel;
  const double dd = (-a_e1 + a_e2 * t);

  const double tpot = t - p * (aa + bb + p * (cc - cc1 + p * dd));

  return tpot;
}

static void
calc_adisit(size_t gridsize, size_t nlevel, const Varray<double> &pressure, const FieldVector &tho, const FieldVector &sao,
            FieldVector &tis)
{
  // pressure units: hPa
  // tho units:      Celsius
  // sao units:      psu

  for (size_t levelID = 0; levelID < nlevel; ++levelID)
    {
      const auto &thovec = tho[levelID].vec_d;
      const auto &saovec = sao[levelID].vec_d;
      auto &tisvec = tis[levelID].vec_d;
      const auto thoMissval = tho[levelID].missval;
      const auto saoMissval = sao[levelID].missval;
      const auto tisMissval = tis[levelID].missval;
      for (size_t i = 0; i < gridsize; ++i)
        {
          const auto isMissing = (DBL_IS_EQUAL(thovec[i], thoMissval) || DBL_IS_EQUAL(saovec[i], saoMissval));
          tisvec[i] = isMissing ? tisMissval : adisit(thovec[i], saovec[i], pressure[levelID]);
        }
    }
}

static void
calc_adipot(size_t gridsize, size_t nlevel, const Varray<double> &pressure, const FieldVector &t, const FieldVector &s,
            FieldVector &tpot)
{
  // pressure units: hPa
  // t units:        Celsius
  // s units:        psu

  for (size_t levelID = 0; levelID < nlevel; ++levelID)
    {
      const auto &tvec = t[levelID].vec_d;
      const auto &svec = s[levelID].vec_d;
      auto &tpotvec = tpot[levelID].vec_d;
      const auto tMissval = t[levelID].missval;
      const auto sMissval = s[levelID].missval;
      const auto tpotMissval = tpot[levelID].missval;
      for (size_t i = 0; i < gridsize; ++i)
        {
          const auto isMissing = (DBL_IS_EQUAL(tvec[i], tMissval) || DBL_IS_EQUAL(svec[i], sMissval));
          tpotvec[i] = isMissing ? tpotMissval : adipot(tvec[i], svec[i], pressure[levelID]);
        }
    }
}

int
getCode(const int vlistID1, const int varID, const std::string &cname)
{
  char varname[CDI_MAX_NAME];
  char stdname[CDI_MAX_NAME];

  int code = vlistInqVarCode(vlistID1, varID);
  if (code <= 0)
    {
      vlistInqVarName(vlistID1, varID, varname);
      cstr_to_lower_case(varname);

      int length = CDI_MAX_NAME;
      cdiInqKeyString(vlistID1, varID, CDI_KEY_STDNAME, stdname, &length);
      cstr_to_lower_case(stdname);

      if (cdo_cmpstr(varname, "s") || cdo_cmpstr(stdname, "sea_water_salinity"))
        {
          code = 5;
        }
      else if (cdo_cmpstr(varname, "t"))
        {
          code = 2;
        }

      if (cdo_cmpstr(stdname, cname.c_str()))
        {
          code = 2;
        }
    }

  return code;
}

struct IOSettings
{
  CdoStreamID streamID2;
  const int vlistID2;
  const size_t gridsize;
  const int nlevels;
  const int taxisID1;
  const int taxisID2;
  const int tisID2;
  const int saoID2;
};

IOSettings
configureOutput(const std::function<void(const int, const int)> &outputSettingFunc, const int vlistID1, const int thoID,
                const int saoID, char *units, FieldVector &tho, FieldVector &sao, FieldVector &tis, Varray<double> &pressure)
{
  const double pin = (cdo_operator_argc() == 1) ? parameter_to_double(cdo_operator_argv(0)) : -1.0;

  int length = CDI_MAX_NAME;
  cdiInqKeyString(vlistID1, thoID, CDI_KEY_UNITS, units, &length);
  if (length == 0)
    {
      strcpy(units, "Celcius");
    }

  const int gridID = vlistGrid(vlistID1, 0);
  const size_t gridsize = vlist_check_gridsize(vlistID1);

  auto zaxisID = vlistInqVarZaxis(vlistID1, saoID);
  const auto nlevels1 = zaxisInqSize(zaxisID);
  zaxisID = vlistInqVarZaxis(vlistID1, thoID);
  const auto nlevels2 = zaxisInqSize(zaxisID);

  if (nlevels1 != nlevels2) cdo_abort("temperature and salinity have different number of levels!");
  const auto nlevels = nlevels1;

  pressure.resize(nlevels);
  cdo_zaxis_inq_levels(zaxisID, pressure.data());

  if (pin >= 0)
    for (int i = 0; i < nlevels; ++i) pressure[i] = pin;
  else
    for (int i = 0; i < nlevels; ++i) pressure[i] /= 10;

  if (Options::cdoVerbose)
    {
      cdo_print("Level Pressure");
      for (int i = 0; i < nlevels; ++i) cdo_print("%5d  %g", i + 1, pressure[i]);
    }

  tho.resize(nlevels, Field{});
  sao.resize(nlevels, Field{});
  tis.resize(nlevels, Field{});
  for (int levelID = 0; levelID < nlevels; ++levelID)
    {
      tho[levelID].resize(gridsize);
      sao[levelID].resize(gridsize);
      tis[levelID].resize(gridsize);
      tho[levelID].missval = vlistInqVarMissval(vlistID1, thoID);
      sao[levelID].missval = vlistInqVarMissval(vlistID1, saoID);
      tis[levelID].missval = tho[levelID].missval;
    }

  int datatype = CDI_DATATYPE_FLT32;
  if (vlistInqVarDatatype(vlistID1, thoID) == CDI_DATATYPE_FLT64 && vlistInqVarDatatype(vlistID1, saoID) == CDI_DATATYPE_FLT64)
    datatype = CDI_DATATYPE_FLT64;

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

  const int tisID2 = vlistDefVar(vlistID2, gridID, zaxisID, TIME_VARYING);

  outputSettingFunc(vlistID2, tisID2);
  cdiDefKeyString(vlistID2, tisID2, CDI_KEY_UNITS, units);
  vlistDefVarMissval(vlistID2, tisID2, tis[0].missval);
  vlistDefVarDatatype(vlistID2, tisID2, datatype);

  const auto saoID2 = vlistDefVar(vlistID2, gridID, zaxisID, TIME_VARYING);
  vlistDefVarParam(vlistID2, saoID2, cdiEncodeParam(5, 255, 255));
  cdiDefKeyString(vlistID2, saoID2, CDI_KEY_NAME, "s");
  cdiDefKeyString(vlistID2, saoID2, CDI_KEY_LONGNAME, "Sea water salinity");
  cdiDefKeyString(vlistID2, saoID2, CDI_KEY_STDNAME, "sea_water_salinity");
  cdiDefKeyString(vlistID2, saoID2, CDI_KEY_UNITS, "psu");
  vlistDefVarMissval(vlistID2, saoID2, sao[0].missval);
  vlistDefVarDatatype(vlistID2, saoID2, datatype);

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

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

  return IOSettings{ streamID2, vlistID2, gridsize, nlevels, taxisID1, taxisID2, tisID2, saoID2 };
}

void *
Adisit(void *process)
{
  int thoID = -1, saoID = -1;
  char units[CDI_MAX_NAME] = { 0 };

  cdo_initialize(process);

  const int ADISIT = cdo_operator_add("adisit", 0, 0, "");
  const int ADIPOT = cdo_operator_add("adipot", 0, 0, "");

  const int operatorID = cdo_operator_id();

  const CdoStreamID streamID1 = cdo_open_read(0);
  const int vlistID1 = cdo_stream_inq_vlist(streamID1);

  const auto nvars = vlistNvars(vlistID1);

  const std::string cname_ADISIT{ "sea_water_potential_temperature" };
  const std::string cname_ADIPOT{ "sea_water_temperature" };
  const std::string cname = std::string{ (operatorID == ADISIT) ? cname_ADISIT : cname_ADIPOT };

  for (int varID = 0; varID < nvars; ++varID)
    {
      const int code = getCode(vlistID1, varID, cname);

      if (code == 2)
        {
          thoID = varID;
        }
      else if (code == 20 && operatorID == ADIPOT)
        {
          thoID = varID;
        }
      else if (code == 5)
        saoID = varID;
    }

  if (saoID == -1) cdo_abort("Sea water salinity not found!");
  if (thoID == -1) cdo_abort("%s temperature not found!", (operatorID == ADISIT) ? "Potential" : "Insitu");

  auto outputSetting_ADISIT = [](const int vlistID2, const int tisID2) {
    vlistDefVarParam(vlistID2, tisID2, cdiEncodeParam(20, 255, 255));
    cdiDefKeyString(vlistID2, tisID2, CDI_KEY_NAME, "to");
    cdiDefKeyString(vlistID2, tisID2, CDI_KEY_LONGNAME, "Sea water temperature");
    cdiDefKeyString(vlistID2, tisID2, CDI_KEY_STDNAME, "sea_water_temperature");
  };

  auto outputSetting_ADIPOT = [](const int vlistID2, const int tisID2) {
    vlistDefVarParam(vlistID2, tisID2, cdiEncodeParam(2, 255, 255));
    cdiDefKeyString(vlistID2, tisID2, CDI_KEY_NAME, "tho");
    cdiDefKeyString(vlistID2, tisID2, CDI_KEY_LONGNAME, "Sea water potential temperature");
    cdiDefKeyString(vlistID2, tisID2, CDI_KEY_STDNAME, "sea_water_potential_temperature");
  };

  const auto outputSettingFunc = (operatorID == ADISIT) ? outputSetting_ADISIT : outputSetting_ADIPOT;

  FieldVector tho, sao, tis;
  Varray<double> pressure;
  const auto &configResults = configureOutput(outputSettingFunc, vlistID1, thoID, saoID, units, tho, sao, tis, pressure);

  const CdoStreamID streamID2 = configResults.streamID2;
  const int vlistID2 = configResults.vlistID2;
  const size_t gridsize = configResults.gridsize;
  const int nlevels = configResults.nlevels;
  const int taxisID1 = configResults.taxisID1;
  const int taxisID2 = configResults.taxisID2;
  const int tisID2 = configResults.tisID2;
  const int saoID2 = configResults.saoID2;

  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);
          if (varID == thoID) cdo_read_record(streamID1, tho[levelID].vec_d.data(), &tho[levelID].nmiss);
          if (varID == saoID) cdo_read_record(streamID1, sao[levelID].vec_d.data(), &sao[levelID].nmiss);

          if (varID == thoID)
            {
              constexpr double MIN_T = -10.0;
              constexpr double MAX_T = 40.0;
              const auto mm = field_min_max(tho[levelID]);
              if (mm.min < MIN_T || mm.max > MAX_T)
                cdo_warning("Temperature in degree Celsius out of range (min=%g max=%g) [timestep:%d levelIndex:%d]!", mm.min,
                            mm.max, tsID + 1, levelID + 1);
            }
        }

      if (operatorID == ADISIT)
        calc_adisit(gridsize, nlevels, pressure, tho, sao, tis);
      else
        calc_adipot(gridsize, nlevels, pressure, tho, sao, tis);

      for (int levelID = 0; levelID < nlevels; ++levelID)
        {
          cdo_def_record(streamID2, tisID2, levelID);
          cdo_write_record(streamID2, tis[levelID].vec_d.data(), field_num_miss(tis[levelID]));
        }

      for (int levelID = 0; levelID < nlevels; ++levelID)
        {
          cdo_def_record(streamID2, saoID2, levelID);
          cdo_write_record(streamID2, sao[levelID].vec_d.data(), field_num_miss(sao[levelID]));
        }

      tsID++;
    }

  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  vlistDestroy(vlistID2);

  cdo_finish();

  return nullptr;
}
