defmodule Microwaveprop.Propagation.CommonVolume do
  @moduledoc """
  Geometry for the "common volume" of a microwave QSO — the lens-shaped
  intersection of two circles of radius R around the endpoints.

  Rain-scatter requires both stations' beams to intersect a precipitating
  cell, so the cell has to live inside both 400 km-radius neighborhoods.
  This module provides the geometric primitives a classifier needs:

    * `in_common_volume?/4` — is a point inside both disks?
    * `bounding_box/3` — lat/lon rectangle that contains the lens
    * `area_km2/3` — area of the intersection, for coverage normalization

  Distances are spherical-great-circle (haversine) at mean Earth radius.
  """

  @earth_radius_km 6371.0
  @km_per_deg_lat 111.32

  @type latlon :: {float(), float()}
  @type bbox :: %{min_lat: float(), max_lat: float(), min_lon: float(), max_lon: float()}

  @doc "Is `point` within `radius_km` of both endpoints?"
  @spec in_common_volume?(latlon(), latlon(), latlon(), float()) :: boolean()
  def in_common_volume?(pos1, pos2, point, radius_km) do
    haversine_km(pos1, point) <= radius_km and haversine_km(pos2, point) <= radius_km
  end

  @doc """
  Lat/lon rectangle containing the common volume. Returns `:empty` when
  the two circles don't overlap.
  """
  @spec bounding_box(latlon(), latlon(), float()) :: bbox() | :empty
  def bounding_box({lat1, lon1} = pos1, {lat2, lon2} = pos2, radius_km) do
    if haversine_km(pos1, pos2) > 2 * radius_km do
      :empty
    else
      lat_pad = radius_km / @km_per_deg_lat

      # Approximate longitude degrees-per-km at the higher-latitude endpoint
      # (narrower at higher lat, so more conservative).
      ref_lat = max(abs(lat1), abs(lat2))
      km_per_deg_lon = @km_per_deg_lat * max(:math.cos(ref_lat * :math.pi() / 180.0), 0.01)
      lon_pad = radius_km / km_per_deg_lon

      %{
        min_lat: max(lat1 - lat_pad, lat2 - lat_pad),
        max_lat: min(lat1 + lat_pad, lat2 + lat_pad),
        min_lon: max(lon1 - lon_pad, lon2 - lon_pad),
        max_lon: min(lon1 + lon_pad, lon2 + lon_pad)
      }
    end
  end

  @doc """
  Area (km²) of the lens-shaped intersection of two circles of radius
  `radius_km` centered at `pos1` and `pos2`. Returns `0.0` when the
  circles don't overlap. Uses the standard two-circle lens formula
  with the great-circle distance between centers.
  """
  @spec area_km2(latlon(), latlon(), float()) :: float()
  def area_km2(pos1, pos2, radius_km) do
    d = haversine_km(pos1, pos2)

    cond do
      d >= 2 * radius_km ->
        0.0

      d <= 0.0 ->
        :math.pi() * radius_km * radius_km

      true ->
        r = radius_km
        # Lens area = 2 * [r² * arccos(d/2r) - (d/4) * sqrt(4r² - d²)]
        part_arc = r * r * :math.acos(d / (2 * r))
        part_tri = d / 4 * :math.sqrt(4 * r * r - d * d)
        2 * (part_arc - part_tri)
    end
  end

  @doc "Great-circle distance (km) between two lat/lon points."
  @spec haversine_km(latlon(), latlon()) :: float()
  def haversine_km({lat1, lon1}, {lat2, lon2}) do
    rlat1 = lat1 * :math.pi() / 180.0
    rlat2 = lat2 * :math.pi() / 180.0
    dlat = (lat2 - lat1) * :math.pi() / 180.0
    dlon = (lon2 - lon1) * :math.pi() / 180.0

    a =
      :math.sin(dlat / 2) * :math.sin(dlat / 2) +
        :math.cos(rlat1) * :math.cos(rlat2) *
          :math.sin(dlon / 2) * :math.sin(dlon / 2)

    c = 2.0 * :math.atan2(:math.sqrt(a), :math.sqrt(1.0 - a))
    @earth_radius_km * c
  end
end
