/*

Jabber Protocol Plugin for Miranda NG

Copyright (c) 2002-04  Santithorn Bunchua
Copyright (c) 2005-12  George Hazan
Copyright (c) 2007     Maxim Mluhov
Copyright (C) 2012-19 Miranda NG team

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

#include "stdafx.h"
#include "jabber_iq.h"
#include "jabber_caps.h"
#include "version.h"

const JabberFeatCapPair g_JabberFeatCapPairs[] =
{
	{ JABBER_FEAT_DISCO_INFO,              JABBER_CAPS_DISCO_INFO,              LPGENW("Supports Service Discovery info") },
	{ JABBER_FEAT_DISCO_ITEMS,             JABBER_CAPS_DISCO_ITEMS,             LPGENW("Supports Service Discovery items list") },
	{ JABBER_FEAT_ENTITY_CAPS,             JABBER_CAPS_ENTITY_CAPS,             LPGENW("Can inform about its Jabber capabilities") },
	{ JABBER_FEAT_SI,                      JABBER_CAPS_SI,                      LPGENW("Supports stream initiation (e.g., for filetransfers)") },
	{ JABBER_FEAT_SI_FT,                   JABBER_CAPS_SI_FT,                   LPGENW("Supports stream initiation for file transfers") },
	{ JABBER_FEAT_BYTESTREAMS,             JABBER_CAPS_BYTESTREAMS,             LPGENW("Supports file transfers via SOCKS5 Bytestreams") },
	{ JABBER_FEAT_IBB,                     JABBER_CAPS_IBB,                     LPGENW("Supports file transfers via In-Band Bytestreams") },
	{ JABBER_FEAT_OOB,                     JABBER_CAPS_OOB,                     LPGENW("Supports file transfers via Out-of-Band Bytestreams") },
	{ JABBER_FEAT_OOB2,                    JABBER_CAPS_OOB,                     LPGENW("Supports file transfers via Out-of-Band Bytestreams") },
	{ JABBER_FEAT_COMMANDS,                JABBER_CAPS_COMMANDS,                LPGENW("Supports execution of Ad-Hoc commands") },
	{ JABBER_FEAT_REGISTER,                JABBER_CAPS_REGISTER,                LPGENW("Supports in-band registration") },
	{ JABBER_FEAT_MUC,                     JABBER_CAPS_MUC,                     LPGENW("Supports multi-user chat") },
	{ JABBER_FEAT_CHATSTATES,              JABBER_CAPS_CHATSTATES,              LPGENW("Can report chat state in a chat session") },
	{ JABBER_FEAT_LAST_ACTIVITY,           JABBER_CAPS_LAST_ACTIVITY,           LPGENW("Can report information about the last activity of the user") },
	{ JABBER_FEAT_VERSION,                 JABBER_CAPS_VERSION,                 LPGENW("Can report own version information") },
	{ JABBER_FEAT_ENTITY_TIME,             JABBER_CAPS_ENTITY_TIME,             LPGENW("Can report local time of the user") },
	{ JABBER_FEAT_PING,                    JABBER_CAPS_PING,                    LPGENW("Can send and receive ping requests") },
	{ JABBER_FEAT_DATA_FORMS,              JABBER_CAPS_DATA_FORMS,              LPGENW("Supports data forms") },
	{ JABBER_FEAT_MESSAGE_EVENTS,          JABBER_CAPS_MESSAGE_EVENTS,          LPGENW("Can request and respond to events relating to the delivery, display, and composition of messages") },
	{ JABBER_FEAT_VCARD_TEMP,              JABBER_CAPS_VCARD_TEMP,              LPGENW("Supports vCard") },
	{ JABBER_FEAT_AVATAR,                  JABBER_CAPS_AVATAR,                  LPGENW("Supports iq-based avatars") },
	{ JABBER_FEAT_XHTML,                   JABBER_CAPS_XHTML,                   LPGENW("Supports XHTML formatting of chat messages") },
	{ JABBER_FEAT_AGENTS,                  JABBER_CAPS_AGENTS,                  LPGENW("Supports Jabber Browsing") },
	{ JABBER_FEAT_BROWSE,                  JABBER_CAPS_BROWSE,                  LPGENW("Supports Jabber Browsing") },
	{ JABBER_FEAT_FEATURE_NEG,             JABBER_CAPS_FEATURE_NEG,             LPGENW("Can negotiate options for specific features") },
	{ JABBER_FEAT_AMP,                     JABBER_CAPS_AMP,                     LPGENW("Can request advanced processing of message stanzas") },
	{ JABBER_FEAT_USER_MOOD,               JABBER_CAPS_USER_MOOD,               LPGENW("Can report information about user moods") },
	{ JABBER_FEAT_USER_MOOD_NOTIFY,        JABBER_CAPS_USER_MOOD_NOTIFY,        LPGENW("Receives information about user moods") },
	{ JABBER_FEAT_PUBSUB,                  JABBER_CAPS_PUBSUB,                  LPGENW("Supports generic publish-subscribe functionality") },
	{ JABBER_FEAT_SECUREIM,                JABBER_CAPS_SECUREIM,                LPGENW("Supports SecureIM plugin for Miranda NG") },
	{ JABBER_FEAT_MIROTR,                  JABBER_CAPS_MIROTR,                  LPGENW("Supports OTR (Off-the-Record Messaging)") },
	{ JABBER_FEAT_NEWGPG,                  JABBER_CAPS_NEWGPG,                  LPGENW("Supports New_GPG plugin for Miranda NG") },
	{ JABBER_FEAT_PRIVACY_LISTS,           JABBER_CAPS_PRIVACY_LISTS,           LPGENW("Blocks packets from other users/group chats using Privacy lists") },
	{ JABBER_FEAT_MESSAGE_RECEIPTS,        JABBER_CAPS_MESSAGE_RECEIPTS,        LPGENW("Supports Message Receipts") },
	{ JABBER_FEAT_USER_TUNE,               JABBER_CAPS_USER_TUNE,               LPGENW("Can report information about the music to which a user is listening") },
	{ JABBER_FEAT_USER_TUNE_NOTIFY,        JABBER_CAPS_USER_TUNE_NOTIFY,        LPGENW("Receives information about the music to which a user is listening") },
	{ JABBER_FEAT_PRIVATE_STORAGE,         JABBER_CAPS_PRIVATE_STORAGE,         LPGENW("Supports private XML Storage (for bookmarks and other)") },
	{ JABBER_FEAT_ATTENTION,               JABBER_CAPS_ATTENTION,               LPGENW("Supports attention requests ('nudge')") },
	{ JABBER_FEAT_ARCHIVE_AUTO,            JABBER_CAPS_ARCHIVE_AUTO,            LPGENW("Supports chat history retrieving") },
	{ JABBER_FEAT_ARCHIVE_MANAGE,          JABBER_CAPS_ARCHIVE_MANAGE,          LPGENW("Supports chat history management") },
	{ JABBER_FEAT_USER_ACTIVITY,           JABBER_CAPS_USER_ACTIVITY,           LPGENW("Can report information about user activity") },
	{ JABBER_FEAT_USER_ACTIVITY_NOTIFY,    JABBER_CAPS_USER_ACTIVITY_NOTIFY,    LPGENW("Receives information about user activity") },
	{ JABBER_FEAT_MIRANDA_NOTES,           JABBER_CAPS_MIRANDA_NOTES,           LPGENW("Supports Miranda NG notes extension") },
	{ JABBER_FEAT_JINGLE,                  JABBER_CAPS_JINGLE,                  LPGENW("Supports Jingle") },
	{ JABBER_FEAT_ROSTER_EXCHANGE,         JABBER_CAPS_ROSTER_EXCHANGE,         LPGENW("Supports Roster Exchange") },
	{ JABBER_FEAT_DIRECT_MUC_INVITE,       JABBER_CAPS_DIRECT_MUC_INVITE,       LPGENW("Supports direct chat invitations (XEP-0249)") },
	{ JABBER_FEAT_OMEMO_DEVICELIST_NOTIFY, JABBER_CAPS_OMEMO_DEVICELIST_NOTIFY, LPGENW("Receives information about OMEMO devices") },
	{ JABBER_FEAT_CARBONS,				      JABBER_CAPS_CARBONS,                 LPGENW("Supports message carbons (XEP-0280)")},
};

const int g_cJabberFeatCapPairs = _countof(g_JabberFeatCapPairs);

const JabberFeatCapPairExt g_JabberFeatCapPairsExt[] =
{
	{ JABBER_EXT_SECUREIM,          JABBER_CAPS_SECUREIM,             "SecureIM/IsContactSecured" },
	{ JABBER_EXT_MIROTR,            JABBER_CAPS_MIROTR,               "MirOTRMenuCheckService"    },
	{ JABBER_EXT_NEWGPG,            JABBER_CAPS_NEWGPG,               "/ExportGPGKeys"            },
	{ JABBER_EXT_OMEMO,             JABBER_CAPS_OMEMO,                                            },
	{ JABBER_EXT_NUDGE,             JABBER_CAPS_ATTENTION,            "NUDGE/Send"                },
	{ JABBER_EXT_JINGLE,            JABBER_CAPS_JINGLE,               "Jingle/StartSession"       },
	{ JABBER_EXT_COMMANDS,          JABBER_CAPS_COMMANDS                                          },
	{ JABBER_EXT_USER_ACTIVITY,     JABBER_CAPS_USER_ACTIVITY_NOTIFY                              },
	{ JABBER_EXT_USER_MOOD,         JABBER_CAPS_USER_MOOD_NOTIFY                                  },
	{ JABBER_EXT_USER_TUNE,         JABBER_CAPS_USER_TUNE_NOTIFY,     "ListeningTo/Enabled"       },
	{ JABBER_EXT_USER_TUNE,         JABBER_CAPS_USER_TUNE_NOTIFY,     "WATrack/GetFileInfo"       },
	{ JABBER_EXT_MIR_NOTES,         JABBER_CAPS_MIRANDA_NOTES                                     },
	{ szCoreVersion,                JABBER_CAPS_MIRANDA_PARTIAL                                   },
	{ JABBER_EXT_PLATFORMX86,       JABBER_CAPS_PLATFORMX86                                       },
	{ JABBER_EXT_PLATFORMX64,       JABBER_CAPS_PLATFORMX64                                       },
};

const int g_cJabberFeatCapPairsExt = _countof(g_JabberFeatCapPairsExt);

void CJabberProto::AddDefaultCaps()
{
	JabberCapsBits myCaps = JABBER_CAPS_MIRANDA_ALL;
	if (m_bUseOMEMO)
		myCaps |= JABBER_CAPS_OMEMO_DEVICELIST_NOTIFY;
	
	for (auto &it : g_JabberFeatCapPairsExt)
		if (it.Valid())
			myCaps |= it.jcbCap;

	wchar_t szOsBuffer[256]; szOsBuffer[0] = 0;
	GetOSDisplayString(szOsBuffer, _countof(szOsBuffer));

	CJabberClientPartialCaps *pCaps = m_clientCapsManager.SetOwnCaps(JABBER_CAPS_MIRANDA_NODE, _T(__VERSION_STRING_DOTS), myCaps);
	pCaps->m_szOs = mir_wstrdup(L"Microsoft Windows");
	pCaps->m_szOsVer = mir_wstrdup(szOsBuffer);
	pCaps->m_szSoft = mir_wstrdup(L"Miranda NG Jabber Protocol");
	pCaps->m_szSoftMir = mir_wstrdup(szCoreVersion);
}

void CJabberProto::OnIqResultCapsDiscoInfo(HXML, CJabberIqInfo *pInfo)
{
	pResourceStatus r(ResourceInfoFromJID(pInfo->GetFrom()));
	if (r == nullptr)
		return;

	HXML query = pInfo->GetChildNode();
	if (pInfo->GetIqType() == JABBER_IQ_TYPE_RESULT && query) {
		JabberCapsBits jcbCaps = 0;

		HXML feature;
		for (int i = 1; (feature = XmlGetNthChild(query, L"feature", i)) != nullptr; i++) {
			const wchar_t *featureName = XmlGetAttrValue(feature, L"var");
			if (!featureName)
				continue;

			for (auto &it : g_JabberFeatCapPairs) {
				if (!mir_wstrcmp(it.szFeature, featureName)) {
					jcbCaps |= it.jcbCap;
					break;
				}
			}
		}

		// no XEP-0115 support? store info & exit
		CJabberClientPartialCaps *pCaps = r->m_pCaps;
		if (pCaps == nullptr) {
			r->m_jcbCachedCaps = jcbCaps;
			r->m_dwDiscoInfoRequestTime = -1;
			return;
		}

		HXML identity;
		for (int i = 1; (identity = XmlGetNthChild(query, L"identity", i)) != nullptr; i++) {
			const wchar_t *identityName = XmlGetAttrValue(identity, L"name");
			if (identityName)
				pCaps->SetVer(identityName);
		}

		HXML xform;
		for (int i = 1; (xform = XmlGetNthChild(query, L"x", i)) != nullptr; i++) {
			wchar_t *szFormTypeValue = XPath(xform, L"field[@var='FORM_TYPE']/value");
			if (!mir_wstrcmp(szFormTypeValue, L"urn:xmpp:dataforms:softwareinfo")) {
				JSONNode root;
				if (pCaps->m_szOs = mir_wstrdup(XPath(xform, L"field[@var='os']/value")))
					root.push_back(JSONNode("o", _T2A(pCaps->m_szOs).get()));
				if (pCaps->m_szOsVer = mir_wstrdup(XPath(xform, L"field[@var='os_version']/value")))
					root.push_back(JSONNode("ov", _T2A(pCaps->m_szOsVer).get()));
				if (pCaps->m_szSoft = mir_wstrdup(XPath(xform, L"field[@var='software']/value")))
					root.push_back(JSONNode("s", _T2A(pCaps->m_szSoft).get()));
				if (pCaps->m_szSoftVer = mir_wstrdup(XPath(xform, L"field[@var='software_version']/value")))
					root.push_back(JSONNode("sv", _T2A(pCaps->m_szSoftVer).get()));
				if (pCaps->m_szSoftMir = mir_wstrdup(XPath(xform, L"field[@var='x-miranda-core-version']/value")))
					root.push_back(JSONNode("sm", _T2A(pCaps->m_szSoftMir).get()));
				root.push_back(JSONNode("c", CMStringA(FORMAT, "%lld", jcbCaps)));

				CMStringA szName(FORMAT, "%S#%S", pCaps->GetNode(), pCaps->GetHash());
				json_string szValue = root.write();
				db_set_s(0, "JabberCaps", szName, szValue.c_str());
			}
		}

		pCaps->SetCaps(jcbCaps, pInfo->GetIqId());

		UpdateMirVer(pInfo->GetHContact(), r);

		JabberUserInfoUpdate(pInfo->GetHContact());
	}
	else {
		if (!r->m_pCaps) { // no XEP-0115 support?
			r->m_jcbCachedCaps = JABBER_RESOURCE_CAPS_NONE;
			r->m_dwDiscoInfoRequestTime = -1;
		}
		else r->m_pCaps->SetCaps(JABBER_RESOURCE_CAPS_ERROR);
	}
}

JabberCapsBits CJabberProto::GetTotalJidCapabilities(const wchar_t *jid)
{
	if (jid == nullptr)
		return JABBER_RESOURCE_CAPS_NONE;

	wchar_t szBareJid[JABBER_MAX_JID_LEN];
	JabberStripJid(jid, szBareJid, _countof(szBareJid));

	JABBER_LIST_ITEM *item = ListGetItemPtr(LIST_ROSTER, szBareJid);
	if (item == nullptr)
		item = ListGetItemPtr(LIST_VCARD_TEMP, szBareJid);

	JabberCapsBits jcbToReturn = JABBER_RESOURCE_CAPS_NONE;

	// get bare jid info only if where is no resources
	if (!item || (item && !item->arResources.getCount())) {
		pResourceStatus r(ResourceInfoFromJID(szBareJid));
		jcbToReturn = GetResourceCapabilities(szBareJid, r);
		if (jcbToReturn & JABBER_RESOURCE_CAPS_ERROR)
			jcbToReturn = JABBER_RESOURCE_CAPS_NONE;
	}

	if (item) {
		for (auto &it : item->arResources) {
			wchar_t szFullJid[JABBER_MAX_JID_LEN];
			mir_snwprintf(szFullJid, L"%s/%s", szBareJid, it->m_tszResourceName);
			pResourceStatus r(it);
			JabberCapsBits jcb = GetResourceCapabilities(szFullJid, r);
			if (!(jcb & JABBER_RESOURCE_CAPS_ERROR))
				jcbToReturn |= jcb;
		}
	}
	return jcbToReturn;
}

JabberCapsBits CJabberProto::GetResourceCapabilities(const wchar_t *jid)
{
	wchar_t fullJid[JABBER_MAX_JID_LEN];
	GetClientJID(jid, fullJid, _countof(fullJid));

	pResourceStatus r(ResourceInfoFromJID(fullJid));
	return GetResourceCapabilities(fullJid, r);
}

JabberCapsBits CJabberProto::GetResourceCapabilities(const wchar_t *jid, pResourceStatus &r)
{
	if (r == nullptr)
		return JABBER_RESOURCE_CAPS_ERROR;

	// XEP-0115 mode
	if (r->m_pCaps) {
		CJabberClientPartialCaps *pCaps = r->m_pCaps;
		JabberCapsBits jcbCaps = 0;
		bool bRequestSent = false;
		JabberCapsBits jcbMainCaps = pCaps->GetCaps();

		if (jcbMainCaps == JABBER_RESOURCE_CAPS_TIMEOUT && !r->m_dwDiscoInfoRequestTime)
			jcbMainCaps = JABBER_RESOURCE_CAPS_ERROR;

		if (jcbMainCaps == JABBER_RESOURCE_CAPS_UNINIT) {
			// send disco#info query
			CJabberIqInfo *pInfo = AddIQ(&CJabberProto::OnIqResultCapsDiscoInfo, JABBER_IQ_TYPE_GET, jid, JABBER_IQ_PARSE_FROM | JABBER_IQ_PARSE_CHILD_TAG_NODE);
			pInfo->SetTimeout(JABBER_RESOURCE_CAPS_QUERY_TIMEOUT);
			pCaps->SetCaps(JABBER_RESOURCE_CAPS_IN_PROGRESS, pInfo->GetIqId());
			r->m_dwDiscoInfoRequestTime = pInfo->GetRequestTime();

			wchar_t queryNode[512];
			mir_snwprintf(queryNode, L"%s#%s", pCaps->GetNode(), pCaps->GetHash());
			m_ThreadInfo->send(XmlNodeIq(pInfo) << XQUERY(JABBER_FEAT_DISCO_INFO) << XATTR(L"node", queryNode));

			bRequestSent = true;
		}
		else if (jcbMainCaps == JABBER_RESOURCE_CAPS_IN_PROGRESS)
			bRequestSent = true;
		else if (jcbMainCaps != JABBER_RESOURCE_CAPS_TIMEOUT)
			jcbCaps |= jcbMainCaps;

		if (jcbMainCaps != JABBER_RESOURCE_CAPS_TIMEOUT && r->m_tszCapsExt) {
			wchar_t *caps = mir_wstrdup(r->m_tszCapsExt);

			wchar_t *token = wcstok(caps, L" ");
			while (token) {
				for (auto &it : g_JabberFeatCapPairsExt) {
					if (!mir_wstrcmp(it.szFeature, token)) {
						jcbCaps |= it.jcbCap;
						break;
					}
				}

				token = wcstok(nullptr, L" ");
			}

			mir_free(caps);
		}

		if (bRequestSent)
			return JABBER_RESOURCE_CAPS_IN_PROGRESS;

		return jcbCaps | r->m_jcbManualDiscoveredCaps;
	}

	// no XEP-0115: send query each time it's needed
	switch (r->m_dwDiscoInfoRequestTime) {
	case -1:
		return r->m_jcbCachedCaps;

	case 0:
		RequestOldCapsInfo(r, jid);
		break;
	}
	return JABBER_RESOURCE_CAPS_IN_PROGRESS;
}

void CJabberProto::RequestOldCapsInfo(pResourceStatus &r, const wchar_t *fullJid)
{
	CJabberIqInfo *pInfo = AddIQ(&CJabberProto::OnIqResultCapsDiscoInfo, JABBER_IQ_TYPE_GET, fullJid, JABBER_IQ_PARSE_FROM | JABBER_IQ_PARSE_CHILD_TAG_NODE);
	pInfo->SetTimeout(JABBER_RESOURCE_CAPS_QUERY_TIMEOUT);
	r->m_dwDiscoInfoRequestTime = pInfo->GetRequestTime();

	m_ThreadInfo->send(XmlNodeIq(pInfo) << XQUERY(JABBER_FEAT_DISCO_INFO));
}

void CJabberProto::GetCachedCaps(const wchar_t *szNode, const wchar_t *szVer, pResourceStatus &r)
{
	CMStringA szName(FORMAT, "%S#%S", szNode, szVer);
	ptrA szValue(db_get_sa(0, "JabberCaps", szName));
	if (szValue != 0) {
		JSONNode root = JSONNode::parse(szValue);
		if (root) {
			CMStringW wszCaps = root["c"].as_mstring();
			r->m_pCaps = m_clientCapsManager.SetClientCaps(szNode, szVer, nullptr, _wtoi64(wszCaps));
			r->m_pCaps->m_szOs = mir_wstrdup(root["o"].as_mstring());
			r->m_pCaps->m_szOsVer = mir_wstrdup(root["ov"].as_mstring());
			r->m_pCaps->m_szSoft = mir_wstrdup(root["s"].as_mstring());
			r->m_pCaps->m_szSoftVer = mir_wstrdup(root["sv"].as_mstring());
			r->m_pCaps->m_szSoftMir = mir_wstrdup(root["sm"].as_mstring());
		}
	}
}

/////////////////////////////////////////////////////////////////////////////////////////
//  CJabberClientPartialCaps class

CJabberClientPartialCaps::CJabberClientPartialCaps(CJabberClientCaps *pParent, const wchar_t *szHash, const wchar_t *szVer)
	: m_parent(pParent),
	m_szHash(mir_wstrdup(szHash)),
	m_szSoftVer(mir_wstrdup(szVer)),
	m_jcbCaps(JABBER_RESOURCE_CAPS_UNINIT),
	m_pNext(nullptr),
	m_nIqId(-1),
	m_dwRequestTime(0)
{
}

CJabberClientPartialCaps::~CJabberClientPartialCaps()
{
	delete m_pNext;
}

CJabberClientPartialCaps* CJabberClientPartialCaps::SetNext(CJabberClientPartialCaps *pCaps)
{
	CJabberClientPartialCaps *pRetVal = m_pNext;
	m_pNext = pCaps;
	return pRetVal;
}

void CJabberClientPartialCaps::SetCaps(JabberCapsBits jcbCaps, int nIqId)
{
	if (jcbCaps == JABBER_RESOURCE_CAPS_IN_PROGRESS)
		m_dwRequestTime = GetTickCount();
	else
		m_dwRequestTime = 0;
	m_jcbCaps = jcbCaps;
	m_nIqId = nIqId;
}

JabberCapsBits CJabberClientPartialCaps::GetCaps()
{
	if (m_jcbCaps == JABBER_RESOURCE_CAPS_IN_PROGRESS && GetTickCount() - m_dwRequestTime > JABBER_RESOURCE_CAPS_QUERY_TIMEOUT) {
		m_jcbCaps = JABBER_RESOURCE_CAPS_TIMEOUT;
		m_dwRequestTime = 0;
	}
	return m_jcbCaps;
}

CJabberClientPartialCaps* CJabberClientCaps::FindByVersion(const wchar_t *szHash)
{
	if (m_pCaps == nullptr)
		return nullptr;

	CJabberClientPartialCaps *pCaps = m_pCaps;
	while (pCaps) {
		if (!mir_wstrcmp(szHash, pCaps->GetHash()))
			return pCaps;
		pCaps = pCaps->GetNext();
	}
	return nullptr;
}

CJabberClientPartialCaps* CJabberClientCaps::FindById(int nIqId)
{
	if (!m_pCaps || nIqId == -1)
		return nullptr;

	CJabberClientPartialCaps *pCaps = m_pCaps;
	while (pCaps) {
		if (pCaps->GetIqId() == nIqId)
			break;
		pCaps = pCaps->GetNext();
	}
	return pCaps;
}

/////////////////////////////////////////////////////////////////////////////////////////
// CJabberClientCaps class

CJabberClientCaps::CJabberClientCaps(const wchar_t *szNode) :
	m_szNode(mir_wstrdup(szNode))
{
	m_pCaps = nullptr;
}

CJabberClientCaps::~CJabberClientCaps()
{
	mir_free(m_szNode);
	delete m_pCaps;
}

JabberCapsBits CJabberClientCaps::GetPartialCaps(const wchar_t *szVer)
{
	CJabberClientPartialCaps *pCaps = FindByVersion(szVer);
	return (pCaps) ? pCaps->GetCaps() : JABBER_RESOURCE_CAPS_UNINIT;
}

CJabberClientPartialCaps* CJabberClientCaps::SetPartialCaps(const wchar_t *szHash, const wchar_t *szVer, JabberCapsBits jcbCaps, int nIqId)
{
	CJabberClientPartialCaps *pCaps = FindByVersion(szHash);
	if (!pCaps) {
		pCaps = new CJabberClientPartialCaps(this, szHash, szVer);
		pCaps->SetNext(m_pCaps);
		m_pCaps = pCaps;
	}

	pCaps->SetCaps(jcbCaps, nIqId);
	return pCaps;
}

/////////////////////////////////////////////////////////////////////////////////////////
// CJabberClientCapsManager class

static int sttCompareNodes(const CJabberClientCaps *p1, const CJabberClientCaps *p2)
{
	return mir_wstrcmp(p1->GetNode(), p2->GetNode());
}

CJabberClientCapsManager::CJabberClientCapsManager(CJabberProto *proto) :
	m_arCaps(10, sttCompareNodes)
{
	ppro = proto;

	UpdateFeatHash();
}

CJabberClientCapsManager::~CJabberClientCapsManager()
{
}

void CJabberClientCapsManager::UpdateFeatHash()
{
	m_szFeaturesCrc.Empty();

	JabberCapsBits jcb = JABBER_CAPS_MIRANDA_ALL;
	for (auto &it : ppro->m_lstJabberFeatCapPairsDynamic)
		jcb |= it->jcbCap;
	if (!ppro->m_bAllowVersionRequests)
		jcb &= ~JABBER_CAPS_VERSION;

	if (ppro->m_bUseOMEMO)
		jcb |= JABBER_CAPS_OMEMO_DEVICELIST_NOTIFY;

	CMStringA feat_buf(FORMAT, "client/pc//Miranda %d.%d.%d.%d<", __MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM);
	for (auto &it : g_JabberFeatCapPairs)
		if (jcb & it.jcbCap) {
			feat_buf.Append(_T2A(it.szFeature));
			feat_buf.AppendChar('<');
		}

	for (auto &it : ppro->m_lstJabberFeatCapPairsDynamic)
		if (jcb & it->jcbCap) {
			feat_buf.Append(_T2A(it->szFeature));
			feat_buf.AppendChar('<');
		}

	wchar_t szOsBuffer[256]; szOsBuffer[0] = 0;
	GetOSDisplayString(szOsBuffer, _countof(szOsBuffer));

	feat_buf.Append("software_version"); feat_buf.AppendChar('<');
	feat_buf.Append(__VERSION_STRING_DOTS); feat_buf.AppendChar('<');

	feat_buf.Append("x-miranda-core-version"); feat_buf.AppendChar('<');
	feat_buf.Append(_T2A(szCoreVersion)); feat_buf.AppendChar('<');

	BYTE hash[MIR_SHA1_HASH_SIZE];
	mir_sha1_hash((BYTE*)feat_buf.c_str(), feat_buf.GetLength(), hash);
	ptrA szHash(mir_base64_encode(&hash, sizeof(hash)));
	m_szFeaturesCrc = szHash;
}

const wchar_t* CJabberClientCapsManager::GetFeaturesCrc()
{
	return m_szFeaturesCrc.c_str();
}

CJabberClientCaps* CJabberClientCapsManager::FindClient(const wchar_t *szNode)
{
	if (szNode == nullptr)
		return nullptr;

	return m_arCaps.find((CJabberClientCaps*)&szNode);
}

JabberCapsBits CJabberClientCapsManager::GetClientCaps(const wchar_t *szNode, const wchar_t *szVer)
{
	mir_cslockfull lck(m_cs);
	CJabberClientCaps *pClient = FindClient(szNode);
	if (!pClient) {
		lck.unlock();
		ppro->debugLogW(L"CAPS: get no caps for: %s, %s", szNode, szVer);
		return JABBER_RESOURCE_CAPS_UNINIT;
	}
	JabberCapsBits jcbCaps = pClient->GetPartialCaps(szVer);
	lck.unlock();
	ppro->debugLogW(L"CAPS: get caps %I64x for: %s, %s", jcbCaps, szNode, szVer);
	return jcbCaps;
}

CJabberClientPartialCaps* CJabberClientCapsManager::GetPartialCaps(const wchar_t *szNode, const wchar_t *szHash)
{
	mir_cslock lck(m_cs);
	CJabberClientCaps *pClient = FindClient(szNode);
	return (pClient == nullptr) ? nullptr : pClient->FindByVersion(szHash);
}

CJabberClientPartialCaps* CJabberClientCapsManager::SetClientCaps(const wchar_t *szNode, const wchar_t *szHash, const wchar_t *szVer, JabberCapsBits jcbCaps, int nIqId)
{
	mir_cslockfull lck(m_cs);
	CJabberClientCaps *pClient = FindClient(szNode);
	if (!pClient) {
		pClient = new CJabberClientCaps(szNode);
		m_arCaps.insert(pClient);
	}

	CJabberClientPartialCaps *res = pClient->SetPartialCaps(szHash, szVer, jcbCaps, nIqId);
	lck.unlock();
	ppro->debugLogW(L"CAPS: set caps %I64x for: %s#%s => [%s]", jcbCaps, szHash, szNode, szVer);
	return res;
}

bool CJabberClientCapsManager::HandleInfoRequest(HXML, CJabberIqInfo *pInfo, const wchar_t *szNode)
{
	JabberCapsBits jcb = 0;

	if (szNode) {
		wchar_t szExtCap[512], szExtCapWHash[560];
		mir_snwprintf(szExtCap, L"%s#%s", JABBER_CAPS_MIRANDA_NODE, m_szFeaturesCrc.c_str());
		if (!mir_wstrcmp(szExtCap, szNode)) {
			szNode = nullptr;
			goto LBL_All;
		}

		for (auto &it : g_JabberFeatCapPairsExt) {
			if (!it.Valid())
				continue;

			// TODO: something better here
			mir_snwprintf(szExtCap, L"%s#%s", JABBER_CAPS_MIRANDA_NODE, it.szFeature);
			mir_snwprintf(szExtCapWHash, L"%s %s", szExtCap, m_szFeaturesCrc.c_str());
			if (!mir_wstrcmp(szNode, szExtCap) || !mir_wstrcmp(szNode, szExtCapWHash)) {
				jcb = it.jcbCap;
				break;
			}
		}

		// check features registered through IJabberNetInterface::RegisterFeature() and IJabberNetInterface::AddFeatures()
		for (auto &it : ppro->m_lstJabberFeatCapPairsDynamic) {
			// TODO: something better here
			mir_snwprintf(szExtCap, L"%s#%s", JABBER_CAPS_MIRANDA_NODE, it->szExt);
			mir_snwprintf(szExtCapWHash, L"%s %s", szExtCap, m_szFeaturesCrc.c_str());
			if (!mir_wstrcmp(szNode, szExtCap) || !mir_wstrcmp(szNode, szExtCapWHash)) {
				jcb = it->jcbCap;
				break;
			}
		}

		// unknown node, not XEP-0115 request
		if (!jcb)
			return false;
	}
	else {
LBL_All:
		jcb = JABBER_CAPS_MIRANDA_ALL;
		for (auto &it : ppro->m_lstJabberFeatCapPairsDynamic)
			jcb |= it->jcbCap;
	}

	if (ppro->m_bUseOMEMO)
		jcb |= JABBER_CAPS_OMEMO_DEVICELIST_NOTIFY;

	if (!ppro->m_bAllowVersionRequests)
		jcb &= ~JABBER_CAPS_VERSION;

	XmlNodeIq iq(L"result", pInfo);

	HXML query = iq << XQUERY(JABBER_FEAT_DISCO_INFO);
	if (szNode)
		query << XATTR(L"node", szNode);

	CMStringW szName(FORMAT, L"Miranda %d.%d.%d.%d", __MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM);
	query << XCHILD(L"identity") << XATTR(L"category", L"client") << XATTR(L"type", L"pc") << XATTR(L"name", szName);

	for (auto &it : g_JabberFeatCapPairs)
		if (jcb & it.jcbCap)
			query << XCHILD(L"feature") << XATTR(L"var", it.szFeature);

	for (auto &it : ppro->m_lstJabberFeatCapPairsDynamic)
		if (jcb & it->jcbCap)
			query << XCHILD(L"feature") << XATTR(L"var", it->szFeature);

	if (ppro->m_bAllowVersionRequests && !szNode) {
		HXML form = query << XCHILDNS(L"x", JABBER_FEAT_DATA_FORMS) << XATTR(L"type", L"result");
		form << XCHILD(L"field") << XATTR(L"var", L"FORM_TYPE") << XATTR(L"type", L"hidden")
			<< XCHILD(L"value", L"urn:xmpp:dataforms:softwareinfo");

		CJabberClientPartialCaps *pCaps = GetPartialCaps(JABBER_CAPS_MIRANDA_NODE, m_szFeaturesCrc);
		if (pCaps) {
			if (ppro->m_bShowOSVersion) {
				form << XCHILD(L"field") << XATTR(L"var", L"os") << XCHILD(L"value", pCaps->GetOs());
				form << XCHILD(L"field") << XATTR(L"var", L"os_version") << XCHILD(L"value", pCaps->GetOsVer());
			}
			form << XCHILD(L"field") << XATTR(L"var", L"software") << XCHILD(L"value", pCaps->GetSoft());
			form << XCHILD(L"field") << XATTR(L"var", L"software_version") << XCHILD(L"value", pCaps->GetSoftVer());
			form << XCHILD(L"field") << XATTR(L"var", L"x-miranda-core-version") << XCHILD(L"value", pCaps->GetSoftMir());
		}
	}

	ppro->m_ThreadInfo->send(iq);
	return true;
}