defmodule Cldr.Locale.Cache do
  @moduledoc false

  # Caches locales in an :ets table
  # during compilation to improve performance

  use GenServer
  require Logger

  alias Cldr.Locale.Loader

  @table_name :cldr_locales
  @gen_server_name :cldr_locale_cache

  # @timeout 5_000

  # Client

  def start(args \\ []) do
    GenServer.start(__MODULE__, args, name: @gen_server_name)
  end

  def get_locale(locale, path) do
    ensure_genserver_started!()
    do_get_locale(locale, path)
  end

  def get_language_tag(locale) do
    ensure_genserver_started!()
    do_get_language_tag(locale)
  end

  def compiling? do
    # TODO: When we depend on Elixir v1.11+, remove function_exported and elixir_compiler_pid
    process_alive?(:can_await_module_compilation?) ||
      process_alive?(:elixir_compiler_pid) ||
      process_alive?(:cldr_locale_cache)
  end

  defp process_alive?(:can_await_module_compilation?) do
    Code.ensure_loaded?(Code) &&
      function_exported?(Code, :can_await_module_compilation?, 0) &&
      apply(Code, :can_await_module_compilation?, [])
  end

  defp process_alive?(name) do
    case Process.get(name) do
      nil -> false
      pid when is_pid(pid) -> true
    end
  end

  def put_all_language_tags(language_tags) do
    for {locale, language_tag} <- language_tags do
      :ets.insert(@table_name, {{:tag, locale}, language_tag})
    end
  end

  defp ensure_genserver_started! do
    if compiling?() and not gen_server_started?() do
      Cldr.maybe_log("Starting the compiler locale cache")

      case start() do
        {:ok, _pid} -> :ok
        {:error, {:already_started, _pid}} -> :ok
      end
    end
  end

  # Server (callbacks)

  def init(_args) do
    # Create the ets table and return its reference
    # as the state.  State therefore is only the
    # reference to the ets table we use for caching
    create_ets_table!()
    all_language_tags = Cldr.Config.all_language_tags()
    put_all_language_tags(all_language_tags)
    Process.flag(:trap_exit, true)
    {:ok, @table_name}
  end

  def handle_call({:get_locale, locale_name, path}, _from, state) do
    locale = do_get_locale(locale_name, path)
    {:reply, locale, state}
  end

  # handle the trapped exit call
  def handle_info({:EXIT, _from, reason}, state) do
    Cldr.maybe_log("Compile locale cache received EXIT message: #{inspect(reason)}")
    {:stop, reason, state}
  end

  def terminate(reason, _) do
    Cldr.maybe_log("Compile locale cache is terminating: #{inspect(reason)}")
  end

  def gen_server_started? do
    case Process.whereis(@gen_server_name) do
      nil -> false
      pid when is_pid(pid) -> true
    end
  end

  defp do_get_locale(locale, path) do
    case :ets.lookup(@table_name, locale) do
      [{^locale, locale_data}] ->
        Cldr.maybe_log("Compiler locale cache: Hit for locale #{inspect(locale)}.")
        locale_data

      [] ->
        Cldr.maybe_log(
          "Compiler locale cache: Miss for #{inspect(locale)}. " <>
            "Reading and decoding the locale file."
        )

        locale_data = Loader.do_get_locale(locale, path, false)

        try do
          :ets.insert(@table_name, {locale, locale_data})
        rescue
          ArgumentError ->
            nil
            # This may actually happen because of timing conditions: someone else may
            # have inserted behind our back.  But since we've now already generated
            # the locale again just use it.
            Cldr.maybe_log(
              "Compiler locale cache: Error: Could not insert " <>
                "locale #{inspect(locale)} into cache. Assuming locale is already cached."
            )
        end

        locale_data

      other ->
        raise RuntimeError, inspect(other)
    end
  rescue
    ArgumentError ->
      # We can very occasionally get an exception when the gen_server
      # is started but before the table is created and another thread
      # tries to get a locale. In this case we just get the locale manually
      Loader.do_get_locale(locale, path, false)
  end

  defp do_get_language_tag(locale) do
    case :ets.lookup(@table_name, {:tag, locale}) do
      [{{:tag, ^locale}, language_tag}] ->
        Cldr.maybe_log("Compiler language tag cache: Hit for locale #{inspect(locale)}.")
        language_tag

      [] ->
        # A new locale for which there is no precomputed language tag yet
        # In fact that's probably what we're about to do later on - create the
        # precomputed tag.
        Cldr.maybe_log("Compiler language tag cache miss for locale #{inspect(locale)}.")
        nil
    end
  end

  # We assign the compiler pid as the heir for our table so
  # that the table doesn't die with each compilation thread
  # This is undoubtedly hacky.
  defp create_ets_table! do
    case :ets.info(@table_name) do
      :undefined ->
        :ets.new(@table_name, [:named_table, :public, {:read_concurrency, true}])

        Cldr.maybe_log("Compiler locale cache: Created cache #{inspect(@table_name)} in :ets")

      _ ->
        :ok
    end
  end
end
