# frozen_string_literal: true
require "graphql/pagination/connection"

module GraphQL
  module Pagination
    # A generic class for working with database query objects.
    class RelationConnection < Pagination::Connection
      def nodes
        load_nodes
        @nodes
      end

      def has_previous_page
        if @has_previous_page.nil?
          @has_previous_page = if after_offset && after_offset > 0
            true
          elsif last
            # See whether there are any nodes _before_ the current offset.
            # If there _is no_ current offset, then there can't be any nodes before it.
            # Assume that if the offset is positive, there are nodes before the offset.
            limited_nodes
            !(@paged_nodes_offset.nil? || @paged_nodes_offset == 0)
          else
            false
          end
        end
        @has_previous_page
      end

      def has_next_page
        if @has_next_page.nil?
          @has_next_page = if before_offset && before_offset > 0
            true
          elsif first
            relation_count(set_limit(sliced_nodes, first + 1)) == first + 1
          else
            false
          end
        end
        @has_next_page
      end

      def cursor_for(item)
        load_nodes
        # index in nodes + existing offset + 1 (because it's offset, not index)
        offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) + (relation_offset(items) || 0)
        encode(offset.to_s)
      end

      private

      # @param relation [Object] A database query object
      # @return [Integer, nil] The offset value, or nil if there isn't one
      def relation_offset(relation)
        raise "#{self.class}#relation_offset(relation) must return the offset value for a #{relation.class} (#{relation.inspect})"
      end

      # @param relation [Object] A database query object
      # @return [Integer, nil] The limit value, or nil if there isn't one
      def relation_limit(relation)
        raise "#{self.class}#relation_limit(relation) must return the limit value for a #{relation.class} (#{relation.inspect})"
      end

      # @param relation [Object] A database query object
      # @return [Integer, nil] The number of items in this relation (hopefully determined without loading all records into memory!)
      def relation_count(relation)
        raise "#{self.class}#relation_count(relation) must return the count of records for a #{relation.class} (#{relation.inspect})"
      end

      # @param relation [Object] A database query object
      # @return [Object] A modified query object which will return no records
      def null_relation(relation)
        raise "#{self.class}#null_relation(relation) must return an empty relation for a #{relation.class} (#{relation.inspect})"
      end

      # @return [Integer]
      def offset_from_cursor(cursor)
        decode(cursor).to_i
      end

      # Abstract this operation so we can always ignore inputs less than zero.
      # (Sequel doesn't like it, understandably.)
      def set_offset(relation, offset_value)
        if offset_value >= 0
          relation.offset(offset_value)
        else
          relation.offset(0)
        end
      end

      # Abstract this operation so we can always ignore inputs less than zero.
      # (Sequel doesn't like it, understandably.)
      def set_limit(relation, limit_value)
        if limit_value > 0
          relation.limit(limit_value)
        elsif limit_value == 0
          null_relation(relation)
        else
          relation
        end
      end

      # Apply `before` and `after` to the underlying `items`,
      # returning a new relation.
      def sliced_nodes
        @sliced_nodes ||= begin
          paginated_nodes = items

          if after_offset
            previous_offset = relation_offset(items) || 0
            paginated_nodes = set_offset(paginated_nodes, previous_offset + after_offset)
          end

          if before_offset && after_offset
            if after_offset < before_offset
              # Get the number of items between the two cursors
              space_between = before_offset - after_offset - 1
              paginated_nodes = set_limit(paginated_nodes, space_between)
            else
              # TODO I think this is untested
              # The cursors overextend one another to an empty set
              paginated_nodes = null_relation(paginated_nodes)
            end
          elsif before_offset
            # Use limit to cut off the tail of the relation
            paginated_nodes = set_limit(paginated_nodes, before_offset - 1)
          end

          paginated_nodes
        end
      end

      # @return [Integer, nil]
      def before_offset
        @before_offset ||= before && offset_from_cursor(before)
      end

      # @return [Integer, nil]
      def after_offset
        @after_offset ||= after && offset_from_cursor(after)
      end

      # Apply `first` and `last` to `sliced_nodes`,
      # returning a new relation
      def limited_nodes
        @limited_nodes ||= begin
          paginated_nodes = sliced_nodes
          previous_limit = relation_limit(paginated_nodes)

          if first && (previous_limit.nil? || previous_limit > first)
            # `first` would create a stricter limit that the one already applied, so add it
            paginated_nodes = set_limit(paginated_nodes, first)
          end

          if last
            if (lv = relation_limit(paginated_nodes))
              if last <= lv
                # `last` is a smaller slice than the current limit, so apply it
                offset = (relation_offset(paginated_nodes) || 0) + (lv - last)
                paginated_nodes = set_offset(paginated_nodes, offset)
                paginated_nodes = set_limit(paginated_nodes, last)
              end
            else
              # No limit, so get the last items
              sliced_nodes_count = relation_count(@sliced_nodes)
              offset = (relation_offset(paginated_nodes) || 0) + sliced_nodes_count - [last, sliced_nodes_count].min
              paginated_nodes = set_offset(paginated_nodes, offset)
              paginated_nodes = set_limit(paginated_nodes, last)
            end
          end

          @paged_nodes_offset = relation_offset(paginated_nodes)
          paginated_nodes
        end
      end

      # Load nodes after applying first/last/before/after,
      # returns an array of nodes
      def load_nodes
        # Return an array so we can consistently use `.index(node)` on it
        @nodes ||= limited_nodes.to_a
      end
    end
  end
end
