# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Gapic
  ##
  # A class to provide the Enumerable interface to the response of a paginated method. PagedEnumerable assumes
  # response message holds a list of resources and the token to the next page.
  #
  # PagedEnumerable provides the enumerations over the resource data, and also provides the enumerations over the
  # pages themselves.
  #
  # @example normal iteration over resources.
  #   paged_enumerable.each { |resource| puts resource }
  #
  # @example per-page iteration.
  #   paged_enumerable.each_page { |page| puts page }
  #
  # @example Enumerable over pages.
  #   paged_enumerable.each_page do |page|
  #     page.each { |resource| puts resource }
  #   end
  #
  # @example more exact operations over pages.
  #   while some_condition()
  #     page = paged_enumerable.page
  #     do_something(page)
  #     break if paged_enumerable.next_page?
  #     paged_enumerable.next_page
  #   end
  #
  class PagedEnumerable
    include Enumerable

    ##
    # @attribute [r] page
    #   @return [Page] The current page object.
    attr_reader :page

    ##
    # @private
    # @param grpc_stub [Gapic::GRPC::Stub] The Gapic gRPC stub object.
    # @param method_name [Symbol] The RPC method name.
    # @param request [Object] The request object.
    # @param response [Object] The response object.
    # @param operation [::GRPC::ActiveCall::Operation] The RPC operation for the response.
    # @param options [Gapic::CallOptions] The options for making the RPC call.
    # @param format_resource [Proc] A Proc object to format the resource object. The Proc should accept response as an
    #   argument, and return a formatted resource object. Optional.
    #
    def initialize grpc_stub, method_name, request, response, operation, options, format_resource: nil
      @grpc_stub = grpc_stub
      @method_name = method_name
      @request = request
      @response = response
      @options = options
      @format_resource = format_resource
      @resource_field = nil # will be set in verify_response!

      verify_request!
      verify_response!

      @page = Page.new @response, @resource_field, operation, format_resource: @format_resource
    end

    ##
    # Iterate over the resources.
    #
    # @yield [Object] Gives the resource objects in the stream.
    #
    # @raise [RuntimeError] if it's not started yet.
    #
    def each &block
      return enum_for :each unless block_given?

      each_page do |page|
        page.each(&block)
      end
    end

    ##
    # Iterate over the pages.
    #
    # @yield [Page] Gives the pages in the stream.
    #
    # @raise if it's not started yet.
    #
    def each_page
      return enum_for :each_page unless block_given?

      loop do
        break if @page.nil?
        yield @page
        next_page!
      end
    end

    ##
    # True if it has the next page.
    #
    def next_page?
      @page.next_page_token?
    end

    ##
    # Update the response in the current page.
    #
    # @return [Page] the new page object.
    #
    def next_page!
      unless next_page?
        @page = nil
        return @page
      end

      next_request = @request.dup
      next_request.page_token = @page.next_page_token
      @grpc_stub.call_rpc @method_name, next_request, options: @options do |next_response, next_operation|
        @page = Page.new next_response, @resource_field, next_operation, format_resource: @format_resource
      end
      @page
    end
    alias next_page next_page!

    ##
    # The page token to be used for the next RPC call.
    #
    # @return [String]
    #
    def next_page_token
      @page.next_page_token
    end

    ##
    # The current response object, for the current page.
    #
    # @return [Object]
    #
    def response
      @page.response
    end

    private

    def verify_request!
      page_token = @request.class.descriptor.find do |f|
        f.name == "page_token" && f.type == :string
      end
      raise ArgumentError, "#{@request.class} must have a page_token field (String)" if page_token.nil?

      page_size = @request.class.descriptor.find do |f|
        f.name == "page_size" && [:int32, :int64].include?(f.type)
      end
      return unless page_size.nil?
      raise ArgumentError, "#{@request.class} must have a page_size field (Integer)"
    end

    def verify_response!
      next_page_token = @response.class.descriptor.find do |f|
        f.name == "next_page_token" && f.type == :string
      end
      raise ArgumentError, "#{@response.class} must have a next_page_token field (String)" if next_page_token.nil?

      # Find all repeated FieldDescriptors on the response Descriptor
      fields = @response.class.descriptor.select do |f|
        f.label == :repeated && f.type == :message
      end

      repeated_field = fields.first
      raise ArgumentError, "#{@response.class} must have one repeated field" if repeated_field.nil?

      min_repeated_field_number = fields.map(&:number).min
      if min_repeated_field_number != repeated_field.number
        raise ArgumentError, "#{@response.class} must have one primary repeated field by both position and number"
      end

      # We have the correct repeated field, save the field's name
      @resource_field = repeated_field.name
    end

    ##
    # A class to represent a page in a PagedEnumerable. This also implements Enumerable, so it can iterate over the
    # resource elements.
    #
    # @attribute [r] response
    #   @return [Object] the response object for the page.
    # @attribute [r] operation
    #   @return [::GRPC::ActiveCall::Operation] the RPC operation for the page.
    class Page
      include Enumerable
      attr_reader :response
      attr_reader :operation

      ##
      # @private
      # @param response [Object] The response object for the page.
      # @param resource_field [String] The name of the field in response which holds the resources.
      # @param operation [::GRPC::ActiveCall::Operation] the RPC operation for the page.
      # @param format_resource [Proc] A Proc object to format the resource object. The Proc should accept response as an
      #   argument, and return a formatted resource object. Optional.
      #
      def initialize response, resource_field, operation, format_resource: nil
        @response = response
        @resource_field = resource_field
        @operation = operation
        @format_resource = format_resource
      end

      ##
      # Iterate over the resources.
      #
      # @yield [Object] Gives the resource objects in the page.
      #
      def each
        return enum_for :each unless block_given?

        return if @response.nil?

        # We trust that the field exists and is an Enumerable
        @response[@resource_field].each do |resource|
          resource = @format_resource.call resource if @format_resource
          yield resource
        end
      end

      ##
      # The page token to be used for the next RPC call.
      #
      # @return [String]
      #
      def next_page_token
        return if @response.nil?

        @response.next_page_token
      end

      ##
      # Truthiness of next_page_token.
      #
      # @return [Boolean]
      #
      def next_page_token?
        return if @response.nil? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition

        !@response.next_page_token.empty?
      end
    end
  end
end
