defmodule Spark.ElixirSense.Entity do
  @moduledoc false
  alias ElixirSense.Core.Introspection
  alias ElixirSense.Providers.Suggestion.Complete

  cond do
    Code.ensure_loaded?(ElixirSense.Providers.Plugins.Util) ->
      @util ElixirSense.Providers.Plugins.Util

    Code.ensure_loaded?(ElixirLS.LanguageServer.Plugins.Util) ->
      @util ElixirLS.LanguageServer.Plugins.Util

    true ->
      @util ElixirSense.Plugins.Util
  end

  def find_entities(type, hint) do
    for {module, _} <- :code.all_loaded(),
        type in (module.module_info(:attributes)[:spark_is] || []),
        mod_str = inspect(module),
        apply(@util, :match_module?, [mod_str, hint]) do
      {doc, _} = apply(Introspection, :get_module_docs_summary, [module])

      %{
        type: :generic,
        kind: :class,
        label: mod_str,
        insert_text: apply(@util, :trim_leading_for_insertion, [hint, mod_str]),
        detail: "Spark resource",
        documentation: doc
      }
    end
  end

  def find_spark_behaviour_impls(behaviour, builtins, hint, module_store) do
    builtins =
      if builtins && !String.contains?(hint, ".") && lowercase_string?(hint) do
        cond do
          Code.ensure_loaded?(ElixirSense.Providers.Completion.CompletionEngine) ->
            apply(ElixirSense.Providers.Completion.CompletionEngine, :complete, [
              to_string("#{inspect(builtins)}.#{hint}"),
              apply(ElixirSense.Core.State.Env, :__struct__, []),
              apply(ElixirSense.Core.Metadata, :__struct__, []),
              0
            ])

          Code.ensure_loaded?(ElixirLS.Utils.CompletionEngine) ->
            apply(ElixirLS.Utils.CompletionEngine, :complete, [
              to_string("#{inspect(builtins)}.#{hint}"),
              apply(ElixirSense.Core.State.Env, :__struct__, []),
              apply(ElixirSense.Core.Metadata, :__struct__, []),
              0
            ])

          function_exported?(Complete, :complete, 4) ->
            apply(Complete, :complete, [
              to_string("#{inspect(builtins)}.#{hint}"),
              apply(ElixirSense.Core.State.Env, :__struct__, []),
              apply(ElixirSense.Core.Metadata, :__struct__, []),
              0
            ])

          true ->
            apply(Complete, :complete, [
              to_string("#{inspect(builtins)}.#{hint}"),
              apply(Complete.Env, :__struct__, [])
            ])
        end
      else
        []
      end
      |> Enum.reject(fn
        %{name: name} when name in ["__info__", "module_info", "module_info"] ->
          true

        _ ->
          false
      end)
      |> Enum.map(fn
        %{type: :function, origin: origin, name: name, arity: arity} = completion ->
          try do
            {:docs_v1, _, _, _, _, _, functions} = Code.fetch_docs(Module.concat([origin]))

            new_summary =
              Enum.find_value(functions, fn
                {{:function, func_name, func_arity}, _, _,
                 %{
                   "en" => docs
                 }, _} ->
                  if to_string(func_name) == name && func_arity == arity do
                    docs
                  end

                _other ->
                  false
              end)

            %{completion | summary: new_summary || completion.summary}
          rescue
            _e ->
              completion
          end

        other ->
          other
      end)

    first = behaviour |> Module.split() |> Enum.at(0)

    custom =
      for module <- module_store.by_behaviour[behaviour] || [],
          mod_str = inspect(module),
          !String.starts_with?(mod_str, "#{first}."),
          apply(@util, :match_module?, [mod_str, hint]) do
        {doc, _} = apply(Introspection, :get_module_docs_summary, [module])

        %{
          type: :generic,
          kind: :class,
          label: mod_str,
          insert_text: apply(@util, :trim_leading_for_insertion, [hint, mod_str]),
          detail: "#{inspect(behaviour)}",
          documentation: doc
        }
      end

    builtins ++ custom
  end

  def find_behaviour_impls(behaviour, hint, module_store) do
    for module <- module_store.by_behaviour[behaviour] || [],
        mod_str = inspect(module),
        apply(@util, :match_module?, [mod_str, hint]),
        !asks_to_skip?(module) do
      {doc, _} = apply(Introspection, :get_module_docs_summary, [module])

      %{
        type: :generic,
        kind: :class,
        label: mod_str,
        insert_text: apply(@util, :trim_leading_for_insertion, [hint, mod_str]),
        detail: "#{inspect(behaviour)}",
        documentation: doc
      }
    end
  end

  defp asks_to_skip?(module) do
    Code.ensure_loaded?(module) && function_exported?(module, :skip_in_spark_autocomplete, 0) &&
      module.skip_in_spark_autocomplete()
  end

  defp lowercase_string?(""), do: true

  defp lowercase_string?(string) do
    first = String.first(string)
    String.downcase(first) == first
  end
end
