# encoding: utf-8

# Ruby.
require 'pp'
require 'log4r'
require 'monitor'

# Java.  Note that paths are mangeled in jmauve_starter.
require 'java'
require 'smack.jar'
require 'smackx.jar'
include_class "org.jivesoftware.smack.XMPPConnection"
include_class "org.jivesoftware.smackx.muc.MultiUserChat"
include_class "org.jivesoftware.smack.RosterListener"

module Mauve

  module Notifiers    

    module Xmpp
      
      class XMPPSmackException < StandardError
      end

      ## Main wrapper to smack java library.
      #
      # @author Yann Golanski
      # @see http://www.igniterealtime.org/builds/smack/docs/3.1.0/javadoc/
      #
      # This is a singleton which is not idea but works well for mauve's 
      # configuration file set up. 
      #
      # In general, this class is meant to be intialized then the method 
      # create_slave_thread must be called.  The latter will spawn a new 
      # thread that will do the connecting and sending of messages to 
      # the XMPP server.  Once this is done, messages can be send via the 
      # send_msg() method.  Those will be queued and depending on the load,
      # should be send quickly enough.  This is done so that the main thread
      # can not worry about sending messages and can do important work. 
      #
      # @example
      #  bot = Mauve::Notifiers::Xmpp::XMPPSmack.new()
      #  bot.run_slave_thread("chat.bytemark.co.uk", 'mauvealert', 'TopSecret')
      #  msg = "What fresh hell is this? -- Dorothy Parker."
      #  bot.send_msg("yann@chat.bytemark.co.uk", msg)
      #  bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg)
      #
      # @FIXME  This won't quiet work with how mauve is set up. 
      #
      class XMPPSmack
        
        # Globals are evil.
        @@instance = nil

        # Default constructor.
        #
        # A queue (@queue) is used to pass information between master/slave.
        def initialize ()
          extend(MonitorMixin)
          @logger =  Log4r::Logger.new "mauve::XMPP_smack<#{Process.pid}>"
          @queue = Queue.new 
          @xmpp = nil
          @name = "mauve alert"
          @slave_thread = nil
          @regexp_muc = Regexp.compile(/^muc\:/)
          @regexp_tail = Regexp.compile(/\/.*$/)
          @jid_created_chat = Hash.new()
          @separator = '<->'
          @logger.info("Created XMPPSmack singleton")
        end

        # Returns the instance of the XMPPSmack singleton.
        #
        # @param [String] login The JID as a full address.
        # @param [String] pwd The password corresponding to the JID.
        # @return [XMPPSmack] The singleton instance.
        def self.instance (login, pwd)
          if true == @@instance.nil?
            @@instance = XMPPSmack.new
            jid, tmp = login.split(/@/)
            srv, name = tmp.split(/\//)
            name = "Mauve Alert Bot" if true == name.nil?
            @@instance.run_slave_thread(srv, jid, pwd, name)
            sleep 5 # FIXME: This really should be synced... But how?
          end
          return @@instance
        end

        # Create the thread that sends messages to the server.
        #
        # @param [String] srv The server address.
        # @param [String] jid The JID.
        # @param [String] pwd The password corresponding to the JID.
        # @param [String] name The bot name.
        # @return [NULL] nada
        def run_slave_thread (srv, jid, pwd, name)
          @srv = srv
          @jid = jid
          @pwd = pwd
          @name = name
          @logger.info("Creating slave thread on #{@jid}@#{@srv}/#{@name}.")
          @slave_thread = Thread.new do 
            self.create_slave_thread()
          end
          return nil
        end

        # Returns whether instance is connected and authenticated.
        #
        # @return [Boolean] True or false.
        def is_connected_and_authenticated? ()
          return false if true == @xmpp.nil?
          return (@xmpp.isConnected() and @xmpp.isAuthenticated())
        end

        # Creates the thread that does the actual sending to XMPP.
        # @return [NULL] nada
        def create_slave_thread ()
          begin
            @logger.info("Slave thread is now alive.")
            self.open()
            loop do
              rcp, msg = @queue.deq().split(@separator, 2)
              @logger.debug("New message for '#{rcp}' saying '#{msg}'.")
              if rcp.match(@regexp_muc)
                room = rcp.gsub(@regexp_muc, '').gsub(@regexp_tail, '')
                self.send_to_muc(room, msg)
              else
                self.send_to_jid(rcp, msg)
              end
            end
          rescue XMPPSmackException
            @logger.fatal("Something is wrong")
          ensure 
            @logger.info("XMPP bot disconnect.")
            @xmpp.disconnect()
          end
          return nil
        end

        # Send a message to the recipient.
        #
        # @param [String] rcp The recipent MUC or JID.
        # @param [String] msg The message.
        # @return [NULL] nada
        def send_msg(rcp, msg)
          #if @slave_thread.nil? or not self.is_connected_and_authenticated?()
          #  str = "There is either no slave thread running or a disconnect..."
          #  @logger.warn(str)
          #  self.reconnect()
          #end
          @queue.enq(rcp + @separator + msg)
          return nil
        end

        # Sends a message to a room.
        #
        # @param [String] room The name of the room.
        # @param [String] mgs The message to send.
        # @return [NULL] nada
        def send_to_muc (room, msg)
          if not @jid_created_chat.has_key?(room)
            @jid_created_chat[room] = MultiUserChat.new(@xmpp, room)
            @jid_created_chat[room].join(@name)
          end
          @logger.debug("Sending to MUC '#{room}' message '#{msg}'.")
          @jid_created_chat[room].sendMessage(msg)
          return nil
        end

        # Sends a message to a jid.
        #
        # Do not destroy the chat, we can reuse it when the user log back in again. 
        # Maybe?
        #
        # @param [String] jid The JID of the recipient.
        # @param [String] mgs The message to send.
        # @return [NULL] nada
        def send_to_jid (jid, msg)
          if true == jid_is_available?(jid)
            if not @jid_created_chat.has_key?(jid)
              @jid_created_chat[jid] = @xmpp.getChatManager.createChat(jid, nil)
            end
            @logger.debug("Sending to JID '#{jid}' message '#{msg}'.")
            @jid_created_chat[jid].sendMessage(msg)
          end
          return nil
        end

        # Check to see if the jid is available or not.
        #
        # @param [String] jid The JID of the recipient.
        # @return [Boolean] Whether we can send a message or not.
        def jid_is_available?(jid)
          if true == @xmpp.getRoster().getPresence(jid).isAvailable()
            @logger.debug("#{jid} is available. Status is " +
                          "#{@xmpp.getRoster().getPresence(jid).getStatus()}")
            return true
          else
            @logger.warn("#{jid} is not available. Status is " +
                         "#{@xmpp.getRoster().getPresence(jid).getStatus()}")
            return false
          end
        end

        # Opens a connection to the xmpp server at given port.
        #
        # @return [NULL] nada
        def open()
          @logger.info("XMPP bot is being created.")
          self.open_connection()
          self.open_authentication()
          self.create_roster()
          sleep 5
          return nil
        end

        # Connect to server.
        #
        # @return [NULL] nada
        def open_connection()
          @xmpp = XMPPConnection.new(@srv)
          if false == self.connect()
            str = "Connection refused"
            @logger.error(str)
            raise XMPPSmackException.new(str)
          end
          @logger.debug("XMPP bot connected successfully.")
          return nil
        end

        # Authenticat connection.
        #
        # @return [NULL] nada
        def open_authentication()
          if false == self.login(@jid, @pwd)
            str = "Authentication failed"
            @logger.error(str)
            raise XMPPSmackException.new(str)
          end
          @logger.debug("XMPP bot authenticated successfully.")
          return nil
        end

        # Create a new roster and listener.
        #
        # @return [NULL] nada
        def create_roster
          @xmpp.getRoster().addRosterListener(RosterListener.new())
          @xmpp.getRoster().reload()
          @xmpp.getRoster().getPresence(@xmpp.getUser).setStatus(
            "Purple alert! Purple alert!")
          @logger.debug("XMPP bot roster aquired successfully.")
          return nil
        end

        # Connects to the server.
        #
        # @return [Boolean] true (aka sucess) or false (aka failure).
        def connect ()
          @xmpp.connect()
          return @xmpp.isConnected()
        end
        
        # Login onto the server.
        #
        # @param [String] jid The JID.
        # @param [String] pwd The password corresponding to the JID.
        # @return [Boolean] true (aka sucess) or false (aka failure).
        def login (jid, pwd)
          @xmpp.login(jid, pwd, @name)
          return @xmpp.isAuthenticated()
        end

        # Reconnects in case of errors.
        #
        # @return [NULL] nada
        def reconnect()
          @xmpp.disconnect
          @slave_thread = Thread.new do 
            self.create_slave_thread()
          end
          return nil
        end

        def presenceChanged ()
        end

      end # XMPPSmack


      ## This is the class that gets called in person.rb. 
      #
      # This class is a wrapper to XMPPSmack which does the hard work. It is
      # done this way to conform to the mauve configuration file way of 
      # defining notifications.
      #
      # @author Yann Golanski
      class Default

        # Name of the class.
        attr_reader :name

        # Atrtribute.
        attr_accessor :jid

        # Atrtribute.
        attr_accessor :password

        # Atrtribute.
        attr_accessor :initial_jid

        # Atrtribute.
        attr_accessor :initial_messages
        
        # Default constructor.
        #
        # @param [String] name The name of the notifier.
        def initialize (name)
          extend(MonitorMixin)
          @name = name
          @logger = Log4r::Logger.new "mauve::XMPP_default<#{Process.pid}>"
        end

        # Sends a message to the relevant jid or muc.
        #
        # We have no way to know if a messages was recieved, only that 
        # we send it.
        # 
        # @param [String] destionation
        # @param [Alert] alert A mauve alert class
        # @param [Array] all_alerts subset of current alerts
        # @param [Hash] conditions Supported conditions, see above.
        # @return [Boolean] Whether a message can be send or not. 
        def send_alert(destination, alert, all_alerts, conditions = nil)
          synchronize { 
            client = XMPPSmack.instance(@jid, @password) 
            if not destination.match(/^muc:/)
              if false == client.jid_is_available?(destination.gsub(/^muc:/, ''))
                return false
              end
            end
            client.send_msg(destination, convert_alert_to_message(alert))
            return true
          }
        end

        # Takes an alert and converts it into a message.
        #
        # @param [Alert] alert The alert to convert.
        # @return [String] The message, either as HTML.
        def convert_alert_to_message(alert)
          arr = alert.summary_three_lines
          str = arr[0] + ": " + arr[1]
          str += " -- " + arr[2] if false == arr[2].nil?
          str += "."
          return str
          #return alert.summary_two_lines.join(" -- ")
          #return "<p>" + alert.summary_two_lines.join("<br />") + "</p>"
        end

        # This is so unit tests can run fine.
        include Debug

      end # Default

    end
  end
end

# This is a simple example of usage.  Run with:
#   ../../../jmauve_starter.rb xmpp-smack.rb 
# Clearly, the mauve jabber password is not correct.  
#
#   /!\ WARNING:   DO NOT COMMIT THE REAL PASSWORD TO MERCURIAL!!!
#
def send_msg()
  bot = Mauve::Notifiers::Xmpp::XMPPSmack.instance(
    "mauvealert@chat.bytemark.co.uk/testing1234", '')
  msg = "What fresh hell is this? -- Dorothy Parker."
  bot.send_msg("yann@chat.bytemark.co.uk", msg)
  bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg)
  sleep 2
end

if __FILE__ == './'+$0
  Thread.abort_on_exception = true
  logger = Log4r::Logger.new('mauve')
  logger.level = Log4r::DEBUG
  logger.add Log4r::Outputter.stdout
  send_msg()
  send_msg()
  logger.info("START")
  logger.info("END")
end