defmodule Microwaveprop.Canopy do
  @moduledoc """
  Tree-canopy heights from a 30 m global canopy-height grid (Potapov et al.
  2019, Lang et al. 2022, or any source able to produce 1° × 1° uint8
  per-pixel rasters).

  Tile format: `@samples × @samples` bytes, row 0 at the north edge,
  one byte per pixel = canopy height in meters (0–255). Filename
  `N{lat}W{lon}.canopy` mirrors the SRTM `.hgt` convention.

  Returns 0 m for points without a tile on disk so the lookup can be
  threaded through the path-clearance pipeline without raising on
  un-fetched areas (treat unknown as treeless).
  """

  require Logger

  @samples 3600

  @spec tile_filename(float(), float()) :: String.t()
  def tile_filename(lat, lon) do
    lat_floor = floor(lat)
    lon_floor = floor(lon)

    lat_prefix = if lat_floor >= 0, do: "N", else: "S"
    lon_prefix = if lon_floor >= 0, do: "E", else: "W"

    lat_str = lat_floor |> abs() |> Integer.to_string() |> String.pad_leading(2, "0")
    lon_str = lon_floor |> abs() |> Integer.to_string() |> String.pad_leading(3, "0")

    "#{lat_prefix}#{lat_str}#{lon_prefix}#{lon_str}.canopy"
  end

  @doc """
  Returns the canopy height in meters at `lat, lon`, or 0 if no tile is
  on disk for that 1° square.
  """
  @spec lookup(float(), float(), String.t(), keyword()) :: non_neg_integer()
  def lookup(lat, lon, tiles_dir \\ default_dir(), opts \\ []) do
    samples = Keyword.get(opts, :samples, @samples)
    path = Path.join(tiles_dir, tile_filename(lat, lon))

    case :file.open(path, [:read, :binary, :raw]) do
      {:ok, fd} ->
        result = read_height(fd, lat, lon, samples)
        _ = :file.close(fd)
        result

      {:error, _} ->
        0
    end
  end

  @doc """
  Batch lookup. Groups points by tile so each tile is opened once.
  Returns `%{ {lat, lon} => height_m }`. Missing tiles yield 0.
  """
  @spec lookup_many([{float(), float()}], String.t(), keyword()) ::
          %{{float(), float()} => non_neg_integer()}
  def lookup_many(points, tiles_dir \\ default_dir(), opts \\ []) do
    samples = Keyword.get(opts, :samples, @samples)

    points
    |> Enum.group_by(fn {lat, lon} -> {floor(lat), floor(lon)} end)
    |> Enum.flat_map(fn {_tile_key, pts} -> read_tile_points(pts, tiles_dir, samples) end)
    |> Map.new()
  end

  defp read_tile_points([{lat0, lon0} | _] = pts, tiles_dir, samples) do
    path = Path.join(tiles_dir, tile_filename(lat0, lon0))

    case :file.open(path, [:read, :binary, :raw]) do
      {:ok, fd} ->
        result =
          Enum.map(pts, fn {lat, lon} = pt -> {pt, read_height(fd, lat, lon, samples)} end)

        _ = :file.close(fd)
        result

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

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

    case :file.pread(fd, offset, 1) do
      {:ok, <<h::unsigned-integer-size(8)>>} -> h
      _ -> 0
    end
  end

  defp default_dir do
    Application.get_env(:microwaveprop, :canopy_tiles_dir, "/data/canopy")
  end
end
