defmodule Xgit.Tag do
  @moduledoc ~S"""
  Represents a git `tag` object in memory.
  """
  alias Xgit.ContentSource
  alias Xgit.Object
  alias Xgit.ObjectId
  alias Xgit.ObjectType
  alias Xgit.PersonIdent
  alias Xgit.Ref

  use Xgit.ObjectType

  import Xgit.Util.ForceCoverage
  import Xgit.Util.ParseHeader, only: [next_header: 1]

  @typedoc ~S"""
  This struct describes a single `tag` object so it can be manipulated in memory.

  ## Struct Members

  * `:object`: (`Xgit.ObjectId`) object referenced by this tag
  * `:type`: (`Xgit.ObjectType`) type of the target object
  * `:name`: (bytelist) name of the tag
  * `:tagger`: (`Xgit.PersonIdent`) person who created the tag
  * `:message`: (bytelist) user-entered tag message (encoding unspecified)

  **TO DO:** Support signatures and other extensions.
  https://github.com/elixir-git/xgit/issues/202
  """
  @type t :: %__MODULE__{
          object: ObjectId.t(),
          type: ObjectType.t(),
          name: [byte],
          tagger: PersonIdent.t() | nil,
          message: [byte]
        }

  @enforce_keys [:object, :type, :name, :message]
  defstruct [:object, :type, :name, :message, tagger: nil]

  @doc ~S"""
  Return `true` if the value is a tag struct that is valid.
  """
  @spec valid?(tag :: any) :: boolean
  def valid?(tag)

  def valid?(%__MODULE__{
        object: object_id,
        type: object_type,
        name: name,
        tagger: tagger,
        message: message
      })
      when is_binary(object_id) and is_object_type(object_type) and is_list(name) and
             is_list(message) do
    ObjectId.valid?(object_id) &&
      not Enum.empty?(name) &&
      (tagger == nil || PersonIdent.valid?(tagger)) &&
      not Enum.empty?(message)
  end

  def valid?(_), do: cover(false)

  @doc ~S"""
  Return `true` if the value provided is valid as a tag name.
  """
  @spec valid_name?(name :: any) :: boolean
  def valid_name?(name) when is_list(name), do: Ref.valid_name?("refs/tags/#{name}")
  def valid_name?(_name), do: cover(false)

  @typedoc ~S"""
  Error response codes returned by `from_object/1`.
  """
  @type from_object_reason :: :not_a_tag | :invalid_tag

  @doc ~S"""
  Renders a tag structure from an `Xgit.Object`.

  ## Return Values

  `{:ok, tag}` if the object contains a valid `tag` object.

  `{:error, :not_a_tag}` if the object contains an object of a different type.

  `{:error, :invalid_tag}` if the object says that is of type `tag`, but
  can not be parsed as such.
  """
  @spec from_object(object :: Object.t()) :: {:ok, tag :: t} | {:error, from_object_reason}
  def from_object(object)

  def from_object(%Object{type: :tag, content: content} = _object) do
    content
    |> ContentSource.stream()
    |> Enum.to_list()
    |> from_object_internal()
  end

  def from_object(%Object{} = _object), do: cover({:error, :not_a_tag})

  defp from_object_internal(data) do
    with {:object, {'object', object_id_str, data}} <- {:object, next_header(data)},
         {:object_id, {object_id, []}} <- {:object_id, ObjectId.from_hex_charlist(object_id_str)},
         {:type_str, {'type', type_str, data}} <- {:type_str, next_header(data)},
         {:type, type} when is_object_type(type) <- {:type, ObjectType.from_bytelist(type_str)},
         {:name, {'tag', [_ | _] = name, data}} <- {:name, next_header(data)},
         {:tagger_id, tagger, data} <- optional_tagger(data),
         message when is_list(message) <- drop_if_lf(data) do
      # TO DO: Support signatures and other extensions.
      # https://github.com/elixir-git/xgit/issues/202
      cover {:ok,
             %__MODULE__{
               object: object_id,
               type: type,
               name: name,
               tagger: tagger,
               message: message
             }}
    else
      _ -> cover {:error, :invalid_tag}
    end
  end

  defp optional_tagger(data) do
    with {:tagger, {'tagger', tagger_str, data}} <- {:tagger, next_header(data)},
         {:tagger_id, %PersonIdent{} = tagger} <-
           {:tagger_id, PersonIdent.from_byte_list(tagger_str)} do
      cover {:tagger_id, tagger, data}
    else
      {:tagger, :no_header_found} ->
        cover {:tagger_id, nil, data}

      {:tagger_id, x} ->
        cover {:tagger_error, x}
    end
  end

  defp drop_if_lf([10 | data]), do: cover(data)
  defp drop_if_lf([]), do: cover([])
  defp drop_if_lf(_), do: cover(:error)

  @doc ~S"""
  Renders this tag structure into a corresponding `Xgit.Object`.

  If the tag structure is not valid, will raise `ArgumentError`.
  """
  @spec to_object(commit :: t) :: Object.t()
  def to_object(commit)

  def to_object(
        %__MODULE__{
          object: object_id,
          type: object_type,
          name: tag_name,
          tagger: %PersonIdent{} = tagger,
          message: message
        } = tag
      ) do
    unless valid?(tag) do
      raise ArgumentError, "Xgit.Tag.to_object/1: tag is not valid"
    end

    rendered_tag =
      'object #{object_id}\n' ++
        'type #{object_type}\n' ++
        'tag #{tag_name}\n' ++
        'tagger #{PersonIdent.to_external_string(tagger)}\n' ++
        '\n' ++
        message

    # TO DO: Support signatures and other extensions.
    # https://github.com/elixir-git/xgit/issues/202

    cover %Object{
      type: :tag,
      content: rendered_tag,
      size: Enum.count(rendered_tag),
      id: ObjectId.calculate_id(rendered_tag, :tag)
    }
  end
end
