module Aws

  # Decorates a {Seahorse::Client::Response} with paging convenience methods.
  # Some AWS calls provide paged responses to limit the amount of data returned
  # with each response.  To optimize for latency, some APIs may return an
  # inconsistent number of responses per page.  You should rely on the values of
  # the `next_page?` method or using enumerable methods such as `each`
  # rather than the number of items returned to iterate through results.
  # See below for examples.
  #
  # # Paged Responses Are Enumerable
  # The simplest way to handle paged response data is to use the built-in
  # enumerator in the response object, as shown in the following example.
  #
  #     s3 = Aws::S3::Client.new
  #
  #     s3.list_objects(bucket:'aws-sdk').each do |response|
  #       puts response.contents.map(&:key)
  #     end
  #
  # This yields one response object per API call made, and enumerates objects
  # in the named bucket. The SDK retrieves additional pages of data to
  # complete the request.
  #
  # # Handling Paged Responses Manually
  # To handle paging yourself, use the response’s `next_page?` method to verify
  # there are more pages to retrieve, or use the last_page? method to verify
  # there are no more pages to retrieve.
  #
  # If there are more pages, use the `next_page` method to retrieve the
  # next page of results, as shown in the following example.
  #
  #     s3 = Aws::S3::Client.new
  #
  #     # Get the first page of data
  #     response = s3.list_objects(bucket:'aws-sdk')
  #
  #     # Get additional pages
  #     while response.next_page? do
  #       response = response.next_page
  #       # Use the response data here...
  #       puts response.contents.map(&:key)
  #     end
  #
  module PageableResponse

    def self.extended(base)
      base.send(:extend, Enumerable)
      base.send(:extend, UnsafeEnumerableMethods)
      base.instance_variable_set("@last_page", nil)
      base.instance_variable_set("@more_results", nil)
    end

    # @return [Paging::Pager]
    attr_accessor :pager

    # Returns `true` if there are no more results.  Calling {#next_page}
    # when this method returns `false` will raise an error.
    # @return [Boolean]
    def last_page?
      if @last_page.nil?
        @last_page = !@pager.truncated?(self)
      end
      @last_page
    end

    # Returns `true` if there are more results.  Calling {#next_page} will
    # return the next response.
    # @return [Boolean]
    def next_page?
      !last_page?
    end

    # @return [Seahorse::Client::Response]
    def next_page(params = {})
      if last_page?
        raise LastPageError.new(self)
      else
        next_response(params)
      end
    end

    # Yields the current and each following response to the given block.
    # @yieldparam [Response] response
    # @return [Enumerable,nil] Returns a new Enumerable if no block is given.
    def each(&block)
      return enum_for(:each_page) unless block_given?
      response = self
      yield(response)
      until response.last_page?
        response = response.next_page
        yield(response)
      end
    end
    alias each_page each

    private

    # @param [Hash] params A hash of additional request params to
    #   merge into the next page request.
    # @return [Seahorse::Client::Response] Returns the next page of
    #   results.
    def next_response(params)
      params = next_page_params(params)
      request = context.client.build_request(context.operation_name, params)
      request.send_request
    end

    # @param [Hash] params A hash of additional request params to
    #   merge into the next page request.
    # @return [Hash] Returns the hash of request parameters for the
    #   next page, merging any given params.
    def next_page_params(params)
      context[:original_params].merge(@pager.next_tokens(self).merge(params))
    end

    # Raised when calling {PageableResponse#next_page} on a pager that
    # is on the last page of results.  You can call {PageableResponse#last_page?}
    # or {PageableResponse#next_page?} to know if there are more pages.
    class LastPageError < RuntimeError

      # @param [Seahorse::Client::Response] response
      def initialize(response)
        @response = response
        super("unable to fetch next page, end of results reached")
      end

      # @return [Seahorse::Client::Response]
      attr_reader :response

    end

    # A handful of Enumerable methods, such as #count are not safe
    # to call on a pageable response, as this would trigger n api calls
    # simply to count the number of response pages, when likely what is
    # wanted is to access count on the data. Same for #to_h.
    # @api private
    module UnsafeEnumerableMethods

      def count
        if data.respond_to?(:count)
          data.count
        else
          raise NoMethodError, "undefined method `count'"
        end
      end

      def respond_to?(method_name, *args)
        if method_name == :count
          data.respond_to?(:count)
        else
          super
        end
      end

      def to_h
        data.to_h
      end

    end
  end
end
