defmodule AprsmeWeb.AprsSymbol do
  @moduledoc """
  Shared library for APRS symbol handling and rendering.

  This module provides centralized functions for:
  - Symbol table and code normalization
  - Sprite file mapping
  - Symbol positioning calculations
  - HTML generation for symbols

  All APRS symbol logic should use this module to ensure consistency
  across the application.
  """

  @doc """
  Gets sprite information for a given symbol table and code.
  Returns a map with sprite_file, background_position, and background_size.

  ## Examples

      iex> AprsmeWeb.AprsSymbol.get_sprite_info("/", "_")
      %{
        sprite_file: "/aprs-symbols/aprs-symbols-128-0@2x.png",
        background_position: "-448.0px -96.0px",
        background_size: "512px 192px"
      }
  """
  @spec get_sprite_info(String.t() | nil, String.t() | nil) :: sprite_info()
  def get_sprite_info(symbol_table, symbol_code) do
    compute_sprite_info(overlay_symbol?(symbol_table), symbol_table, symbol_code)
  end

  @typedoc "Sprite-sheet positioning information for a single symbol."
  @type sprite_info :: %{
          sprite_file: String.t(),
          background_position: String.t(),
          background_size: String.t()
        }

  @spec overlay_symbol?(any()) :: boolean()
  defp overlay_symbol?(table) when is_binary(table), do: String.match?(table, ~r/^[A-Z0-9]$/)
  defp overlay_symbol?(_), do: false

  @spec compute_sprite_info(boolean(), String.t() | nil, String.t() | nil) :: sprite_info()
  defp compute_sprite_info(true, _symbol_table, symbol_code) do
    get_overlay_base_symbol_info(symbol_code)
  end

  defp compute_sprite_info(false, symbol_table, symbol_code) do
    symbol_table = normalize_symbol_table(symbol_table)
    symbol_code = normalize_symbol_code(symbol_code)
    table_id = get_table_id(symbol_table)
    build_sprite_info(table_id, symbol_code)
  end

  # Builds the sprite_info map for a given table and symbol char.
  # The 128x128 sprite sheet is a 16-column grid; positions are scaled to 32px display.
  @spec build_sprite_info(String.t(), String.t() | nil) :: sprite_info()
  defp build_sprite_info(table_id, symbol_char) do
    safe_index = symbol_char |> get_symbol_code_ord() |> Kernel.-(33) |> max(0) |> min(93)
    x = -rem(safe_index, 16) * 128
    y = -div(safe_index, 16) * 128

    %{
      sprite_file: "/aprs-symbols/aprs-symbols-128-#{table_id}@2x.png",
      background_position: "#{x / 4}px #{y / 4}px",
      background_size: "512px 192px"
    }
  end

  @doc """
  Gets sprite information for overlay symbols (A-Z, 0-9).
  These symbols display the base symbol from the overlay table.
  """
  @spec get_overlay_base_symbol_info(String.t()) :: sprite_info()
  def get_overlay_base_symbol_info(base_symbol_code) do
    build_sprite_info(get_overlay_base_table_id(base_symbol_code), base_symbol_code)
  end

  @doc """
  Determines which sprite table to use for overlay base symbols.
  Some symbols are in the alternate table (1), others in overlay table (2).
  """
  @spec get_overlay_base_table_id(String.t()) :: String.t()
  def get_overlay_base_table_id(_base_symbol_code) do
    # All overlay symbols are in the alternate table (1) per APRS specification
    # This includes digipeaters, diamonds, squares, arrows, etc.
    "1"
  end

  @doc """
  Gets sprite information for overlay characters (A-Z, 0-9).
  These are rendered from the overlay table.
  """
  @spec get_overlay_character_sprite_info(String.t()) :: sprite_info()
  def get_overlay_character_sprite_info(overlay_char), do: build_sprite_info("2", overlay_char)

  @doc """
  Normalizes a symbol table identifier.

  ## Examples

      iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("/")
      "/"
      
      iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("A")
      "]"
      
      iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("invalid")
      "/"
  """
  @spec normalize_symbol_table(String.t() | nil) :: String.t()
  def normalize_symbol_table(table) when table in ["/", "\\", "]"], do: table

  def normalize_symbol_table(table) when is_binary(table) do
    # A single alphanumeric character is an overlay — map to the overlay table.
    if String.match?(table, ~r/^[A-Z0-9]$/) do
      "]"
    else
      "/"
    end
  end

  def normalize_symbol_table(_), do: "/"

  @doc """
  Normalizes a symbol code.

  ## Examples

      iex> AprsmeWeb.AprsSymbol.normalize_symbol_code("_")
      "_"
      
      iex> AprsmeWeb.AprsSymbol.normalize_symbol_code(nil)
      ">"
      
      iex> AprsmeWeb.AprsSymbol.normalize_symbol_code("")
      ">"
  """
  @spec normalize_symbol_code(String.t() | nil) :: String.t()
  def normalize_symbol_code(nil), do: ">"
  def normalize_symbol_code(""), do: ">"
  def normalize_symbol_code(symbol_code), do: symbol_code

  @doc """
  Maps a symbol table to its sprite file ID.

  ## Examples

      iex> AprsmeWeb.AprsSymbol.get_table_id("/")
      "0"
      
      iex> AprsmeWeb.AprsSymbol.get_table_id("\\\\")
      "1"
      
      iex> AprsmeWeb.AprsSymbol.get_table_id("]")
      "2"
  """
  @spec get_table_id(String.t()) :: String.t()
  def get_table_id("/"), do: "0"
  def get_table_id("\\"), do: "1"
  def get_table_id("]"), do: "2"
  def get_table_id(_), do: "0"

  @doc """
  Renders an APRS symbol as HTML for use in Leaflet markers.
  Returns HTML string that can be used as marker content.

  ## Examples

      iex> AprsmeWeb.AprsSymbol.render_marker_html("/", "_", "W1AW") |> String.contains?("W1AW")
      true
  """
  @spec render_marker_html(String.t() | nil, String.t() | nil, String.t() | nil, integer()) :: String.t()
  def render_marker_html(symbol_table, symbol_code, callsign \\ nil, size \\ 32) do
    # For symbols without callsigns, use Cachex for better caching
    if is_nil(callsign) do
      cache_key = "symbol_html:#{symbol_table}:#{symbol_code}:#{size}"

      case Aprsme.Cache.get(:symbol_cache, cache_key) do
        {:ok, html} when not is_nil(html) ->
          html

        _ ->
          html = generate_marker_html(symbol_table, symbol_code, nil, size)
          # Cache for 1 hour since symbols don't change
          _ = Aprsme.Cache.put(:symbol_cache, cache_key, html, ttl: Aprsme.Cache.to_timeout(hour: 1))
          html
      end
    else
      # For symbols with callsigns, generate directly (callsigns are dynamic)
      generate_marker_html(symbol_table, symbol_code, callsign, size)
    end
  end

  @spec generate_marker_html(String.t() | nil, String.t() | nil, String.t() | nil, integer()) :: String.t()
  defp generate_marker_html(symbol_table, symbol_code, callsign, size) do
    sprite_info = get_sprite_info(symbol_table, symbol_code)

    # Check if this is an overlay symbol
    is_overlay = symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/)

    symbol_html =
      if is_overlay do
        # For overlay symbols, we need both the base symbol background and the overlay character
        overlay_sprite_info = get_overlay_character_sprite_info(symbol_table)

        """
        <div style="
          position: relative;
          width: #{size}px;
          height: #{size}px;
          background-image: url(#{overlay_sprite_info.sprite_file}), url(#{sprite_info.sprite_file});
          background-position: #{overlay_sprite_info.background_position}, #{sprite_info.background_position};
          background-size: #{overlay_sprite_info.background_size}, #{sprite_info.background_size};
          background-repeat: no-repeat, no-repeat;
          image-rendering: pixelated;
        " title="#{symbol_table}#{symbol_code}">
        </div>
        """
      else
        """
        <div style="
          width: #{size}px;
          height: #{size}px;
          background-image: url(#{sprite_info.sprite_file});
          background-position: #{sprite_info.background_position};
          background-size: #{sprite_info.background_size};
          background-repeat: no-repeat;
          image-rendering: pixelated;
        " title="#{symbol_table}#{symbol_code}"></div>
        """
      end

    if callsign do
      """
      <div style="position: relative; width: #{size}px; height: #{size}px; display: flex; align-items: center;">
        #{symbol_html}
        <div style="
          position: absolute;
          left: #{size + 4}px;
          top: 50%;
          transform: translateY(-50%);
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          font-size: 11px;
          font-weight: 600;
          color: #1e40af;
          background-color: rgba(255, 255, 255, 0.9);
          padding: 1px 4px;
          border-radius: 3px;
          white-space: nowrap;
          border: 1px solid rgba(30, 64, 175, 0.3);
          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
          pointer-events: auto;
          z-index: 1000;
        ">#{callsign}</div>
      </div>
      """
    else
      symbol_html
    end
  end

  @doc """
  Renders an APRS symbol as a style string for use in templates.
  Returns a CSS style string that can be used directly in HTML.

  ## Examples

      iex> AprsmeWeb.AprsSymbol.render_style("/", "_", 32)
      "width: 32px; height: 32px; background-image: url(/aprs-symbols/aprs-symbols-128-0@2x.png); background-position: -448.0px -96.0px; background-size: 512px 192px; background-repeat: no-repeat; image-rendering: pixelated; opacity: 1.0; display: inline-block; vertical-align: middle; margin-bottom: -6px;"
  """
  @spec render_style(String.t() | nil, String.t() | nil, integer()) :: String.t()
  def render_style(symbol_table, symbol_code, size \\ 32) do
    sprite_info = get_sprite_info(symbol_table, symbol_code)

    "width: #{size}px; height: #{size}px; background-image: url(#{sprite_info.sprite_file}); background-position: #{sprite_info.background_position}; background-size: #{sprite_info.background_size}; background-repeat: no-repeat; image-rendering: pixelated; opacity: 1.0; display: inline-block; vertical-align: middle; margin-bottom: -6px;"
  end

  @doc """
  Extracts symbol information from a packet with fallbacks.

  ## Examples

      iex> AprsmeWeb.AprsSymbol.extract_from_packet(%{symbol_table_id: "/", symbol_code: "_"})
      {"/", "_"}
      
      iex> AprsmeWeb.AprsSymbol.extract_from_packet(%{})
      {"/", ">"}
  """
  @spec extract_from_packet(map()) :: {String.t(), String.t()}
  def extract_from_packet(packet) do
    symbol_table_id = get_packet_field(packet, :symbol_table_id, "/")
    symbol_code = get_packet_field(packet, :symbol_code, ">")

    {symbol_table_id, symbol_code}
  end

  # Helper function to safely extract a value from a packet or data_extended map
  @spec get_packet_field(map(), atom(), String.t()) :: String.t()
  defp get_packet_field(packet, field, default) do
    data_extended = Map.get(packet, :data_extended, Map.get(packet, "data_extended", %{})) || %{}

    Map.get(packet, field) ||
      Map.get(packet, to_string(field)) ||
      Map.get(data_extended, field) ||
      Map.get(data_extended, to_string(field)) ||
      default
  end

  @spec get_symbol_code_ord(String.t() | nil) :: non_neg_integer()
  defp get_symbol_code_ord(symbol_code) when is_binary(symbol_code) do
    case String.to_charlist(symbol_code) do
      [c | _] when is_integer(c) -> c
      _ -> 63
    end
  end

  defp get_symbol_code_ord(_), do: 63
end
