/* KPilot
**
** Copyright (C) 2001 by Dan Pilone
** Copyright (C) 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
**
** This file defines the base class of all KPilot conduit plugins configuration
** dialogs. This is necessary so that we have a fixed API to talk to from
** inside KPilot.
**
** The factories used by KPilot plugins are also documented here.
*/

/*
** This program 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 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 Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public License
** along with this program in a file called COPYING; if not, write to
** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
** MA 02110-1301, USA.
*/

/*
** Bug reports and questions can be sent to kde-pim@kde.org
*/

#include "options.h"

#include <stdlib.h>

#include <tqdir.h>
#include <tqfileinfo.h>
#include <tqhbox.h>
#include <tqlabel.h>
#include <tqlayout.h>
#include <tqpushbutton.h>
#include <tqregexp.h>
#include <tqstringlist.h>
#include <tqtabwidget.h>
#include <tqtextview.h>
#include <tqtimer.h>

#include <dcopclient.h>
#include <tdeaboutapplication.h>
#include <kactivelabel.h>
#include <tdeapplication.h>
#include <tdeglobal.h>
#include <kiconloader.h>
#include <tdeinstance.h>
#include <klibloader.h>
#include <tdemessagebox.h>
#include <kservice.h>
#include <kservicetype.h>
#include <tdestandarddirs.h>

#include "pilotSerialDatabase.h"
#include "pilotLocalDatabase.h"

#include "plugin.moc"

ConduitConfigBase::ConduitConfigBase(TQWidget *parent,
	const char *name) :
	TQObject(parent,name),
	fModified(false),
	fWidget(0L),
	fConduitName(i18n("Unnamed"))
{
	FUNCTIONSETUP;
}

ConduitConfigBase::~ConduitConfigBase()
{
	FUNCTIONSETUP;
}

/* slot */ void ConduitConfigBase::modified()
{
	fModified=true;
	emit changed(true);
}

/* virtual */ TQString ConduitConfigBase::maybeSaveText() const
{
	FUNCTIONSETUP;

	return i18n("<qt>The <i>%1</i> conduit's settings have been changed. Do you "
		"want to save the changes before continuing?</qt>").arg(this->conduitName());
}

/* virtual */ bool ConduitConfigBase::maybeSave()
{
	FUNCTIONSETUP;

	if (!isModified()) return true;

	int r = KMessageBox::questionYesNoCancel(fWidget,
		maybeSaveText(),
		i18n("%1 Conduit").arg(this->conduitName()), KStdGuiItem::save(), KStdGuiItem::discard());
	if (r == KMessageBox::Cancel) return false;
	if (r == KMessageBox::Yes) commit();
	return true;
}

TQWidget *ConduitConfigBase::aboutPage(TQWidget *parent, TDEAboutData *ad)
{
	FUNCTIONSETUP;

	TQWidget *w = new TQWidget(parent, "aboutpage");

	TQString s;
	TQLabel *text;
	TDEIconLoader *l = TDEGlobal::iconLoader();
	const TDEAboutData *p = ad ? ad : TDEGlobal::instance()->aboutData();

	TQGridLayout *grid = new TQGridLayout(w, 5, 4, SPACING);

	grid->addColSpacing(0, SPACING);
	grid->addColSpacing(4, SPACING);


	TQPixmap applicationIcon =
		l->loadIcon(TQString::fromLatin1(p->appName()),
		TDEIcon::Desktop,
		64, TDEIcon::DefaultState, 0L,
		true);

	if (applicationIcon.isNull())
	{
		applicationIcon = l->loadIcon(TQString::fromLatin1("kpilot"),
			TDEIcon::Desktop);
	}

	text = new TQLabel(w);
	// Experiment with a long non-<qt> string. Use that to find
	// sensible widths for the columns.
	//
	text->setText(i18n("Send questions and comments to tdepim-users@kde.org"));
	text->adjustSize();

	int linewidth = text->size().width();
	int lineheight = text->size().height();

	// Use the label to display the applciation icon
	text->setText(TQString());
	text->setPixmap(applicationIcon);
	text->adjustSize();
	grid->addWidget(text, 0, 1);


	KActiveLabel *linktext = new KActiveLabel(w);
	grid->addRowSpacing(1,kMax(100,6*lineheight));
	grid->addRowSpacing(2,kMax(100,6*lineheight));
	grid->addColSpacing(2,SPACING+linewidth/2);
	grid->addColSpacing(3,SPACING+linewidth/2);
	grid->setRowStretch(1,50);
	grid->setRowStretch(2,50);
	grid->setColStretch(2,50);
	grid->setColStretch(3,50);
	linktext->setMinimumSize(linewidth,kMax(260,60+12*lineheight));
	linktext->setFixedHeight(kMax(260,60+12*lineheight));
	linktext->setVScrollBarMode(TQScrollView::Auto/*AlwaysOn*/);
	text = new TQLabel(w);
	grid->addMultiCellWidget(text,0,0,2,3);
	grid->addMultiCellWidget(linktext,1,2,1,3);

	// Now set the program and copyright information.
	s = CSL1("<qt><h3>");
	s += p->programName();
	s += ' ';
	s += p->version();
	s += CSL1("</h3>");
	s += p->copyrightStatement() + CSL1("<br></qt>");
	text->setText(s);

	linktext->append(p->shortDescription() + CSL1("<br>"));

	if (!p->homepage().isEmpty())
	{
		s = TQString();
		s += CSL1("<a href=\"%1\">").arg(p->homepage());
		s += p->homepage();
		s += CSL1("</a><br>");
		linktext->append(s);
	}

	s = TQString();
	s += i18n("Send questions and comments to <a href=\"mailto:%1\">%2</a>.")
		.arg( CSL1("tdepim-users@kde.org") )
		.arg( CSL1("tdepim-users@kde.org") );
	s += ' ';
	s += i18n("Send bug reports to <a href=\"mailto:%1\">%2</a>.")
		.arg(p->bugAddress())
		.arg(p->bugAddress());
	s += ' ';
	s += i18n("For trademark information, see the "
		"<a href=\"help:/kpilot/trademarks.html\">KPilot User's Guide</a>.");
	s += CSL1("<br>");
	linktext->append(s);
	linktext->append(TQString());



	TQValueList<TDEAboutPerson> pl = p->authors();
	TQValueList<TDEAboutPerson>::ConstIterator i;

	s = i18n("<b>Authors:</b> ");

	TQString comma = CSL1(", ");

	unsigned int count=1;
	for (i=pl.begin(); i!=pl.end(); ++i)
	{
		s.append(CSL1("%1 (<i>%2</i>)%3")
			.arg((*i).name())
			.arg((*i).task())
			.arg(count<pl.count() ? comma : TQString())
			);
		count++;
	}
	linktext->append(s);

	s = TQString();
	pl = p->credits();
	if (pl.count()>0)
	{
		count=1;
		s.append(i18n("<b>Credits:</b> "));
		for (i=pl.begin(); i!=pl.end(); ++i)
		{
			s.append(CSL1("%1 (<i>%2</i>)%3")
				.arg((*i).name())
				.arg((*i).task())
				.arg(count<pl.count() ? comma : TQString())
				);
			count++;
		}
	}
	linktext->append(s);
	linktext->ensureVisible(0,0);

	w->adjustSize();

	return w;
}

/* static */ void ConduitConfigBase::addAboutPage(TQTabWidget *tw,
	TDEAboutData *ad)
{
	FUNCTIONSETUP;

	Q_ASSERT(tw);

	TQWidget *w = aboutPage(tw,ad);
	TQSize sz = w->size();

	if (sz.width() < tw->size().width())
	{
		sz.setWidth(tw->size().width());
	}
	if (sz.height() < tw->size().height())
	{
		sz.setHeight(tw->size().height());
	}

	tw->resize(sz);
	tw->addTab(w, i18n("About"));
	tw->adjustSize();
}



ConduitAction::ConduitAction(KPilotLink *p,
	const char *name,
	const TQStringList &args) :
	SyncAction(p,name),
	fDatabase(0L),
	fLocalDatabase(0L),
	fCtrHH(0L),
	fCtrPC(0L), 
	fSyncDirection(args),
	fConflictResolution(SyncAction::eAskUser),
	fFirstSync(false)
{
	FUNCTIONSETUP;

	TQString cResolution(args.grep(TQRegExp(CSL1("--conflictResolution \\d*"))).first());
	if (cResolution.isEmpty())
	{
		fConflictResolution=(SyncAction::ConflictResolution)
			cResolution.replace(TQRegExp(CSL1("--conflictResolution (\\d*)")), CSL1("\\1")).toInt();
	}

	for (TQStringList::ConstIterator it = args.begin();
		it != args.end();
		++it)
	{
		DEBUGKPILOT << fname << ": " << *it << endl;
	}

	DEBUGKPILOT << fname << ": Direction=" << fSyncDirection.name() << endl;
	fCtrHH = new CUDCounter(i18n("Handheld"));
	fCtrPC = new CUDCounter(i18n("PC")); 
}

/* virtual */ ConduitAction::~ConduitAction()
{
	FUNCTIONSETUP;
	
	KPILOT_DELETE(fDatabase);
	KPILOT_DELETE(fLocalDatabase);

	KPILOT_DELETE(fCtrHH);
	KPILOT_DELETE(fCtrPC);
}

bool ConduitAction::openDatabases(const TQString &name, bool *retrieved)
{
	FUNCTIONSETUP;

	DEBUGKPILOT << fname
		<< ": Trying to open database "
		<< name << endl;
	DEBUGKPILOT << fname
		<< ": Mode="
		<< (syncMode().isTest() ? "test " : "")
		<< (syncMode().isLocal() ? "local " : "")
		<< endl ;

	KPILOT_DELETE(fLocalDatabase);

	TQString localPathName = PilotLocalDatabase::getDBPath() + name;

	// we always want to use the conduits/ directory for our local
	// databases. this keeps our backups and data that our conduits use
	// for record keeping separate
	localPathName.replace(CSL1("DBBackup/"), CSL1("conduits/"));

	DEBUGKPILOT << fname << ": localPathName: [" << localPathName 
		<< "]" << endl;

	PilotLocalDatabase *localDB = new PilotLocalDatabase( localPathName );

	if (!localDB)
	{
		WARNINGKPILOT << "Could not initialize object for local copy of database \""
			<< name
			<< "\"" << endl;
		if (retrieved) *retrieved = false;
		return false;
	}

	// if there is no backup db yet, fetch it from the palm, open it and set the full sync flag.
	if (!localDB->isOpen() )
	{
		TQString dbpath(localDB->dbPathName());
		KPILOT_DELETE(localDB);
		DEBUGKPILOT << fname
			<< ": Backup database " << dbpath
			<< " not found." << endl;
		struct DBInfo dbinfo;

// TODO Extend findDatabase() with extra overload?
		if (deviceLink()->findDatabase(Pilot::toPilot( name ), &dbinfo)<0 )
		{
			WARNINGKPILOT << "Could not get DBInfo for " << name << endl;
			if (retrieved) *retrieved = false;
			return false;
		}

		DEBUGKPILOT << fname
				<< ": Found Palm database: " << dbinfo.name <<endl
				<< fname << ": type = " << dbinfo.type
				<< " creator = " << dbinfo.creator
				<< " version = " << dbinfo.version
				<< " index = " << dbinfo.index << endl;
		dbinfo.flags &= ~dlpDBFlagOpen;

		// make sure the dir for the backup db really exists!
		TQFileInfo fi(dbpath);
		TQString path(TQFileInfo(dbpath).dir(true).absPath());
		if (!path.endsWith(CSL1("/"))) path.append(CSL1("/"));
		if (!TDEStandardDirs::exists(path))
		{
			DEBUGKPILOT << fname << ": Trying to create path for database: <"
				<< path << ">" << endl;
			TDEStandardDirs::makeDir(path);
		}
		if (!TDEStandardDirs::exists(path))
		{
			DEBUGKPILOT << fname << ": Database directory does not exist." << endl;
			if (retrieved) *retrieved = false;
			return false;
		}

		if (!deviceLink()->retrieveDatabase(dbpath, &dbinfo) )
		{
			WARNINGKPILOT << "Could not retrieve database "
				<< name << " from the handheld." << endl;
			if (retrieved) *retrieved = false;
			return false;
		}
		localDB = new PilotLocalDatabase( localPathName );
		if (!localDB || !localDB->isOpen())
		{
			WARNINGKPILOT << "local backup of database " << name << " could not be initialized." << endl;
			if (retrieved) *retrieved = false;
			return false;
		}
		if (retrieved) *retrieved=true;
	}
	fLocalDatabase = localDB;

	fDatabase = deviceLink()->database( name );

	if (!fDatabase)
	{
		WARNINGKPILOT << "Could not open database \""
			<< name
			<< "\" on the pilot."
			<< endl;
	}
	else
	{
		fCtrHH->setStartCount(fDatabase->recordCount());
	}

	return (fDatabase && fDatabase->isOpen() &&
	        fLocalDatabase && fLocalDatabase->isOpen() );
}


bool ConduitAction::changeSync(SyncMode::Mode m)
{
	FUNCTIONSETUP;

	if ( fSyncDirection.isSync() && SyncMode::eFullSync == m)
	{
		fSyncDirection.setMode(m);
		return true;
	}
	return false;
}

void ConduitAction::finished()
{
	FUNCTIONSETUP;

	if (fDatabase && fCtrHH)
		fCtrHH->setEndCount(fDatabase->recordCount());

	if (fCtrHH && fCtrPC) 
	{
		addSyncLogEntry(fCtrHH->moo() +"\n",false); 
		DEBUGKPILOT << fname << ": " << fCtrHH->moo() << endl;
		addSyncLogEntry(fCtrPC->moo() +"\n",false);
		DEBUGKPILOT << fname << ": " << fCtrPC->moo() << endl;

		// STEP2 of making sure we don't delete our little user's
		// precious data...
		// sanity checks for handheld...
		int hhVolatility = fCtrHH->percentDeleted() + 
				 fCtrHH->percentUpdated() +
		    		 fCtrHH->percentCreated();

		int pcVolatility = fCtrPC->percentDeleted() + 
				 fCtrPC->percentUpdated() +
		    		 fCtrPC->percentCreated();

		// TODO: allow user to configure this...
		// this is a percentage...
		int allowedVolatility = 70;

		TQString caption = i18n("Large Changes Detected");
		// args are already i18n'd
		TQString query = i18n("The %1 conduit has made a "
			"large number of changes to your %2.  Do you want "
			"to allow this change?\nDetails:\n\t%3");

		if (hhVolatility > allowedVolatility)
		{
			query = query.arg(fConduitName)
				.arg(fCtrHH->type()).arg(fCtrHH->moo());

			DEBUGKPILOT << fname << ": Yikes, lots of volatility "
				<< "caught.  Check with user: [" << query
				<< "]." << endl;

			/*
			int rc = questionYesNo(query, caption,
				TQString(), 0 );
			if (rc == KMessageBox::Yes)
			{
				// TODO: add commit and rollback code.
				// note: this will require some thinking,
				// since we have to undo changes to the
				// pilot databases, changes to the PC
				// resources, changes to the mappings files
				// (record id mapping, etc.)
			}
			*/
		}


	}

}


ConduitProxy::ConduitProxy(KPilotLink *p,
	const TQString &name,
	const SyncAction::SyncMode &m) :
	ConduitAction(p,name.latin1(),m.list()),
	fDesktopName(name)
{
	FUNCTIONSETUP;
}

/* virtual */ bool ConduitProxy::exec()
{
	FUNCTIONSETUP;

	// query that service
	TDESharedPtr < KService > o = KService::serviceByDesktopName(fDesktopName);
	if (!o)
	{
		WARNINGKPILOT << "Can't find desktop file for conduit "
			<< fDesktopName
			<< endl;
		addSyncLogEntry(i18n("Could not find conduit %1.").arg(fDesktopName));
		return false;
	}


	// load the lib
	fLibraryName = o->library();
	DEBUGKPILOT << fname
		<< ": Loading desktop "
		<< fDesktopName
		<< " with lib "
		<< fLibraryName
		<< endl;

	KLibrary *library = KLibLoader::self()->library(
		TQFile::encodeName(fLibraryName));
	if (!library)
	{
		WARNINGKPILOT << "Can't load library "
			<< fLibraryName
			<< " - "
			<< KLibLoader::self()->lastErrorMessage()
			<< endl;
		addSyncLogEntry(i18n("Could not load conduit %1.").arg(fDesktopName));
		return false;
	}

	unsigned long version = PluginUtility::pluginVersion(library);
	if ( Pilot::PLUGIN_API != version )
	{
		WARNINGKPILOT << "Library "
			<< fLibraryName
			<< " has version "
			<< version
			<< endl;
		addSyncLogEntry(i18n("Conduit %1 has wrong version (%2).").arg(fDesktopName).arg(version));
		return false;
	}

	KLibFactory *factory = library->factory();
	if (!factory)
	{
		WARNINGKPILOT << "Can't find factory in library "
			<< fLibraryName
			<< endl;
		addSyncLogEntry(i18n("Could not initialize conduit %1.").arg(fDesktopName));
		return false;
	}

	TQStringList l = syncMode().list();

	DEBUGKPILOT << fname << ": Flags: " << syncMode().name() << endl;

	TQObject *object = factory->create(fHandle,name(),"SyncAction",l);

	if (!object)
	{
		WARNINGKPILOT << "Can't create SyncAction." << endl;
		addSyncLogEntry(i18n("Could not create conduit %1.").arg(fDesktopName));
		return false;
	}

	fConduit = dynamic_cast<ConduitAction *>(object);

	if (!fConduit)
	{
		WARNINGKPILOT << "Can't cast to ConduitAction." << endl;
		addSyncLogEntry(i18n("Could not create conduit %1.").arg(fDesktopName));
		return false;
	}

	addSyncLogEntry(i18n("[Conduit %1]").arg(fDesktopName));

	// Handle the syncDone signal properly & unload the conduit.
	TQObject::connect(fConduit,TQ_SIGNAL(syncDone(SyncAction *)),
		this,TQ_SLOT(execDone(SyncAction *)));
	// Proxy all the log and error messages.
	TQObject::connect(fConduit,TQ_SIGNAL(logMessage(const TQString &)),
		this,TQ_SIGNAL(logMessage(const TQString &)));
	TQObject::connect(fConduit,TQ_SIGNAL(logError(const TQString &)),
		this,TQ_SIGNAL(logError(const TQString &)));
	TQObject::connect(fConduit,TQ_SIGNAL(logProgress(const TQString &,int)),
		this,TQ_SIGNAL(logProgress(const TQString &,int)));

	TQTimer::singleShot(0,fConduit,TQ_SLOT(execConduit()));
	return true;
}

void ConduitProxy::execDone(SyncAction *p)
{
	FUNCTIONSETUP;

	if (p!=fConduit)
	{
		WARNINGKPILOT << "Unknown conduit @"
			<< (void *) p
			<< " finished."
			<< endl;
		emit syncDone(this);
		return;
	}

	// give our worker a chance to sanity check the results...
	fConduit->finished();

	addSyncLogEntry(CSL1("\n"),false); // Put bits of the conduit logs on separate lines

	KPILOT_DELETE(p);

	emit syncDone(this);
}


namespace PluginUtility
{

TQString findArgument(const TQStringList &a, const TQString &arg)
{
	FUNCTIONSETUP;

	TQString search;

	if (arg.startsWith( CSL1("--") ))
	{
		search = arg;
	}
	else
	{
		search = CSL1("--") + arg;
	}
	search.append( CSL1("=") );


	TQStringList::ConstIterator end = a.end();
	for (TQStringList::ConstIterator i = a.begin(); i != end; ++i)
	{
		if ((*i).startsWith( search ))
		{
			TQString s = (*i).mid(search.length());
			return s;
		}
	}

	return TQString();
}

/* static */ bool isRunning(const TQCString &n)
{
	DCOPClient *dcop = tdeApp->dcopClient();
	QCStringList apps = dcop->registeredApplications();
	return apps.contains(n);
}


/* static */ unsigned long pluginVersion(const KLibrary *lib)
{
	TQString symbol = CSL1("version_");
	symbol.append(lib->name());

	if (!lib->hasSymbol(symbol.latin1())) return 0;

	unsigned long *p = (unsigned long *)(lib->symbol(symbol.latin1()));
	return *p;
}


/* static */ TQString pluginVersionString(const KLibrary *lib)
{
	TQString symbol= CSL1("id_");
	symbol.append(lib->name());

	if (!lib->hasSymbol(symbol.latin1())) return TQString();

	return TQString::fromLatin1(*((const char **)(lib->symbol(symbol.latin1()))));
}


}


CUDCounter::CUDCounter(TQString s) :
	fC(0),fU(0),fD(0),fStart(0),fEnd(0),fType(s)
{
}

void CUDCounter::created(unsigned int c)
{
	fC += c;
}

void CUDCounter::updated(unsigned int c)
{
	fU += c;
}

void CUDCounter::deleted(unsigned int c)
{
	fD += c;
}

void CUDCounter::setStartCount(unsigned int t)
{
	fStart = t;
}

void CUDCounter::setEndCount(unsigned int t)
{
	fEnd = t;
}

TQString CUDCounter::moo() const
{
	TQString result = fType + ": " + 
		i18n("Start: %1. End: %2. ").arg(fStart).arg(fEnd);

	if (fC > 0) result += i18n("%1 new. ").arg(fC);
	if (fU > 0) result += i18n("%1 changed. ").arg(fU);
	if (fD > 0) result += i18n("%1 deleted. ").arg(fD);

	if ( (fC+fU+fD) <= 0) result += i18n("No changes made. ");

	return result;
}


