lib/live_phone.ex

defmodule LivePhone do
  @external_resource "./README.md"
  @moduledoc """
  #{File.read!(@external_resource)}
  """

  alias LivePhone.Countries

  @doc ~S"""
  This is used to verify a given `phone` number and see if it is a valid
  number according to ExPhoneNumber.

  ## Examples

      iex> LivePhone.is_valid?("")
      false

      iex> LivePhone.is_valid?("+1555")
      false

      iex> LivePhone.is_valid?("+1555")
      false

      iex> LivePhone.is_valid?("+1 (555) 555-1234")
      false

      iex> LivePhone.is_valid?("+1 (555) 555-1234")
      false

      iex> LivePhone.is_valid?("+1 (650) 253-0000")
      true

      iex> LivePhone.is_valid?("+16502530000")
      true

  """
  @spec is_valid?(String.t()) :: boolean()
  def is_valid?(phone) do
    with {:ok, parsed_phone} <- ExPhoneNumber.parse(phone, nil),
         true <- ExPhoneNumber.is_valid_number?(parsed_phone) do
      true
    else
      _ -> false
    end
  end

  @doc ~S"""
  This is used to try and get a `Country` for a given phone number.

  ## Examples

      iex> LivePhone.get_country("")
      {:error, :invalid_number}

      iex> LivePhone.get_country("+1555")
      {:error, :invalid_number}

      iex> LivePhone.get_country("+1555")
      {:error, :invalid_number}

      iex> LivePhone.get_country("+1 (555) 555-1234")
      {:error, :invalid_number}

      iex> LivePhone.get_country("+1 (555) 555-1234")
      {:error, :invalid_number}

      iex> LivePhone.get_country("+1 (650) 253-0000")
      {:ok, %LivePhone.Country{code: "US", flag_emoji: "🇺🇸", name: "United States of America (the)", preferred: false, region_code: "1"}}

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

  """
  @spec get_country(String.t()) ::
          {:ok, Countries.Country.t()} | {:error, :invalid_number}
  def get_country(phone) do
    with {:ok, parsed_phone} <- ExPhoneNumber.parse(phone, nil),
         true <- ExPhoneNumber.is_valid_number?(parsed_phone),
         {:ok, country} <- Countries.lookup(parsed_phone) do
      {:ok, country}
    else
      _ -> {:error, :invalid_number}
    end
  end

  @doc ~S"""
  This is used to normalize a given `phone` number to E.164 format,
  and immediately return the value whether it is formatted or not.

  ## Examples

      iex> LivePhone.normalize!("1234", nil)
      "1234"

      iex> LivePhone.normalize!("+1 (650) 253-0000", "US")
      "+16502530000"

  """
  @spec normalize!(String.t(), String.t()) :: String.t()
  def normalize!(phone, country) do
    phone
    |> normalize(country)
    |> case do
      {:ok, formatted} -> formatted
      {:error, unformatted} -> unformatted
    end
  end

  @doc ~S"""
  This is used to normalize a given `phone` number to E.164 format,
  and returns a tuple with `{:ok, formatted_phone}` for valid numbers
  and `{:error, unformatted_phone}` for invalid numbers.

  ## Examples

      iex> LivePhone.normalize("1234", nil)
      {:error, "1234"}

      iex> LivePhone.normalize("+1 (650) 253-0000", "US")
      {:ok, "+16502530000"}

  """
  @spec normalize(String.t(), String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def normalize(phone, country) do
    phone
    |> String.replace(~r/[^\d]/, "")
    |> ExPhoneNumber.parse(country)
    |> case do
      {:ok, result} ->
        {:ok, result |> ExPhoneNumber.format(:e164)}

      _ ->
        {:error, phone}
    end
  end

  @doc ~S"""
  Parses the given `country_code` into an emoji, but I should
  note that the emoji is not validated so it might return an
  invalid emoji (this will also depend on the unicode version
  supported by your operating system, and which flags are included.)

  ## Examples

      iex> LivePhone.emoji_for_country(nil)
      ""

      iex> LivePhone.emoji_for_country("US")
      "🇺🇸"

  """
  @spec emoji_for_country(String.t() | nil) :: String.t()
  def emoji_for_country(nil), do: ""

  def emoji_for_country(country_code) do
    country_code
    |> String.upcase()
    |> String.to_charlist()
    |> Enum.map(&(&1 - 65 + 127_462))
    |> List.to_string()
  end
end