require 'geared_pagination/ratios'
require 'geared_pagination/cursor'
require 'active_support/core_ext/array'

module GearedPagination
  class PortionAtCursor
    attr_reader :cursor, :orders, :ratios
    delegate :page_number, to: :cursor

    def initialize(cursor: Cursor.new, ordered_by:, per_page: Ratios.new)
      @cursor, @orders, @ratios = cursor, ordered_by, per_page
    end

    def from(scope)
      @froms ||= {}
      @froms[scope] ||= begin
        if scope.order_values.none? && scope.limit_value.nil?
          selection_from(scope).order(orderings).limit(limit)
        else
          raise ArgumentError, "Can't paginate relation with ORDER BY or LIMIT clauses (got #{scope.to_sql})"
        end
      end
    end

    def next_param(scope)
      Cursor.encode page_number: page_number + 1, values: from(scope).last&.slice(*attributes) || {}
    end


    def cache_key
      "#{page_number}:#{ratios.cache_key}"
    end

    private
      def selection_from(scope)
        Selection.new(scope, orders).from(cursor)
      end

      def orderings
        orders.map { |order| [ order.attribute, order.direction ] }.to_h
      end

      def limit
        ratios[page_number]
      end

      def attributes
        orders.collect(&:attribute)
      end


      class Selection
        attr_reader :scope, :orders

        def initialize(scope, orders)
          @scope, @orders = scope, orders
        end

        def from(cursor)
          if condition = condition_on(cursor)
            scope.where(condition)
          else
            scope.all
          end
        end

        private
          delegate :table, to: :scope

          def condition_on(cursor)
            conditions_on(cursor).reduce { |left, right| left.or(right) }
          end

          def conditions_on(cursor)
            if orders.all? { |order| cursor.include?(order.attribute) }
              matches.collect { |match| match.condition_on(table, cursor) }
            else
              []
            end
          end

          def matches
            orders.size.downto(1).collect { |i| Match.new(orders[i - 1], orders.take(i - 1)) }
          end
      end

      class Match
        attr_reader :head, :tail

        def initialize(head, tail)
          @head, @tail = head, tail
        end

        def condition_on(table, cursor)
          conditions_on(table, cursor).reduce { |left, right| left.and(right) }
        end

        private
          def conditions_on(table, cursor)
            predicates.collect { |predicate| predicate.condition_on(table, cursor.fetch(predicate.attribute)) }
          end

          def predicates
            tail.collect { |order| Equal.new(order) }.including(Inequal.new(head))
          end
      end

      class Predicate
        attr_reader :order
        delegate :attribute, to: :order

        def initialize(order)
          @order = order
        end

        def condition_on(table, value)
          raise NotImplementedError
        end
      end

      class Equal < Predicate
        def condition_on(table, value)
          table[attribute].eq(value)
        end
      end

      class Inequal < Predicate
        def condition_on(table, value)
          if order.asc?
            table[attribute].gt(value)
          else
            table[attribute].lt(value)
          end
        end
      end
  end
end