require 'http/headers'
require 'openssl'
require 'socket'

module HTTP
  class Options
    # How to format the response [:object, :body, :parse_body]
    attr_accessor :response

    # HTTP headers to include in the request
    attr_accessor :headers

    # Query string params to add to the url
    attr_accessor :params

    # Form data to embed in the request
    attr_accessor :form

    # JSON data to embed in the request
    attr_accessor :json

    # Explicit request body of the request
    attr_accessor :body

    # HTTP proxy to route request
    attr_accessor :proxy

    # Socket classes
    attr_accessor :socket_class, :ssl_socket_class

    # SSL context
    attr_accessor :ssl_context

    # Follow redirects
    attr_accessor :follow

    protected :response=, :headers=, :proxy=, :params=, :form=, :json=, :follow=

    @default_socket_class     = TCPSocket
    @default_ssl_socket_class = OpenSSL::SSL::SSLSocket

    class << self
      attr_accessor :default_socket_class, :default_ssl_socket_class

      def new(options = {})
        return options if options.is_a?(self)
        super
      end
    end

    def initialize(options = {})
      @response  = options[:response]  || :auto
      @proxy     = options[:proxy]     || {}
      @body      = options[:body]
      @params    = options[:params]
      @form      = options[:form]
      @json      = options[:json]
      @follow    = options[:follow]

      @headers   = HTTP::Headers.coerce(options[:headers] || {})

      @socket_class     = options[:socket_class]     || self.class.default_socket_class
      @ssl_socket_class = options[:ssl_socket_class] || self.class.default_ssl_socket_class
      @ssl_context      = options[:ssl_context]
    end

    def with_headers(headers)
      dup do |opts|
        opts.headers = self.headers.merge(headers)
      end
    end

    %w(proxy params form json body follow).each do |method_name|
      class_eval <<-RUBY, __FILE__, __LINE__
        def with_#{method_name}(value)
          dup { |opts| opts.#{method_name} = value }
        end
      RUBY
    end

    def [](option)
      send(option) rescue nil
    end

    def merge(other)
      h1, h2 = to_hash, other.to_hash
      merged = h1.merge(h2) do |k, v1, v2|
        case k
        when :headers
          v1.merge(v2)
        else
          v2
        end
      end

      self.class.new(merged)
    end

    def to_hash
      # FIXME: hardcoding these fields blows! We should have a declarative
      # way of specifying all the options fields, and ensure they *all*
      # get serialized here, rather than manually having to add them each time
      {
        :response         => response,
        :headers          => headers.to_h,
        :proxy            => proxy,
        :params           => params,
        :form             => form,
        :json             => json,
        :body             => body,
        :follow           => follow,
        :socket_class     => socket_class,
        :ssl_socket_class => ssl_socket_class,
        :ssl_context      => ssl_context
      }
    end

    def dup
      dupped = super
      yield(dupped) if block_given?
      dupped
    end

  private

    def argument_error!(message)
      fail(Error, message, caller[1..-1])
    end
  end
end
