defmodule IsoLang do
  @moduledoc """
  Documentation for `IsoLang`.
  Provides utilities for dealing with [ISO 639](https://en.wikipedia.org/wiki/ISO_639)
  languages.
  ## See Also
  - https://en.wikipedia.org/wiki/IETF_language_tag
  - https://tools.ietf.org/search/bcp47
  - https://datahub.io/core/language-codes#resource-language-codes-full
  """
  use Gettext, otp_app: :iso_lang
  @type t :: %__MODULE__{
          alpha2: String.t(),
          alpha3b: String.t(),
          alpha3t: String.t(),
          name: String.t(),
          native_name: String.t()
        }
  defstruct alpha2: nil, alpha3b: nil, alpha3t: nil, name: nil, native_name: nil
  @doc """
  Returns a list of all available ISO language codes.
  """
  defdelegate all(opts \\ []), to: IsoLang.Data
  @doc """
  Gets a single language struct identified by a field.
  If the `:by` field is not specified, fields are checked in the following order:
  - `:alpha2`
  - `:alpha3b`
  - `:alpha3t`
  - `:name`
  ## Options
  - `:by` specifies which struct field to be used in the search. (optional)
  ## Examples
      iex> IsoLang.get("de")
      {:ok, %IsoLang{alpha2: "de", alpha3b: "ger", alpha3t: "deu", name: "German"}}
  """
  @spec get(value :: String.t(), opts :: Keyword.t()) :: {:ok, IsoLang.t()} | {:error, any()}
  def get(value, opts \\ []) do
    key = Keyword.get(opts, :by, nil)
    opts
    |> all()
    |> Enum.find(:not_found, fn
      lang when not is_nil(key) -> Map.get(lang, key) == value
      %__MODULE__{alpha2: ^value} -> true
      %__MODULE__{alpha3b: ^value} -> true
      %__MODULE__{alpha3t: ^value} -> true
      %__MODULE__{name: ^value} -> true
      _ -> false
    end)
    |> case do
      :not_found -> {:error, "Language not found"}
      lang -> {:ok, lang}
    end
  end
  @doc """
  As `get/2`, but raises on error
  """
  def get!(query, opts \\ []) do
    case get(query, opts) do
      {:ok, lang} -> lang
      {:error, error} -> raise error
    end
  end
  @doc """
  Searches for matching languages using a case-insensitive query string
  ## Options
  - `:by` specifies which struct field to be used in the search. Default: `:name`
  ## Examples
      iex> IsoLang.find("eng")
      {:ok,
        [
          %IsoLang{alpha2: "bn", alpha3b: "ben", alpha3t: "", name: "Bengali"},
          %IsoLang{alpha2: "en", alpha3b: "eng", alpha3t: "", name: "English"}
        ]}
  """
  @spec find(query :: String.t(), opts :: Keyword.t()) :: {:ok, [IsoLang.t()]} | {:error, any()}
  def find(query, opts \\ []) do
    key = Keyword.get(opts, :by, :name)
    {:ok,
     opts
     |> all()
     |> Enum.filter(fn lang ->
       query
       |> Regex.compile!("i")
       |> Regex.match?(
         lang
         |> Map.fetch!(key)
       )
     end)}
  rescue
    e -> {:error, e}
  end
  @doc """
  As `find/2`, but raises on error
  """
  def find!(query, opts \\ []) do
    case find(query, opts) do
      {:ok, lang} -> lang
      {:error, error} -> raise error
    end
  end
end