defmodule Microwaveprop.Radio.Maidenhead do
  @moduledoc false

  # Maidenhead grid: Field (2 letters A-R) + Square (2 digits) + optional
  # repeating pairs of Subsquare (2 letters) and Extended Square (2 digits).
  # Minimum 4 chars, always even length. E.g. EM12, EM12kp, EM12kp37, EM12kp37ab
  @spec valid?(any()) :: boolean()
  def valid?(nil), do: false

  def valid?(grid) when is_binary(grid) do
    len = byte_size(grid)
    len >= 4 and rem(len, 2) == 0 and String.match?(grid, grid_regex())
  end

  def valid?(_), do: false

  # Field: [A-Ra-r]{2}, Square: [0-9]{2}, then alternating letter/digit pairs
  defp grid_regex do
    ~r/^[A-Ra-r]{2}[0-9]{2}(?:[A-Xa-x]{2}(?:[0-9]{2}(?:[A-Xa-x]{2}(?:[0-9]{2})*)*)*)*$/
  end

  @spec to_latlon(any()) :: {:ok, {float(), float()}} | :error
  def to_latlon(nil), do: :error

  def to_latlon(grid) when is_binary(grid) do
    grid = String.upcase(grid)

    if valid?(grid) do
      <<f1, f2, s1, s2>> <> rest = grid

      # Field: 18 divisions (A-R), 20° lon / 10° lat each
      lon = (f1 - ?A) * 20.0 - 180.0
      lat = (f2 - ?A) * 10.0 - 90.0

      # Square: 10 divisions (0-9), 2° lon / 1° lat each
      lon = lon + (s1 - ?0) * 2.0
      lat = lat + (s2 - ?0) * 1.0

      chars = String.to_charlist(rest)
      {lat, lon, lat_size, lon_size} = decode_pairs(chars, lat, lon, 1.0, 2.0, :letter)
      {:ok, {lat + lat_size / 2, lon + lon_size / 2}}
    else
      :error
    end
  end

  def to_latlon(_), do: :error

  @doc """
  Converts a lat/lon pair into a Maidenhead grid at the requested
  precision (must be an even number between 4 and 8). Default precision
  is 6 (subsquare level, ~5'×2.5', ~7 km × 4.6 km at the equator).
  """
  @spec from_latlon(number(), number(), pos_integer()) :: String.t()
  def from_latlon(lat, lon, precision \\ 6) when is_number(lat) and is_number(lon) and precision in [4, 6, 8] do
    lat = lat + 90.0
    lon = lon + 180.0

    # Field (A-R), 20° lon / 10° lat
    f1 = trunc(lon / 20.0)
    f2 = trunc(lat / 10.0)
    lon = lon - f1 * 20.0
    lat = lat - f2 * 10.0

    # Square (0-9), 2° lon / 1° lat
    s1 = trunc(lon / 2.0)
    s2 = trunc(lat / 1.0)
    lon = lon - s1 * 2.0
    lat = lat - s2 * 1.0

    grid = <<?A + f1, ?A + f2, ?0 + s1, ?0 + s2>>

    cond do
      precision == 4 ->
        grid

      precision >= 6 ->
        # Subsquare (a-x), 5'/60 lon, 2.5'/60 lat
        ss1 = trunc(lon * 60.0 / 5.0)
        ss2 = trunc(lat * 60.0 / 2.5)
        lon = lon - ss1 * (5.0 / 60.0)
        lat = lat - ss2 * (2.5 / 60.0)
        grid = grid <> <<?a + ss1, ?a + ss2>>

        if precision == 8 do
          # Extended square (0-9), (5/60)/10 lon, (2.5/60)/10 lat
          e1 = trunc(lon * 60.0 / 0.5)
          e2 = trunc(lat * 60.0 / 0.25)
          grid <> <<?0 + e1, ?0 + e2>>
        else
          grid
        end
    end
  end

  # Decode remaining alternating letter(24)/digit(10) pairs after field+square
  defp decode_pairs([c1, c2 | rest], lat, lon, lat_size, lon_size, :letter) do
    cell_lon = lon_size / 24
    cell_lat = lat_size / 24
    lon = lon + (c1 - ?A) * cell_lon
    lat = lat + (c2 - ?A) * cell_lat
    decode_pairs(rest, lat, lon, cell_lat, cell_lon, :digit)
  end

  defp decode_pairs([c1, c2 | rest], lat, lon, lat_size, lon_size, :digit) do
    cell_lon = lon_size / 10
    cell_lat = lat_size / 10
    lon = lon + (c1 - ?0) * cell_lon
    lat = lat + (c2 - ?0) * cell_lat
    decode_pairs(rest, lat, lon, cell_lat, cell_lon, :letter)
  end

  defp decode_pairs([], lat, lon, lat_size, lon_size, _type) do
    {lat, lon, lat_size, lon_size}
  end
end
