defmodule Parser.MicE do
  @moduledoc """
  Parses Mic-E encoded APRS packets.
  """

  @spec parse(binary(), String.t()) :: map()
  def parse(data, destination \\ nil) do
    with {:ok, dest_info} <- parse_destination(destination),
         {:ok, info_info} <- parse_information(data, dest_info.longitude_offset) do
      lat =
        Decimal.add(
          Decimal.new(dest_info.lat_degrees),
          Decimal.div(
            Decimal.add(
              Decimal.new(dest_info.lat_minutes),
              dest_info.lat_hundredths |> Decimal.new() |> Decimal.div(100)
            ),
            60
          )
        )

      lat = if dest_info.lat_direction == :south, do: Decimal.negate(lat), else: lat

      lon =
        Decimal.add(
          Decimal.new(info_info.lon_degrees),
          Decimal.div(
            Decimal.add(
              Decimal.new(info_info.lon_minutes),
              info_info.lon_hundredths |> Decimal.new() |> Decimal.div(100)
            ),
            60
          )
        )

      lon = if dest_info.lon_direction == :west, do: Decimal.negate(lon), else: lon

      %{
        latitude: lat,
        longitude: lon,
        message_bits: dest_info.message_bits,
        message_type: dest_info.message_type,
        speed: info_info.speed,
        course: info_info.course,
        symbol_code: info_info.symbol_code,
        symbol_table_id: info_info.symbol_table_id,
        comment: info_info.comment,
        data_type: :mic_e
      }
    else
      _error ->
        %{
          latitude: nil,
          longitude: nil,
          error: "Failed to parse Mic-E packet",
          data_type: :mic_e_error
        }
    end
  end

  defp parse_destination(destination) do
    if byte_size(destination) == 6 do
      try do
        <<c1, c2, c3, c4, c5, c6>> = destination

        digits = decode_destination_digits([c1, c2, c3, c4, c5, c6])
        lat_info = calculate_latitude_info(digits, c4)
        lon_info = calculate_longitude_info(c5, c6)
        message_info = extract_message_info(digits)

        {:ok, Map.merge(lat_info, Map.merge(lon_info, message_info))}
      rescue
        _ -> {:error, :invalid_character_in_destination}
      end
    else
      {:error, :invalid_destination_length}
    end
  end

  defp decode_destination_digits([c1, c2, c3, c4, c5, c6]) do
    [
      decode_digit(c1),
      decode_digit(c2),
      decode_digit(c3),
      decode_digit(c4),
      decode_digit(c5),
      decode_digit(c6)
    ]
  end

  defp calculate_latitude_info([d1, d2, d3, d4, d5, d6], c4) do
    lat_degrees = d1.digit * 10 + d2.digit
    lat_minutes = d3.digit * 10 + d4.digit
    lat_hundredths = d5.digit * 10 + d6.digit
    lat_direction = determine_lat_direction(c4)

    %{
      lat_degrees: lat_degrees,
      lat_minutes: lat_minutes,
      lat_hundredths: lat_hundredths,
      lat_direction: lat_direction
    }
  end

  defp determine_lat_direction(c4) do
    case c4 do
      c when c in ?0..?9 -> :south
      ?L -> :south
      c when c in ?P..?Z -> :north
      _ -> :unknown
    end
  end

  defp calculate_longitude_info(c5, c6) do
    longitude_offset = determine_longitude_offset(c5)
    lon_direction = determine_lon_direction(c6)

    %{
      lon_direction: lon_direction,
      longitude_offset: longitude_offset
    }
  end

  defp determine_longitude_offset(c5) do
    case c5 do
      c when c in ?0..?9 -> 0
      ?L -> 0
      c when c in ?P..?Z -> 100
      _ -> 0
    end
  end

  defp determine_lon_direction(c6) do
    case c6 do
      c when c in ?0..?9 -> :east
      ?L -> :east
      c when c in ?P..?Z -> :west
      _ -> :unknown
    end
  end

  defp extract_message_info([d1, d2, d3, _d4, _d5, _d6]) do
    message_bits = {d1.msg_bit, d2.msg_bit, d3.msg_bit}
    message_type = determine_message_type([d1, d2, d3])

    %{
      message_bits: message_bits,
      message_type: message_type
    }
  end

  defp determine_message_type([d1, d2, d3]) do
    cond do
      d1.msg_type != nil -> d1.msg_type
      d2.msg_type != nil -> d2.msg_type
      d3.msg_type != nil -> d3.msg_type
      true -> nil
    end
  end

  defp decode_digit(char) do
    case char do
      c when c in ?0..?9 -> %{digit: c - ?0, msg_bit: 0, msg_type: nil}
      c when c in ?A..?K -> %{digit: c - ?A, msg_bit: 1, msg_type: :custom}
      ?L -> %{digit: 0, msg_bit: 0, msg_type: nil}
      c when c in ?P..?Z -> %{digit: c - ?P, msg_bit: 1, msg_type: :standard}
    end
  end

  defp parse_information(data, lon_offset) do
    if byte_size(data) < 8 do
      {:error, :invalid_information_field_length}
    else
      <<lon_deg_c, lon_min_c, lon_hmin_c, sp_c, dc_c, se_c, symbol_code, symbol_table_id, comment::binary>> = data

      lon_deg =
        case lon_deg_c - 28 do
          d when d >= 108 and d <= 117 -> d - 80
          d when d >= 118 and d <= 127 -> d - 190
          d -> d
        end + lon_offset

      lon_min =
        case lon_min_c - 28 do
          m when m >= 60 -> m - 60
          m -> m
        end

      lon_hmin = lon_hmin_c - 28

      sp = sp_c - 28
      dc = dc_c - 28
      se = se_c - 28

      speed = div(sp, 10) * 100 + rem(sp, 10) * 10 + div(dc, 10)
      speed = if speed >= 800, do: speed - 800, else: speed
      # Convert to knots from mph
      speed = speed * 0.868976

      course = rem(dc, 10) * 100 + se
      course = if course >= 400, do: course - 400, else: course

      {:ok,
       %{
         lon_degrees: lon_deg,
         lon_minutes: lon_min,
         lon_hundredths: lon_hmin,
         speed: speed,
         course: course,
         symbol_code: <<symbol_code>>,
         symbol_table_id: <<symbol_table_id>>,
         comment: comment
       }}
    end
  end
end
