lib/cadastre/subdivision.ex

defmodule Cadastre.Subdivision do
  @moduledoc """
  Subdivision implementation.
  """

  alias Cadastre.Country
  alias Cadastre.Language

  @external_resource Application.app_dir(:cadastre, "priv/data/subdivisions.etf")

  @enforce_keys [:id, :country_id]
  defstruct [:id, :country_id]

  @type id :: String.t()
  @type t :: %__MODULE__{id: id, country_id: Country.id()}

  external_data = @external_resource |> File.read!() |> :erlang.binary_to_term()
  id_per_country_id = external_data |> Map.new(fn {k, v} -> {k, Enum.map(v, &elem(&1, 0))} end)

  @doc """
  Returns all subdivision ids (ISO_3166-2) for country or empty list for
  invalid argument.

  ## Examples

      iex> Cadastre.Subdivision.ids("SL")
      ["E", "N", "NW", "S", "W"]

      iex> "SL" |> Cadastre.Country.new() |> Cadastre.Subdivision.ids()
      ["E", "N", "NW", "S", "W"]

      iex> Cadastre.Subdivision.ids("XX")
      []

      iex> Cadastre.Subdivision.ids(nil)
      []

  """
  @spec ids(Country.t() | Country.id() | any) :: [id]
  def ids(country_or_country_id) do
    country_or_country_id |> get_country_id() |> get_ids()
  end

  @doc """
  Returns subdivisions for country or empty list for invalid argument.

  ## Examples

      iex> Cadastre.Subdivision.all("SL")
      [
        %Cadastre.Subdivision{country_id: "SL", id: "E"},
        %Cadastre.Subdivision{country_id: "SL", id: "N"},
        %Cadastre.Subdivision{country_id: "SL", id: "NW"},
        %Cadastre.Subdivision{country_id: "SL", id: "S"},
        %Cadastre.Subdivision{country_id: "SL", id: "W"}
      ]

      iex> "SL" |> Cadastre.Country.new() |> Cadastre.Subdivision.all()
      [
        %Cadastre.Subdivision{country_id: "SL", id: "E"},
        %Cadastre.Subdivision{country_id: "SL", id: "N"},
        %Cadastre.Subdivision{country_id: "SL", id: "NW"},
        %Cadastre.Subdivision{country_id: "SL", id: "S"},
        %Cadastre.Subdivision{country_id: "SL", id: "W"}
      ]

      iex> Cadastre.Subdivision.all("XX")
      []

      iex> Cadastre.Subdivision.all(nil)
      []

  """
  @spec all(Country.t() | Country.id() | any) :: [t]
  def all(country_or_country_id) do
    country_id = country_or_country_id |> get_country_id()
    country_id |> get_ids() |> Enum.map(&%__MODULE__{id: &1, country_id: country_id})
  end

  @doc """
  Returns `%Cadastre.Subdivision{}` for valid country/country_id and id or
  `nil` for invalid country/country_id and id.

  ## Examples

      iex> Cadastre.Subdivision.new("SL", "W")
      %Cadastre.Subdivision{country_id: "SL", id: "W"}

      iex> Cadastre.Subdivision.new("sl", "w")
      %Cadastre.Subdivision{country_id: "SL", id: "W"}

      iex> "SL" |> Cadastre.Country.new() |> Cadastre.Subdivision.new("W")
      %Cadastre.Subdivision{country_id: "SL", id: "W"}

      iex> Cadastre.Subdivision.new("NL", "W")
      nil

      iex> Cadastre.Subdivision.new("SL", "X")
      nil

      iex> Cadastre.Subdivision.new(nil, nil)
      nil

  """
  @spec new(Country.t() | Country.id() | any, id | any) :: t | nil
  def new(country_or_country_id, id) when is_binary(id) do
    country_id = country_or_country_id |> get_country_id()
    ids = country_id |> get_ids()

    cond do
      id in ids ->
        %__MODULE__{country_id: country_id, id: id}

      ids != [] ->
        id = id |> String.upcase()
        if id in ids, do: %__MODULE__{country_id: country_id, id: id}

      true ->
        nil
    end
  end

  def new(_, _), do: nil

  @doc """
  Returns subdivision name translation for `locale`.

  ## Examples

      iex> Cadastre.Subdivision.new("SL", "W") |> Cadastre.Subdivision.name("be")
      "Заходняя вобласць"

      iex> Cadastre.Subdivision.new("SL", "W") |> Cadastre.Subdivision.name(":)")
      "Western Area (Freetown)"

      iex> Cadastre.Subdivision.name("something wrong", "be")
      nil

  """
  @spec name(t, Language.id()) :: String.t()
  def name(subdivision, locale)

  external_data
  |> Enum.each(fn {country_id, subdivisions} ->
    subdivisions
    |> Enum.map(fn {subdivision_id, translations} ->
      translations = translations |> Map.new()
      en = translations |> Map.fetch!("en")

      def name(%__MODULE__{country_id: unquote(country_id), id: unquote(subdivision_id)}, locale) do
        unquote(Macro.escape(translations)) |> Map.get(locale, unquote(en))
      end
    end)
  end)

  def name(_, _), do: nil

  defp get_ids(country_id) do
    unquote(Macro.escape(id_per_country_id)) |> Map.get(country_id, [])
  end

  defp get_country_id(%Country{id: country_id}), do: country_id

  defp get_country_id(country_id) do
    country_id
    |> Country.new()
    |> case do
      %{id: id} -> id
      _ -> nil
    end
  end
end