defmodule Microwaveprop.Weather.Grib2.LambertConformal do
  @moduledoc false

  @r_earth 6_371_229.0

  @doc """
  Convert a lat/lon to grid indices (i, j) on a Lambert Conformal Conic grid.

  Returns `{:ok, {i, j}}` or `{:error, :outside_grid}`.
  """
  @spec to_grid_index(map(), float(), float()) ::
          {:ok, {non_neg_integer(), non_neg_integer()}} | {:error, :outside_grid}
  def to_grid_index(grid, lat, lon) do
    %{
      nx: nx,
      ny: ny,
      la1: la1,
      lo1: lo1,
      dx: dx,
      dy: dy,
      latin1: latin1,
      lov: lov
    } = grid

    # Normalize longitudes to 0..360
    lo1 = normalize_lon(lo1)
    lon = normalize_lon(lon)
    lov = normalize_lon(lov)

    latin1_rad = deg_to_rad(latin1)
    la1_rad = deg_to_rad(la1)
    lat_rad = deg_to_rad(lat)

    # Cone constant (for HRRR, latin1 == latin2 == 38.5, so n = sin(latin1))
    n = :math.sin(latin1_rad)

    # F factor
    f =
      :math.cos(latin1_rad) *
        :math.pow(:math.tan(:math.pi() / 4 + latin1_rad / 2), n) / n

    # rho for a given latitude
    rho_origin = rho(la1_rad, n, f)
    rho_target = rho(lat_rad, n, f)

    # Theta angles (difference from central meridian, scaled by cone constant)
    theta_origin = n * deg_to_rad(lo1 - lov)
    theta_target = n * deg_to_rad(adjusted_lon_diff(lon, lov))

    # Grid coordinates
    x_origin = rho_origin * :math.sin(theta_origin)
    y_origin = rho_origin * :math.cos(theta_origin)

    x_target = rho_target * :math.sin(theta_target)
    y_target = rho_target * :math.cos(theta_target)

    i = round((x_target - x_origin) / dx)
    j = round((y_origin - y_target) / dy)

    if i >= 0 and i < nx and j >= 0 and j < ny do
      {:ok, {i, j}}
    else
      {:error, :outside_grid}
    end
  end

  defp rho(lat_rad, n, f) do
    @r_earth * f / :math.pow(:math.tan(:math.pi() / 4 + lat_rad / 2), n)
  end

  defp deg_to_rad(deg), do: deg * :math.pi() / 180.0

  defp normalize_lon(lon) when lon < 0, do: lon + 360.0
  defp normalize_lon(lon), do: lon + 0.0

  # Compute longitude difference handling wrap-around
  defp adjusted_lon_diff(lon, lov) do
    diff = lon - lov

    cond do
      diff > 180.0 -> diff - 360.0
      diff < -180.0 -> diff + 360.0
      true -> diff
    end
  end
end
