/*
   Copyright (C) 2005-2006 by Olivier Goffart <ogoffart at kde.org>
   Copyright (C) 2008 by Dmitry Suzdalev <dimsuz@gmail.com>

   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, 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

 */

#include "notifybypopup.h"
#include "knotifyconfig.h"

#include <kdebug.h>
#include <kpassivepopup.h>
#include <kiconloader.h>
#include <kdialog.h>
#include <khbox.h>
#include <kvbox.h>
#include <QLabel>
#include <QTextDocument>
#include <QApplication>
#include <QDesktopWidget>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <kconfiggroup.h>

static const QString dbusServiceName = "org.kde.VisualNotifications";
static const QString dbusInterfaceName = "org.kde.VisualNotifications";
static const QString dbusPath = "/VisualNotifications";

NotifyByPopup::NotifyByPopup(QObject *parent) 
  : KNotifyPlugin(parent) , m_animationTimer(0), m_dbusServiceExists(false)
{
	QRect screen = QApplication::desktop()->availableGeometry();
	m_nextPosition = screen.top();

	// check if service already exists on plugin instantiation
	QDBusConnectionInterface* interface = QDBusConnection::sessionBus().interface();
	m_dbusServiceExists = interface && interface->isServiceRegistered(dbusServiceName);

	if( m_dbusServiceExists )
		kDebug(300) << "using" << dbusServiceName << "for popups";

	// to catch register/unregister events from service in runtime
	connect(interface, SIGNAL(serviceOwnerChanged(const QString&, const QString&, const QString&)),
			SLOT(slotServiceOwnerChanged(const QString&, const QString&, const QString&)));
}


NotifyByPopup::~NotifyByPopup()
{
	foreach(KPassivePopup *p,m_popups)
		p->deleteLater();
}

void NotifyByPopup::notify( int id, KNotifyConfig * config )
{
	kDebug(300) << id;

	// if Notifications DBus service exists on bus,
	// it'll be used instead
	if(m_dbusServiceExists)
	{
		sendNotificationDBus(id, 0, config);
		return;
	}

	if(m_popups.contains(id))
	{
		//the popup is already shown
		finish(id);
		return;
	}

	KPassivePopup *pop = new KPassivePopup( config->winId );
	m_popups[id]=pop;
	fillPopup(pop,id,config);
	QRect screen = QApplication::desktop()->availableGeometry();
	pop->setAutoDelete( true );
	connect(pop, SIGNAL(destroyed()) , this, SLOT(slotPopupDestroyed()) );

	// NOTE readEntry here is not KConfigGroup::readEntry - this is a custom class
	// It returns QString.
	QString timeoutStr = config->readEntry( "Timeout" );
	pop->setTimeout( !timeoutStr.isEmpty() ? timeoutStr.toInt() : 0 );

	pop->show(QPoint(screen.left() + screen.width()/2  , m_nextPosition));
	m_nextPosition+=pop->height();
}

void NotifyByPopup::slotPopupDestroyed( )
{
	const QObject *s=sender();
	if(!s)
		return;
	QMap<int,KPassivePopup*>::iterator it;
	for(it=m_popups.begin() ; it!=m_popups.end(); ++it   )
	{
		QObject *o=it.value();
		if(o && o == s)
		{
			finish(it.key());
			m_popups.remove(it.key());
			break;
		}
	}

	//relocate popup
	if(!m_animationTimer)
		m_animationTimer = startTimer(10);
}

void NotifyByPopup::timerEvent(QTimerEvent * event)
{
	if(event->timerId() != m_animationTimer)
		return KNotifyPlugin::timerEvent(event);
	
	bool cont=false;
	QRect screen = QApplication::desktop()->availableGeometry();
	m_nextPosition = screen.top();
	foreach(KPassivePopup *pop,m_popups)
	{
		int posy=pop->pos().y();
		if(posy > m_nextPosition)
		{
			posy=qMax(posy-5,m_nextPosition);
			m_nextPosition = posy + pop->height();
			cont = cont || posy != m_nextPosition;
			pop->move(pop->pos().x(),posy);
		}
		else
			m_nextPosition += pop->height();
	}
	if(!cont)
	{
		killTimer(m_animationTimer);
		m_animationTimer = 0;
	}
}

void NotifyByPopup::slotLinkClicked( const QString &adr )
{
	unsigned int id=adr.section("/" , 0 , 0).toUInt();
	unsigned int action=adr.section("/" , 1 , 1).toUInt();

//	kDebug(300) << id << " " << action;
        
	if(id==0 || action==0)
		return;
		
	emit actionInvoked(id,action);
}

void NotifyByPopup::close( int id )
{
	// if Notifications DBus service exists on bus,
	// it'll be used instead
	if( m_dbusServiceExists)
	{
		closeNotificationDBus(id);
		return;
	}

	delete m_popups[id];
	m_popups.remove(id);
}

void NotifyByPopup::update(int id, KNotifyConfig * config)
{
	// if Notifications DBus service exists on bus,
	// it'll be used instead
	if( m_dbusServiceExists)
	{
		sendNotificationDBus(id, id, config);
		return;
	}

	if(!m_popups.contains(id))
		return;
	KPassivePopup *p=m_popups[id];
	fillPopup(p, id, config);
}

void NotifyByPopup::fillPopup(KPassivePopup *pop,int id,KNotifyConfig * config)
{
	const QString &appname=config->appname;
	
	KConfigGroup globalgroup( &(*config->eventsfile), "Global" );
	QString iconName = globalgroup.readEntry( "IconName", appname );
	KIconLoader iconLoader( appname );
	QPixmap appIcon = iconLoader.loadIcon( iconName, KIconLoader::Small );
	QString appCaption = globalgroup.readEntry( "Name", appname );

	KVBox *vb = pop->standardView( appCaption , config->pix.isNull() ? config->text : QString() , appIcon );
	KVBox *vb2 = vb;

	if(!config->pix.isNull())
	{
		const QPixmap &pix=config->pix;
		KHBox *hb = new KHBox(vb);
		hb->setSpacing(KDialog::spacingHint());
		QLabel *pil=new QLabel(hb);
		pil->setPixmap( config->pix );
		pil->setScaledContents(true);
		if(pix.height() > 80 && pix.height() > pix.width() )
		{
			pil->setMaximumHeight(80);
			pil->setMaximumWidth(80*pix.width()/pix.height());
		}
		else if(pix.width() > 80 && pix.height() <= pix.width())
		{
			pil->setMaximumWidth(80);
			pil->setMaximumHeight(80*pix.height()/pix.width());
		}
		vb=new KVBox(hb);
		QLabel *msg = new QLabel( config->text, vb );
		msg->setAlignment( Qt::AlignLeft );
	}


	if ( !config->actions.isEmpty() )
	{
		QString linkCode=QString::fromLatin1("<p align=\"right\">");
		int i=0;
		foreach ( const QString & it , config->actions ) 
		{
			i++;
			linkCode+=QString::fromLatin1("&nbsp;<a href=\"%1/%2\">%3</a> ").arg( id ).arg( i ).arg( Qt::escape(it) );
		}
		linkCode+=QString::fromLatin1("</p>");
		QLabel *link = new QLabel(linkCode , vb );
		link->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
		link->setOpenExternalLinks(false);
		//link->setAlignment( AlignRight );
		QObject::connect(link, SIGNAL(linkActivated(const QString &)), this, SLOT(slotLinkClicked(const QString& ) ) );
		QObject::connect(link, SIGNAL(linkActivated(const QString &)), pop, SLOT(hide()));
	}

	pop->setView( vb2 );
}

void NotifyByPopup::slotServiceOwnerChanged( const QString & serviceName,
		const QString & oldOwner, const QString & newOwner )
{
	if(serviceName == dbusServiceName)
	{
		if(oldOwner.isEmpty())
		{
			// delete existing popups if any
			// NOTE: can't use qDeletaAll here, because it can happen that
			// some passive popup auto-deletes itself and slotPopupDestroyed
			// will get called *while* qDeleteAll will be iterating over QMap
			// and that slot will remove corresponding key from QMap which will
			// break qDeleteAll.
			// This foreach takes this possibility into account:
			foreach ( int id, m_popups.keys() )
				delete m_popups.value(id,0);
			m_popups.clear();

			m_dbusServiceExists = true;
			kDebug(300) << dbusServiceName << " was registered on bus, now using it to show popups";

			// not forgetting to clear old assignments if any
			m_idMap.clear();

			// connect to action invocation signals
			bool connected = QDBusConnection::sessionBus().connect(QString(), // from any service
					dbusPath,
					dbusInterfaceName,
					"ActionInvoked",
					this,
					SLOT(slotDBusNotificationActionInvoked(uint,const QString&)));
			if (!connected) {
				kDebug(300) << "warning: failed to connect to ActionInvoked dbus signal";
			}

			connected = QDBusConnection::sessionBus().connect(QString(), // from any service
					dbusPath,
					dbusInterfaceName,
					"NotificationClosed",
					this,
					SLOT(slotDBusNotificationClosed(uint,uint)));
			if (!connected) {
				kDebug(300) << "warning: failed to connect to NotificationClosed dbus signal";
			}
		}
		if(newOwner.isEmpty())
		{
			m_dbusServiceExists = false;
			// tell KNotify that all existing notifications which it sent
			// to DBus had been closed
			foreach (int id, m_idMap.keys()) {
				finished(id);
            }
			m_idMap.clear();
			kDebug(300) << dbusServiceName << " was unregistered from bus, using passive popups from now on";
		}
	}
}

void NotifyByPopup::slotDBusNotificationActionInvoked(uint dbus_id, const QString& actKey)
{
	// find out knotify id
	int id = m_idMap.key(dbus_id, 0);
	if (id == 0) {
		kDebug(300) << "failed to find knotify id for dbus_id" << dbus_id;
		return;
	}
	kDebug(300) << "action" << actKey << "invoked for notification " << id;
	// emulate link clicking
	slotLinkClicked( QString("%1/%2").arg(id).arg(actKey) );
    // now close notification - similar to popup behaviour
    // (popups are hidden after link activation - see 'connects' of linkActivated signal above)
    closeNotificationDBus(id);
}

void NotifyByPopup::slotDBusNotificationClosed(uint dbus_id, uint reason)
{
	Q_UNUSED(reason)
	// find out knotify id
	int id = m_idMap.key(dbus_id, 0);
	if (id == 0) {
		kDebug(300) << "failed to find knotify id for dbus_id" << dbus_id;
		return;
	}
	// tell KNotify that this notification has been closed
	finished(id);
}

void NotifyByPopup::sendNotificationDBus(int id, int replacesId, KNotifyConfig* config)
{
	QDBusMessage m = QDBusMessage::createMethodCall( dbusServiceName, dbusPath, dbusInterfaceName, "Notify" );

	// NOTE readEntry here is not KConfigGroup::readEntry - this is a custom class
	// It returns QString.
	QString timeoutStr = config->readEntry( "Timeout" );
	int timeout = !timeoutStr.isEmpty() ? timeoutStr.toInt() : 0;

	// if timeout is still zero, try to set it to default knotify timeout
	if (timeout == 0) {
		// NOTE: this is a little hack. Currently there's no way to easily determine
		// if KNotify will close notification after certain timeout or if it's persistent.
		// The only thing that comes to mind is to check Persistent option in config and
		// if it's absent, use default timeout that KNotify uses for non-persistent popups
		bool persistent = (config->readEntry("Persistent") == "true" ||
						   config->readEntry("Persistant") == "true");
		if (!persistent) {
			timeout = 6*1000;
		}
    }

	QList<QVariant> args;

	// figure out dbus id to replace if needed
	uint dbus_replaces_id = 0;
	if (replacesId != 0 ) {
	    dbus_replaces_id = m_idMap.value(replacesId, 0);
	}
	args.append( config->appname ); // app_name
	args.append( dbus_replaces_id ); // replaces_id
	args.append( config->appname ); // app_icon
	args.append( QString()); // summary
	args.append( config->text ); // body
	// galago spec defines action list to be list like
	// (act_id1, action1, act_id2, action2, ...)
	//
	// assign id's to actions like it's done in fillPopup() method
	// (i.e. starting from 1)
	QStringList actionList;
	int actId = 0;
	foreach (const QString& actName, config->actions) {
		actId++;
		actionList.append(QString::number(actId));
		actionList.append(actName);
	}

	args.append( actionList ); // actions
	args.append( QVariantMap() ); // hints - unused atm
	args.append( timeout ); // expire timout

	m.setArguments( args );
	QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m);
	if(replyMsg.type() == QDBusMessage::ReplyMessage) {
		if (!replyMsg.arguments().isEmpty()) {
			uint dbus_id = replyMsg.arguments().at(0).toUInt();
			m_idMap.insert(id, dbus_id);
	 		kDebug() << "mapping knotify id to dbus id:"<< id << "=>" << dbus_id;
		} else {
	 		kDebug() << "error: received reply with no arguments";
		}
	} else if (replyMsg.type() == QDBusMessage::ErrorMessage) {
		kDebug() << "error: failed to send dbus message";
	} else {
		kDebug() << "unexpected reply type";
	}
}

void NotifyByPopup::closeNotificationDBus(int id)
{
	uint dbus_id = m_idMap.value(id, 0);
	if (dbus_id == 0) {
	    kDebug() << "not found dbus id to close";
	    return;
	}

	QDBusMessage m = QDBusMessage::createMethodCall( dbusServiceName, dbusPath, 
            dbusInterfaceName, "CloseNotification" );
	QList<QVariant> args;
	args.append( dbus_id );
	m.setArguments( args );
	bool queued = QDBusConnection::sessionBus().send(m);
	if(!queued)
	{
		kDebug() << "warning: failed to queue dbus message";
	}
}

#include "notifybypopup.moc"
