/*
 * protocol.h - XMPP-Core protocol state machine
 * Copyright (C) 2004  Justin Karneges
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#ifndef PROTOCOL_H
#define PROTOCOL_H

#include "sm.h"
#include "xmlprotocol.h"
#include "xmpp.h"

#include <QList>
#include <QObject>
#include <QPair>
#include <QTimer>

#define NS_ETHERX "http://etherx.jabber.org/streams"
#define NS_CLIENT "jabber:client"
#define NS_SERVER "jabber:server"
#define NS_DIALBACK "jabber:server:dialback"
#define NS_STREAMS "urn:ietf:params:xml:ns:xmpp-streams"
#define NS_TLS "urn:ietf:params:xml:ns:xmpp-tls"
#define NS_SASL "urn:ietf:params:xml:ns:xmpp-sasl"
#define NS_SESSION "urn:ietf:params:xml:ns:xmpp-session"
#define NS_STANZAS "urn:ietf:params:xml:ns:xmpp-stanzas"
#define NS_BIND "urn:ietf:params:xml:ns:xmpp-bind"
#define NS_CAPS "http://jabber.org/protocol/caps"
#define NS_CAPS_OPTIMIZE "http://jabber.org/protocol/caps#optimize"
#define NS_COMPRESS_FEATURE "http://jabber.org/features/compress"
#define NS_COMPRESS_PROTOCOL "http://jabber.org/protocol/compress"
#define NS_HOSTS "http://barracuda.com/xmppextensions/hosts"

namespace XMPP {
class Version {
public:
    Version(int maj = 0, int min = 0);

    int major, minor;
};

class StreamFeatures {
public:
    StreamFeatures();

    bool        tls_supported, sasl_supported, bind_supported, compress_supported;
    bool        tls_required;
    bool        sm_supported;
    bool        session_supported;
    bool        session_required;
    QStringList sasl_mechs;
    QStringList compression_mechs;
    QStringList hosts;
};

class BasicProtocol : public XmlProtocol {
public:
    // xmpp 1.0 error conditions // rfc6120
    enum SASLCond {
        Aborted,              // server confirms auth abort
        AccountDisabled,      // account temporrily disabled
        CredentialsExpired,   // credential expired
        EncryptionRequired,   // can't use mech without TLS
        IncorrectEncoding,    // Incorrect encoding. should not happen
        InvalidAuthzid,       // bad input JID
        InvalidMech,          // bad mechanism
        MalformedRequest,     // malformded request
        MechTooWeak,          // can't use mech with this authzid
        NotAuthorized,        // bad user, bad password, bad creditials
        TemporaryAuthFailure, // please try again later!
    };
    enum StreamCond {
        BadFormat,
        BadNamespacePrefix,
        Conflict,
        ConnectionTimeout,
        HostGone,
        HostUnknown,
        ImproperAddressing,
        InternalServerError,
        InvalidFrom,
        InvalidNamespace,
        InvalidXml,
        StreamNotAuthorized,
        PolicyViolation,
        RemoteConnectionFailed,
        StreamReset,
        ResourceConstraint,
        RestrictedXml,
        SeeOtherHost,
        SystemShutdown,
        UndefinedCondition,
        UnsupportedEncoding,
        UnsupportedStanzaType,
        UnsupportedVersion,
        NotWellFormed
    };
    enum BindCond { BindBadRequest, BindNotAllowed, BindConflict };

    // extend the XmlProtocol enums
    enum Need {
        NSASLMechs = XmlProtocol::NCustom, // need SASL mechlist
        NStartTLS,                         // need to switch on TLS layer
        NCompress,                         // need to switch on compression layer
        NSASLFirst,                        // need SASL first step
        NSASLNext,                         // need SASL next step
        NSASLLayer,                        // need to switch on SASL layer
        NCustom = XmlProtocol::NCustom + 10
    };
    enum Event {
        EFeatures = XmlProtocol::ECustom, // breakpoint after features packet is received
        ESASLSuccess,                     // breakpoint after successful sasl auth
        EStanzaReady,                     // a stanza was received
        EStanzaSent,                      // a stanza was sent
        EReady,                           // stream is ready for stanza use
        EAck,                             // received SM ack response from server
        ECustom = XmlProtocol::ECustom + 10
    };
    enum Error {
        ErrProtocol = XmlProtocol::ErrCustom, // there was an error in the xmpp-core protocol exchange
        ErrStream,                            // <stream:error>, see errCond, errText, and errAppSpec for details
        ErrStartTLS,                          // server refused starttls
        ErrCompress,                          // server refused compression
        ErrAuth, // authorization error.  errCond holds sasl condition (or numeric code for old-protocol)
        ErrBind, // server refused resource bind
        ErrCustom = XmlProtocol::ErrCustom + 10
    };

    BasicProtocol();
    ~BasicProtocol();

    void reset();

    // for outgoing xml
    QDomDocument doc;

    // sasl-related
    QString    saslMech() const;
    QByteArray saslStep() const;
    void       setSASLMechList(const QStringList &list);
    void       setSASLFirst(const QString &mech, const QByteArray &step);
    void       setSASLNext(const QByteArray &step);
    void       setSASLAuthed();

    // send / recv
    void        sendStanza(const QDomElement &e);
    void        sendDirect(const QString &s);
    void        sendWhitespace();
    void        clearSendQueue();
    QDomElement recvStanza();

    // shutdown
    void shutdown();
    void shutdownWithError(int cond, const QString &otherHost = "");

    // <stream> information
    QString to, from, id, lang;
    Version version;

    // error output
    int                     errCond;
    QString                 errText;
    QHash<QString, QString> errLangText;
    QDomElement             errAppSpec;
    QString                 otherHost;

    QByteArray spare; // filled with unprocessed data on NStartTLS and NSASLLayer

    bool isReady() const;

    enum { TypeElement, TypeStanza, TypeDirect, TypePing };

protected:
    static int     stringToSASLCond(const QString &s);
    static int     stringToStreamCond(const QString &s);
    static QString saslCondToString(int);
    static QString streamCondToString(int);

    void send(const QDomElement &e, bool clip = false);
    void sendUrgent(const QDomElement &e, bool clip = false);
    void sendStreamError(int cond, const QString &text = "", const QDomElement &appSpec = QDomElement());
    void sendStreamError(const QString &text); // old-style

    bool errorAndClose(int cond, const QString &text = "", const QDomElement &appSpec = QDomElement());
    bool error(int code);
    void delayErrorAndClose(int cond, const QString &text = "", const QDomElement &appSpec = QDomElement());
    void delayError(int code);

    // reimplemented
    QDomElement docElement();
    void        handleDocOpen(const Parser::Event &pe);
    bool        handleError();
    bool        handleCloseFinished();
    bool        doStep(const QDomElement &e);
    void        itemWritten(int id, int size);

    virtual QString     defaultNamespace();
    virtual QStringList extraNamespaces(); // stringlist: prefix,uri,prefix,uri, [...]
    virtual void        handleStreamOpen(const Parser::Event &pe);
    virtual bool        doStep2(const QDomElement &e) = 0;

    void setReady(bool b);

    QString     sasl_mech;
    QStringList sasl_mechlist;
    QByteArray  sasl_step;
    bool        sasl_authed;

    QDomElement stanzaToRecv;

private:
    struct SASLCondEntry {
        const char *str;
        int         cond;
    };
    static SASLCondEntry saslCondTable[];

    struct StreamCondEntry {
        const char *str;
        int         cond;
    };
    static StreamCondEntry streamCondTable[];

    struct SendItem {
        QDomElement stanzaToSend;
        QString     stringToSend;
        bool        doWhitespace;
    };
    QList<SendItem> sendList;

    bool doShutdown, delayedError, closeError, ready;
    int  stanzasPending, stanzasWritten;

    void init();
    void extractStreamError(const QDomElement &e);
};

class CoreProtocol : public BasicProtocol {
public:
    enum {
        NPassword = NCustom,  // need password for old-mode
        EDBVerify = ECustom,  // breakpoint after db:verify request
        ErrPlain  = ErrCustom // server only supports plain, but allowPlain is false locally
    };

    CoreProtocol();
    ~CoreProtocol();

    void reset();
    void needTimer(int seconds);

    // reimplemented to do SM
    void sendStanza(const QDomElement &e);

    void startClientOut(const Jid &jid, bool oldOnly, bool tlsActive, bool doAuth, bool doCompression);
    void startServerOut(const QString &to);
    void startDialbackOut(const QString &to, const QString &from);
    void startDialbackVerifyOut(const QString &to, const QString &from, const QString &id, const QString &key);
    void startClientIn(const QString &id);
    void startServerIn(const QString &id);

    void       setLang(const QString &s);
    void       setAllowTLS(bool b);
    void       setAllowBind(bool b);
    void       setAllowPlain(bool b); // old-mode
    const Jid &jid() const;

    void setPassword(const QString &s);
    void setFrom(const QString &s);
    void setDialbackKey(const QString &s);

    // input
    QString user, host;

    // status
    bool old;

    StreamFeatures     features;
    QList<QDomElement> unhandledFeatures;
    QStringList        hosts;

    // static QString xmlToString(const QDomElement &e, bool clip=false);

    StreamManagement sm;

    class DBItem {
    public:
        enum { ResultRequest, ResultGrant, VerifyRequest, VerifyGrant, Validated };
        int     type;
        Jid     to, from;
        QString key, id;
        bool    ok;
    };

private:
    enum Step {
        Start,
        Done,
        SendFeatures,
        GetRequest,
        HandleTLS,
        GetSASLResponse,
        IncHandleSASLSuccess,
        GetFeatures,        // read features packet
        HandleFeatures,     // act on features, by initiating tls, sasl, or bind
        GetTLSProceed,      // read <proceed/> tls response
        GetCompressProceed, // read <compressed/> compression response
        GetSASLFirst,       // perform sasl first step using provided data
        GetSASLChallenge,   // read server sasl challenge
        GetSASLNext,        // perform sasl next step using provided data
        HandleSASLSuccess,  // handle what must be done after reporting sasl success
        GetBindResponse,    // read bind response
        HandleAuthGet,      // send old-protocol auth-get
        GetAuthGetResponse, // read auth-get response
        HandleAuthSet,      // send old-protocol auth-set
        GetAuthSetResponse, // read auth-set response
        GetSMResponse       // read SM init response
    };

    QList<DBItem> dbrequests, dbpending, dbvalidated;

    bool server, dialback, dialback_verify;
    int  step;

    bool digest;
    bool tls_started, sasl_started, compress_started;

    Jid     jid_;
    bool    oldOnly;
    bool    allowPlain;
    bool    doTLS, doAuth, doBinding, doCompress;
    QString password;

    QString dialback_id, dialback_key;
    QString self_from;

    void       init();
    static int getOldErrorCode(const QDomElement &e);
    bool       loginComplete();

    bool isValidStanza(const QDomElement &e) const;
    bool streamManagementHandleStanza(const QDomElement &e);
    bool grabPendingItem(const Jid &to, const Jid &from, int type, DBItem *item);
    bool normalStep(const QDomElement &e);
    bool dialbackStep(const QDomElement &e);

    bool needSMRequest();

    // reimplemented
    bool        stepAdvancesParser() const;
    bool        stepRequiresElement() const;
    void        stringSend(const QString &s);
    void        stringRecv(const QString &s);
    QString     defaultNamespace();
    QStringList extraNamespaces();
    void        handleStreamOpen(const Parser::Event &pe);
    bool        doStep2(const QDomElement &e);
    void        elementSend(const QDomElement &e);
    void        elementRecv(const QDomElement &e);
};
} // namespace XMPP

#endif // PROTOCOL_H
