/*
  Copyright (c) 2004-2009 by Jakob Schroeter <js@camaya.net>
  This file is part of the gloox library. http://camaya.net/gloox

  This software is distributed under a license. The full license
  agreement can be found in the file LICENSE in this distribution.
  This software may not be copied, modified, sold or distributed
  other than expressed in the named license agreement.

  This software is distributed without any warranty.
*/

#include "client.h"
#include "capabilities.h"
#include "rostermanager.h"
#include "disco.h"
#include "error.h"
#include "logsink.h"
#include "nonsaslauth.h"
#include "prep.h"
#include "stanzaextensionfactory.h"
#include "stanzaextension.h"
#include "tag.h"
#include "tlsbase.h"
#include "util.h"

#if !defined( _WIN32 ) && !defined( _WIN32_WCE )
# include <unistd.h>
#endif

#include <cstdio>

namespace gloox
{

  // ---- Client::ResourceBind ----
  Client::ResourceBind::ResourceBind( const std::string& resource, bool bind )
    : StanzaExtension( ExtResourceBind ), m_jid( JID() ), m_bind( bind )
  {
    prep::resourceprep( resource, m_resource );
    m_valid = true;
  }

  Client::ResourceBind::ResourceBind( const Tag* tag )
    : StanzaExtension( ExtResourceBind ), m_resource( EmptyString ), m_bind( true )
  {
    if( !tag )
      return;

    if( tag->name() == "unbind" )
      m_bind = false;
    else if( tag->name() == "bind" )
      m_bind = true;
    else
      return;

    if( tag->hasChild( "jid" ) )
      m_jid.setJID( tag->findChild( "jid" )->cdata() );
    else if( tag->hasChild( "resource" ) )
      m_resource = tag->findChild( "resource" )->cdata();

    m_valid = true;
  }

  Client::ResourceBind::~ResourceBind()
  {
  }

  const std::string& Client::ResourceBind::filterString() const
  {
    static const std::string filter = "/iq/bind[@xmlns='" + XMLNS_STREAM_BIND + "']"
        "|/iq/unbind[@xmlns='" + XMLNS_STREAM_BIND + "']";
    return filter;
  }

  Tag* Client::ResourceBind::tag() const
  {
    if( !m_valid )
      return 0;

    Tag* t = new Tag( m_bind ? "bind" : "unbind" );
    t->setXmlns( XMLNS_STREAM_BIND );

    if( m_bind && m_resource.empty() && m_jid )
      new Tag( t, "jid", m_jid.full() );
    else
      new Tag( t, "resource", m_resource );

    return t;
  }
  // ---- ~Client::ResourceBind ----

  // ---- Client::SessionCreation ----
  Tag* Client::SessionCreation::tag() const
  {
    Tag* t = new Tag( "session" );
    t->setXmlns( XMLNS_STREAM_SESSION );
    return t;
  }
  // ---- Client::SessionCreation ----

  // ---- Client ----
  Client::Client( const std::string& server )
    : ClientBase( XMLNS_CLIENT, server ),
      m_rosterManager( 0 ), m_auth( 0 ),
      m_presence( Presence::Available, JID() ), m_resourceBound( false ),
      m_forceNonSasl( false ), m_manageRoster( true ),
      m_streamFeatures( 0 )
  {
    m_jid.setServer( server );
    init();
  }

  Client::Client( const JID& jid, const std::string& password, int port )
    : ClientBase( XMLNS_CLIENT, password, EmptyString, port ),
      m_rosterManager( 0 ), m_auth( 0 ),
      m_presence( Presence::Available, JID() ), m_resourceBound( false ),
      m_forceNonSasl( false ), m_manageRoster( true ),
      m_streamFeatures( 0 )
  {
    m_jid = jid;
    m_server = m_jid.serverRaw();
    init();
  }

  Client::~Client()
  {
    delete m_rosterManager;
    delete m_auth;
  }

  void Client::init()
  {
    m_rosterManager = new RosterManager( this );
    m_disco->setIdentity( "client", "bot" );
    registerStanzaExtension( new ResourceBind( 0 ) );
    registerStanzaExtension( new Capabilities() );
    m_presenceExtensions.push_back( new Capabilities( m_disco ) );
  }

  void Client::setUsername( const std::string &username )
  {
    m_jid.setUsername( username );
  }

  bool Client::handleNormalNode( Tag* tag )
  {
    if( tag->name() == "features" && tag->xmlns() == XMLNS_STREAM )
    {
      m_streamFeatures = getStreamFeatures( tag );

      if( m_tls == TLSRequired && !m_encryptionActive
          && ( !m_encryption || !( m_streamFeatures & StreamFeatureStartTls ) ) )
      {
        logInstance().err( LogAreaClassClient, "Client is configured to require"
                                " TLS but either the server didn't offer TLS or"
                                " TLS support is not compiled in." );
        disconnect( ConnTlsNotAvailable );
      }
      else if( m_tls > TLSDisabled && m_encryption && !m_encryptionActive
          && ( m_streamFeatures & StreamFeatureStartTls ) )
      {
        notifyStreamEvent( StreamEventEncryption );
        startTls();
      }
      else if( m_compress && m_compression && !m_compressionActive
          && ( m_streamFeatures & StreamFeatureCompressZlib ) )
      {
        notifyStreamEvent( StreamEventCompression );
        logInstance().warn( LogAreaClassClient, "The server offers compression, but negotiating Compression at this stage is not recommended. See XEP-0170 for details. We'll continue anyway." );
        negotiateCompression( StreamFeatureCompressZlib );
      }
      else if( m_sasl )
      {
        if( m_authed )
        {
          if( m_streamFeatures & StreamFeatureBind )
          {
            notifyStreamEvent( StreamEventResourceBinding );
            bindResource( resource() );
          }
        }
        else if( !username().empty() && !password().empty() )
        {
          if( !login() )
          {
            logInstance().err( LogAreaClassClient, "The server doesn't support"
                                           " any auth mechanisms we know about" );
            disconnect( ConnNoSupportedAuth );
          }
        }
        else if( !m_clientCerts.empty() && !m_clientKey.empty()
                 && m_streamFeatures & SaslMechExternal && m_availableSaslMechs & SaslMechExternal )
        {
          notifyStreamEvent( StreamEventAuthentication );
          startSASL( SaslMechExternal );
        }
#if defined( _WIN32 ) && !defined( __SYMBIAN32__ )
        else if( m_streamFeatures & SaslMechGssapi && m_availableSaslMechs & SaslMechGssapi )
        {
          notifyStreamEvent( StreamEventAuthentication );
          startSASL( SaslMechGssapi );
        }
        else if( m_streamFeatures & SaslMechNTLM && m_availableSaslMechs & SaslMechNTLM )
        {
          notifyStreamEvent( StreamEventAuthentication );
          startSASL( SaslMechNTLM );
        }
#endif
        else if( m_streamFeatures & SaslMechAnonymous
                 && m_availableSaslMechs & SaslMechAnonymous )
        {
          notifyStreamEvent( StreamEventAuthentication );
          startSASL( SaslMechAnonymous );
        }
        else
        {
          notifyStreamEvent( StreamEventFinished );
          connected();
        }
      }
      else if( m_compress && m_compression && !m_compressionActive
          && ( m_streamFeatures & StreamFeatureCompressZlib ) )
      {
        notifyStreamEvent( StreamEventCompression );
        negotiateCompression( StreamFeatureCompressZlib );
      }
//       else if( ( m_streamFeatures & StreamFeatureCompressDclz )
//               && m_connection->initCompression( StreamFeatureCompressDclz ) )
//       {
//         negotiateCompression( StreamFeatureCompressDclz );
//       }
      else if( m_streamFeatures & StreamFeatureIqAuth )
      {
        notifyStreamEvent( StreamEventAuthentication );
        nonSaslLogin();
      }
      else
      {
        logInstance().err( LogAreaClassClient, "fallback: the server doesn't "
                                   "support any auth mechanisms we know about" );
        disconnect( ConnNoSupportedAuth );
      }
    }
    else
    {
      const std::string& name  = tag->name(),
                         xmlns = tag->findAttribute( XMLNS );
      if( name == "proceed" && xmlns == XMLNS_STREAM_TLS )
      {
        logInstance().dbg( LogAreaClassClient, "starting TLS handshake..." );

        if( m_encryption )
        {
          m_encryptionActive = true;
          m_encryption->handshake();
        }
      }
      else if( name == "failure" )
      {
        if( xmlns == XMLNS_STREAM_TLS )
        {
          logInstance().err( LogAreaClassClient, "TLS handshake failed (server-side)!" );
          disconnect( ConnTlsFailed );
        }
        else if( xmlns == XMLNS_COMPRESSION )
        {
          logInstance().err( LogAreaClassClient, "Stream compression init failed!" );
          disconnect( ConnCompressionFailed );
        }
        else if( xmlns == XMLNS_STREAM_SASL )
        {
          logInstance().err( LogAreaClassClient, "SASL authentication failed!" );
          processSASLError( tag );
          disconnect( ConnAuthenticationFailed );
        }
      }
      else if( name == "compressed" && xmlns == XMLNS_COMPRESSION )
      {
        logInstance().dbg( LogAreaClassClient, "Stream compression initialized" );
        m_compressionActive = true;
        header();
      }
      else if( name == "challenge" && xmlns == XMLNS_STREAM_SASL )
      {
        logInstance().dbg( LogAreaClassClient, "Processing SASL challenge" );
        processSASLChallenge( tag->cdata() );
      }
      else if( name == "success" && xmlns == XMLNS_STREAM_SASL )
      {
        logInstance().dbg( LogAreaClassClient, "SASL authentication successful" );
        processSASLSuccess();
        setAuthed( true );
        header();
      }
      else
        return false;
    }

    return true;
  }

  int Client::getStreamFeatures( Tag* tag )
  {
    if( tag->name() != "features" || tag->xmlns() != XMLNS_STREAM )
      return 0;

    int features = 0;

    if( tag->hasChild( "starttls", XMLNS, XMLNS_STREAM_TLS ) )
      features |= StreamFeatureStartTls;

    if( tag->hasChild( "mechanisms", XMLNS, XMLNS_STREAM_SASL ) )
      features |= getSaslMechs( tag->findChild( "mechanisms" ) );

    if( tag->hasChild( "bind", XMLNS, XMLNS_STREAM_BIND ) )
      features |= StreamFeatureBind;

    if( tag->hasChild( "unbind", XMLNS, XMLNS_STREAM_BIND ) )
      features |= StreamFeatureUnbind;

    if( tag->hasChild( "session", XMLNS, XMLNS_STREAM_SESSION ) )
      features |= StreamFeatureSession;

    if( tag->hasChild( "auth", XMLNS, XMLNS_STREAM_IQAUTH ) )
      features |= StreamFeatureIqAuth;

    if( tag->hasChild( "register", XMLNS, XMLNS_STREAM_IQREGISTER ) )
      features |= StreamFeatureIqRegister;

    if( tag->hasChild( "compression", XMLNS, XMLNS_STREAM_COMPRESS ) )
      features |= getCompressionMethods( tag->findChild( "compression" ) );

    if( features == 0 )
      features = StreamFeatureIqAuth;

    return features;
  }

  int Client::getSaslMechs( Tag* tag )
  {
    int mechs = SaslMechNone;

    const std::string mech = "mechanism";

    if( tag->hasChildWithCData( mech, "DIGEST-MD5" ) )
      mechs |= SaslMechDigestMd5;

    if( tag->hasChildWithCData( mech, "PLAIN" ) )
      mechs |= SaslMechPlain;

    if( tag->hasChildWithCData( mech, "ANONYMOUS" ) )
      mechs |= SaslMechAnonymous;

    if( tag->hasChildWithCData( mech, "EXTERNAL" ) )
      mechs |= SaslMechExternal;

    if( tag->hasChildWithCData( mech, "GSSAPI" ) )
      mechs |= SaslMechGssapi;

    if( tag->hasChildWithCData( mech, "NTLM" ) )
      mechs |= SaslMechNTLM;

    if( tag->hasChildWithCData( mech, "X-GOOGLE-TOKEN" ) )
      mechs |= SaslMechGoogleToken;

    return mechs;
  }

  int Client::getCompressionMethods( Tag* tag )
  {
    int meths = 0;

    if( tag->hasChildWithCData( "method", "zlib" ) )
      meths |= StreamFeatureCompressZlib;

    if( tag->hasChildWithCData( "method", "lzw" ) )
      meths |= StreamFeatureCompressDclz;

    return meths;
  }

  bool Client::login()
  {
    bool retval = true;

    if( m_streamFeatures & SaslMechDigestMd5 && m_availableSaslMechs & SaslMechDigestMd5
        && !m_forceNonSasl )
    {
      notifyStreamEvent( StreamEventAuthentication );
      startSASL( SaslMechDigestMd5 );
    }
    else if( m_streamFeatures & SaslMechPlain && m_availableSaslMechs & SaslMechPlain
             && !m_forceNonSasl )
    {
      notifyStreamEvent( StreamEventAuthentication );
      startSASL( SaslMechPlain );
    }
    else if( m_streamFeatures & SaslMechGoogleToken &&
             m_availableSaslMechs & SaslMechGoogleToken
             && !m_forceNonSasl )
    {
      notifyStreamEvent( StreamEventAuthentication );
      startSASL( SaslMechGoogleToken );
    }
    else if( m_streamFeatures & StreamFeatureIqAuth || m_forceNonSasl )
    {
      notifyStreamEvent( StreamEventAuthentication );
      nonSaslLogin();
    }
    else
      retval = false;

    return retval;
  }

  void Client::handleIqIDForward( const IQ& iq, int context )
  {
    switch( context )
    {
      case CtxResourceUnbind:
        // we don't store known resources anyway
        break;
      case CtxResourceBind:
        processResourceBind( iq );
        break;
      case CtxSessionEstablishment:
        processCreateSession( iq );
        break;
      default:
        break;
    }
  }

  bool Client::bindOperation( const std::string& resource, bool bind )
  {
    if( !( m_streamFeatures & StreamFeatureUnbind ) && m_resourceBound )
      return false;

    IQ iq( IQ::Set, JID(), getID() );
    iq.addExtension( new ResourceBind( resource, bind ) );

    send( iq, this, bind ? CtxResourceBind : CtxResourceUnbind );
    return true;
  }

  bool Client::selectResource( const std::string& resource )
  {
    if( !( m_streamFeatures & StreamFeatureUnbind ) )
      return false;

    m_selectedResource = resource;

    return true;
  }

  void Client::processResourceBind( const IQ& iq )
  {
    switch( iq.subtype() )
    {
      case IQ::Result:
      {
        const ResourceBind* rb = iq.findExtension<ResourceBind>( ExtResourceBind );
        if( !rb || !rb->jid() )
        {
          notifyOnResourceBindError( 0 );
          break;
        }

        m_jid = rb->jid();
        m_resourceBound = true;
        m_selectedResource = m_jid.resource();
        notifyOnResourceBind( m_jid.resource() );

        if( m_streamFeatures & StreamFeatureSession )
          createSession();
        else
          connected();
        break;
      }
      case IQ::Error:
      {
        notifyOnResourceBindError( iq.error() );
        break;
      }
      default:
        break;
    }
  }

  void Client::createSession()
  {
    notifyStreamEvent( StreamEventSessionCreation );
    IQ iq( IQ::Set, JID(), getID() );
    iq.addExtension( new SessionCreation() );
    send( iq, this, CtxSessionEstablishment );
  }

  void Client::processCreateSession( const IQ& iq )
  {
    switch( iq.subtype() )
    {
      case IQ::Result:
        connected();
        break;
      case IQ::Error:
        notifyOnSessionCreateError( iq.error() );
        break;
      default:
        break;
    }
  }

  void Client::negotiateCompression( StreamFeature method )
  {
    Tag* t = new Tag( "compress", XMLNS, XMLNS_COMPRESSION );

    if( method == StreamFeatureCompressZlib )
      new Tag( t, "method", "zlib" );

    if( method == StreamFeatureCompressDclz )
      new Tag( t, "method", "lzw" );

    send( t );
  }

  void Client::setPresence( Presence::PresenceType pres, int priority,
                            const std::string& status )
  {
    m_presence.setPresence( pres );
    m_presence.setPriority( priority );
    m_presence.addStatus( status );
    sendPresence( m_presence );
  }

  void Client::setPresence( const JID& to, Presence::PresenceType pres, int priority,
                            const std::string& status )
  {
    Presence p( pres, to, status, priority );
    sendPresence( p );
  }

  void Client::sendPresence( Presence& pres )
  {
    if( state() < StateConnected )
      return;

    send( pres );
  }

  void Client::disableRoster()
  {
    m_manageRoster = false;
    delete m_rosterManager;
    m_rosterManager = 0;
  }

  void Client::nonSaslLogin()
  {
    if( !m_auth )
      m_auth = new NonSaslAuth( this );
    m_auth->doAuth( m_sid );
  }

  void Client::connected()
  {
    if( m_authed )
    {
      if( m_manageRoster )
      {
        notifyStreamEvent( StreamEventRoster );
        m_rosterManager->fill();
      }
      else
        rosterFilled();
    }
    else
    {
      notifyStreamEvent( StreamEventFinished );
      notifyOnConnect();
    }
  }

  void Client::rosterFilled()
  {
    sendPresence( m_presence );
    notifyStreamEvent( StreamEventFinished );
    notifyOnConnect();
  }

  void Client::disconnect()
  {
    disconnect( ConnUserDisconnected );
  }

  void Client::disconnect( ConnectionError reason )
  {
    m_resourceBound = false;
    m_authed = false;
    m_streamFeatures = 0;
    ClientBase::disconnect( reason );
  }

  void Client::cleanup()
  {
    m_authed = false;
    m_resourceBound = false;
    m_streamFeatures = 0;
  }

}
