lib/azan/astronomical.ex

defmodule Azan.Astronomical do
  @moduledoc """
  Documentation for `Astronomical`.
  """
  @northern_offset 10

  alias Azan.{
    DateUtils,
    MathUtils
  }

  def mean_solar_longitude(julian_century) do
    term_1 = 280.4664567
    term_2 = 36_000.76983 * julian_century
    term_3 = 0.0003032 * julian_century * julian_century
    l_0 = term_1 + term_2 + term_3

    l_0 |> MathUtils.unwind_angle()
  end

  def mean_lunar_longitude(julian_century) do
    term_1 = 218.3165
    term_2 = 481_267.8813 * julian_century
    l_0 = term_1 + term_2

    l_0 |> MathUtils.unwind_angle()
  end

  def ascending_lunar_node_longitude(julian_century) do
    term_1 = 125.04452
    term_2 = -1934.136261 * julian_century
    term_3 = 0.0020708 * julian_century * julian_century
    term_4 = julian_century * julian_century * julian_century / 450_000
    l_0 = term_1 + term_2 + term_3 + term_4

    l_0 |> MathUtils.unwind_angle()
  end

  def mean_solar_anomaly(julian_century) do
    term_1 = 357.52911
    term_2 = 35_999.05029 * julian_century
    term_3 = 0.0001537 * julian_century * julian_century
    m_0 = term_1 + term_2 + term_3

    m_0 |> MathUtils.unwind_angle()
  end

  def solar_equation_of_the_center(julian_century, mean_anomaly) do
    m_rad = mean_anomaly |> Math.deg2rad()

    term_1 =
      Math.sin(m_rad) * (1.914602 - julian_century * (0.004817 + 0.000014 * julian_century))

    term_2 = Math.sin(2 * m_rad) * (0.019993 - 0.000101 * julian_century)
    term_3 = Math.sin(3 * m_rad) * 0.000289
    term_1 + term_2 + term_3
  end

  def apparent_solar_longitude(julian_century, mean_longitude) do
    t = julian_century
    l_0 = mean_longitude
    longitude = l_0 + solar_equation_of_the_center(t, mean_solar_anomaly(t))
    omega = 125.04 - 1934.136 * t
    lambda = longitude - 0.00569 - 0.00478 * (omega |> Math.deg2rad() |> Math.sin())
    lambda |> MathUtils.unwind_angle()
  end

  def mean_obliquity_of_the_ecliptic(julian_century) do
    term1 = 23.439291
    term2 = 0.013004167 * julian_century
    term3 = 0.0000001639 * Math.pow(julian_century, 2)
    term4 = 0.0000005036 * Math.pow(julian_century, 3)
    term1 - term2 - term3 + term4
  end

  def apparent_obliquity_of_the_ecliptic(julian_century, mean_obliquity_of_the_eliptic) do
    t = julian_century
    epsilon_0 = mean_obliquity_of_the_eliptic

    o = 125.04 - 1934.136 * t
    epsilon_0 + 0.00256 * (o |> Math.deg2rad() |> Math.cos())
  end

  def mean_sidereal_time(julian_century) do
    t = julian_century
    jd = t * 36_525 + 2_451_545.0
    term1 = 280.46061837
    term2 = 360.98564736629 * (jd - 2_451_545)
    term3 = 0.000387933 * Math.pow(t, 2)
    term4 = Math.pow(t, 3) / 38_710_000
    (term1 + term2 + term3 - term4) |> MathUtils.unwind_angle()
  end

  def nutation_in_longitude(solar_longitude, lunar_longitude, ascending_node) do
    l_0 = solar_longitude
    l_p = lunar_longitude
    omega = ascending_node
    term1 = omega |> Math.deg2rad() |> Math.sin() |> Kernel.*(-17.2 / 3600)
    term2 = l_0 |> Math.deg2rad() |> Kernel.*(2) |> Math.sin() |> Kernel.*(1.32 / 3600)
    term3 = l_p |> Math.deg2rad() |> Kernel.*(2) |> Math.sin() |> Kernel.*(0.23 / 3600)
    term4 = omega |> Math.deg2rad() |> Kernel.*(2) |> Math.sin() |> Kernel.*(0.21 / 3600)
    term1 - term2 - term3 + term4
  end

  def nutation_in_obliquity(solar_longitude, lunar_longitude, ascending_node) do
    l_0 = solar_longitude
    l_p = lunar_longitude
    omega = ascending_node
    term1 = omega |> Math.deg2rad() |> Math.cos() |> Kernel.*(9.2 / 3600)
    term2 = l_0 |> Math.deg2rad() |> Kernel.*(2) |> Math.cos() |> Kernel.*(0.57 / 3600)
    term3 = l_p |> Math.deg2rad() |> Kernel.*(2) |> Math.cos() |> Kernel.*(0.1 / 3600)
    term4 = omega |> Math.deg2rad() |> Kernel.*(2) |> Math.cos() |> Kernel.*(0.09 / 3600)
    term1 + term2 + term3 - term4
  end

  def altitude_of_celestial_body(observer_latitude, declination, local_hour_angle) do
    phi = observer_latitude
    delta = declination
    h = local_hour_angle

    term1 = MathUtils.sin_deg(phi) * MathUtils.sin_deg(delta)
    term2 = MathUtils.cos_deg(phi) * MathUtils.cos_deg(delta) * MathUtils.cos_deg(h)

    (term1 + term2) |> Math.asin() |> Math.rad2deg()
  end

  def approximate_transit(longitude, sidereal_time, right_ascension) do
    l = longitude
    theta0 = sidereal_time
    a2 = right_ascension
    l_w = l * -1
    (a2 + l_w - theta0) |> Kernel./(360) |> MathUtils.normalize_to_scale(1)
  end

  def corrected_transit(
        approximate_transit,
        longitude,
        sidereal_time,
        right_ascension,
        previous_right_ascension,
        next_right_ascension
      ) do
    m0 = approximate_transit
    l = longitude
    theta0 = sidereal_time
    a2 = right_ascension
    a1 = previous_right_ascension
    a3 = next_right_ascension
    l_w = l * -1
    theta = (theta0 + 360.985647 * m0) |> MathUtils.unwind_angle()
    a = __MODULE__.interpolate_angles(a2, a1, a3, m0) |> MathUtils.unwind_angle()
    h = (theta - l_w - a) |> MathUtils.quadrant_shift_angle()
    dm = h / -360
    (m0 + dm) * 24
  end

  def interpolate(y2, y1, y3, n) do
    a = y2 - y1
    b = y3 - y2
    c = b - a
    y2 + n * (a + b + c * n) / 2
  end

  def interpolate_angles(y2, y1, y3, n) do
    a = (y2 - y1) |> MathUtils.unwind_angle()
    b = (y3 - y2) |> MathUtils.unwind_angle()
    c = b - a
    y2 + n / 2 * (a + b + n * c)
  end

  @spec southern_offset(pos_integer()) :: pos_integer()
  def southern_offset(year) do
    if year |> Timex.is_leap?(), do: 173, else: 172
  end

  @spec days_since_solstice(pos_integer(), pos_integer(), float()) :: integer()
  def days_since_solstice(day_of_year, year, latitude) when latitude >= 0 do
    days_since_solstice = day_of_year + @northern_offset
    days_in_year = year |> DateUtils.days_in_year()

    case days_since_solstice >= days_in_year do
      true -> days_since_solstice - days_in_year
      false -> days_since_solstice
    end
  end

  def days_since_solstice(day_of_year, year, latitude) when latitude < 0 do
    southern_offset = year |> __MODULE__.southern_offset()
    days_since_solstice = day_of_year - southern_offset
    days_in_year = year |> DateUtils.days_in_year()

    case days_since_solstice < 0 do
      true -> days_since_solstice + days_in_year
      false -> days_since_solstice
    end
  end

  def season_adjusted_morning_twilight(latitude, day_of_year, year, %DateTime{} = sunrise) do
    a = 75 + 28.65 / 55.0 * abs(latitude)
    b = 75 + 19.44 / 55.0 * abs(latitude)
    c = 75 + 32.74 / 55.0 * abs(latitude)
    d = 75 + 48.1 / 55.0 * abs(latitude)

    adjustment =
      case day_of_year |> __MODULE__.days_since_solstice(year, latitude) do
        dyy when dyy < 91 -> a + (b - a) / 91.0 * dyy
        dyy when dyy < 137 -> b + (c - b) / 46.0 * (dyy - 91)
        dyy when dyy < 183 -> c + (d - c) / 46.0 * (dyy - 137)
        dyy when dyy < 229 -> d + (c - d) / 46.0 * (dyy - 183)
        dyy when dyy < 275 -> c + (b - c) / 46.0 * (dyy - 229)
        dyy -> b + (a - b) / 91.0 * (dyy - 275)
      end

    sunrise |> Timex.shift(seconds: adjustment |> Kernel.*(-60) |> round())
  end

  def season_adjusted_evening_twilight(latitude, day_of_year, year, sunset, shafaq) do
    %{a: a, b: b, c: c, d: d} = latitude |> abcd_seasoned_adjusted_evening_twilight(shafaq)

    adjustment =
      case day_of_year |> __MODULE__.days_since_solstice(year, latitude) do
        dyy when dyy < 91 -> a + (b - a) / 91.0 * dyy
        dyy when dyy < 137 -> b + (c - b) / 46.0 * (dyy - 91)
        dyy when dyy < 183 -> c + (d - c) / 46.0 * (dyy - 137)
        dyy when dyy < 229 -> d + (c - d) / 46.0 * (dyy - 183)
        dyy when dyy < 275 -> c + (b - c) / 46.0 * (dyy - 229)
        dyy -> b + (a - b) / 91.0 * (dyy - 275)
      end

    sunset |> DateUtils.shift_by_seconds(adjustment |> Kernel.*(60) |> round())
  end

  defp abcd_seasoned_adjusted_evening_twilight(latitude, :ahmer) do
    abs_latitude = latitude |> abs()

    %{
      a: 62 + 17.4 / 55.0 * abs_latitude,
      b: 62 - 7.16 / 55.0 * abs_latitude,
      c: 62 + 5.12 / 55.0 * abs_latitude,
      d: 62 + 19.44 / 55.0 * abs_latitude
    }
  end

  defp abcd_seasoned_adjusted_evening_twilight(latitude, :abyad) do
    abs_latitude = latitude |> abs()

    %{
      a: 75 + 25.6 / 55.0 * abs_latitude,
      b: 75 + 7.16 / 55.0 * abs_latitude,
      c: 75 + 36.84 / 55.0 * abs_latitude,
      d: 75 + 81.84 / 55.0 * abs_latitude
    }
  end

  defp abcd_seasoned_adjusted_evening_twilight(latitude, _) do
    abs_latitude = latitude |> abs()

    %{
      a: 75 + 25.6 / 55.0 * abs_latitude,
      b: 75 + 2.05 / 55.0 * abs_latitude,
      c: 75 - 9.21 / 55.0 * abs_latitude,
      d: 75 + 6.14 / 55.0 * abs_latitude
    }
  end

  def julian_century(julian_day) do
    (julian_day - 2_451_545.0) / 36_525
  end

  def julian_day(year, month, day, hours \\ 0) do
    y = if(month > 2, do: year, else: year - 1) |> trunc()
    m = if(month > 2, do: month, else: month + 12) |> trunc()
    d = day + hours / 24

    a = (y / 100) |> trunc()
    b = (2 - a + trunc(a / 4)) |> trunc()

    i0 = trunc(365.25 * (y + 4716))
    i1 = trunc(30.6001 * (m + 1))

    i0 + i1 + d + b - 1524.5
  end
end