# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Emoji do
  @moduledoc """
  This GenServer stores in an ETS table the list of the loaded emojis,
  and also allows to reload the list at runtime.
  """
  use GenServer

  alias Pleroma.Emoji.Combinations
  alias Pleroma.Emoji.Loader

  require Logger

  @ets __MODULE__.Ets
  @ets_options [
    :ordered_set,
    :protected,
    :named_table,
    {:read_concurrency, true}
  ]
  @emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/

  defstruct [:code, :file, :tags, :safe_code, :safe_file]

  @doc "Build emoji struct"
  def build({code, file, tags}) do
    %__MODULE__{
      code: code,
      file: file,
      tags: tags,
      safe_code: Pleroma.HTML.strip_tags(code),
      safe_file: Pleroma.HTML.strip_tags(file)
    }
  end

  def build({code, file}), do: build({code, file, []})

  @doc false
  def start_link(_) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc "Reloads the emojis from disk."
  @spec reload() :: :ok
  def reload do
    GenServer.call(__MODULE__, :reload)
  end

  @doc "Returns the path of the emoji `name`."
  @spec get(String.t()) :: String.t() | nil
  def get(name) do
    name =
      if String.starts_with?(name, ":") do
        name
        |> String.replace_leading(":", "")
        |> String.replace_trailing(":", "")
      else
        name
      end

    case :ets.lookup(@ets, name) do
      [{_, path}] -> path
      _ -> nil
    end
  end

  @spec exist?(String.t()) :: boolean()
  def exist?(name), do: not is_nil(get(name))

  @doc "Returns all the emojos!!"
  @spec get_all() :: list({String.t(), String.t(), String.t()})
  def get_all do
    :ets.tab2list(@ets)
  end

  @doc "Clear out old emojis"
  def clear_all, do: :ets.delete_all_objects(@ets)

  @doc false
  def init(_) do
    @ets = :ets.new(@ets, @ets_options)
    GenServer.cast(self(), :reload)
    {:ok, nil}
  end

  @doc false
  def handle_cast(:reload, state) do
    update_emojis(Loader.load())
    {:noreply, state}
  end

  @doc false
  def handle_call(:reload, _from, state) do
    update_emojis(Loader.load())
    {:reply, :ok, state}
  end

  @doc false
  def terminate(_, _) do
    :ok
  end

  @doc false
  def code_change(_old_vsn, state, _extra) do
    update_emojis(Loader.load())
    {:ok, state}
  end

  defp update_emojis(emojis) do
    :ets.insert(@ets, emojis)
  end

  @external_resource "lib/pleroma/emoji-test.txt"

  regional_indicators =
    Enum.map(127_462..127_487, fn codepoint ->
      <<codepoint::utf8>>
    end)

  emojis =
    @external_resource
    |> File.read!()
    |> String.split("\n")
    |> Enum.filter(fn line ->
      line != "" and not String.starts_with?(line, "#") and
        String.contains?(line, "fully-qualified")
    end)
    |> Enum.map(fn line ->
      line
      |> String.split(";", parts: 2)
      |> hd()
      |> String.trim()
      |> String.split()
      |> Enum.map(fn codepoint ->
        <<String.to_integer(codepoint, 16)::utf8>>
      end)
      |> Enum.join()
    end)
    |> Enum.uniq()

  emojis = emojis ++ regional_indicators

  for emoji <- emojis do
    def is_unicode_emoji?(unquote(emoji)), do: true
  end

  def is_unicode_emoji?(_), do: false

  def stripped_name(name) when is_binary(name) do
    name
    |> String.replace_leading(":", "")
    |> String.replace_trailing(":", "")
  end

  def stripped_name(name), do: name

  def maybe_quote(name) when is_binary(name) do
    if is_unicode_emoji?(name) do
      name
    else
      if String.starts_with?(name, ":") do
        name
      else
        ":#{name}:"
      end
    end
  end

  def maybe_quote(name), do: name

  def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil

  def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do
    tag =
      tags
      |> Enum.find(fn tag -> tag["type"] == "Emoji" && tag["name"] == stripped_name(emoji) end)

    if is_nil(tag) do
      nil
    else
      tag
      |> Map.get("icon")
      |> Map.get("url")
    end
  end

  def emoji_url(_), do: nil

  def emoji_name_with_instance(name, url) do
    url = url |> URI.parse() |> Map.get(:host)
    "#{name}@#{url}"
  end

  emoji_qualification_map =
    emojis
    |> Enum.filter(&String.contains?(&1, "\uFE0F"))
    |> Combinations.variate_emoji_qualification()

  for {qualified, unqualified_list} <- emoji_qualification_map do
    for unqualified <- unqualified_list do
      def fully_qualify_emoji(unquote(unqualified)), do: unquote(qualified)
    end
  end

  def fully_qualify_emoji(emoji), do: emoji

  def matches_shortcode?(nil), do: false
  def matches_shortcode?(s), do: Regex.match?(@emoji_regex, s)
end
