/*
 * discodlg.cpp - main dialog for the Service Discovery protocol
 * Copyright (C) 2003  Michail Pishchagin
 *
 * 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include "discodlg.h"

#include <qlistview.h>
#include <qcombobox.h>
#include <qcheckbox.h>
#include <qmessagebox.h>
#include <qaction.h>
#include <qstatusbar.h>
#include <qpopupmenu.h>
#include <qsignalmapper.h>
#include <qpushbutton.h>
#include <qdragobject.h>
#include <qtooltip.h>
#include <qtoolbutton.h>

#include "im.h"
#include "xmpp_tasks.h"

#include "psitoolbar.h"
#include "tasklist.h"
#include "psiaccount.h"
#include "psicon.h"
#include "busywidget.h"
#include "common.h"
#include "iconaction.h"

//----------------------------------------------------------------------------
// DiscoData -- a shared data struct
//----------------------------------------------------------------------------

class DiscoListItem;
class DiscoConnector : public QObject
{
	Q_OBJECT
public:
	DiscoConnector(QObject *parent)
	: QObject(parent) {}

signals:
	void itemUpdated(QListViewItem *);

private:
	friend class DiscoListItem;
};

struct DiscoData {
	PsiAccount *pa;
	TaskList *tasks;
	DiscoConnector *d;

	enum Protocol {
		Auto,

		Disco,
		Browse,
		Agents
	};
	Protocol protocol;
};

//----------------------------------------------------------------------------
// ProtocolAction
//----------------------------------------------------------------------------

class ProtocolAction : public QAction
{
	Q_OBJECT
public:
	ProtocolAction(QString text, QString toolTip, QObject *parent, QSignalMapper *sm, int parm);

	bool addTo(QWidget *w);

public slots:
	void setOn(bool);
private:
	QToolButton *btn;
};

ProtocolAction::ProtocolAction(QString text, QString toolTip, QObject *parent, QSignalMapper *sm, int parm)
: QAction(text, text, 0, parent)
{
	setText( text );
	setToggleAction(true);
	setToolTip(toolTip);
	connect(this, SIGNAL(activated()), sm, SLOT(map()));
	sm->setMapping(this, parm);
	btn = 0;
}

bool ProtocolAction::addTo(QWidget *w)
{
	if ( w->inherits("QToolBar") ) {
		if ( btn )
			delete btn;
		QCString bname = name() + QCString("_action_button");
		btn = new QToolButton ( w, bname );

		btn->setToggleButton ( isToggleAction() );
		btn->setOn( isOn() );
		btn->setTextLabel ( text() );
		btn->setEnabled ( isEnabled() );
		btn->setUsesTextLabel( true );
		QToolTip::add(btn, toolTip());

		connect(btn, SIGNAL(toggled(bool)), this, SLOT(setOn(bool)));

		return true;
	}

	return false;
}

void ProtocolAction::setOn(bool b)
{
	if ( btn ) {
		if ( b )
			emit activated();
		btn->setOn(b);
	}

	QAction::setOn(b);
}

//----------------------------------------------------------------------------
// DiscoListItem
//----------------------------------------------------------------------------

class DiscoListItem : public QObject, public QListViewItem
{
	Q_OBJECT
public:
	DiscoListItem(DiscoItem it, DiscoData *d, QListView *parent);
	DiscoListItem(DiscoItem it, DiscoData *d, QListViewItem *parent);
	~DiscoListItem();

	QString text(int columns) const;
	void setOpen(bool open);
	const DiscoItem &item() const;

	void itemSelected();

public slots: // the two are used internally by class, and also called by DiscoDlg::Private::refresh()
	void updateInfo();
	void updateItems(bool parentAutoItems = false);

private slots:
	void discoItemsFinished();
	void discoInfoFinished();

	void doBrowse(bool parentAutoItems = false);
	void doAgents(bool parentAutoItems = false);
	void browseFinished();
	void agentsFinished();

private:
	DiscoItem di;
	DiscoData *d;
	bool isRoot;
	bool alreadyItems, alreadyInfo;
	bool autoItems; // used in updateItemsFinished
	bool autoInfo;

	void copyItem(const DiscoItem &);
	void updateInfo(const DiscoItem &);
	void updateItemsFinished(const DiscoList &);
	void autoItemsChildren() const; // automatically call disco#items for children :-)
	QString hash() { return computeHash( item().jid().full(), item().node() ); }
	QString computeHash( QString jid, QString node );

	// helper functions
	void init(DiscoItem it, DiscoData *dd);

	bool autoItemsEnabled() const;
	bool autoInfoEnabled() const;
	DiscoDlg *dlg() const;
};

DiscoListItem::DiscoListItem(DiscoItem it, DiscoData *_d, QListView *parent)
: QListViewItem (parent)
{
	isRoot = true;

	init(it, _d);
}

DiscoListItem::DiscoListItem(DiscoItem it, DiscoData *_d, QListViewItem *parent)
: QListViewItem (parent)
{
	isRoot = false;

	init(it, _d);
}

DiscoListItem::~DiscoListItem()
{
}

void DiscoListItem::init(DiscoItem _item, DiscoData *_d)
{
	d = _d;
	di = _item;
	copyItem(_item);

	alreadyItems = alreadyInfo = false;

	if ( !autoItemsEnabled() )
		setExpandable (true);

	autoInfo = false;
	if ( autoInfoEnabled() || isRoot ) {
		updateInfo();
		if ( !isRoot )
			autoInfo = true;
	}

	//setDragEnabled(true); // EXPERIMENTAL
}

void DiscoListItem::copyItem(const DiscoItem &it)
{
	if ( !(!di.jid().full().isEmpty() && it.jid().full().isEmpty()) )
		di.setJid ( it.jid() );
	if ( !(!di.node().isEmpty() && it.node().isEmpty()) )
		di.setNode ( it.node() );
	if ( !(!di.name().isEmpty() && it.name().isEmpty()) )
		di.setName ( it.name() );

	di.setAction ( it.action() );

	if ( !(!di.features().list().isEmpty() && it.features().list().isEmpty()) )
		di.setFeatures ( it.features() );
	if ( !(!di.identities().isEmpty() && it.identities().isEmpty()) )
		di.setIdentities ( it.identities() );

	if ( di.jid().userHost().left(4) == "jud." || di.jid().userHost().left(6) == "users." ) {
		// nasty hack for the nasty (and outdated) JUD service :-/
		if ( !di.features().canSearch() ) {
			QStringList features = di.features().list();
			features << "jabber:iq:search";
			di.setFeatures( features );
		}

		bool found = false;
		DiscoItem::Identities::ConstIterator it = di.identities().begin();
		for ( ; it != di.identities().end(); ++it) {
			if ( (*it).category == "service" && (*it).type == "jud" ) {
				found = true;
				break;
			}
		}
		if ( !found ) {
			DiscoItem::Identity id;
			id.category = "service";
			id.type     = "jud";
			DiscoItem::Identities ids;
			ids << id;
			di.setIdentities( ids );
		}
	}

	bool pixmapOk = false;
	if ( !di.identities().isEmpty() ) {
		DiscoItem::Identity id = di.identities().first();

		if ( !id.category.isEmpty() ) {
			Icon ic = category2icon(id.category, id.type);

			if ( !ic.impix().isNull() ) {
				setPixmap (0, ic.impix().pixmap());
				pixmapOk = true;
			}
		}
	}

	if ( !pixmapOk )
		setPixmap (0, is->status(di.jid(), STATUS_ONLINE));

	repaint();

	if ( isSelected() ) // update actions
		emit d->d->itemUpdated( this );
}

QString DiscoListItem::text (int c) const
{
	if (c == 0)
		return di.name();
	else if (c == 1)
		return di.jid().full();
	else if (c == 2)
		return di.node();
	return "";
}

const DiscoItem &DiscoListItem::item() const
{
	return di;
}

DiscoDlg *DiscoListItem::dlg() const
{
	return (DiscoDlg *)listView()->parent()->parent();
}

bool DiscoListItem::autoItemsEnabled() const
{
	return dlg()->ck_autoItems->isChecked();
}

bool DiscoListItem::autoInfoEnabled() const
{
	return dlg()->ck_autoInfo->isChecked();
}

void DiscoListItem::setOpen (bool o)
{
	if ( o ) {
		if ( !alreadyItems )
			updateItems();
		else
			autoItemsChildren();
	}

	QListViewItem::setOpen(o);
}

void DiscoListItem::itemSelected()
{
	if ( !alreadyInfo )
		updateInfo();
}

void DiscoListItem::updateItems(bool parentAutoItems)
{
	if ( parentAutoItems ) {
		// save traffic
		if ( alreadyItems )
			return;

		// FIXME: currently, JUD doesn't seem to answer to browsing requests
		if ( item().identities().size() ) {
			DiscoItem::Identity id = item().identities().first();
			if ( id.category == "service" && id.type == "jud" )
				return;
		}
		QString j = item().jid().host(); // just another method to discover if we're gonna to browse JUD
		if ( item().jid().user().isEmpty() && (j.left(4) == "jud." || j.left(6) == "users.") )
			return;
	}

	autoItems = !parentAutoItems;

	if ( !autoItemsEnabled() )
		autoItems = false;

	if ( d->protocol == DiscoData::Auto || d->protocol == DiscoData::Disco ) {
		JT_DiscoItems *jt = new JT_DiscoItems(d->pa->client()->rootTask());
		connect(jt, SIGNAL(finished()), SLOT(discoItemsFinished()));
		jt->get(di.jid(), di.node());
		jt->go(true);
		d->tasks->append(jt);
	}
	else if ( d->protocol == DiscoData::Browse )
		doBrowse(parentAutoItems);
	else if ( d->protocol == DiscoData::Agents )
		doAgents(parentAutoItems);
}

void DiscoListItem::discoItemsFinished()
{
	JT_DiscoItems *jt = (JT_DiscoItems *)sender();

	if ( jt->success() ) {
		updateItemsFinished(jt->items());
	}
	else if ( d->protocol == DiscoData::Auto ) {
		doBrowse();
		return;
	}
	else if ( !autoItems ) {
		QString error = jt->statusString();
		QMessageBox::critical(dlg(), tr("Error"), tr("There was an error getting items for <b>%1</b>.\nReason: %2").arg(di.jid().full()).arg(error));
	}

	alreadyItems = true;
}

void DiscoListItem::doBrowse(bool parentAutoItems)
{
	if ( parentAutoItems ) {
		// save traffic
		if ( alreadyItems )
			return;

		if ( item().identities().size() ) {
			DiscoItem::Identity id = item().identities().first();
			if ( id.category == "service" && id.type == "jud" )
				return;
		}
	}

	autoItems = !parentAutoItems;
	if ( !autoItemsEnabled() )
		autoItems = false;

	JT_Browse *jt = new JT_Browse(d->pa->client()->rootTask());
	connect(jt, SIGNAL(finished()), SLOT(browseFinished()));
	jt->get(di.jid());
	jt->go(true);
	d->tasks->append(jt);
}

void DiscoListItem::browseFinished()
{
	JT_Browse *jt = (JT_Browse *)sender();

	if ( jt->success() ) {
		// update info
		DiscoItem root;
		root.fromAgentItem( jt->root() );
		updateInfo(root);
		alreadyInfo = true;
		autoInfo = false;

		// update items
		AgentList from = jt->agents();
		DiscoList to;
		AgentList::Iterator it = from.begin();
		for ( ; it != from.end(); ++it) {
			DiscoItem item;
			item.fromAgentItem( *it );

			to.append( item );
		}

		updateItemsFinished(to);
	}
	else if ( d->protocol == DiscoData::Auto ) {
		doAgents();
		return;
	}
	else if ( !autoItems ) {
		QString error = jt->statusString();
		QMessageBox::critical(dlg(), tr("Error"), tr("There was an error browsing items for <b>%1</b>.\nReason: %2").arg(di.jid().full()).arg(error));
	}

	alreadyItems = true;
}

void DiscoListItem::doAgents(bool parentAutoItems)
{
	if ( parentAutoItems ) {
		// save traffic
		if ( alreadyItems )
			return;

		if ( item().identities().size() ) {
			DiscoItem::Identity id = item().identities().first();
			if ( id.category == "service" && id.type == "jud" )
				return;
		}
	}

	autoItems = !parentAutoItems;
	if ( !autoItemsEnabled() )
		autoItems = false;

	JT_GetServices *jt = new JT_GetServices(d->pa->client()->rootTask());
	connect(jt, SIGNAL(finished()), SLOT(agentsFinished()));
	jt->get(di.jid());
	jt->go(true);
	d->tasks->append(jt);
}

void DiscoListItem::agentsFinished()
{
	JT_GetServices *jt = (JT_GetServices *)sender();

	if ( jt->success() ) {
		// update info
		DiscoItem root;
		DiscoItem::Identity id;
		id.name     = tr("Jabber Service");
		id.category = "service";
		id.type     = "jabber";
		DiscoItem::Identities ids;
		ids.append(id);
		root.setIdentities(ids);
		updateInfo(root);
		alreadyInfo = true;
		autoInfo = false;

		// update items
		AgentList from = jt->agents();
		DiscoList to;
		AgentList::Iterator it = from.begin();
		for ( ; it != from.end(); ++it) {
			DiscoItem item;
			item.fromAgentItem( *it );

			to.append( item );
		}

		updateItemsFinished(to);
	}
	else if ( !autoItems ) {
		QString error = jt->statusString();
		QMessageBox::critical(dlg(), tr("Error"), tr("There was an error getting agents for <b>%1</b>.\nReason: %2").arg(di.jid().full()).arg(error));
	}

	alreadyItems = true;
}

QString DiscoListItem::computeHash( QString jid, QString node )
{
	QString ret = jid.replace( '@', "\\@" );
	ret += "@";
	ret += node.replace( '@', "\\@" );
	return ret;
}

void DiscoListItem::updateItemsFinished(const DiscoList &list)
{
	QDict<DiscoListItem> children;
	DiscoListItem *child = (DiscoListItem *)firstChild();
	while ( child ) {
		children.insert( child->hash(), child );

		child = (DiscoListItem *)child->nextSibling();
	}

	// add/update items
	for(DiscoList::ConstIterator it = list.begin(); it != list.end(); ++it) {
		const DiscoItem a = *it;

		QString key = computeHash(a.jid().full(), a.node());
		DiscoListItem *child = children[ key ];

		if ( child ) {
			child->copyItem ( a );
			children.remove( key );
		}
		else {
			new DiscoListItem (a, d, this);
		}
	}

	// remove all items that are not on new DiscoList
	children.setAutoDelete( true );
	children.clear();

	if ( autoItems && isOpen() )
		autoItemsChildren();

	// don't forget to remove '+' (or '-') sign in case, that the child list is empty
	setExpandable ( !list.isEmpty() );

	repaint();

	// root item is initially hidden
	if ( isRoot && !isVisible() )
		setVisible (true);
}

void DiscoListItem::autoItemsChildren() const
{
	if ( !autoItemsEnabled() )
		return;

	DiscoListItem *child = (DiscoListItem *)firstChild();
	while ( child ) {
		child->updateItems(true);

		child = (DiscoListItem *)child->nextSibling();
	}
}

void DiscoListItem::updateInfo()
{
	if ( d->protocol != DiscoData::Auto && d->protocol != DiscoData::Disco )
		return;

	JT_DiscoInfo *jt = new JT_DiscoInfo(d->pa->client()->rootTask());
	connect(jt, SIGNAL(finished()), SLOT(discoInfoFinished()));
	jt->get(di.jid(), di.node());
	jt->go(true);
	d->tasks->append(jt);
}

void DiscoListItem::discoInfoFinished()
{
	JT_DiscoInfo *jt = (JT_DiscoInfo *)sender();

	if ( jt->success() ) {
		updateInfo( jt->item() );
	}
	else if ( !autoInfo && d->protocol != DiscoData::Auto ) {
		QString error = jt->statusString();
		QMessageBox::critical(dlg(), tr("Error"), tr("There was an error getting item's info for <b>%1</b>.\nReason: %2").arg(di.jid().full()).arg(error));
	}

	alreadyInfo = true;
	autoInfo = false;
}

void DiscoListItem::updateInfo(const DiscoItem &item)
{
	copyItem( item );

	if ( isRoot && !isVisible() )
		setVisible (true);
}

//----------------------------------------------------------------------------
// DiscoList
//----------------------------------------------------------------------------

class DiscoListView : public QListView, public QToolTip
{
	Q_OBJECT
public:
	DiscoListView(QWidget *parent);

protected:
	void maybeTip(const QPoint &);
	QDragObject *dragObject();
};

DiscoListView::DiscoListView(QWidget *parent)
: QListView(parent), QToolTip(viewport())
{
	addColumn( tr( "Name" ) );
	addColumn( tr( "JID" ) );
	addColumn( tr( "Node" ) );
}

void DiscoListView::maybeTip(const QPoint &pos)
{
	DiscoListItem *i = (DiscoListItem *)itemAt(pos);
	if(!i)
		return;

	// NAME <JID> (Node "NODE")
	//
	// Identities:
	// (icon) NAME (Category "CATEGORY"; Type "TYPE")
	// (icon) NAME (Category "CATEGORY"; Type "TYPE")
	//
	// Features:
	// NAME (http://jabber.org/feature)
	// NAME (http://jabber.org/feature)

	// top row
	QString text = "<qt><nobr>";
	DiscoItem item = i->item();

	if ( !item.name().isEmpty() )
		text += item.name() + " ";

	text += "&lt;" + item.jid().full() + "&gt;";

	if ( !item.node().isEmpty() )
		text += " (" + tr("Node") + " \"" + item.node() + "\")";

	text += "<nobr>";

	if ( !item.identities().isEmpty() || !item.features().list().isEmpty() )
		text += "<br>\n";

	// identities
	if ( !item.identities().isEmpty() ) {
		text += "<br>\n<b>" + tr("Identities:") + "</b>\n";

		DiscoItem::Identities::ConstIterator it = item.identities().begin();
		for ( ; it != item.identities().end(); ++it) {
			text += "<br>";
			Icon icon( category2icon((*it).category, (*it).type) );
			if ( !icon.name().isEmpty() )
				text += "<icon name=\"" + icon.name() + "\"> ";
			text += (*it).name;
			text += " (" + tr("Category") + " \"" + (*it).category + "\"; " + tr("Type") + " \"" + (*it).type + "\")\n";
		}

		if ( !item.features().list().isEmpty() )
			text += "<br>\n";
	}

	// features
	if ( !item.features().list().isEmpty() ) {
		text += "<br>\n<b>" + tr("Features:") + "</b>\n";

		QStringList features = item.features().list();
		QStringList::ConstIterator it = features.begin();
		for ( ; it != features.end(); ++it) {
			Features f( *it );
			text += "\n<br>";
			if ( f.id() > Features::FID_None )
				text += f.name() + " (";
			text += *it;
			if ( f.id() > Features::FID_None )
				text += ")";
		}
	}

	text += "</qt>";
	QRect r( itemRect(i) );
	tip(r, text);
}

QDragObject *DiscoListView::dragObject()
{
	DiscoListItem *i = (DiscoListItem *)selectedItem();
	if(!i)
		return 0;

	QDragObject *d = new QTextDrag(i->item().jid().full(), this);
	d->setPixmap(IconsetFactory::icon("status/online"), QPoint(8,8));
	return d;
}

//----------------------------------------------------------------------------
// DiscoDlg::Private
//----------------------------------------------------------------------------

class DiscoDlg::Private : public QObject
{
	Q_OBJECT

private:
	// helper class for use in toolbar
	class StretchWidget : public QWidget
	{
	public:
		StretchWidget(QWidget *parent)
		: QWidget(parent)
		{
			setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
		}
	};

	// helper class to store browser history
	class History {
	private:
		QListViewItem *item;
	public:
		History(QListViewItem *it) {
			item = it;
		}

		~History() {
			if ( item )
				delete item;
		}

		QListViewItem *takeItem() {
			QListViewItem *i = item;
			item = 0;
			return i;
		}
	};

public: // data
	DiscoDlg *dlg;
	Jid jid;
	QString node;

	DiscoData data;

	PsiToolBar *toolBar;
	IconAction *actBrowse, *actBack, *actForward, *actRefresh, *actStop;

	// custom actions, that will be added to toolbar and context menu
	IconAction *actRegister, *actSearch, *actJoin, *actVCard, *actAdd;

	typedef QPtrList<History> HistoryList;
	HistoryList backHistory, forwardHistory;

	BusyWidget *busy;

public: // functions
	Private(DiscoDlg *parent, PsiAccount *pa);
	~Private();

public slots:
	void doDisco(QString host = QString::null, QString node = QString::null);

	void actionStop();
	void actionRefresh();
	void actionBrowse();

	void actionBack();
	void actionForward();
	void updateBackForward();
	void backForwardHelper(QListViewItem *);

	void updateComboBoxes(Jid j, QString node);

	void itemUpdateStarted();
	void itemUpdateFinished();

	void disableButtons();
	void enableButtons(const DiscoItem &);

	void itemSelected (QListViewItem *);
	void itemDoubleclicked (QListViewItem *);
	bool eventFilter (QObject *, QEvent *);

	void setProtocol(int);

	// features...
	void actionActivated(int);

	void objectDestroyed(QObject *);

private:
	friend class DiscoListItem;
};

DiscoDlg::Private::Private(DiscoDlg *parent, PsiAccount *pa)
{
	dlg = parent;
	data.pa = pa;
	data.tasks = new TaskList;
	connect(data.tasks, SIGNAL(started()),  SLOT(itemUpdateStarted()));
	connect(data.tasks, SIGNAL(finished()), SLOT(itemUpdateFinished()));
	data.d = new DiscoConnector(this);
	connect(data.d, SIGNAL(itemUpdated(QListViewItem *)), SLOT(itemSelected (QListViewItem *)));
	data.protocol = DiscoData::Auto;

	backHistory.setAutoDelete(true);
	forwardHistory.setAutoDelete(true);

	// mess with widgets
	busy = parent->busy;
	connect(busy, SIGNAL(destroyed(QObject *)), SLOT(objectDestroyed(QObject *)));

	QListView *lv_discoOld = dlg->lv_disco;
	dlg->lv_disco = new DiscoListView(dlg->centralWidget());
	replaceWidget(lv_discoOld, dlg->lv_disco);

	dlg->lv_disco->installEventFilter (this);
	connect(dlg->lv_disco, SIGNAL(selectionChanged (QListViewItem *)), SLOT(itemSelected (QListViewItem *)));;
	connect(dlg->lv_disco, SIGNAL(doubleClicked (QListViewItem *)),    SLOT(itemDoubleclicked (QListViewItem *)));;

	// protocol actions
	QSignalMapper *pm = new QSignalMapper(this);
	connect(pm, SIGNAL(mapped(int)), SLOT(setProtocol(int)));
	QActionGroup *protocolActions = new QActionGroup (this);
	protocolActions->setExclusive(true);

	ProtocolAction *autoProtocol = new ProtocolAction (tr("Auto"), tr("Automatically determine protocol"), protocolActions, pm, DiscoData::Auto);
	ProtocolAction *discoProtocol = new ProtocolAction ("D", tr("Service Discovery"), protocolActions, pm, DiscoData::Disco);
	ProtocolAction *browseProtocol = new ProtocolAction ("B", tr("Browse Services"), protocolActions, pm, DiscoData::Browse);
	ProtocolAction *agentsProtocol = new ProtocolAction ("A", tr("Browse Agents"), protocolActions, pm, DiscoData::Agents);
	autoProtocol->setOn(true);

	// create actions
	actBrowse = new IconAction (tr("Browse"), "psi/jabber", tr("&Browse"), 0, dlg);
	connect (actBrowse, SIGNAL(activated()), SLOT(actionBrowse()));
	actRefresh = new IconAction (tr("Refresh Item"), "psi/reload", tr("&Refresh Item"), 0, dlg);
	connect (actRefresh, SIGNAL(activated()), SLOT(actionRefresh()));
	actStop = new IconAction (tr("Stop"), "psi/stop", tr("Sto&p"), 0, dlg);
	connect (actStop, SIGNAL(activated()), SLOT(actionStop()));
	actBack = new IconAction (tr("Back"), "psi/arrowLeft", tr("&Back"), 0, dlg);
	connect (actBack, SIGNAL(activated()), SLOT(actionBack()));
	actForward = new IconAction (tr("Forward"), "psi/arrowRight", tr("&Forward"), 0, dlg);
	connect (actForward, SIGNAL(activated()), SLOT(actionForward()));

	// custom actions
	QSignalMapper *sm = new QSignalMapper(this);
	connect(sm, SIGNAL(mapped(int)), SLOT(actionActivated(int)));
	actRegister = new IconAction (tr("Register"), "psi/register", tr("&Register"), 0, dlg);
	connect (actRegister, SIGNAL(activated()), sm, SLOT(map()));
	sm->setMapping(actRegister, Features::FID_Register);
	actSearch = new IconAction (tr("Search"), "psi/search", tr("&Search"), 0, dlg);
	connect (actSearch, SIGNAL(activated()), sm, SLOT(map()));
	sm->setMapping(actSearch, Features::FID_Search);
	actJoin = new IconAction (tr("Join"), "psi/groupChat", tr("&Join"), 0, dlg);
	connect (actJoin, SIGNAL(activated()), sm, SLOT(map()));
	sm->setMapping(actJoin, Features::FID_Groupchat);
	actVCard = new IconAction (tr("vCard"), "psi/vCard", tr("&vCard"), 0, dlg);
	connect (actVCard, SIGNAL(activated()), sm, SLOT(map()));
	sm->setMapping(actVCard, Features::FID_VCard);
	actAdd = new IconAction (tr("Add to roster"), "psi/addContact", tr("&Add to roster"), 0, dlg);
	connect (actAdd, SIGNAL(activated()), sm, SLOT(map()));
	sm->setMapping(actAdd, Features::FID_Add);

	// create toolbar
	toolBar = new PsiToolBar(dlg);
	toolBar->setCustomizeable( false );

	actBack->addTo(toolBar);
	actBrowse->addTo(toolBar);
	actForward->addTo(toolBar);

	toolBar->addSeparator();
	actRefresh->addTo(toolBar);
	actStop->addTo(toolBar);

	// custom actions
	toolBar->addSeparator();
	actRegister->addTo(toolBar);
	actSearch->addTo(toolBar);
	actJoin->addTo(toolBar);

	toolBar->addSeparator();
	actAdd->addTo(toolBar);
	actVCard->addTo(toolBar);

	// select protocol
	toolBar->addSeparator();
	autoProtocol->addTo(toolBar);
	discoProtocol->addTo(toolBar);
	browseProtocol->addTo(toolBar);
	agentsProtocol->addTo(toolBar);

	toolBar->setStretchableWidget(new StretchWidget(toolBar));
	pa->accountLabel(toolBar, true);

	// misc stuff
	disableButtons();
	actStop->setEnabled(false);	// stop action is not handled by disableButtons()
	updateBackForward();		// same applies to back & forward
}

DiscoDlg::Private::~Private()
{
	delete data.tasks;
}

void DiscoDlg::Private::doDisco(QString _host, QString _node)
{
	PsiAccount *pa = data.pa;
	if ( !pa->checkConnected(dlg) )
		return;

	// Strip whitespace
	Jid j;
	QString host = _host;
	if ( host.isEmpty() )
		host = dlg->cb_address->currentText();
	j = host.stripWhiteSpace();
	if ( !j.isValid() )
		return;

	QString n = _node.stripWhiteSpace();
	if ( n.isEmpty() )
		n = dlg->cb_node->currentText().stripWhiteSpace();

	// check, whether we need to update history
	if ( (jid.full() != j.full()) || (node != n) ) {
		QListViewItem *item = dlg->lv_disco->firstChild(); // get the root item

		if ( item ) {
			dlg->lv_disco->takeItem( item );

			backHistory.append( new History(item) );
			forwardHistory.clear();
		}
	}

	jid  = j;
	node = n;

	updateComboBoxes(jid, node);

	data.tasks->clear(); // also will call all all necessary functions
	disableButtons();
	updateBackForward();

	dlg->lv_disco->clear();

	// create new root item
	DiscoItem di;
	di.setJid( jid );
	di.setNode( node );

	DiscoListItem *root = new DiscoListItem (di, &data, dlg->lv_disco);
	root->setVisible (false); // don't confuse users with empty root

	root->setOpen(true); // begin browsing
}

void DiscoDlg::Private::updateComboBoxes(Jid j, QString n)
{
	data.pa->psi()->recentBrowseAdd( j.full() );
	dlg->cb_address->clear();
	dlg->cb_address->insertStringList(data.pa->psi()->recentBrowseList());

	data.pa->psi()->recentNodeAdd( n );
	dlg->cb_node->clear();
	dlg->cb_node->insertStringList(data.pa->psi()->recentNodeList());
}

void DiscoDlg::Private::actionStop()
{
	data.tasks->clear();
}

void DiscoDlg::Private::actionRefresh()
{
	DiscoListItem *it = (DiscoListItem *)dlg->lv_disco->selectedItem();
	if ( !it )
		return;

	it->updateItems();
	it->updateInfo();
}

void DiscoDlg::Private::actionBrowse()
{
	DiscoListItem *it = (DiscoListItem *)dlg->lv_disco->selectedItem();
	if ( !it )
		return;

	doDisco(it->item().jid().full(), it->item().node());
}

void DiscoDlg::Private::actionBack()
{
	// add current selection to forward history
	QListViewItem *item = dlg->lv_disco->firstChild();
	if ( item ) {
		dlg->lv_disco->takeItem( item );

		forwardHistory.append( new History(item) );
	}

	// now, take info from back history...
	QListViewItem *i = backHistory.last()->takeItem();
	backHistory.removeLast();

	// and restore view
	backForwardHelper(i);
}

void DiscoDlg::Private::actionForward()
{
	// add current selection to back history
	QListViewItem *item = dlg->lv_disco->firstChild();
	if ( item ) {
		dlg->lv_disco->takeItem( item );

		backHistory.append( new History(item) );
	}

	// now, take info from forward history...
	QListViewItem *i = forwardHistory.last()->takeItem();
	forwardHistory.removeLast();

	// and restore view
	backForwardHelper(i);
}

void DiscoDlg::Private::backForwardHelper(QListViewItem *root)
{
	DiscoListItem *i = (DiscoListItem *)root;

	jid  = i->item().jid();
	node = i->item().node();

	updateComboBoxes(jid, node);

	data.tasks->clear(); // also will call all all necessary functions
	disableButtons();
	updateBackForward();

	dlg->lv_disco->insertItem( root );

	// fixes multiple selection bug
	QListViewItemIterator it( dlg->lv_disco );
	while ( it.current() ) {
		QListViewItem *item = it.current();
		++it;

		if ( item->isSelected() )
			for (int i = 0; i <= 1; i++) // it's boring to write same line twice :-)
				dlg->lv_disco->setSelected(item, (bool)i);
	}
}

void DiscoDlg::Private::updateBackForward()
{
	actBack->setEnabled ( !backHistory.isEmpty() );
	actForward->setEnabled ( !forwardHistory.isEmpty() );
}

void DiscoDlg::Private::itemUpdateStarted()
{
	actStop->setEnabled(true);
	if ( busy )
		busy->start();
}

void DiscoDlg::Private::itemUpdateFinished()
{
	actStop->setEnabled(false);
	if ( busy )
		busy->stop();
}

void DiscoDlg::Private::disableButtons()
{
	DiscoItem di;
	enableButtons ( di );
}

void DiscoDlg::Private::enableButtons(const DiscoItem &it)
{
	bool itemSelected = !it.jid().full().isEmpty();
	actRefresh->setEnabled( itemSelected );
	actBrowse->setEnabled( itemSelected );

	// custom actions
	Features f = it.features();
	actRegister->setEnabled( f.canRegister() );
	actSearch->setEnabled( f.canSearch() );
	actJoin->setEnabled( f.canGroupchat() );
	actAdd->setEnabled( itemSelected );
	actVCard->setEnabled( f.haveVCard() );
}

void DiscoDlg::Private::itemSelected (QListViewItem *item)
{
	DiscoListItem *it = (DiscoListItem *)item;
	if ( !it ) {
		disableButtons();
		return;
	}

	it->itemSelected();

	const DiscoItem di = it->item();
	enableButtons ( di );
}

void DiscoDlg::Private::itemDoubleclicked (QListViewItem *item)
{
	DiscoListItem *it = (DiscoListItem *)item;
	if ( !it )
		return;

	const DiscoItem d = it->item();
	const Features &f = d.features();

	// set the prior state of item
	// FIXME: causes minor flickering
	if ( f.canGroupchat() || f.canRegister() || f.canSearch() ) {
		if ( !it->isOpen() ) {
			if ( it->isExpandable() || it->childCount() )
				it->setOpen( true );
		}
		else {
			it->setOpen( false );
		}
	}

	long id = 0;

	// trigger default action
	if ( f.canGroupchat() ) {
		id = Features::FID_Groupchat;
	}
	else {
		// FIXME: check the category and type for JUD!
		DiscoItem::Identity ident = d.identities().first();
		bool searchFirst = ident.category == "service" && ident.type == "jud";

		if ( searchFirst && f.canSearch() ) {
			id = Features::FID_Search;
		}
		else {
			if ( f.canRegister() )
				id = Features::FID_Register;
		}
	}

	if ( id > 0 )
		emit dlg->featureActivated( Features::feature(id), d.jid(), d.node() );
}

bool DiscoDlg::Private::eventFilter (QObject *object, QEvent *event)
{
	if ( object == dlg->lv_disco ) {
		if ( event->type() == QEvent::ContextMenu ) {
			QContextMenuEvent *e = (QContextMenuEvent *)event;

			DiscoListItem *it = (DiscoListItem *)dlg->lv_disco->selectedItem();
			if ( !it )
				return true;

			// prepare features list
			QValueList<long> idFeatures;
			QStringList features = it->item().features().list();
			{	// convert all features to their IDs
				QStringList::Iterator it = features.begin();
				for ( ; it != features.end(); ++it) {
					Features f( *it );
					if ( f.id() > Features::FID_None )
						idFeatures.append( Features::id(*it) );
				}
				//qHeapSort(idFeatures);
			}

			QValueList<long> ids;
			{	// ensure, that there's in no duplicated IDs inside. FIXME: optimize this, anyone?
				long id = 0, count = 0;
				QValueList<long>::Iterator it;
				while ( count < (long)idFeatures.count() ) {
					bool found = false;

					for (it = idFeatures.begin(); it != idFeatures.end(); ++it) {
						if ( id == *it ) {
							if ( !found ) {
								found = true;
								ids.append( id );
							}
							count++;
						}
					}
					id++;
				}
			}

			// prepare popup menu
			QPopupMenu p;

			actBrowse->addTo (&p);
			actRefresh->addTo (&p);
			actStop->addTo (&p);

			// custom actions
			p.insertSeparator();
			actRegister->addTo(&p);
			actSearch->addTo(&p);
			actJoin->addTo(&p);

			p.insertSeparator();
			actAdd->addTo(&p);
			actVCard->addTo(&p);

			// popup with all available features
			QPopupMenu *fm = new QPopupMenu(&p);
			{
				QValueList<long>::Iterator it = ids.begin();
				for ( ; it != ids.end(); ++it)
					fm->insertItem(Features::name(*it), *it + 10000); // TODO: add pixmap
			}

			//p.insertSeparator();
			//int menuId = p.insertItem(tr("Activate &Feature"), fm);
			//p.setItemEnabled(menuId, !ids.isEmpty());

			// display popup
			e->accept();
			int r = p.exec ( e->globalPos() );

			if ( r > 10000 )
				actionActivated(r-10000);

			return true;
		}
	}

	return false;
}

void DiscoDlg::Private::actionActivated(int id)
{
	DiscoListItem *it = (DiscoListItem *)dlg->lv_disco->selectedItem();
	if ( !it )
		return;

	emit dlg->featureActivated(Features::feature(id), it->item().jid(), it->item().node());
}

void DiscoDlg::Private::objectDestroyed(QObject *obj)
{
	if ( obj == busy )
		busy = 0;
}

void DiscoDlg::Private::setProtocol(int p)
{
	data.protocol = (DiscoData::Protocol)p;
}

//----------------------------------------------------------------------------
// DiscoDlg
//----------------------------------------------------------------------------

DiscoDlg::DiscoDlg(PsiAccount *pa, const Jid &jid, const QString &node)
: DiscoUI (0, 0, WDestructiveClose)
{
	// restore options
	ck_autoItems->setChecked(option.discoItems);
	ck_autoInfo->setChecked(option.discoInfo);

	// initialize
	d = new Private(this, pa);
	d->jid  = jid;
	d->node = node;

	setCaption(CAP(caption()));
	setIcon(is->transportStatus("transport", STATUS_ONLINE));
	X11WM_CLASS("disco");

	statusBar()->hide();

	pb_browse->setDefault(true);
	connect (pb_browse, SIGNAL(clicked()), d, SLOT(doDisco()));


	cb_address->setInsertionPolicy(QComboBox::NoInsertion);
	cb_address->insertStringList(pa->psi()->recentBrowseList()); // FIXME
	cb_address->setFocus();
	connect(cb_address, SIGNAL(activated(const QString &)), d, SLOT(doDisco()));
	cb_address->setCurrentText(d->jid.full());

	cb_node->setInsertionPolicy(QComboBox::NoInsertion);
	cb_node->insertStringList(pa->psi()->recentNodeList());
	connect(cb_node, SIGNAL(activated(const QString &)), d, SLOT(doDisco()));
	cb_node->setCurrentText(node);

	if ( pa->loggedIn() )
		doDisco();
}

DiscoDlg::~DiscoDlg()
{
	delete d;

	// save options
	option.discoItems = ck_autoItems->isChecked();
	option.discoInfo  = ck_autoInfo->isChecked();
}

void DiscoDlg::doDisco(QString host, QString node)
{
	d->doDisco(host, node);
}

#include "discodlg.moc"
