module Ethon
  class Easy

    # This module contains all the logic around the callbacks,
    # which are needed to interact with libcurl.
    #
    # @api private
    module Callbacks

      # :nodoc:
      def self.included(base)
        base.send(:attr_accessor, *[:response_body, :response_headers, :debug_info])
      end

      # Set writefunction and headerfunction callback.
      # They are called by libcurl in order to provide the header and
      # the body from the request.
      #
      # @example Set callbacks.
      #   easy.set_callbacks
      def set_callbacks
        Curl.set_option(:writefunction, body_write_callback, handle)
        Curl.set_option(:headerfunction, header_write_callback, handle)
        Curl.set_option(:debugfunction, debug_callback, handle)
        @response_body = ""
        @response_headers = ""
        @headers_called = false
        @debug_info = Ethon::Easy::DebugInfo.new
      end

      # Returns the body write callback.
      #
      # @example Return the callback.
      #   easy.body_write_callback
      #
      # @return [ Proc ] The callback.
      def body_write_callback
        @body_write_callback ||= proc do |stream, size, num, object|
          headers
          result = body(chunk = stream.read_string(size * num))
          @response_body << chunk if result == :unyielded
          result != :abort ? size * num : -1
        end
      end

      # Returns the header write callback.
      #
      # @example Return the callback.
      #   easy.header_write_callback
      #
      # @return [ Proc ] The callback.
      def header_write_callback
        @header_write_callback ||= proc {|stream, size, num, object|
          @response_headers << stream.read_string(size * num)
          size * num
        }
      end

      # Returns the debug callback. This callback is currently used
      # write the raw http request headers.
      #
      # @example Return the callback.
      #   easy.body_write_callback
      #
      # @return [ Proc ] The callback.
      def debug_callback
        @debug_callback ||= proc {|handle, type, data, size, udata|
          message = data.read_string(size)
          @debug_info.add type, message
          print message unless [:data_in, :data_out].include?(type)
          0
        }
      end

      # Set the read callback. This callback is used by libcurl to
      # read data when performing a PUT request.
      #
      # @example Set the callback.
      #   easy.set_read_callback("a=1")
      #
      # @param [ String ] body The body.
      def set_read_callback(body)
        @request_body_read = 0
        readfunction do |stream, size, num, object|
          size = size * num
          body_size = if body.respond_to?(:bytesize)
            body.bytesize
          elsif body.respond_to?(:size)
            body.size
          elsif body.is_a?(File)
            File.size(body.path)
          end

          left = body_size - @request_body_read
          size = left if size > left

          if size > 0
            chunk = if body.respond_to?(:byteslice)
              body.byteslice(@request_body_read, size)
            elsif body.respond_to?(:read)
              body.read(size)
            else
              body[@request_body_read, size]
            end

            stream.write_string(
              chunk, size
            )
            @request_body_read += size
          end
          size
        end
      end

      # Returns the body read callback.
      #
      # @example Return the callback.
      #   easy.read_callback
      #
      # @return [ Proc ] The callback.
      def read_callback
        @read_callback
      end
    end
  end
end
