/***************************** LICENSE START ***********************************

 Copyright 2018 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

#include "MvSci.h"

#include <cmath>

#ifdef METVIEW
#include "MvDate.h"
#endif

#if defined(METVIEW_AIX_XLC)
inline double exp10(const double r)
{
    return exp(r * 2.302585093);
}  // exp(r*ln(10))
#endif

const double MvSci::cEarthRadiusInKm = 6378.388;  // Earth radius in km
const double MvSci::cEarthG = 9.80665;

const double cTMELT = 273.16;
const double cR3LES = 17.502;
const double cR3IES = 22.587;
const double cR4LES = 32.19;
const double cR4IES = -0.7;
const double cRD = 287.05;
// specific heat of air on constant volume
const double cRV = 461.51;
// specific heat of air on constant pressure
const double cRP = 1005.46;
const double cKAPPA = cRD / cRP;
const double cEP = cRD / cRV;
const double cCI = 2710.;
const double cRESTT = 611.21;
const double cR2ES = cRESTT * cRD / cRV;
const double cVTMPC1 = cRV / cRD - 1.;
const double MvSci::cRadian = 180. / M_PI;
const double MvSci::cDegree = 1 / MvSci::cRadian;
const double RAD2 = 2 * MvSci::cRadian;  // 360/pi
const double MvSci::cRadianToMetre = 1852 * 180.0 * 60.0 / M_PI;
const double MvSci::cMetreToRadian = 1. / MvSci::cRadianToMetre;

// const double cEPSILON = 0.621981;

// Computes the distance on Earth in km
double MvSci::geoDistanceInKm(double fi1, double la1, double fi2, double la2)
{
    double rfi1 = degToRad(fi1);
    double rla1 = degToRad(la1);
    double rfi2 = degToRad(fi2);
    double rla2 = degToRad(la2);

    if (rla1 != rla2) {
        double d = sin(rfi1) * sin(rfi2) + cos(rfi1) * cos(rfi2) * cos(fabs(rla1 - rla2));
        return acos(d) * cEarthRadiusInKm;
    }
    else {
        return fabs(rfi1 - rfi2) * cEarthRadiusInKm;
    }
}

// void MvSci::latLonToXyz(double lat, double lon, double &x, double &y, double &z)
//{
//     double lat = degToRad(lat);
//     double lon = degToRad(lon);
//     x = cos(lon)*cos(lat);
//     y = sin(lon)*cos(lat);
//     z = sin(lat);
// }

/*!
  \brief computes the saturation  mixing ratio

  This method computes the saturation mixing ratio.

  \param t  temperature in K
  \param p  pressure in Pa
  \return   saturation mixing ratio in kg/kg
*/

double MvSci::saturationMixingRatio(double t, double p)
{
    double e = saturationVapourPressure(t);
    return 0.621981 * e / (p - e);
}

/*!
  \brief computes the relative humidity

  This method computes the relative humidity.

  \param t  temperature in K
  \param p  pressure in Pa
  \param q  specific humidity in kg/kg
  \return   relative humidity (0-1)
*/

double MvSci::relativeHumidity(double t, double p, double q)
{
    double es = saturationVapourPressure(t);
    double e = vapourPressure(p, q);

    return e / es;
}

/*!
  \brief computes the relative humidity from dewpoint temperature

  This method computes the relative humidity from dewpoint temperature.

  \param t  temperature in K
  \param td dewpoint temperature in K
  \return   relative humidity (0-1)
*/

double MvSci::relativeHumidityFromTd(double t, double td)
{
    double es = saturationVapourPressure(td) / saturationVapourPressure(t);
    return es;
}

/*!
  \brief computes the specific humidity from relative humidity

  This method computes specific humidity from relative humidity

  \param t  temperature in K
  \param p  pressure in Pa
  \param r  relative humidity (0-1)
  \return   specific humidity in kg/kg
*/

double MvSci::specificHumidity(double t, double p, double r)
{
    double ws = saturationMixingRatio(t, p);
    double w = r * ws;

    return w / (1. + w);
}

/*!
  \brief computes the specific humidity from dewpoint temperature

  This method computes specific humidity from dewpoint temperature

  \param t  temperature in K
  \param p  pressure in Pa
  \param td  dewpoint temperature in K
  \return   specific humidity in kg/kg
*/

double MvSci::specificHumidityFromTd(double t, double p, double td)
{
    double r = relativeHumidityFromTd(t, td);
    return specificHumidity(t, p, r);
}

/*!
  \brief computes the saturation vapour pressure for mixed-phase from temperature

  This method computes the saturation vapour pressure
  for mixed-phase from temperature.

  \param t temperature in K
  \return pressure in Pa
*/

double MvSci::saturationVapourPressure(double t)
{
    double c1 = 611.21;
    double c3l = 17.502;
    double c4l = 32.19;
    double c3i = 22.587;
    double c4i = -0.7;
    double t0 = 273.16;
    double ti = 250.16;

    // Saturation vapour pressure over water
    double es_water = c1 * exp(c3l * (t - t0) / (t - c4l));

    // Saturation vapour pressure over ice
    double es_ice = c1 * exp(c3i * (t - t0) / (t - c4i));

    // fraction of liquid water
    double alpha;
    if (t <= ti)
        alpha = 0.;
    else if (t > ti && t < t0)
        alpha = pow((t - ti) / (t0 - ti), 2.);
    else
        alpha = 1.;

    return alpha * es_water + (1. - alpha) * es_ice;
}

/*!
  \brief computes the vapour pressure from pressure and spec humidity

  This method computes the vapour pressure
  from pressure and specific humidity.

  \param p pressure in Pa
  \param q specific humidity in kg/kg
  \return vapour pressure in Pa
*/
double MvSci::vapourPressure(double p, double q)
{
    double w = q / (1 - q);
    return p * w / (0.621981 + w);
}

/*!
  \brief computes the water vapour ppmv from pressure and specific humidity

  This method computes the water vapour ppmv
  from pressure and specific humidity.

  \param p pressure in Pa
  \param q specific humidity in kg/kg
  \return humidity in ppm
*/

double MvSci::vapourPPMV(double p, double q)
{
    double pw = vapourPressure(p, q);
    return pw / (p - pw) * 1000000;
}

/*!
  \brief computes the ozone ppmv from mixing ratio

  This method computes the ozone ppmv
  from from mixing ratio.

  \param r ozone mixing ratio in kg/kg
  \return ozone in ppmv
*/

double MvSci::ozonePPMV(double r)
{
    double mmr2vmr = 28.97 / 48.0;
    return 1000000 * r * mmr2vmr;
}

double MvSci::dewPointFromQ(double p, double t, double q, const std::string& formula)
{
    const std::string cMixedFormula("MIXED_PHASE_0_TO_-23");
    const std::string cSaturationIce("SATURATION_OVER_ICE");
    double ZCVM3, ZCVM4;

    if (formula == cMixedFormula) {
        double rtwat = cTMELT;
        double rtice = cTMELT - 23.0;
        double foealfa = std::min(1., pow(((std::max(rtice, std::min(rtwat, t)) - rtice) / (rtwat - rtice)), 2.0));

        ZCVM3 = foealfa * cR3LES + (1. - foealfa) * cR3IES;
        ZCVM4 = foealfa * cR4LES + (1. - foealfa) * cR4IES;
    }
    else if (formula == cSaturationIce) {
        if (t > cTMELT) {
            ZCVM3 = cR3LES;
            ZCVM4 = cR4LES;
        }
        else {
            ZCVM3 = cR3IES;
            ZCVM4 = cR4IES;
        }
    }
    else  // SATURATION OVER WATER
    {
        ZCVM3 = cR3LES;
        ZCVM4 = cR4LES;
    }

    double ZFRAC = log(p * q / (cR2ES * (1. + cVTMPC1 * q))) / ZCVM3;

    return (cTMELT - ZFRAC * ZCVM4) / (1. - ZFRAC);
}

double MvSci::speed(double u, double v)
{
    return sqrt(pow(u, 2) + pow(v, 2));
}

double MvSci::direction(double u, double v)
{
    return (-90.0 - atan2(v, u) * cRadian);
}

// Used in Pott.cc
// t temperature in K
// p pressure in Pa
double MvSci::saturationSpecHumidity(double t, double p, bool flag)
{
    double A = 0.;
    double B = 0.;

    if (t >= cTMELT || flag) {
        A = 17.269;
        B = 35.86;
    }
    else {
        A = 21.875;
        B = 7.66;
    }

    double v = 610.78 * exp(A * (t - cTMELT) / (t - B));
    return (flag) ? (cEP * v / p) : ((cEP * v) / (p - (1. - cEP) * v));
}


// As used in Pott.cc
// t temperature in K
// p pressure in Pa
// q specific humidity in kg/kg
bool MvSci::saturationLevel(double t, double p, double q, double& tSat, double& pSat, bool flag)
{
    int count = 0;
    const int maxIter = 20;

    if (q < 0.000002 && p > 10000.)
        q = 0.000002;

    double pSatLimit = p * 0.2;
    pSat = p * 0.98;
    tSat = t;

    while (count < maxIter) {
        tSat = t * pow(pSat / p, cKAPPA);
        double qSat = saturationSpecHumidity(tSat, pSat, flag);
        double tSatTop = t * pow((pSat - 100.) / p, cKAPPA);
        double qSatTop = saturationSpecHumidity(tSatTop, (pSat - 100.), flag);
        double ratio = (qSat - q) / (qSat - qSatTop);
        // dp = p - pSat;
        if (fabs(ratio) < 1.) {
            return true;
        }
        pSat -= ratio * 100;
        if (pSat < pSatLimit) {
            pSat += ratio * 100;
            return true;
        }

        count++;
    }

    // error
    return false;
}

// t temperature in K
// p pressure in Pa
double MvSci::potentialTemperature(double t, double p)
{
    return t * pow(100000. / p, cKAPPA);
}

double MvSci::potentialTemperaturePressureTerm(double p)
{
    return pow(100000. / p, cKAPPA);
}

// As used in Pott.cc
// t temperature in K
// p pressure in Pa
// q specific humidity in kg/kg
double MvSci::equivalentPotentialTemperature(double t, double p, double q, bool flag)
{
    double pSat, tSat;
    if (saturationLevel(t, p, q, tSat, pSat, flag)) {
        double qSat = saturationSpecHumidity(tSat, pSat, flag);
        return tSat * pow(100000. / pSat, cKAPPA) * exp(qSat * cCI / tSat);
    }

    return -1.;
}

// As used in Pott.cc
// t temperature in K
// p pressure in Pa
double MvSci::saturatedEquivalentPotentialTemperature(double t, double p, bool flag)
{
    double qSat = saturationSpecHumidity(t, p, flag);
    return t * pow(100000. / p, cKAPPA) * exp(qSat * cCI / t);
}

// As used in Pott.cc
// t temperature in K
// p pressure in Pa
// pTerm precomputed potentialTemperaturePressureTerm
double MvSci::saturatedEquivalentPotentialTemperatureWithPTerm(double t, double p, double pTerm, bool flag)
{
    double qSat = saturationSpecHumidity(t, p, flag);
    return t * pTerm * exp(qSat * cCI / t);
}

#if 0
double MvSci::lifted_condensation_level(double t, double td, double p, double& t_lcl, double& p_lcl)
{
    //fn_name = "lifted_condensation_level:"

    //check values/units
    if (t < 60) {
         //print(fn_name," invalid temperature=",t," K")
         return -1;
    }

     if (td < 60) {
         //print(fn_name," invalid dew point=",td," K")
        return -1;
     }

     if (p < 1200) {
         //print(fn_name," invalid pressure=",p," Pa")
         return -1;
     }

     double t_lcl = 0.0;
     double p_lcl = p;
     double dp = 25000; // 25 hPa

     //we do not compute the LCL if the start is above 280 hPa
     if (p <= 28000) {
         p_lcl = 28000;
         dp = p/2;
     }

     int iter_num = 20;
     double eps = 1E-4;

     // saturation mixing ratio for dewpoint ( the target mixing ratio)
     auto mr_target = saturation_mixing_ratio(td, p);
     if (mr_target <= 0) {
         return -1;
     }

     // potential temperature for the dry adiabat
     auto th = potential_temperature(t, p);

     // iterate along the dry adabat
     for (int i=1; i < iter_num; i++) {

         // the temperature at pressure p_lcl
         t_lcl = temperature_from_potential_temperature(th, p_lcl);

         // the ratio of the current saturation mixing ratio and the target
         r = saturation_mixing_ratio(t_lcl,p_lcl)/mr_target;

         // check convergence and adjust pressure
         if (abs(r-1.0) < eps) {
             return q;
         } else if ( r > 1.0 ) {
             p_lcl = p_lcl - dp;
         } else {
             p_lcl = p_lcl + dp;
         }

         if (p_lcl <=0 || p_lcl > p) {
             return -1;
         }

         //decrease pressure delta as we converge
         dp = dp / 2.0;
     }

     // we are below the starting height
     if (p_lcl > p) {
         p_lcl = p;
         t_lcl = t;
     }

     return 1;
}
#endif

#ifdef METVIEW
double MvSci::solarDeclinationInRad(double dateTime)
{
    static const double cSOLARDEC1 = sin(-cDegree * 23.44);
    static const double cSOLARDEC2 = 360.0 / 365.24;
    static double cSOLARDEC3 = RAD2 * 0.0167;

    MvDate vdt(dateTime);
    auto N = vdt.dayOfTheYear();
    auto v = cSOLARDEC1 * cos(degToRad(cSOLARDEC2 * (N + 10) + cSOLARDEC3 * sin(degToRad(cSOLARDEC2 * (N - 2)))));
    if (fabs(v) <= 1.0) {
        return asin(v);
    }
    else if (fabs(v - 1.) < 1e-9) {
        return M_PI / 2.;
    }
    else if (fabs(v + 1.) < 1e-9) {
        return -M_PI / 2.;
    }
    return -999.;
    // return MvSci::degToRad(23.44 * cos(MvSci::degToRad((360./365.)*(N+10.))));
}

double MvSci::cosineSolarZenithAngle(double lat, double lon, double declInRad, double hourAngleGM)
{
    auto latRad = degToRad(lat);
    // local hour angle
    auto h = MvSci::degToRad(hourAngleGM + lon);
    return sin(latRad) * sin(declInRad) + cos(latRad) * cos(declInRad) * cos(h);
}
#endif
