# frozen_string_literal: true

require 'forwardable'

class Paginator
  VERSION = '1.0.9'

  class ArgumentError < ::ArgumentError; end
  class MissingCountError < ArgumentError; end
  class MissingSelectError < ArgumentError; end

  attr_reader :per_page, :count

  # Instantiate a new Paginator object
  #
  # Provide:
  # * A total count of the number of objects to paginate
  # * The number of objects in each page
  # * A block that returns the array of items
  #   * The block is passed the item offset
  #     (and the number of items to show per page, for
  #     convenience, if the arity is 2)
  def initialize(count, per_page, &select)
    @count = count
    @per_page = per_page
    raise MissingSelectError, 'Must provide block to select data for each page' unless select

    @select = select
  end

  # Total number of pages
  def number_of_pages
    (@count / @per_page).to_i + ((@count % @per_page).positive? ? 1 : 0)
  end

  # First page object
  def first
    page 1
  end

  # Last page object
  def last
    page number_of_pages
  end

  # Iterate through pages
  def each
    each_with_index do |item, _|
      yield item
    end
  end

  # Iterate through pages with indices
  def each_with_index
    1.upto(number_of_pages) do |number|
      yield(page(number), number - 1)
    end
  end

  # Retrieve page object by number
  def page(number)
    number = [1, number.to_i].max
    Page.new(self, number) do
      offset = (number - 1) * @per_page
      args = [offset]
      args << @per_page if @select.arity == 2
      @select.call(*args)
    end
  end

  # Page object
  #
  # Retrieves items for a page and provides metadata about the position
  # of the page in the paginator
  class Page
    extend Forwardable
    def_delegator :@pager, :first, :first
    def_delegator :@pager, :last, :last
    def_delegator :items, :each
    def_delegator :items, :each_with_index

    attr_reader :number, :pager

    def initialize(pager, number, &select) # :nodoc:
      @pager = pager
      @number = number
      @offset = (number - 1) * pager.per_page
      @select = select
    end

    # Retrieve the items for this page
    # * Caches
    def items
      @items ||= @select.call
    end

    # Checks to see if there's a page before this one
    def prev?
      @number > 1
    end

    # Get previous page (if possible)
    def prev
      @pager.page(@number - 1) if prev?
    end

    # Checks to see if there's a page after this one
    def next?
      @number < @pager.number_of_pages
    end

    # Get next page (if possible)
    def next
      @pager.page(@number + 1) if next?
    end

    # The "item number" of the first item on this page
    def first_item_number
      1 + @offset
    end

    # The "item number" of the last item on this page
    def last_item_number
      if next?
        @offset + @pager.per_page
      else
        @pager.count
      end
    end

    def ==(other) # :nodoc:
      @pager == other.pager && number == other.number
    end
  end
end
