/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2015 European Southern Observatory
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#include <math.h>
#include <string.h>

#include "muse_phys.h"

#include "muse_pfits.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_phys         Physics functions for the MUSE pipeline
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the saturation pressure using the Owens calibration.
  @param   temp   temperature (in Kelvin)
  @return  the saturation pressure for the given temperature

  This function is currently also used when choosing the Filippenko formulae.
 */
/*----------------------------------------------------------------------------*/
double
muse_phys_nrindex_owens_saturation_pressure(double temp)
{
  return -10474.0 + 116.43*temp - 0.43284*temp*temp + 0.00053840*pow(temp,3);
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the two coefficients for the Owens method.
  @param   temp   temperature (in Kelvin)
  @param   rhum   the relative humidity (in %)
  @param   pres   atmospheric pressure (in hPa)
  @param   d1     coefficient 1
  @param   d2     coefficient 2
 */
/*----------------------------------------------------------------------------*/
void
muse_phys_nrindex_owens_coeffs(double temp, double rhum, double pres,
                               double *d1, double *d2)
{
  /* pressure helpers: saturation pressure */
  double ps = muse_phys_nrindex_owens_saturation_pressure(temp),
         p2 = rhum * ps, /* water vapor pressure */
         p1 = pres - p2; /* dry air pressure */
  /* helpers/coefficients */
  *d1 = p1/temp * (1. + p1*(57.90e-8 - 9.3250e-4/temp + 0.25844/temp/temp));
  *d2 = p2/temp * (1. + p2*(1. + 3.7e-4*p2)
                        * (-2.37321e-3 + 2.23366/temp - 710.792/temp/temp
                           + 7.75141e4/pow(temp,3)));
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the refractive index for the given wavelength following
           Owens.
  @param   l    the wavelength [um]
  @param   d1   coefficient 1
  @param   d2   coefficient 2
  @return  the refractive index
 */
/*----------------------------------------------------------------------------*/
double
muse_phys_nrindex_owens(double l, double d1, double d2)
{
  double lisq = 1./(l*l); /* inverse square of the wavelength */
  return 1. + ((2371.34 + 683939.7/(130. - lisq) + 4547.3/(38.9 - lisq)) * d1
               + (6487.31 + 58.058 * lisq - 0.71150 * lisq*lisq
                  + 0.08851 * lisq*lisq*lisq) * d2) * 1.0e-8;
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the refractive index for the given wavelength following
           Edlén.
  @param   l      the wavelength [um]
  @param   t      temperature [Celsius]
  @param   p      pressure [Pa]
  @param   pv     partial pressure [Pa]
  @return  the refractive index
 */
/*----------------------------------------------------------------------------*/
double
muse_phys_nrindex_edlen(double l, double t, double p, double pv)
{
  /* define constants */
  const double A = 8342.54, B = 2406147,
               C = 15998, D = 96095.43,
               E = 0.601, F = 0.00972, G = 0.003661;
  double S = 1./(l*l), /* supposed to be "laser vacuum wavelength", but for   *
                        * MUSE the approximation is OK if we use air as input */
         /* intermediate result for p, pv, and t */
         n_5 = 1 + 1e-8 * (A + B / (130 - S) + C / (38.9 - S)),
         X = (1 + 1e-8 * (E - F * t) * p) / (1 + G * t),
         n_tp = 1 + p * (n_5 - 1) * X / D,
         n = n_tp - 10e-10 * (292.75 / (t + 273.15)) * (3.7345 - 0.0401 * S) * pv;
  return n;
} /* muse_phys_nrindex_edlen() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the refractive index for the given wavelength following
           Ciddor.
  @param   l      the wavelength [um]
  @param   T      temperature [K]
  @param   p      pressure [Pa]
  @param   xv     mole fraction
  @param   xCO2   CO2 concentration [umol / mol], use 450.0 for a good default
                  value
  @return  the refractive index

  The source of the value xCO2 = 450 is Birch et al., 1993, Metrologia 30, 7
  (see the routine p3d_darc_amplitude_iag in the p3d IFS reduction package).
 */
/*----------------------------------------------------------------------------*/
double
muse_phys_nrindex_ciddor(double l, double T, double p, double xv, double xCO2)
{
  /* define constants */
  const double w0 = 295.235, w1 = 2.6422, w2 = -0.03238, w3 = 0.004028,
               k0 = 238.0185, k1 = 5792105, k2 = 57.362, k3 = 167917,
               a0 = 1.58123e-6, a1 = -2.9331e-8, a2 = 1.1043e-10,
               b0 = 5.707e-6, b1 = -2.051e-8,
               c0 = 1.9898e-4, c1 = -2.376e-6,
               d = 1.83e-11, e = -0.765e-8,
               p_R1 = 101325, T_R1 = 288.15,
               Z_a = 0.9995922115,
               rho_vs = 0.00985938,
               R = 8.314472, M_v = 0.018015;
  double t = T - 273.15, /* temperature [Celsius] */
         S = 1./(l*l), /* supposed to be "laser vacuum wavelength", but for   *
                        * MUSE the approximation is OK if we use air as input */
         /* intermediate results that depend on S */
         r_as = 1e-8 * ((k1 / (k0 - S)) + (k3 / (k2 - S))),
         r_vs = 1.022e-8 * (w0 + w1 * S + w2 * S*S + w3 * S*S*S),
         /* using the CO2 concentration, calculate intermediate values for CO2 */
         M_a = 0.0289635 + 1.2011e-8 * (xCO2 - 400),
         r_axs = r_as * (1 + 5.34e-7 * (xCO2 - 450)),
         /* compressibility and density components */
         Z_m = 1 - (p / T) * (a0 + a1 * t + a2 * t*t + (b0 + b1 * t) * xv
                              + (c0 + c1 * t) * xv*xv)
             + (p/T)*(p/T) * (d + e * xv*xv),
         rho_axs = p_R1 * M_a / (Z_a * R * T_R1),
         rho_v = xv * p * M_v / (Z_m * R * T),
         rho_a = (1 - xv) * p * M_a / (Z_m * R * T),
         n = 1 + (rho_a / rho_axs) * r_axs + (rho_v / rho_vs) * r_vs;
  return n;
} /* muse_phys_nrindex_ciddor() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the refractive index for the given wavelength following
           Filippenko.
  @param   l    the wavelength (in um)
  @param   T    temperature (in degrees Celsius)
  @param   f    water vapor pressure (in mmHg)
  @param   P    atmospheric pressure (in mmHg)
  @return  the refractive index
 */
/*----------------------------------------------------------------------------*/
double
muse_phys_nrindex_filippenko(double l, double T, double f, double P)
{
  double lisq = 1./(l*l); /* inverse square of the wavelength */
  /* 10^6 [n(lambda) - 1] at standard environmental conditions, Eq. (1) */
  double nl = 64.328 + 29498.1 / (146 - lisq) + 255.4 / (41 - lisq);
  /* correction for non-standard conditions, Eq. (2) */
  nl *= P / 720.883 * (1 + (1.049 - 0.0157 * T) * 1e-6 * P) / (1 + 0.003661 * T);
  /* correction for water vapor, Eq. (3) */
  nl -= (0.0624 - 0.000680 * lisq) / (1 + 0.003661 * T) * f;
  /* convert to refractive index n(lambda) */
  nl = nl * 1e-6 + 1;
  return nl;
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert a pixel table from air to vacuum wavelengths.
  @param   aPixtable   input MUSE pixel table in air wavelength
  @param   aFlags      flags to describe air properties used and the method
  @return  CPL_ERROR_NONE on success, another CPL error code on failure

  The argument aFlags is thought to be or'd with one to two of the flags of the
  muse_phys_flags enum.  The @c MUSE_PHYS_AIR_* selection is mandatory to choose
  either conversion using "International Standard Atmosphere"
  (@ref MUSE_PHYS_AIR_STANDARD; dry air, 15 degC, 1013.25 hPa, see e.g.
  https://de.wikipedia.org/w/index.php?title=Normatmosph%C3%A4re&oldid=145634782)
  or air condition measurements from the pixel table header (@ref
  MUSE_PHYS_AIR_MEASURED).

  Four methods are available to compute the refractive index of air:
  - "Ciddor": the algorithm from Phillip E. Ciddor, 1996 Applied Optics 35,
    1566 - 1573. This method is supposed to be the most accurate way to compute
    the refractive index of air, with a validity range of at least 3000 to 17000
    Angstrom, and was chosen by the International Association of Geodesy as
    their standard (http://emtoolbox.nist.gov/Wavelength/Documentation.asp#EdlenorCiddor).
    See http://emtoolbox.nist.gov/Wavelength/Documentation.asp#AppendixA for all
    formulae used here.
    This is active if aFlags was or'ed with MUSE_PHYS_METHOD_CIDDOR or by
    default.
  - "Owens": the formulae come from J. C. Owens, 1967 Applied Optics 6, 51 - 59,
    and are used here in the form as implemented by Sandin et al., 2008 A&A 486,
    545 in the p3d program.
    This is active if aFlags was or'ed with MUSE_PHYS_METHOD_OWENS.
  - "Edlen": the algorithm from B. Edlén, 1966 Metrologia 2, 71 - 80 and
    K. P. Birch & M. J. Downs, 1993 Metrologia 30, 155 - 162. See
    http://emtoolbox.nist.gov/Wavelength/Documentation.asp
    for all formulae used.
    This is active if aFlags was or'ed with MUSE_PHYS_METHOD_EDLEN.
  - "Filippenko": The algorithm from Filippenko, 1982 PASP 94, 715. This uses
    the formula from Owens which converts relative humidity to water vapor
    pressure.
    This is active if aFlags was or'ed with MUSE_PHYS_METHOD_FILIPPENKO.

  @note This function sets the keyword @ref MUSE_HDR_PT_SPEC_TYPE with the
        content "WAVE" when finished successfully.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aPixtable or its header or table components are NULL}
  @error{set and return CPL_ERROR_DATA_NOT_FOUND,
         aPixtable is not conforming to format muse_pixtable_def}
  @error{set and return CPL_ERROR_UNSUPPORTED_MODE, aFlags is zero}
  @error{output warning\, set and return CPL_ERROR_TYPE_MISMATCH,
         header of aPixtable already contains MUSE_HDR_PT_SPEC_TYPE and it's not "AWAV"}
  @error{set and return CPL_ERROR_DATA_NOT_FOUND,
         atmosphere handling type is MUSE_PHYS_AIR_MEASURED\, but pressure\, temperature\, humidity are not present in the input pixel table header}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_phys_air_to_vacuum(muse_pixtable *aPixtable, unsigned int aFlags)
{
  cpl_ensure_code(aPixtable && aPixtable->header && aPixtable->table,
                  CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(muse_cpltable_check(aPixtable->table, muse_pixtable_def)
                  == CPL_ERROR_NONE, CPL_ERROR_DATA_NOT_FOUND);
  cpl_ensure_code(aFlags != 0, CPL_ERROR_UNSUPPORTED_MODE);

  /* check to see if the incoming table is in air wavelengths; *
   * start by assuming that it is                              */
  if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_SPEC_TYPE)) {
    const char *spectype = cpl_propertylist_get_string(aPixtable->header,
                                                       MUSE_HDR_PT_SPEC_TYPE);
    if (spectype && !strncmp(spectype, "WAVE", 4)) {
      cpl_msg_warning(__func__, "Pixel table has spectral type \"%s\", not in "
                      "air wavelengths!", spectype);
      return cpl_error_set(__func__, CPL_ERROR_TYPE_MISMATCH);
    }
    if (spectype && strncmp(spectype, "AWAV", 4)) {
      cpl_msg_warning(__func__, "Pixel table has unknown spectral type \"%s\", "
                      "not in air wavelengths, not doing conversion to vacuum!",
                      spectype);
      return cpl_error_set(__func__, CPL_ERROR_TYPE_MISMATCH);
    }
  } /* if incoming spec type */

  /* set up International Standard Atmosphere by default */
  double rhum = 0., /* dry aig */
         temp = 15. + 273.15, /* [K] */
         pres = 1013.25; /* [hPa] */
  /* check if the 2nd bit is set, if yes query measured air parameters */
  cpl_boolean realair = (aFlags & 0x3) >> 1;
  if (realair) {
    /* query measured values from pixel table header */
    cpl_errorstate prestate = cpl_errorstate_get();
    temp = muse_pfits_get_temp(aPixtable->header); /* temperature [Celsius] */
    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_msg_warning(__func__, "Pixel table header does not contain temperature"
                      ", no conversion to vacuum: %s", cpl_error_get_message());
      return CPL_ERROR_DATA_NOT_FOUND;
    }
    temp += 273.15; /* T[K] */

    prestate = cpl_errorstate_get();
    rhum = muse_pfits_get_rhum(aPixtable->header); /* relative humidity [%] */
    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_msg_warning(__func__, "Pixel table header does not contain relative "
                      "humidity, no conversion to vacuum: %s",
                      cpl_error_get_message());
      return CPL_ERROR_DATA_NOT_FOUND;
    }
    rhum /= 100.; /* convert from % to fraction */

    prestate = cpl_errorstate_get();
    pres = (muse_pfits_get_pres_start(aPixtable->header) /* pressure [mbar] */
            + muse_pfits_get_pres_end(aPixtable->header)) / 2.;
    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_msg_warning(__func__, "Pixel table header does not contain pressure "
                      "values, no conversion to vacuum: %s",
                      cpl_error_get_message());
      return CPL_ERROR_DATA_NOT_FOUND;
    }
  } /* if MUSE_PHYS_CONVERT_MEASURED */

  /* check the method to use */
  unsigned int method = aFlags & 0xC; /* cannot produce invalid method */

  /* compute refractive index for reference wavelength in um and *
   * output properties in "natural" (for the formulae) units     */
  double d1, d2,
         fp, /* water vapor pressure in mmHg */
         p = pres * 100, /* atmopheric pressure [Pa] */
         t = temp - 273.15, /* temperature [Celsius] */
         pv, /* partial vapor pressure */
         xv; /* mole fraction */
  if (method == MUSE_PHYS_METHOD_OWENS) {
    muse_phys_nrindex_owens_coeffs(temp, rhum, pres, &d1, &d2);
    cpl_msg_info(__func__, "Air to vacuum conversion for T=%.2f K, RH=%.2f %%, "
                 "pres=%.1f mbar (%s, Owens)", temp, rhum*100., pres,
                 realair ? "measured parameters" : "standard air");
  } else if (method == MUSE_PHYS_METHOD_EDLEN ||
             method == MUSE_PHYS_METHOD_CIDDOR) {
    /* follow the detailed procedure from                                  *
     *    http://emtoolbox.nist.gov/Wavelength/Documentation.asp#AppendixA */
    /* Calculate saturation vapor pressure psv(t) over water, *
     * equations (A1) to (A7)                                 */
    const double T = temp, /* temperature [K] */
                 K1 = 1.16705214528E+03,
                 K2 = -7.24213167032E+05,
                 K3 = -1.70738469401E+01,
                 K4 = 1.20208247025E+04,
                 K5 = -3.23255503223E+06,
                 K6 = 1.49151086135E+01,
                 K7 = -4.82326573616E+03,
                 K8 = 4.05113405421E+05,
                 K9 = -2.38555575678E-01,
                 K10 = 6.50175348448E+02,
                 Omega = T + K9 / (T - K10),
                 A = Omega*Omega + K1 * Omega + K2,
                 B = K3 * Omega*Omega + K4 * Omega + K5,
                 C = K6 * Omega*Omega + K7 * Omega + K8,
                 X = -B + sqrt(B*B - 4 * A * C);
    double psv_w = 1e6 * pow(2 * C / X, 4);
    /* Calculate saturation vapor pressure psv(t) over ice, *
     * equations (A8) tl (A13)                              */
    const double A1 = -13.928169,
                 A2 = 34.7078238,
                 Theta = T / 273.16,
                 Y = A1 * (1 - pow(Theta, -1.5)) + A2 * (1 - pow(Theta, -1.25));
    double psv_i = 611.657 * exp(Y);
    /* determine Humidity */
    /* for the Edlén equation: for relative humidity RH [%], calculate      *
     * partial pressure using psw_w for t > 0 [Celsius] and psw_i for t < 0 */
    if (t > 0.) {
      pv = rhum * psv_w; /* use saturation pressure over water */
    } else {
      pv = rhum * psv_i; /* use saturation pressure over ice */
    }
    /* for the Ciddor equation: express humidity as a mole fraction */
    const double alpha = 1.00062,
                 beta = 3.14e-8,
                 gamma = 5.60e-7;
    double f = alpha + beta * p + gamma * t*t; /* enhancement factor f(p,t) */
    /* for relative humidity RH [%], calculate mole fraction xv using psw(t) */
    if (t > 0.) {
      xv = rhum * f * psv_w / p; /* use saturation pressure over water */
    } else {
      xv = rhum * f * psv_i / p; /* use saturation pressure over ice */
    }
    cpl_msg_info(__func__, "Air to vacuum conversion for T=%.2f degC, RH=%.2f "
                 "%%, p=%.1f Pa (%s, %s)", t, rhum*100., p,
                 realair ? "measured parameters" : "standard air",
                 method == MUSE_PHYS_METHOD_EDLEN ? "Edlen" : "Ciddor");
  } else {
    /* use the Owens formula to derive saturation pressure, still needs T[K] */
    double ps = muse_phys_nrindex_owens_saturation_pressure(temp);
    /* using that, derive the water vapor pressure in mmHg */
    fp = rhum * ps * MUSE_PHYS_hPa_TO_mmHg;
    temp -= 273.15; /* temperature (again) in degrees Celsius */
    pres *= MUSE_PHYS_hPa_TO_mmHg; /* need the pressure in mmHg */
    cpl_msg_info(__func__, "Air to vacuum conversion for T=%.2f degC, fp=%.3f "
                 "mmHg, P=%.1f mmHg (%s, Filippenko)", temp, fp, pres,
                 realair ? "measured parameters" : "standard air");
  } /* else: Filippenko */

  float *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA);
  cpl_size i, nmax = muse_pixtable_get_nrow(aPixtable);
  #pragma omp parallel for default(none)                                       \
          shared(d1, d2, fp, lbda, method, nmax, p, pres, pv, t, temp, xv)
  for (i = 0; i < nmax; i++) {
    double nr, /* the refractive index that we want */
           lambda = lbda[i] * 1e-4; /* the wavelength in um */
    if (method == MUSE_PHYS_METHOD_OWENS) {
      nr = muse_phys_nrindex_owens(lambda, d1, d2);
    } else if (method == MUSE_PHYS_METHOD_EDLEN) {
      nr = muse_phys_nrindex_edlen(lambda, t, p, pv);
    } else if (method == MUSE_PHYS_METHOD_CIDDOR) {
      /* The standard CO2 content at ground level is 0.0322% (see US 1976 *
       * atmosphere), but Birch et al. apparently give 450., so use that. */
      nr = muse_phys_nrindex_ciddor(lambda, temp, p, xv, 450.);
    } else {
      nr = muse_phys_nrindex_filippenko(lambda, temp, fp, pres);
    }
    /* directly apply the correction to the pixel table */
    lbda[i] *= nr; /* lambda_vac = nrindex * lambda_air */
  } /* for i (all pixel table rows) */

  /* need to recompute the (spectral) pixel table limits now */
  muse_pixtable_compute_limits(aPixtable);

  /* add the header with type code as defined in FITS for vacuum */
  cpl_propertylist_update_string(aPixtable->header, MUSE_HDR_PT_SPEC_TYPE,
                                 "WAVE");
  cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_SPEC_TYPE,
                               MUSE_HDR_PT_SPEC_TYPE_COMMENT);

  return CPL_ERROR_NONE;
} /* muse_phys_air_to_vacuum() */

/**@}*/
