lib/reference.ex

defmodule BibleEx.Reference do
  @moduledoc """
  Represents a normalized Bible reference.

  A `%BibleEx.Reference{}` ties together book, chapter(s), verse(s), and
  derived metadata such as OSIS code, abbreviation, and the list of
  `BibleEx.Chapter` and `BibleEx.Verse` structs that make up the reference.
  """

  alias BibleEx.Librarian
  alias BibleEx.Chapter
  alias BibleEx.Verse

  @enforce_keys [:book]

  defstruct book: "",
            book_names: %{},
            book_number: nil,
            reference: nil,
            reference_type: nil,
            start_chapter: nil,
            start_chapter_number: nil,
            start_verse: nil,
            start_verse_number: nil,
            end_chapter: nil,
            end_chapter_number: nil,
            end_verse: nil,
            end_verse_number: nil,
            is_valid: false,
            chapters: [],
            verses: [],
            osis: nil,
            abbr: nil,
            short: nil

  @doc ~S"""
  Builds a new `%BibleEx.Reference{}` for a given book and optional range.

  The `book` parameter may be any recognized form for a book name:

    * Full name: `"Genesis"`
    * OSIS code: `"Gen"`
    * Paratext abbreviation: `"GEN"`
    * Short form: `"Gn"`

  Missing chapter or verse boundaries are filled in using `BibleEx.Librarian`,
  defaulting to the full book, chapter, or verse range as appropriate.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.new(book: "Genesis", start_chapter: 2, start_verse: 3, end_chapter: 4, end_verse: 5)
      iex> ref.book
      "Genesis"
      iex> {ref.start_chapter_number, ref.start_verse_number, ref.end_chapter_number, ref.end_verse_number}
      {2, 3, 4, 5}
      iex> ref.reference_type in [:chapter_range, :verse_range]
      true

      iex> alias BibleEx.Reference
      iex> ref = Reference.new(book: "3 John")
      iex> ref.book
      "3 John"
      iex> {ref.start_chapter_number, ref.start_verse_number}
      {1, 1}
      iex> ref.is_valid
      true

  """
  def new(book: book) do
    new(book: book, start_chapter: nil, start_verse: nil, end_chapter: nil, end_verse: nil)
  end

  @doc ~S"""
  Builds a `%BibleEx.Reference{}` for a book starting at a given chapter.

  This is a convenience wrapper for `new/1` when only `start_chapter` is known.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.new(book: "Genesis", start_chapter: 1)
      iex> {ref.book, ref.start_chapter_number, ref.start_verse_number}
      {"Genesis", 1, 1}

  """
  def new(
        book: book,
        start_chapter: start_chapter
      ) do
    new(
      book: book,
      start_chapter: start_chapter,
      start_verse: nil,
      end_chapter: nil,
      end_verse: nil
    )
  end

  @doc ~S"""
  Builds a `%BibleEx.Reference{}` for a single starting verse.

  This is a convenience wrapper for `new/1` when `book`, `start_chapter`
  and `start_verse` are known but no end boundary is supplied.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.new(book: "Gen", start_chapter: 1, start_verse: 1)
      iex> ref.reference
      "Genesis 1:1"

  """
  def new(
        book: book,
        start_chapter: start_chapter,
        start_verse: start_verse
      ) do
    new(
      book: book,
      start_chapter: start_chapter,
      start_verse: start_verse,
      end_chapter: nil,
      end_verse: nil
    )
  end

  @doc ~S"""
  Builds a `%BibleEx.Reference{}` for a cross-chapter range without an end verse.

  This form is used when the start verse is known but the range ends at the
  end of `end_chapter`.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.new(book: "John", start_chapter: 3, start_verse: 16, end_chapter: 4)
      iex> {ref.start_chapter_number, ref.start_verse_number, ref.end_chapter_number}
      {3, 16, 4}

  """
  def new(
        book: book,
        start_chapter: start_chapter,
        start_verse: start_verse,
        end_chapter: end_chapter
      ) do
    new(
      book: book,
      start_chapter: start_chapter,
      start_verse: start_verse,
      end_chapter: end_chapter,
      end_verse: nil
    )
  end

  @doc ~S"""
  Builds the most general `%BibleEx.Reference{}` with optional start and end.

  This function:

    * Normalizes the book name using `BibleEx.Librarian.get_book_names/1`.
    * Computes default start and end chapters/verses when omitted.
    * Loads `BibleEx.Chapter` and `BibleEx.Verse` structs for the range.
    * Sets `reference`, `reference_type`, and `is_valid` using `BibleEx.Librarian`.

  Prefer calling the convenience helpers (`chapter/2`, `verse/3`,
  `chapter_range/3`, `verse_range/4`) instead of this function directly
  in most application code.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.new(book: "Romans", start_chapter: 8, start_verse: 28, end_chapter: 8, end_verse: 30)
      iex> ref.reference
      "Romans 8:28-30"
      iex> ref.is_valid
      true

  """
  def new(
        book: book,
        start_chapter: start_chapter,
        start_verse: start_verse,
        end_chapter: end_chapter,
        end_verse: end_verse
      ) do
    bname =
      case Librarian.get_book_names(book: book) do
        book_names = %{
          osis: _osis,
          abbr: _abbr,
          name: _name,
          short: _short
        } ->
          Map.get(book_names, :name, book)

        %{} ->
          book
      end

    sc =
      if !is_nil(start_chapter) do
        Chapter.new(book: bname, chapter_number: start_chapter)
      else
        Chapter.new(book: bname, chapter_number: 1)
      end

    scn =
      if !is_nil(start_chapter) do
        start_chapter
      else
        1
      end

    ec =
      if !is_nil(end_chapter) do
        Chapter.new(book: bname, chapter_number: end_chapter)
      else
        if !is_nil(start_chapter) do
          Chapter.new(book: bname, chapter_number: start_chapter)
        else
          Chapter.new(book: bname, chapter_number: end_chapter)
        end
      end

    ecn =
      if !is_nil(end_chapter) do
        end_chapter
      else
        if !is_nil(start_chapter) do
          scn
        else
          Librarian.get_last_chapter_number(book: bname)
        end
      end

    sv =
      if !is_nil(start_verse) do
        Verse.new(book: bname, chapter_number: start_chapter, verse_number: start_verse)
      else
        Verse.new(book: bname, chapter_number: 1, verse_number: 1)
      end

    svn =
      if !is_nil(start_verse) do
        start_verse
      else
        1
      end

    ev =
      if !is_nil(end_verse) do
        Verse.new(book: bname, chapter_number: scn, verse_number: end_verse)
      else
        if !is_nil(start_verse) do
          Verse.new(book: bname, chapter_number: scn, verse_number: start_verse)
        else
          Librarian.get_last_verse_number(book: bname, chapter: scn)
        end
      end

    evn =
      if !is_nil(end_verse) do
        end_verse
      else
        if !is_nil(start_verse) do
          start_verse
        else
          Librarian.get_last_verse_number(book: bname, chapter: ecn)
        end
      end

    chapters =
      Librarian.get_chapters(
        book: bname,
        start_chapter: scn,
        end_chapter: ecn
      )

    chapter_list =
      scn..ecn
      |> Enum.to_list()

    verse_collection =
      Enum.map(chapter_list, fn x ->
        find_start_verse =
          if !is_nil(svn) and x == Enum.at(chapter_list, 0) do
            svn
          else
            1
          end

        find_last_verse =
          if !is_nil(evn) and x == List.last(chapter_list) do
            evn
          else
            Librarian.get_last_verse_number(book: bname, chapter: x)
          end

        Librarian.get_verses(
          book: bname,
          chapter: x,
          start_verse: find_start_verse,
          end_verse: find_last_verse
        )
      end)

    book_names = Librarian.get_book_names(book: bname)
    osis_book = Map.get(book_names, :osis, nil)
    abbr_book = Map.get(book_names, :abbr, nil)
    short_book = Map.get(book_names, :short, nil)

    %__MODULE__{
      book: bname,
      book_names: book_names,
      book_number: Librarian.find_book_number(book: bname),
      reference:
        Librarian.create_reference_string(
          book: bname,
          start_chapter: start_chapter,
          start_verse: start_verse,
          end_chapter: ecn,
          end_verse: end_verse
        ),
      reference_type:
        Librarian.identify_reference_type(
          book: bname,
          start_chapter: scn,
          start_verse: start_verse,
          end_chapter: ecn,
          end_verse: end_verse
        ),
      start_chapter: sc,
      start_chapter_number: scn,
      end_chapter: ec,
      end_chapter_number: ecn,
      start_verse: sv,
      start_verse_number: svn,
      end_verse: ev,
      end_verse_number: evn,
      is_valid:
        Librarian.verify_reference(
          book: bname,
          start_chapter: scn,
          start_verse: start_verse,
          end_chapter: ecn,
          end_verse: end_verse
        ),
      chapters: chapters,
      verses: List.flatten(verse_collection),
      osis: osis_book,
      abbr: abbr_book,
      short: short_book
    }
  end

  @doc ~S"""
  Builds a `%BibleEx.Reference{}` for a whole chapter.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.chapter(book: "Genesis", chapter: 1)
      iex> {ref.book, ref.reference_type}
      {"Genesis", :chapter}
      iex> ref.reference
      "Genesis 1"

  """
  def chapter(book: book, chapter: chapter) do
    new(book: book, start_chapter: chapter)
  end

  @doc ~S"""
  Builds a `%BibleEx.Reference{}` for a single verse.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.verse(book: "Genesis", chapter: 1, verse: 1)
      iex> {ref.book, ref.reference_type}
      {"Genesis", :verse}
      iex> ref.reference
      "Genesis 1:1"

  """
  def verse(book: book, chapter: chapter, verse: verse) do
    new(book: book, start_chapter: chapter, start_verse: verse)
  end

  @doc ~S"""
  Builds a `%BibleEx.Reference{}` for a chapter range.

  The range spans from `start_chapter` to `end_chapter`, inclusive.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.chapter_range(book: "Genesis", start_chapter: 1, end_chapter: 2)
      iex> ref.reference
      "Genesis 1-2"
      iex> ref.reference_type
      :chapter_range

  """
  def chapter_range(book: book, start_chapter: start_chapter, end_chapter: end_chapter) do
    new(
      book: book,
      start_chapter: start_chapter,
      start_verse: nil,
      end_chapter: end_chapter,
      end_verse: nil
    )
  end

  @doc ~S"""
  Builds a `%BibleEx.Reference{}` for a verse range within a chapter.

  ## Examples

      iex> alias BibleEx.Reference
      iex> ref = Reference.verse_range(book: "Matt", chapter: 2, start_verse: 4, end_verse: 10)
      iex> ref.book
      "Matthew"
      iex> {ref.start_verse_number, ref.end_verse_number}
      {4, 10}
      iex> ref.reference_type
      :verse_range

  """
  def verse_range(book: book, chapter: chapter, start_verse: start_verse, end_verse: end_verse) do
    new(
      book: book,
      start_chapter: chapter,
      start_verse: start_verse,
      end_chapter: nil,
      end_verse: end_verse
    )
  end
end