/**
 * @file
 *
 * @date Created  on Sep 30, 2025
 * @author Attila Kovacs
 */

/// \cond PRIVATE
#define __NOVAS_INTERNAL_API__      ///< Use definitions meant for internal use by SuperNOVAS only
/// \endcond

#include "supernovas.h"



namespace supernovas {

static void use_weather(const Weather& weather, on_surface *s) {
  s->temperature = weather.temperature().celsius();
  s->pressure = weather.pressure().mbar();
  s->humidity = weather.humidity() / Unit::percent;
}

/**
 * Instantiates new horizontal (Az/El) coordinates with the specified components
 *
 * @param azimuth     [rad] azimuth angle
 * @param elevation   [rad] elevation angle
 *
 * @since 1.6
 */
Horizontal::Horizontal(double azimuth, double elevation)
: Spherical(azimuth, elevation) {
  if(!is_valid())
    novas_trace_invalid("Horizontal()");
}

/**
 * Instantiates new horizontal (Az/El) coordinates with the specified components
 *
 * @param azimuth     azimuth angle
 * @param elevation   elevation angle
 *
 * @since 1.6
 */
Horizontal::Horizontal(const Angle& azimuth, const Angle& elevation)
: Spherical(azimuth, elevation) {}

/**
 * Instantiates horizontal coordinates with the specified string representations of the azimuth
 * and elevation coordinates, optionally specifying a system and a distance if needed. After
 * instantiation, you should check that the resulting coordinates are valid, e.g. as:
 *
 * ```c++
 *   Horizontal coords = Horizontal(..., ...);
 *   if(!coords.is_valid()) {
 *     // oops, looks like the angles could not be parsed...
 *     return;
 *   }
 * ```
 *
 * @param azimuth     string representation of the azimuth coordinate in DMS or a decimnal
 *                    degrees.
 * @param elevation   string representation of the elevation coordinate as DMS or decimal
 *                    degrees.
 *
 * @since 1.6
 * @sa novas_str_degrees() for details on string representation that can be parsed.
 * @sa novas_parse_degrees() for more managed parsing from strings.
 */
Horizontal::Horizontal(const std::string& azimuth, const std::string& elevation)
: Horizontal(Angle(azimuth), Angle(elevation)) {}

/**
 * Returns the azimuth angle. Same as Spherical::longitude().
 *
 * @return  the reference to the azimuth angle stored internally.
 *
 * @since 1.6
 * @sa longitude(), elevation(), zenith_angle()
 */
const Angle& Horizontal::azimuth() const {
  return longitude();
}

/**
 * Returns the elevation angle. Same as Spherical::latitude().
 *
 * @return  the reference to the elevation angle stored internally.
 *
 * @since 1.6
 * @sa latitude(), zenith_angle(), azimuth()
 */
const Angle& Horizontal::elevation() const {
  return latitude();
}

/**
 * Returns the zenith distance angle. It is 90&deg; - elevation.
 *
 * @return  the zenith angle.
 *
 * @since 1.6
 * @sa elevation(), azimuth()
 */
Angle Horizontal::zenith_angle() const {
  Angle a(Constant::half_pi - latitude().rad());
  if(!a.is_valid())
    novas_trace_invalid("Horizontal::zenith_angle()");
  return a;
}

/**
 * Checks if these horizontal coordinates are the same as another, within the specified
 * precision.
 *
 * @param other           the reference horizontal coordinates
 * @param precision_rad   [rad] (optional) precision for equality test (default: 1 &mu;as).
 * @return                `true` if these coordinates are the same as the reference within the
 *                        precision, or else `false`.
 *
 * @since 1.6
 * @sa operator==()
 */
bool Horizontal::equals(const Horizontal& other, double precision_rad) const {
  return Spherical::equals(other, precision_rad);
}

/**
 * Checks if these horizontal coordinates are the same as another, within the specified
 * precision.
 *
 * @param other           the reference horizontal coordinates
 * @param precision       (optional) precision for equality test (default: 1 &mu;as).
 * @return                `true` if these coordinates are the same as the reference within the
 *                        precision, or else `false`.
 *
 * @since 1.6
 * @sa operator==()
 */
// cppcheck-suppress functionStatic
bool Horizontal::equals(const Horizontal& other, const Angle& precision) const {
  return equals(other, precision.rad());
}

/**
 * Checks if these horizontal coordinates are the same as another, within 1 &mu;as.
 *
 * @param other           the reference horizontal coordinates
 * @return                `true` if these coordinates are the same as the reference within 1 &mu;as,
 *                        or else `false`.
 *
 * @sa operator!=()
 */
bool Horizontal::operator==(const Horizontal& other) const {
  return equals(other);
}

/**
 * Checks if these horizontal coordinates differ from another, by more than 1 uas.
 *
 * @param other           the reference horizontal coordinates
 * @return                `true` if these coordinates differ from the reference, by more than
 *                        1 &mu;as, or else `false`.
 *
 * @since 1.6
 * @sa operator==()
 */
bool Horizontal::operator!=(const Horizontal& other) const {
  return !equals(other);
}

/**
 * Returns the angular distance of these horizontal coordiantes to/from the specified other
 * horizontal coordinates.
 *
 * @param other   the reference horizontal coordinates
 * @return        the angular distance of these coordinates to/from the argument.
 *
 * @since 1.6
 */
Angle Horizontal::distance_to(const Horizontal& other) const {
  Angle a = Spherical::distance_to(other);
  if(!a.is_valid())
    novas_trace_invalid("Horizontal::distance_to()");
  return a;
}

/**
 * Applies atmospheric refraction correction for these coordinates, returning the result.
 *
 * @param ref       refraction model to use, or NULL to skip refraction correction
 * @param weather   (optional) local weather parameters to use for the refraction correction.
 *                  (default: standard atmopshere).
 * @param time      (optional) Time of observation, for time-dependent refraction models.
 * @return          refracted horizontal coordinates.
 *
 * @since 1.6
 * @sa to_unrefracted()
 */
Horizontal Horizontal::to_refracted(RefractionModel ref, const Weather& weather, const Time& time) {
  on_surface loc = {};
  use_weather(weather, &loc);
  double del = ref ? ref(time.jd(), &loc, NOVAS_REFRACT_ASTROMETRIC, elevation().deg()) : 0.0;

  Horizontal hor(longitude().rad(), latitude().rad() + del * Unit::deg);
  if(!hor.is_valid())
    novas_trace_invalid("Horizontal::to_refracted()");
  return hor;
}

/**
 * Undoes atmospheric refraction correction for these coordinates, returning the result.
 *
 * @param ref       refraction model to use, or NULL to skip refraction correction
 * @param weather   (optional) local weather parameters to use for the refraction correction.
 *                  (default: standard atmopshere).
 * @param time      (optional) Time of observation, for time-dependent refraction models.
 * @return          refracted horizontal coordinates.
 * @return          unrefracted (astrometric) horizontal coordinates.
 *
 * @since 1.6
 * @sa to_refracted()
 */
Horizontal Horizontal::to_unrefracted(RefractionModel ref, const Weather& weather, const Time& time) {
  on_surface loc = {};
  use_weather(weather, &loc);
  double del = ref ? ref(time.jd(), &loc, NOVAS_REFRACT_OBSERVED, elevation().deg()) : 0.0;

  Horizontal hor(longitude().rad(), latitude().rad() - del * Unit::deg);
  if(!hor.is_valid())
    novas_trace_invalid("Horizontal::to_unrefracted()");
  return hor;
}

/**
 * Converts these horizontal coordinates to an apparent place on the sky. Typically you should
 * call this on unrefracted (astrometric) horizontal coordinates. If starting with observed
 * (refracted) coordinates you should call to_unrefracted() first, before calling this function.
 * The returned value may be invalid, e.g. if the frame is not that for an Earth-bound observer
 * with a horizon. As such, it is best practice to check on the valifity of the returned value
 * before using it, e.g. as:
 *
 * ```c++
 *  Horizontal hor = ...;
 *  Apparent app = hor.to_apparent(frame);
 *  if(!app) {
 *    // Oops, could not calculate valid apparent positions.
 *    return;
 *  }
 * ```
 *
 * @param frame     an Earth-based observing frame, defining the time of observation and the
 *                  observer location, above (or slightly below) Earth's surface.
 * @param rv        [m/s] (optional) observed radial velocity, if any (default: 0.0).
 * @param distance  [m] (optional) apparent distance at which the observed light originated.
 * @return          the apparent equatorial place corresponding to these astrometric horizontal
 *                  coordinates on the sky, or Apparent::undefined() if the observing frame is not
 *                  Earth surface based.
 *
 * @since 1.6
 * @sa to_unrefracted(), Apparent::to_horizontal()
 */
Apparent Horizontal::to_apparent(const Frame& frame, double rv, double distance) const {
  static const char *fn = "Horizontal::to_apparent()";

  sky_pos p = {};
  if(novas_hor_to_app(frame._novas_frame(), longitude().deg(), latitude().deg(), NULL, NOVAS_TOD, &p.ra, &p.dec) != 0) {
    novas_trace_invalid(fn);
    return Apparent::undefined();
  }

  p.rv = rv / (Unit::AU_per_day);
  p.dis = distance / Unit::au;
  radec2vector(p.ra, p.dec, 1.0, p.r_hat);

  Apparent a  =Apparent::from_tod_sky_pos(frame, &p);
  if(!a.is_valid())
    novas_trace_invalid(fn);
  return a;
}

/**
 * Converts these horizontal coordinates to an apparent place on the sky for an observer on or
 * near Earth's surface. Typically you should call this on unrefracted (astrometric) horizontal
 * coordinates. If starting with observed (refracted) coordinates you should call to_unrefracted()
 * first, before calling this function. The returned value may be invalid, e.g. if the frame is
 * not that for an Earth-bound observer with a horizon. As such, it is best practice to check
 * on the valifity of the returned value before using it, e.g. as:
 *
 * ```c++
 *  Horizontal hor = ...;
 *  Apparent app = hor.to_apparent(frame);
 *  if(!app) {
 *    // Oops, could not calculate valid apparent positions.
 *    return;
 *  }
 * ```
 *
 * @param frame     an Earth-based observing frame, defining the time of observation and the
 *                  observer location, above (or slightly below) Earth's surface.
 * @param rv        (optional) observed radial velocity, if any (default: 0.0).
 * @param distance  (optional) apparent distance at which the observed light originated.
 * @return          the apparent equatorial place corresponding to these astrometric horizontal
 *                  coordinates on the sky, or Apparent::udefined() if the observing frame is not
 *                  Earth surface based.
 *
 * @since 1.6
 * @sa to_unrefracted(), Apparent::to_horizontal()
 */
Apparent Horizontal::to_apparent(const Frame& frame, const ScalarVelocity& rv, const Coordinate& distance) const {
  return to_apparent(frame, rv.m_per_s(), distance.m());
}

/**
 * Returns a string representation of these horizontal coordinates, optionally defining the separator type
 * to use for the DMS angle representations, and the number of decimal places to print also.
 *
 * @param separator   the DMS separator type (default: units and spaces).
 * @param decimals    the number of decimal places to print for the seconds (default: 3).
 * @return            a new string with the human-readable representation of these horizontal coordinates.
 *
 * @since 1.6
 */
std::string Horizontal::to_string(enum novas_separator_type separator, int decimals) const {
  return "HOR " + Spherical::to_string(separator, decimals);
}

/**
 * Returns a reference to a statically defined standard invalid horizontal coordinates. These invalid
 * coordinates may be used inside any object that is invalid itself.
 *
 * @return    a reference to the static standard invalid coordinates.
 *
 * @since 1.6
 */
const Horizontal& Horizontal::undefined() {
  static const Horizontal _invalid = Horizontal();
  return _invalid;
}

} // namespace supernovas

