defmodule AprsWeb.MapLive.MapHelpers do
  @moduledoc """
  Shared helpers for APRS map LiveViews (main map, callsign map, etc).
  Provides coordinate extraction, position checks, and bounds logic.
  """

  alias Parser.Types.MicE

  @spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil}
  def get_coordinates(%{data_extended: %MicE{} = mic_e}) do
    {lat, lon} = get_coordinates_from_mic_e(mic_e)
    {lat, lon, mic_e}
  end

  def get_coordinates(%{data_extended: %{latitude: lat, longitude: lon}} = packet) do
    {lat, lon, packet.data_extended}
  end

  def get_coordinates(packet) do
    lat = Map.get(packet, :lat) || Map.get(packet, "lat")
    lon = Map.get(packet, :lon) || Map.get(packet, "lon")
    data_extended = Map.get(packet, :data_extended) || Map.get(packet, "data_extended")
    {lat, lon, data_extended}
  end

  @spec get_coordinates_from_mic_e(MicE.t()) :: {number() | nil, number() | nil}
  def get_coordinates_from_mic_e(mic_e) do
    lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
    lat = if mic_e.lat_direction == :south, do: -lat, else: lat
    lon = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
    lon = if mic_e.lon_direction == :west, do: -lon, else: lon
    if lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180, do: {lat, lon}, else: {nil, nil}
  end

  @spec has_position_data?(map()) :: boolean()
  def has_position_data?(packet) do
    lat = Map.get(packet, :lat) || Map.get(packet, "lat")
    lon = Map.get(packet, :lon) || Map.get(packet, "lon")

    if not is_nil(lat) and not is_nil(lon) do
      true
    else
      data_extended = Map.get(packet, :data_extended) || Map.get(packet, "data_extended")

      case data_extended do
        %MicE{} -> true
        %{latitude: lat, longitude: lon} when not is_nil(lat) and not is_nil(lon) -> true
        _ -> false
      end
    end
  end

  @spec within_bounds?(map() | tuple(), map()) :: boolean()
  def within_bounds?(packet_or_coords, bounds) do
    # Handle nil bounds
    if is_nil(bounds) do
      false
    else
      to_float = fn
        n when is_float(n) ->
          n

        n when is_integer(n) ->
          n * 1.0

        %Decimal{} = d ->
          Decimal.to_float(d)

        n when is_binary(n) ->
          case Float.parse(n) do
            {f, _} -> f
            :error -> 0.0
          end

        _ ->
          0.0
      end

      {lat, lon} =
        cond do
          is_map(packet_or_coords) and Map.has_key?(packet_or_coords, :lat) and
              Map.has_key?(packet_or_coords, :lon) ->
            {packet_or_coords.lat, packet_or_coords.lon}

          is_map(packet_or_coords) and Map.has_key?(packet_or_coords, "lat") and
              Map.has_key?(packet_or_coords, "lon") ->
            {packet_or_coords["lat"], packet_or_coords["lon"]}

          is_map(packet_or_coords) and Map.has_key?(packet_or_coords, :latitude) and
              Map.has_key?(packet_or_coords, :longitude) ->
            {packet_or_coords.latitude, packet_or_coords.longitude}

          is_tuple(packet_or_coords) and tuple_size(packet_or_coords) == 2 ->
            packet_or_coords

          true ->
            {nil, nil}
        end

      if is_nil(lat) or is_nil(lon) do
        false
      else
        lat = to_float.(lat)
        lon = to_float.(lon)
        south = to_float.(bounds.south)
        north = to_float.(bounds.north)
        west = to_float.(bounds.west)
        east = to_float.(bounds.east)

        lat_in_bounds = lat >= south && lat <= north

        lng_in_bounds =
          if west <= east do
            lon >= west && lon <= east
          else
            lon >= west || lon <= east
          end

        lat_in_bounds && lng_in_bounds
      end
    end
  end
end
