defmodule Doctor.Config do
  @moduledoc """
  This module defines a struct which houses all the
  configuration data for Doctor.
  """

  @config_file ".doctor.exs"

  require Logger
  alias __MODULE__

  @typedoc """
  * `:min_module_doc_coverage` - Minimum ratio of @doc vs public functions
    per module.
  * `:min_overall_doc_coverage` - Minimum ratio of @doc vs public functions
    across the codebase.
  * `:min_overall_moduledoc_coverage` - Minimum ratio of @moduledoc to modules
    across the codebase.
  * `:moduledoc_required` - If true, `:min_overall_moduledoc_coverage` is
    automatically set to 100%. Deprecated.
  """
  @type t :: %Config{
          ignore_modules: [Regex.t() | String.t()],
          ignore_paths: [Regex.t() | module()],
          min_module_doc_coverage: integer() | float(),
          min_module_spec_coverage: integer() | float(),
          min_overall_doc_coverage: integer() | float(),
          min_overall_moduledoc_coverage: integer() | float(),
          min_overall_spec_coverage: integer() | float(),
          moduledoc_required: boolean(),
          exception_moduledoc_required: boolean() | nil,
          raise: boolean(),
          reporter: module(),
          struct_type_spec_required: boolean(),
          umbrella: boolean(),
          failed: false
        }

  defstruct ignore_modules: [],
            ignore_paths: [],
            min_module_doc_coverage: 40,
            min_module_spec_coverage: 0,
            min_overall_doc_coverage: 50,
            min_overall_moduledoc_coverage: 100,
            min_overall_spec_coverage: 0,
            moduledoc_required: nil,
            exception_moduledoc_required: true,
            raise: false,
            reporter: Doctor.Reporters.Full,
            struct_type_spec_required: true,
            umbrella: false,
            failed: false

  @doc """
  Create a new Config struct from a map, keyword list or preexisting Config.
  """
  @spec new(keyword | map) :: Config.t()
  def new(attrs \\ %{}) do
    config =
      case attrs do
        %Config{} = c -> c
        map_or_keyword -> struct(Config, map_or_keyword)
      end

    interpret_moduledoc_required(config)
  end

  @doc """
  Returns true if a specific module should fail validation if it lacks a
  moduledoc."
  """
  @spec moduledoc_required?(t) :: boolean
  def moduledoc_required?(%{min_overall_moduledoc_coverage: 100}), do: true
  def moduledoc_required?(_), do: false

  @doc """
  Get the configuration defaults as a string
  """
  def config_defaults_as_string do
    config = quote do: unquote(%Config{})

    config
    |> Macro.to_string()
    |> Code.format_string!()
    |> to_string()
    |> String.replace(~r/\s+moduledoc_required:.*/, "")
  end

  @doc """
  Get the configuration file name
  """
  def config_file, do: @config_file

  # If `:moduledoc_required` is defined in the config, warn the user about the
  # deprecation. In a future version, the struct key and associated
  # backwards-compatibility code could be removed.
  @spec interpret_moduledoc_required(Config.t()) :: Config.t()
  defp interpret_moduledoc_required(%{moduledoc_required: nil} = config) do
    config
  end

  defp interpret_moduledoc_required(%{moduledoc_required: true} = config) do
    warn_deprecation(true, 100)
    %{config | min_overall_moduledoc_coverage: 100}
  end

  defp interpret_moduledoc_required(%{moduledoc_required: false} = config) do
    warn_deprecation(false, 0)
    %{config | min_overall_moduledoc_coverage: 0}
  end

  defp warn_deprecation(_bool, val) do
    Logger.warning("""
    :moduledoc_required in #{Config.config_file()} is a deprecated option. \
    Now running with the equivalent :min_overall_moduledoc_coverage #{val} \
    but you should replace the deprecated option with the new one to avoid \
    this warning.\
    """)
  end
end
