defmodule Microwaveprop.Rover.Elevation do
  @moduledoc """
  Bulk SRTM elevation lookup for a list of `{lat, lon}` points.

  Groups requested points by SRTM tile filename so each `.hgt` is opened
  exactly once per call, then closed. Cells whose tile is missing or
  whose value is the SRTM void sentinel are mapped to `nil`.
  """

  alias Microwaveprop.Terrain.Srtm

  @samples 3601
  @void -32_768

  @spec lookup_many([{float(), float()}]) :: %{{float(), float()} => integer() | nil}
  def lookup_many([]), do: %{}

  def lookup_many(points) when is_list(points) do
    tiles_dir = Application.get_env(:microwaveprop, :srtm_tiles_dir, "/data/srtm")
    unique = Enum.uniq(points)

    grouped = Enum.group_by(unique, fn {lat, lon} -> Srtm.tile_filename(lat, lon) end)

    grouped
    |> Enum.flat_map(fn {filename, pts} -> read_tile(Path.join(tiles_dir, filename), pts) end)
    |> Map.new()
  end

  defp read_tile(path, pts) do
    case :file.open(path, [:read, :binary, :raw]) do
      {:ok, fd} ->
        try do
          Enum.map(pts, fn {lat, lon} = key -> {key, read_point(fd, lat, lon)} end)
        after
          :file.close(fd)
        end

      {:error, _} ->
        Enum.map(pts, fn key -> {key, nil} end)
    end
  end

  defp read_point(fd, lat, lon) do
    row = round((floor(lat) + 1 - lat) * (@samples - 1))
    col = round((lon - floor(lon)) * (@samples - 1))
    offset = (row * @samples + col) * 2

    case :file.pread(fd, offset, 2) do
      {:ok, <<elev::signed-big-integer-size(16)>>} when elev == @void -> nil
      {:ok, <<elev::signed-big-integer-size(16)>>} -> elev
      _ -> nil
    end
  end
end
