#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage::  http://rubyeventmachine.com
# Date:: 15 November 2006
# 
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) the GNU General Public License
# as published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version; or 2) Ruby's License.
# 
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
# 
# 

module EventMachine
  module Protocols

    # Implements Stomp (http://docs.codehaus.org/display/STOMP/Protocol).
    #
    # == Usage example
    #
    #  module StompClient
    #    include EM::Protocols::Stomp
    #
    #    def connection_completed
    #      connect :login => 'guest', :passcode => 'guest'
    #    end
    #
    #    def receive_msg msg
    #      if msg.command == "CONNECTED"
    #        subscribe '/some/topic'
    #      else
    #        p ['got a message', msg]
    #        puts msg.body
    #      end
    #    end
    #  end
    #
    #  EM.run{
    #    EM.connect 'localhost', 61613, StompClient
    #  }
    #
    module Stomp
      include LineText2

      class Message
        # The command associated with the message, usually 'CONNECTED' or 'MESSAGE'
        attr_accessor :command
        # Hash containing headers such as destination and message-id
        attr_accessor :header
        alias :headers :header
        # Body of the message
        attr_accessor :body

        # @private
        def initialize
          @header = {}
          @state = :precommand
          @content_length = nil
        end
        # @private
        def consume_line line
          if @state == :precommand
            unless line =~ /\A\s*\Z/
              @command = line
              @state = :headers
            end
          elsif @state == :headers
            if line == ""
              if @content_length
                yield( [:sized_text, @content_length+1] )
              else
                @state = :body
                yield( [:unsized_text] )
              end
            elsif line =~ /\A([^:]+):(.+)\Z/
              k = $1.dup.strip
              v = $2.dup.strip
              @header[k] = v
              if k == "content-length"
                @content_length = v.to_i
              end
            else
              # This is a protocol error. How to signal it?
            end
          elsif @state == :body
            @body = line
            yield( [:dispatch] )
          end
        end
      end

      # @private
      def send_frame verb, headers={}, body=""
        body = body.to_s
        ary = [verb, "\n"]
        body_bytesize = body.bytesize if body.respond_to? :bytesize
        body_bytesize ||= body.size
        headers.each {|k,v| ary << "#{k}:#{v}\n" }
        ary << "content-length: #{body_bytesize}\n"
        ary << "content-type: text/plain; charset=UTF-8\n" unless headers.has_key? 'content-type'
        ary << "\n"
        ary << body
        ary << "\0"
        send_data ary.join
      end

      # @private
      def receive_line line
        @stomp_initialized || init_message_reader
        @stomp_message.consume_line(line) {|outcome|
          if outcome.first == :sized_text
            set_text_mode outcome[1]
          elsif outcome.first == :unsized_text
            set_delimiter "\0"
          elsif outcome.first == :dispatch
            receive_msg(@stomp_message) if respond_to?(:receive_msg)
            init_message_reader
          end
        }
      end

      # @private
      def receive_binary_data data
        @stomp_message.body = data[0..-2]
        receive_msg(@stomp_message) if respond_to?(:receive_msg)
        init_message_reader
      end

      # @private
      def init_message_reader
        @stomp_initialized = true
        set_delimiter "\n"
        set_line_mode
        @stomp_message = Message.new
      end

      # Invoked with an incoming Stomp::Message received from the STOMP server
      def receive_msg msg
        # stub, overwrite this in your handler
      end

      # CONNECT command, for authentication
      #
      #  connect :login => 'guest', :passcode => 'guest'
      #
      def connect parms={}
        send_frame "CONNECT", parms
      end

      # SEND command, for publishing messages to a topic
      #
      #  send '/topic/name', 'some message here'
      #
      def send destination, body, parms={}
        send_frame "SEND", parms.merge( :destination=>destination ), body.to_s
      end

      # SUBSCRIBE command, for subscribing to topics
      #
      #  subscribe '/topic/name', false
      #
      def subscribe dest, ack=false
        send_frame "SUBSCRIBE", {:destination=>dest, :ack=>(ack ? "client" : "auto")}
      end

      # ACK command, for acknowledging receipt of messages
      #
      #  module StompClient
      #    include EM::P::Stomp
      #
      #    def connection_completed
      #      connect :login => 'guest', :passcode => 'guest'
      #      # subscribe with ack mode
      #      subscribe '/some/topic', true
      #    end
      #
      #    def receive_msg msg
      #      if msg.command == "MESSAGE"
      #        ack msg.headers['message-id']
      #        puts msg.body
      #      end
      #    end
      #  end
      #
      def ack msgid
        send_frame "ACK", 'message-id'=> msgid
      end

    end
  end
end

