require 'ffi/http/parser/parser'
require 'ffi/http/parser/settings'

require 'ffi'

module FFI
  module HTTP
    module Parser
      class Instance < FFI::Struct

        layout :type_flags,   :uchar,
               :state,        :uchar,
               :header_state, :uchar,
               :index,        :uchar,

               :nread,          :uint32,
               :content_length, :int64,

               # READ-ONLY
               :http_major,  :ushort,
               :http_minor,  :ushort,
               :status_code, :ushort, # responses only
               :method,      :uchar,  # requests only

               # 1 = Upgrade header was present and the parser has exited because of that.
               # 0 = No upgrade header present.
               #
               # Should be checked when http_parser_execute() returns in addition to
               # error checking.
               :upgrade,     :char,

               # PUBLIC
               :data, :pointer

        # The parser type (`:request`, `:response` or `:both`)
        attr_accessor :type

        #
        # Initializes the Parser instance.
        #
        # @param [FFI::Pointer] ptr
        #   Optional pointer to an existing `http_parser` struct.
        #
        def initialize(ptr=nil)
          if ptr then super(ptr)
          else        super()
          end

          @settings = Settings.new

          yield self if block_given?

          Parser.http_parser_init(self,type) unless ptr
        end

        #
        # Registers an `on_message_begin` callback.
        #
        # @yield []
        #   The given block will be called when the HTTP message begins.
        #
        def on_message_begin(&block)
          @settings[:on_message_begin] = wrap_callback(block)
        end

        #
        # Registers an `on_path` callback.
        #
        # @yield [path]
        #   The given block will be called when the path is recognized within
        #   the Request URI.
        #
        # @yieldparam [String] path
        #   The recognized URI path.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
        #
        def on_path(&block)
          @settings[:on_path] = wrap_data_callback(block)
        end

        #
        # Registers an `on_query_string` callback.
        #
        # @yield [query]
        #   The given block will be called when the query-string is recognized
        #   within the Request URI.
        #
        # @yieldparam [String] query
        #   The recognized URI query-string.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
        #
        def on_query_string(&block)
          @settings[:on_query_string] = wrap_data_callback(block)
        end

        #
        # Registers an `on_fragment` callback.
        #
        # @yield [fragment]
        #   The given block will be called when the fragment is recognized
        #   within the Request URI.
        #
        # @yieldparam [String] fragment
        #   The recognized URI fragment.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
        #
        def on_fragment(&block)
          @settings[:on_fragment] = wrap_data_callback(block)
        end

        #
        # Registers an `on_url` callback.
        #
        # @yield [url]
        #   The given block will be called when the Request URI is recognized
        #   within the Request-Line.
        #
        # @yieldparam [String] url
        #   The recognized Request URI.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
        #
        def on_url(&block)
          @settings[:on_url] = wrap_data_callback(block)
        end

        #
        # Registers an `on_header_field` callback.
        #
        # @yield [field]
        #   The given block will be called when a Header name is recognized
        #   in the Headers.
        #
        # @yieldparam [String] field
        #   A recognized Header name.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.5
        #
        def on_header_field(&block)
          @settings[:on_header_field] = wrap_data_callback(block)
        end

        #
        # Registers an `on_header_value` callback.
        #
        # @yield [value]
        #   The given block will be called when a Header value is recognized
        #   in the Headers.
        #
        # @yieldparam [String] value
        #   A recognized Header value.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.5
        #
        def on_header_value(&block)
          @settings[:on_header_value] = wrap_data_callback(block)
        end

        #
        # Registers an `on_headers_complete` callback.
        #
        # @yield []
        #   The given block will be called when the Headers stop.
        #
        def on_headers_complete(&block)
          @settings[:on_headers_complete] = proc { |parser|
            case block.call()
            when :error then -1
            when :stop  then  1
            else              0
            end
          }
        end

        #
        # Registers an `on_body` callback.
        #
        # @yield [body]
        #   The given block will be called when the body is recognized in the
        #   message body.
        #
        # @yieldparam [String] body
        #   The full body or a chunk of the body from a chunked
        #   Transfer-Encoded stream.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.5
        #
        def on_body(&block)
          @settings[:on_body] = wrap_data_callback(block)
        end

        #
        # Registers an `on_message_begin` callback.
        #
        # @yield []
        #   The given block will be called when the message completes.
        #
        def on_message_complete(&block)
          @settings[:on_message_complete] = wrap_callback(block)
        end

        #
        # Parses data.
        #
        # @param [String] data
        #   The data to parse.
        #
        # @return [Integer]
        #   The number of bytes parsed. `0` will be returned if the parser
        #   encountered an error.
        #
        def parse(data)
          Parser.http_parser_execute(self,@settings,data,data.length)
        end

        #
        # Parses data.
        #
        # @param [String] data
        #   The data to parse.
        #
        # @return [Instance]
        #   The Instance parser.
        #
        def <<(data)
          parse(data)
          return self
        end

        #
        # Resets the parser.
        #
        def reset!
          Parser.http_parser_init(self,type)
        end

        alias reset reset!

        #
        # The type of the parser.
        #
        # @return [:request, :response, :both]
        #   The parser type.
        #
        def type
          TYPES[self[:type_flags] & 0x3]
        end

        #
        # Sets the type of the parser.
        #
        # @param [:request, :response, :both] new_type
        #   The new parser type.
        #
        def type=(new_type)
          self[:type_flags] = ((flags << 2) | TYPES[new_type])
        end

        #
        # Flags for the parser.
        #
        # @return [Integer]
        #   Parser flags.
        #
        def flags
          (self[:type_flags] & 0xfc) >> 2
        end

        #
        # The parsed HTTP major version number.
        #
        # @return [Integer]
        #   The HTTP major version number.
        #
        def http_major
          self[:http_major]
        end

        #
        # The parsed HTTP minor version number.
        #
        # @return [Integer]
        #   The HTTP minor version number.
        #
        def http_minor
          self[:http_minor]
        end

        #
        # The parsed HTTP version.
        #
        # @return [String]
        #   The HTTP version.
        #
        def http_version
          "%d.%d" % [self[:http_major], self[:http_minor]]
        end

        #
        # The parsed HTTP response Status Code.
        #
        # @return [Integer]
        #   The HTTP Status Code.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
        #
        def http_status
          self[:status_code]
        end

        #
        # The parsed HTTP Method.
        #
        # @return [Symbol]
        #   The HTTP Method name.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1
        #
        def http_method
          METHODS[self[:method]]
        end

        #
        # Determines whether the `Upgrade` header has been parsed.
        #
        # @return [Boolean]
        #   Specifies whether the `Upgrade` header has been seen.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.42
        #
        def upgrade?
          self[:upgrade] == 1
        end

        #
        # Additional data attached to the parser.
        #
        # @return [FFI::Pointer]
        #   Pointer to the additional data.
        #
        def data
          self[:data]
        end

        #
        # Determines whether the `Connection: keep-alive` header has been
        # parsed.
        #
        # @return [Boolean]
        #   Specifies whether the Connection should be kept alive.
        #
        # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.10
        #
        def keep_alive?
          Parser.http_should_keep_alive(self) > 0
        end

        protected

        #
        # Wraps a callback, so if it returns `:error`, `-1` will be returned.
        # `0` will be returned by default.
        #
        # @param [Proc] callback
        #   The callback to wrap.
        #
        # @return [Proc]
        #   The wrapped callback.
        #
        def wrap_callback(callback)
          proc { |parser| (callback.call() == :error) ? -1 : 0 }
        end

        #
        # Wraps a data callback, so if it returns `:error`, `-1` will be
        # returned. `0` will be returned by default.
        #
        # @param [Proc] callback
        #   The callback to wrap.
        #
        # @return [Proc]
        #   The wrapped callback.
        #
        def wrap_data_callback(callback)
          proc { |parser,buffer,length|
            data = buffer.get_bytes(0,length)

            (callback.call(data) == :error) ? -1 : 0
          }
        end

      end
    end
  end
end