# =XMPP4R - XMPP Library for Ruby
# License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
# Website::http://home.gna.org/xmpp4r/

require 'resolv'
require 'xmpp4r/connection'
require 'xmpp4r/sasl'

module Jabber

  # The client class provides everything needed to build a basic XMPP
  # Client.
  #
  # If you want your connection to survive disconnects and timeouts,
  # catch exception in Stream#on_exception and re-call Client#connect
  # and Client#auth. Don't forget to re-send initial Presence and
  # everything else you need to setup your session.
  class Client  < Connection

    # The client's JID
    attr_reader :jid

    ##
    # Create a new Client.
    #
    # Remember to *always* put a resource in your JID unless the server can do SASL.
    def initialize(jid)
      super()
      @jid = (jid.kind_of?(JID) ? jid : JID.new(jid.to_s))
    end

    ##
    # connect to the server
    # (chaining-friendly)
    #
    # If you omit the optional host argument SRV records for your jid will
    # be resolved. If none works, fallback is connecting to the domain part
    # of the jid.
    # host:: [String] Optional c2s host, will be extracted from jid if nil
    # use_ssl:: [Boolean] Optional. Use (old, deprecated) SSL when connecting.
    # return:: self
    def connect(host = nil, port = 5222)
      if host.nil?
        begin
          srv = []
          Resolv::DNS.open { |dns|
            # If ruby version is too old and SRV is unknown, this will raise a NameError
            # which is caught below
            Jabber::debuglog("RESOLVING:\n_xmpp-client._tcp.#{@jid.domain} (SRV)")
            srv = dns.getresources("_xmpp-client._tcp.#{@jid.domain}", Resolv::DNS::Resource::IN::SRV)
          }
          # Sort SRV records: lowest priority first, highest weight first
          srv.sort! { |a,b| (a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight) }

          srv.each { |record|
            begin
              connect(record.target.to_s, record.port)
              # Success
              return self
            rescue SocketError, Errno::ECONNREFUSED
              # Try next SRV record
            end
          }
        rescue NameError
          Jabber::debuglog "Resolv::DNS does not support SRV records. Please upgrade to ruby-1.8.3 or later!"
        end
        # Fallback to normal connect method
      end

      super(host.nil? ? jid.domain : host, port)
      self
    end

    ##
    # Close the connection,
    # sends <tt></stream:stream></tt> tag first
    def close
      if @status == CONNECTED
        send("</stream:stream>")
      end
      super
    end

    ##
    # Start the stream-parser and send the client-specific stream opening element
    def start
      super
      send(generate_stream_start(@jid.domain)) { |e|
        if e.name == 'stream'
          true
        else
          false
        end
      }
    end

    ##
    # Authenticate with the server
    #
    # Throws ClientAuthenticationFailure
    #
    # Authentication mechanisms are used in the following preference:
    # * SASL DIGEST-MD5
    # * SASL PLAIN
    # * Non-SASL digest
    # password:: [String]
    def auth(password)
      begin
        if @stream_mechanisms.include? 'DIGEST-MD5'
          auth_sasl SASL.new(self, 'DIGEST-MD5'), password
        elsif @stream_mechanisms.include? 'PLAIN'
          auth_sasl SASL.new(self, 'PLAIN'), password
        else
          auth_nonsasl(password)
        end
      rescue
        Jabber::debuglog("#{$!.class}: #{$!}\n#{$!.backtrace.join("\n")}")
        raise ClientAuthenticationFailure.new, $!.to_s
      end
    end

    ##
    # Resource binding (RFC3920bis-06 - section 8.)
    #
    # XMPP allows to bind to multiple resources
    def bind(desired_resource=nil)
      iq = Iq.new(:set)
      bind = iq.add REXML::Element.new('bind')
      bind.add_namespace @stream_features['bind']
      if desired_resource
        resource = bind.add REXML::Element.new('resource')
        resource.text = desired_resource
      end

      jid = nil
      send_with_id(iq) do |reply|
        reply_bind = reply.first_element('bind')
        if reply_bind
          reported_jid = reply_bind.first_element('jid')
          if reported_jid and reported_jid.text
            jid = JID.new(reported_jid.text)
          end
        end
      end
      jid
    end

    ##
    # Resource unbinding (RFC3920bis-06 - section 8.6.3.)
    def unbind(desired_resource)
      iq = Iq.new(:set)
      unbind = iq.add REXML::Element.new('unbind')
      unbind.add_namespace @stream_features['unbind']
      resource = unbind.add REXML::Element.new('resource')
      resource.text = desired_resource

      send_with_id(iq)
    end

    ##
    # Use a SASL authentication mechanism and bind to a resource
    #
    # If there was no resource given in the jid, the jid/resource
    # generated by the server will be accepted.
    #
    # This method should not be used directly. Instead, Client#auth
    # may look for the best mechanism suitable.
    # sasl:: Descendant of [Jabber::SASL::Base]
    # password:: [String]
    def auth_sasl(sasl, password)
      sasl.auth(password)

      # Restart stream after SASL auth
      stop
      start
      # And wait for features - again
      @features_sem.wait

      # Resource binding (RFC3920 - 7)
      if @stream_features.has_key? 'bind'
        @jid = bind(@jid.resource)
      end

      # Session starting
      if @stream_features.has_key? 'session'
        iq = Iq.new(:set)
        session = iq.add REXML::Element.new('session')
        session.add_namespace @stream_features['session']

        send_with_id(iq)
      end
    end

    ##
    # See Client#auth_anonymous_sasl
    def auth_anonymous
      auth_anonymous_sasl
    end


    ##
    # Shortcut for anonymous connection to server
    #
    # Throws ClientAuthenticationFailure
    def auth_anonymous_sasl
      if self.supports_anonymous?
        begin
          auth_sasl SASL.new(self, 'ANONYMOUS'), ""
        rescue
          Jabber::debuglog("#{$!.class}: #{$!}\n#{$!.backtrace.join("\n")}")
          raise ClientAuthenticationFailure, $!.to_s
        end
      else
        raise ClientAuthenticationFailure, 'Anonymous authentication unsupported'
      end
    end

    ##
    # Reports whether or not anonymous authentication is reported
    # by the client.
    #
    # Returns true or false
    def supports_anonymous?
      @stream_mechanisms.include? 'ANONYMOUS'
    end

    ##
    # Send auth with given password and wait for result
    # (non-SASL)
    #
    # Throws ServerError
    # password:: [String] the password
    # digest:: [Boolean] use Digest authentication
    def auth_nonsasl(password, digest=true)
      authset = nil
      if digest
        authset = Iq.new_authset_digest(@jid, @streamid.to_s, password)
      else
        authset = Iq.new_authset(@jid, password)
      end
      send_with_id(authset)
      $defout.flush

      true
    end

    ##
    # Get instructions and available fields for registration
    # return:: [instructions, fields] Where instructions is a String and fields is an Array of Strings
    def register_info
      instructions = nil
      fields = []

      reg = Iq.new_registerget
      reg.to = jid.domain
      send_with_id(reg) do |answer|
        if answer.query
          answer.query.each_element { |e|
            if e.namespace == 'jabber:iq:register'
              if e.name == 'instructions'
                instructions = e.text.strip
              else
                fields << e.name
              end
            end
          }
        end

        true
      end

      [instructions, fields]
    end

    ##
    # Register a new user account
    # (may be used instead of Client#auth)
    #
    # This method may raise ServerError if the registration was
    # not successful.
    #
    # password:: String
    # fields:: {String=>String} additional registration information
    #
    # XEP-0077 Defines the following fields for registration information:
    # http://www.xmpp.org/extensions/xep-0077.html
    #
    # 'username'    => 'Account name associated with the user'
    # 'nick'        => 'Familiar name of the user'
    # 'password'    => 'Password or secret for the user'
    # 'name'        => 'Full name of the user'
    # 'first'       => 'First name or given name of the user'
    # 'last'        => 'Last name, surname, or family name of the user'
    # 'email'       => 'Email address of the user'
    # 'address'     => 'Street portion of a physical or mailing address'
    # 'city'        => 'Locality portion of a physical or mailing address'
    # 'state'       => 'Region portion of a physical or mailing address'
    # 'zip'         => 'Postal code portion of a physical or mailing address'
    # 'phone'       => 'Telephone number of the user'
    # 'url'         => 'URL to web page describing the user'
    # 'date'        => 'Some date (e.g., birth date, hire date, sign-up date)'
    #
    def register(password, fields={})
      reg = Iq.new_register(jid.node, password)
      reg.to = jid.domain
      fields.each { |name,value|
        reg.query.add(REXML::Element.new(name)).text = value
      }

      send_with_id(reg)
    end

    ##
    # Remove the registration of a user account
    #
    # *WARNING:* this deletes your roster and everything else
    # stored on the server!
    def remove_registration
      reg = Iq.new_register
      reg.to = jid.domain
      reg.query.add(REXML::Element.new('remove'))
      send_with_id(reg)
    end

    ##
    # Change the client's password
    #
    # Threading is suggested, as this code waits
    # for an answer.
    #
    # Raises an exception upon error response (ServerError from
    # Stream#send_with_id).
    # new_password:: [String] New password
    def password=(new_password)
      iq = Iq.new_query(:set, @jid.domain)
      iq.query.add_namespace('jabber:iq:register')
      iq.query.add(REXML::Element.new('username')).text = @jid.node
      iq.query.add(REXML::Element.new('password')).text = new_password

      err = nil
      send_with_id(iq)
    end
  end
end
