/*
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */

#ifndef PROTOCOL_H
#define PROTOCOL_H

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

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

#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_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;
		QStringList sasl_mechs;
		QStringList compression_mechs;
		QStringList hosts;
	};

	class BasicProtocol : public XmlProtocol
	{
	public:
		// xmpp 1.0 error conditions
		enum SASLCond {
			Aborted,
			IncorrectEncoding,
			InvalidAuthzid,
			InvalidMech,
			MechTooWeak,
			NotAuthorized,
			TemporaryAuthFailure
		};
		enum StreamCond {
			BadFormat,
			BadNamespacePrefix,
			Conflict,
			ConnectionTimeout,
			HostGone,
			HostUnknown,
			ImproperAddressing,
			InternalServerError,
			InvalidFrom,
			InvalidNamespace,
			InvalidXml,
			StreamNotAuthorized,
			PolicyViolation,
			RemoteConnectionFailed,
			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() override;

		// 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();
		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;
		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 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() override;
		void handleDocOpen(const Parser::Event &pe) override;
		bool handleError() override;
		bool handleCloseFinished() override;
		bool doStep(const QDomElement &e) override;
		void itemWritten(int id, int size) override;

		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() override;
		void startTimer(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 override;
		bool stepRequiresElement() const override;
		void stringSend(const QString &s) override;
		void stringRecv(const QString &s) override;
		QString defaultNamespace() override;
		QStringList extraNamespaces() override;
		void handleStreamOpen(const Parser::Event &pe) override;
		bool doStep2(const QDomElement &e) override;
		void elementSend(const QDomElement &e) override;
		void elementRecv(const QDomElement &e) override;
	};
}

#endif
