lib/live_phone/country.ex

defmodule LivePhone.Country do
  @moduledoc """
  The `LivePhone.Country` struct holds minimal information about a country, but
  it should be enough data for `LivePhone` to work its magic.
  """

  alias ExPhoneNumber.Metadata
  alias ExPhoneNumber.Model.PhoneNumber

  @enforce_keys [:code, :name, :flag_emoji, :region_code]
  defstruct [:code, :name, :flag_emoji, :region_code, preferred: false]

  @type phone() :: %PhoneNumber{}
  @type t() :: %__MODULE__{
          preferred: boolean(),
          flag_emoji: String.t(),
          region_code: String.t(),
          code: String.t(),
          name: String.t()
        }

  @doc ~S"""
  Converts the given `iso_country` tuple into a `LivePhone.Country` struct.

  ## Examples

      iex> ISO.countries() |> Map.to_list() |> Enum.find(fn {cc, _} -> cc == "SL" end) |> LivePhone.Country.from_iso()
      %LivePhone.Country{
        preferred: false,
        region_code: "232",
        flag_emoji: "🇸🇱",
        code: "SL",
        name: "Sierra Leone"
      }

      iex> LivePhone.Country.from_iso({"US", %{"name" => "United States"}})
      %LivePhone.Country{
        preferred: false,
        region_code: "1",
        flag_emoji: "🇺🇸",
        code: "US",
        name: "United States"
      }

  """
  @spec from_iso({String.t(), %{String.t() => String.t()}}) :: t()
  def from_iso({country_code, %{"name" => name}}) do
    %__MODULE__{
      region_code: find_region_code(country_code),
      flag_emoji: LivePhone.Util.emoji_for_country(country_code),
      code: country_code,
      name: name
    }
  end

  @spec find_region_code(String.t()) :: String.t()
  defp find_region_code(country_code) do
    case Metadata.get_for_region_code(country_code) do
      nil -> ""
      code -> to_string(code.country_code)
    end
  end

  @doc """
  This function returns all known countries as `LivePhone.Country` structs,
  sorted alphabetically by country name.

  Optionally you can specify a list of preferred country codes, and these will
  be put at the top of the list.

  ```elixir
  # This will return everything alphabetically
  abc_countries = LivePhone.Country.list()

  # This will return it alphabetically as well, but push
  # the US and GB `LivePhone.Country` structs to the top
  # of the list.
  my_countries = LivePhone.Country.list(["US", "GB"])
  ```
  """
  @spec list([String.t()]) :: [t()]
  def list(preferred \\ []) when is_list(preferred) do
    preferred = preferred |> Enum.uniq() |> Enum.with_index()

    ISO.countries()
    |> Enum.map(fn country ->
      country
      |> from_iso()
      |> set_preferred_flag(preferred)
    end)
    |> Enum.filter(&(&1.region_code && &1.region_code != ""))
    |> Enum.sort_by(& &1.name)
    |> Enum.sort_by(&sort_by_preferred(&1, preferred), :desc)
  end

  @doc """
  This function will retrieve a `Country` by its country code. Also accepts a
  `%PhoneNumber{}` struct.

  ## Examples

  ```elixir
    iex> LivePhone.Country.get("US")
    {:ok, %LivePhone.Country{code: "US", flag_emoji: "🇺🇸", name: "United States of America (the)", preferred: false, region_code: "1"}}

    iex> LivePhone.Country.get("FAKE")
    {:error, :not_found}

  ```
  """
  @spec get(%PhoneNumber{} | String.t()) :: {:ok, t()} | {:error, :not_found}
  def get(%PhoneNumber{} = phone) do
    phone
    |> ExPhoneNumber.Metadata.get_region_code_for_number()
    |> do_get()
  end

  def get(country_code) do
    do_get(country_code)
  end

  defp do_get(country_code) do
    list()
    |> Enum.find(&(&1.code == country_code))
    |> case do
      nil -> {:error, :not_found}
      country -> {:ok, country}
    end
  end

  @spec set_preferred_flag(t(), list(String.t())) :: t()
  defp set_preferred_flag(%__MODULE__{} = country, preferred) do
    preferred
    |> Enum.find(fn {value, _index} -> value == country.code end)
    |> case do
      nil -> country
      {_, _index} -> %{country | preferred: true}
    end
  end

  @spec sort_by_preferred(t(), list(String.t())) :: integer()
  defp sort_by_preferred(%__MODULE__{preferred: false}, _), do: 0

  defp sort_by_preferred(%__MODULE__{code: country_code}, preferred) do
    preferred
    |> Enum.find(fn {value, _index} -> value == country_code end)
    |> case do
      nil -> 0
      {_, index} -> length(preferred) - index
    end
  end
end