defmodule Exmoji do
  @moduledoc """
  An Elixir/Erlang library providing low level operations for dealing with Emoji
  glyphs in the Unicode standard.

  Exmoji is like a swiss-army knife for dealing with Emoji encoding issues. If
  all you need to do is translate `:poop:` into a smiling poop glyph, then there
  are plenty of other libs out there that will probably do what you want.  But
  once you are dealing with Emoji as a fundamental part of your application, and
  you start to realize the nightmare of doublebyte encoding or variants, then
  this library may be your new best friend.
  """

  alias Exmoji.EmojiChar

  #
  # Read and parse the Emoji library from our vendored data file.
  #
  vendor_data_file = "lib/vendor/emoji-data/emoji.json"
  @external_resource vendor_data_file

  rawfile = File.read!(vendor_data_file)
  rawdata = Jason.decode!(rawfile, keys: :atoms)

  emoji_chars =
    for char <- rawdata do
      %EmojiChar{
        name: char.name,
        unified: char.unified,
        variations: char.variations,
        short_name: char.short_name,
        short_names: char.short_names,
        text: char.text
      }
    end

  @emoji_chars emoji_chars

  @doc """
  Returns a list of all #{Enum.count(@emoji_chars)} Emoji characters as `EmojiChar`.
  """
  def all, do: @emoji_chars

  @doc """
  Returns a list of all `EmojiChar` that are represented as doublebyte encoding.
  """
  @all_doublebyte_cache Enum.filter(@emoji_chars, &EmojiChar.doublebyte?/1)
  def all_doublebyte, do: @all_doublebyte_cache

  @doc """
  Returns a list of all `EmojiChar` that have at least one variant encoding.
  """
  @all_variant_cache Enum.filter(@emoji_chars, &EmojiChar.variant?/1)
  def all_with_variants, do: @all_variant_cache

  @doc """
  Returns a list of all known Emoji characters rendered as Unicode bitstrings.

  By default, the default rendering options for this library will be used.
  However, if you pass the option of `include_variants: true` then all possible
  renderings of a single glyph will be included, meaning that:

  1. You will have "duplicate" emojis in your list.
  2. This list is now suitable for exhaustably matching against in a search.

  """
  def chars(opts \\ []) do
    case Keyword.get(opts, :include_variants, false) do
      false ->
        Enum.map(@emoji_chars, &EmojiChar.render/1)

      true ->
        Enum.map(@emoji_chars, &EmojiChar.chars/1)
        |> List.flatten()
    end
  end

  @doc """
  Returns a list of all known codepoints representing Emoji characters.

  This function also accepts the `include_variants` option, for details on its
  significance, see similar discussion for the `Exmoji.chars/1` function.
  """
  def codepoints(opts \\ []) do
    case Keyword.get(opts, :include_variants, false) do
      false ->
        Enum.map(@emoji_chars, & &1.unified)

      true ->
        Enum.map(@emoji_chars, &EmojiChar.codepoint_ids/1)
        |> List.flatten()
    end
  end

  @doc """
  Finds any `EmojiChar` that contains given string in its official name.

  ## Examples

      iex> Exmoji.find_by_name "father"
      [%Exmoji.EmojiChar{name: "FATHER CHRISTMAS", short_name: "santa",
      short_names: ["santa"], text: nil, unified: "1F385", variations: []}]

      iex> for t <- Exmoji.find_by_name("tree"), do: t.name
      ["EVERGREEN TREE", "DECIDUOUS TREE", "PALM TREE", "CHRISTMAS TREE",
      "TANABATA TREE"]

  """
  def find_by_name(name) do
    name = String.upcase(name)
    Enum.filter(@emoji_chars, &String.contains?(&1.name, name))
  end

  @doc """
  Find all `EmojiChar` that match substring in any of their associated short
  name keywords.
  """
  def find_by_short_name(sname) do
    target = String.downcase(sname)
    Enum.filter(@emoji_chars, &matches_short_name(&1, target))
  end

  defp matches_short_name(%EmojiChar{} = ec, target) do
    Enum.any?(ec.short_names, &String.contains?(&1, target))
  end

  @doc """
  Finds an `EmojiChar` based on its short name keyword.

  Case insensitive. Otherwise must match exactly. Do not include the `:colon:`
  identifiers if you are parsing text that uses them to indicate the presence of
  a keyword.
  """
  def from_short_name(sname) do
    sname |> String.downcase() |> _from_short_name
  end

  for ec <- @emoji_chars, sn <- ec.short_names do
    defp _from_short_name(unquote(sn)), do: unquote(Macro.escape(ec))
  end

  defp _from_short_name(_), do: nil

  @doc """
  Finds a specific `EmojiChar` based on the unified codepoint ID.
  """
  def from_unified(uid) do
    uid |> String.upcase() |> _from_unified
  end

  for ec <- @emoji_chars, cp <- EmojiChar.codepoint_ids(ec) do
    defp _from_unified(unquote(cp)), do: unquote(Macro.escape(ec))
  end

  defp _from_unified(_), do: nil

  @doc """
  Convert a unified ID directly to its bitstring glyph representation.

  ## Example

      iex> Exmoji.unified_to_char("1F47E")
      "👾"

  """
  def unified_to_char(uid) do
    uid
    |> String.split("-")
    |> Enum.map(&String.to_integer(&1, 16))
    |> List.to_string()
  end

  @doc """
  Convert a native bitstring glyph to its unified codepoint ID.

  This is a conversion operation, not a match, so it may produce unexpected
  results with different types of values.

  ## Examples

      iex> Exmoji.char_to_unified("👾")
      "1F47E"

      iex> Exmoji.char_to_unified("\x23\u{fe0f}\u{20e3}")
      "0023-FE0F-20E3"

  """
  def char_to_unified(char) do
    char
    |> String.codepoints()
    |> Enum.map(&padded_hex_string/1)
    |> Enum.join("-")
    |> String.upcase()
  end

  # produce a string representation of the integer value of a codepoint, in hex
  # this should be zero-padded to a minimum of 4 digits
  defp padded_hex_string(<<cp_int_value::utf8>>) do
    cp_int_value |> Integer.to_string(16) |> String.pad_leading(4, "0")
  end
end
