# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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.

require 'aws/errors'
require 'aws/inflection'
require 'aws/naming'
require 'aws/response'
require 'aws/async_handle'
require 'aws/http/handler'
require 'aws/http/request'
require 'aws/http/response'
require 'aws/xml_grammar'
require 'aws/option_grammar'
require 'aws/client_logging'
require 'benchmark'
require 'set'

module AWS
  
  # Base class for all of the Amazon AWS service clients.
  # @private
  class BaseClient

    include ClientLogging

    extend Naming

    CACHEABLE_REQUESTS = Set.new

    # Creates a new low-level client.
    #
    # == Required Options
    #
    # To create a client you must provide access to AWS credentials.
    # There are two options:
    #
    # * +:signer+ -- An object that responds to +access_key_id+
    #   (to return the AWS Access Key ID) and to
    #   <code>sign(string_to_sign)</code> (to return a signature
    #   for a given string).  An example implementation is
    #   AWS::DefaultSigner.  This option is useful if you want to
    #   more tightly control access to your secret access key (for
    #   example by moving the signature computation into a
    #   different process).
    #
    # * +:access_key_id+ and +:secret_access_key+ -- You can use
    #   these options to provide the AWS Access Key ID and AWS
    #   Secret Access Key directly to the client.
    #
    # == Optional
    #
    # * +:http_handler+ -- Any object that implements a
    #   <code>handle(request, response)</code> method; an example
    #   is BuiltinHttpHandler.  This method is used to perform the
    #   HTTP requests that this client constructs.
    #
    def initialize options = {}

      if options[:endpoint]
        options[:"#{self.class.service_ruby_name}_endpoint"] = 
          options.delete(:endpoint)
      end

      options_without_config = options.dup
      @config = options_without_config.delete(:config)
      @config ||= AWS.config
      @config = @config.with(options_without_config)
      @signer = @config.signer
      @http_handler = @config.http_handler
      @stubs = {}

    end

    # @return [Configuration] This clients configuration.
    attr_reader :config

    # @return [DefaultSigner,Object] Returns the signer for this client.
    #   This is normally a DefaultSigner, but it can be configured to
    #   an other object.
    attr_reader :signer

    # @return [String] the configured endpoint for this client.
    def endpoint
      config.send(:"#{self.class.service_ruby_name}_endpoint")
    end

    # Returns a copy of the client with a different HTTP handler.
    # You can pass an object like BuiltinHttpHandler or you can
    # use a block; for example:
    #
    #   s3_with_logging = s3.with_http_handler do |request, response|
    #     $stderr.puts request.inspect
    #     super
    #   end
    #
    # The block executes in the context of an HttpHandler
    # instance, and +super+ delegates to the HTTP handler used by
    # this client.  This provides an easy way to spy on requests
    # and responses.  See HttpHandler, HttpRequest, and
    # HttpResponse for more details on how to implement a fully
    # functional HTTP handler using a different HTTP library than
    # the one that ships with Ruby.
    # @param handler (nil) A new http handler.  Leave blank and pass a
    #   block to wrap the current handler with the block.
    # @return [BaseClient] Returns a new instance of the client class with
    #   the modified or wrapped http handler.
    def with_http_handler(handler = nil, &blk)
      handler ||= Http::Handler.new(@http_handler, &blk)
      with_options(:http_handler => handler)
    end

    # @param [Hash] options
    # @see AWS.config detailed list of accepted options.
    def with_options options
      with_config(config.with(options))
    end

    # @param [Configuration] The configuration object to use.
    # @return [BaseClient] Returns a new client object with the given
    #   configuration.
    def with_config config
      self.class.new(:config => config)
    end

    # The stub returned is memoized.
    # @see new_stub_for
    # @private
    def stub_for method_name
      @stubs[method_name] ||= new_stub_for(method_name)
    end

    # Primarily used for testing, this method returns an empty psuedo 
    # service response without making a request.  Its used primarily for
    # testing the ligher level service interfaces.
    # @private
    def new_stub_for method_name
      response = Response.new(Http::Request.new, Http::Response.new)
      response.request_type = method_name
      response.request_options = {}
      send("simulate_#{method_name}_response", response)
      response.signal_success
      response
    end

    protected
    def new_request
      req = self.class::REQUEST_CLASS.new
      req.http_method = 'POST'
      req.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
      req.add_param 'Timestamp', Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
      req.add_param 'Version', self.class::API_VERSION
      req
    end

    protected
    def new_response(*args, &block)
      Response.new(*args, &block)
    end

    private
    def make_async_request response

      pauses = async_request_with_retries(response, response.http_request)

      response

    end

    private
    def async_request_with_retries response, http_request, retry_delays = nil

      response.http_response = AWS::Http::Response.new

      handle = Object.new
      handle.extend AsyncHandle
      handle.on_complete do |status|
        case status
        when :failure
          response.error = StandardError.new("failed to contact the service")
          response.signal_failure
        when :success
          populate_error(response)
          retry_delays ||= sleep_durations(response)
          if should_retry?(response) and !retry_delays.empty?
            response.rebuild_request
            @http_handler.sleep_with_callback(retry_delays.shift) do
              async_request_with_retries(response, response.http_request, retry_delays)
            end
          else
            response.error ?
              response.signal_failure :
              response.signal_success
          end
        end
      end

      @http_handler.handle_async(http_request, response.http_response, handle)

    end

    private
    def make_sync_request response
      retry_server_errors do

        response.http_response = http_response =
          AWS::Http::Response.new

        @http_handler.handle(response.http_request, http_response)

        populate_error(response)
        response.signal_success unless response.error
        response

      end
    end

    private
    def retry_server_errors &block

      response = yield

      sleeps = sleep_durations(response)
      while should_retry?(response)
        break if sleeps.empty?
        Kernel.sleep(sleeps.shift)

        # rebuild the request to get a fresh signature
        response.rebuild_request
        response = yield
      end

      response

    end

    private
    def sleep_durations response
      factor = scaling_factor(response)
      Array.new(config.max_retries) {|n| (2 ** n) * factor }
    end

    private
    def scaling_factor response
      response.throttled? ? (0.5 + Kernel.rand * 0.1) : 0.3
    end

    private
    def should_retry? response
      response.timeout? or
        response.throttled? or
        response.error.kind_of?(Errors::ServerError)
    end

    private
    def return_or_raise options, &block
      response = yield
      unless options[:async]
        raise response.error if response.error
      end
      response
    end

    protected
    def populate_error response

      # clear out a previous error
      response.error = nil
      status = response.http_response.status
      code = nil
      code = xml_error_grammar.parse(response.http_response.body).code if
        xml_error_response?(response)
      

      case
      when response.timeout?
        response.error = Timeout::Error.new

      when code
        response.error =
          service_module::Errors.error_class(code).new(response.http_request,
                                                       response.http_response)
      when status >= 500
        response.error =
          Errors::ServerError.new(response.http_request, response.http_response)

      when status >= 300
        response.error =
          Errors::ClientError.new(response.http_request, response.http_response)
      end

    end

    protected
    def xml_error_response? response
      response.http_response.status >= 300 and
        response.http_response.body and
        xml_error_grammar.parse(response.http_response.body).respond_to?(:code)
    end

    protected
    def xml_error_grammar
      if service_module::const_defined?(:Errors) and
          service_module::Errors::const_defined?(:BASE_ERROR_GRAMMAR)
        service_module::Errors::BASE_ERROR_GRAMMAR
      else
        XmlGrammar
      end
    end

    protected
    def service_module
      AWS.const_get(self.class.to_s[/(\w+)::Client/, 1])
    end

    private
    def client_request name, options, &block
      return_or_raise(options) do
        log_client_request(name, options) do

          if config.stub_requests?

            response = stub_for(name)
            response.http_request = build_request(name, options, &block)
            response.request_options = options
            response

          else

            client = self
            response = new_response { client.send(:build_request, name, options, &block) }
            response.request_type = name
            response.request_options = options

            if self.class::CACHEABLE_REQUESTS.include?(name) and
                cache = AWS.response_cache and
                cached_response = cache.cached(response)
              cached_response.cached = true
              cached_response
            else
              # process the http request
              options[:async] ?
              make_async_request(response) :
                make_sync_request(response)

              # process the http response
              response.on_success do
                send("process_#{name}_response", response)
                if cache = AWS.response_cache
                  cache.add(response)
                end
              end

              response

            end

          end

        end
      end
    end

    private
    def build_request(name, options, &block)
      # we dont want to pass the async option to the configure block
      opts = options.dup
      opts.delete(:async)

      http_request = new_request

      # configure the http request
      http_request.host = endpoint
      http_request.proxy_uri = config.proxy_uri
      http_request.use_ssl = config.use_ssl?
      http_request.ssl_verify_peer = config.ssl_verify_peer?
      http_request.ssl_ca_file = config.ssl_ca_file

      send("configure_#{name}_request", http_request, opts, &block)
      http_request.headers["user-agent"] = user_agent_string
      http_request.add_authorization!(signer)
      http_request
    end

    private
    def user_agent_string
      engine = (RUBY_ENGINE rescue nil or "ruby")
      user_agent = "%s aws-sdk-ruby/#{VERSION} %s/%s %s" %
        [config.user_agent_prefix, engine, RUBY_VERSION, RUBY_PLATFORM]
      user_agent.strip!
      if AWS.memoizing?
        user_agent << " memoizing"
      end
      user_agent
    end

    private
    def self.add_client_request_method method_name, options = {}, &block

      method = ClientRequestMethodBuilder.new(self, method_name, &block)

      if xml_grammar = options[:xml_grammar]

        method.process_response do |resp|
          xml_grammar.parse(resp.http_response.body, :context => resp)
          super(resp)
        end

        method.simulate_response do |resp|
          xml_grammar.simulate(resp)
          super(resp)
        end

      end

      module_eval <<-END
        def #{method_name}(*args, &block)
          options = args.first ? args.first : {}
          client_request(#{method_name.inspect}, options, &block)
        end
      END

    end

    protected
    def self.configure_client

      module_eval('module Options; end')
      module_eval('module XML; end')

      name = :"#{service_ruby_name}_client"
      needs = [:signer, :http_handler, "#{service_ruby_name}_endpoint"]
      create_block = lambda {|config| new(:config => config) }

      Configuration.add_option_with_needs(name, needs, &create_block)

    end

    # @private
    class ClientRequestMethodBuilder

      def initialize client_class, method_name, &block
        @client_class = client_class
        @method_name = method_name
        configure_request {|request, options|}
        process_response {|response|}
        simulate_response {|response|}
        instance_eval(&block)
      end

      def configure_request options = {}, &block
        name = "configure_#{@method_name}_request"
        MetaUtils.class_extend_method(@client_class, name, &block)

        if block.arity == 3
          m = Module.new
          m.module_eval(<<-END)
            def #{name}(req, options, &block)
              super(req, options, block)
            end
          END
          @client_class.send(:include, m)
        end
      end

      def process_response &block
        name = "process_#{@method_name}_response"
        MetaUtils.class_extend_method(@client_class, name, &block)
      end

      def simulate_response &block
        name = "simulate_#{@method_name}_response"
        MetaUtils.class_extend_method(@client_class, name, &block)
      end

    end

  end
end