/*
 *  This file is part of the KDE libraries
 *  Copyright (c) 2001 Michael Goffioul <kdeprint@swing.be>
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library General Public
 *  License version 2 as published by the Free Software Foundation.
 *
 *  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
 *  Library General Public License for more details.
 *
 *  You should have received a copy of the GNU Library General Public License
 *  along with this library; see the file COPYING.LIB.  If not, write to
 *  the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 *  Boston, MA 02111-1307, USA.
 **/

#include "kxmlcommand.h"
#include "driver.h"
#include "kmfactory.h"
#include "kdeprintcheck.h"
#include "driverview.h"

#include <qfile.h>
#include <qregexp.h>
#include <qdir.h>
#include <qinputdialog.h>
#include <qmap.h>
#include <qvaluelist.h>
#include <kstandarddirs.h>
#include <klocale.h>
#include <ksimpleconfig.h>
#include <kdialogbase.h>
#include <kdebug.h>
#include <kprocess.h>
#include <klibloader.h>
#include <kmessagebox.h>

static void setOptionText(DrBase *opt, const QString& s)
{
	if (s.isEmpty())
		opt->set("text", opt->name());
	else
		opt->set("text", i18n(s.utf8()));
}

class KXmlCommand::KXmlCommandPrivate
{
public:
	QString	m_name;
	QString	m_command;
	DrMain	*m_driver;
	struct
	{
		QString	m_format[2];	// 0 -> file, 1 -> pipe
	}	m_io[2];				// 0 -> input, 1 -> output
	QString	m_description;
	QString	m_outputMime;
	QStringList	m_inputMime;
	QStringList	m_requirements;
	bool	m_loaded[2];	// 0 -> Desktop, 1 -> XML
	QString m_comment;
};

KXmlCommand::KXmlCommand(const QString& xmlId)
: QObject(KXmlCommandManager::self(), "XmlCommand")
{
	init();
	d->m_name = xmlId;
}

KXmlCommand::~KXmlCommand()
{
	//kdDebug(500) << "deleting driver" << endl;
	delete d->m_driver;
	//kdDebug(500) << "deleting private data" << endl;
	delete d;
	//kdDebug(500) << "finished" << endl;
}

void KXmlCommand::init()
{
	d = new KXmlCommandPrivate;
	d->m_driver = 0;
	d->m_loaded[0] = d->m_loaded[1] = false;
}

QString KXmlCommand::name() const
{ return d->m_name; }

void KXmlCommand::setName(const QString& s)
{ d->m_name = s; }

QString KXmlCommand::command()
{
	check(true);
	return d->m_command;
}

void KXmlCommand::setCommand(const QString& s)
{
	d->m_command = s;
}

DrMain* KXmlCommand::driver()
{
	check(true);
	return d->m_driver;
}

DrMain* KXmlCommand::takeDriver()
{
	check(true);
	DrMain	*dr = d->m_driver;
	d->m_driver = 0;
	d->m_loaded[1] = false;
	return dr;
}

void KXmlCommand::setDriver(DrMain *driver)
{
	delete d->m_driver;
	d->m_driver = driver;
}

QString KXmlCommand::io(bool io_input, bool io_pipe)
{
	check(true);
	return d->m_io[(io_input?0:1)].m_format[(io_pipe?1:0)];
}

void KXmlCommand::setIo(const QString& s, bool io_input, bool io_pipe)
{
	d->m_io[(io_input?0:1)].m_format[(io_pipe?1:0)] = s;
}

QString KXmlCommand::description()
{
	check();
	return d->m_description;
}

void KXmlCommand::setDescription(const QString& s)
{
	d->m_description = s;
}

QString KXmlCommand::mimeType()
{
	check();
	return d->m_outputMime;
}

void KXmlCommand::setMimeType(const QString& s)
{
	d->m_outputMime = s;
}

bool KXmlCommand::acceptMimeType(const QString& s)
{
	check();
	return (d->m_inputMime.find(s) != d->m_inputMime.end());
}

QStringList KXmlCommand::inputMimeTypes()
{
	check();
	return d->m_inputMime;
}

void KXmlCommand::setInputMimeTypes(const QStringList& l)
{
	d->m_inputMime = l;
}

QStringList KXmlCommand::requirements()
{
	check();
	return d->m_requirements;
}

void KXmlCommand::setRequirements(const QStringList& l)
{
	d->m_requirements = l;
}

QString KXmlCommand::comment()
{
	check();
	return d->m_comment;
}

void KXmlCommand::setComment( const QString& s )
{
	d->m_comment = s;
}

bool KXmlCommand::isValid()
{
	return (!locate("data", "kdeprint/filters/"+name()+".desktop").isEmpty());
}

void KXmlCommand::check(bool use_xml)
{
	if (!d->m_loaded[0])
	{
		loadDesktop();
		d->m_loaded[0] = true;
	}
	if (use_xml && !d->m_loaded[1])
	{
		loadXml();
		d->m_loaded[1] = true;
	}
}

void KXmlCommand::loadDesktop()
{
	KSimpleConfig	conf(locate("data", "kdeprint/filters/"+name()+".desktop"));
	conf.setGroup("KDE Print Filter Entry");
	d->m_description = conf.readEntry("Comment");
	d->m_outputMime = conf.readEntry("MimeTypeOut");
	d->m_inputMime = conf.readListEntry("MimeTypeIn");
	d->m_requirements = conf.readListEntry("Require");
	d->m_comment = conf.readEntry( "Description" );
}

void KXmlCommand::saveDesktop()
{
	KSimpleConfig	conf(locateLocal("data", "kdeprint/filters/"+name()+".desktop"));
	conf.setGroup("KDE Print Filter Entry");
	conf.writeEntry("Comment", d->m_description);
	conf.writeEntry("MimeTypeIn", d->m_inputMime);
	conf.writeEntry("MimeTypeOut", d->m_outputMime);
	conf.writeEntry("Require", d->m_requirements);
	conf.writeEntry( "Description", d->m_comment );
}

void KXmlCommand::loadXml()
{
	QFile	f(locate("data", "kdeprint/filters/"+name()+".xml"));
	QDomDocument	doc;
	if (f.open(IO_ReadOnly) && doc.setContent(&f) && doc.documentElement().tagName() == "kprintfilter")
	{
		QDomElement	e, docElem = doc.documentElement();
		d->m_name = docElem.attribute("name");

		// command
		e = docElem.namedItem("filtercommand").toElement();
		if (!e.isNull())
			d->m_command = e.attribute("data");

		// arguments
		e = docElem.namedItem("filterargs").toElement();
		if (!e.isNull())
		{
			d->m_driver = new DrMain;
			d->m_driver->setName(d->m_name);
			parseGroup(e, d->m_driver);
			setOptionText(d->m_driver, d->m_description);
		}

		// input/output
		e = docElem.namedItem("filterinput").toElement();
		if (!e.isNull())
			parseIO(e, 0);
		e = docElem.namedItem("filteroutput").toElement();
		if (!e.isNull())
			parseIO(e, 1);
	}
}

void KXmlCommand::parseIO(const QDomElement& e, int n)
{
	QDomElement	elem = e.firstChild().toElement();
	while (!elem.isNull())
	{
		if (elem.tagName() == "filterarg")
		{
			int	format = (elem.attribute("name") == "file" ? 0 : 1);
			d->m_io[n].m_format[format] = elem.attribute("format");
		}
		elem = elem.nextSibling().toElement();
	}
}

DrGroup* KXmlCommand::parseGroup(const QDomElement& e, DrGroup *grp)
{
	if (!grp)
		grp = new DrGroup;
	grp->setName(e.attribute("name"));
	setOptionText(grp, e.attribute("description"));

	QDomElement	elem = e.firstChild().toElement();
	while (!elem.isNull())
	{
		if (elem.tagName() == "filterarg")
		{
			DrBase	*opt = parseArgument(elem);
			if (opt)
				grp->addOption(opt);
		}
		else if (elem.tagName() == "filtergroup")
		{
			DrGroup	*group = parseGroup(elem, 0);
			if (group)
				grp->addGroup(group);
		}
		elem = elem.nextSibling().toElement();
	}

	return grp;
}

DrBase* KXmlCommand::parseArgument(const QDomElement& e)
{
	DrBase	*opt(0);
	QString	type = e.attribute("type");

	if (type == "int" || type == "float")
	{
		if (type == "int")
			opt = new DrIntegerOption;
		else
			opt = new DrFloatOption;
		opt->set("minval", e.attribute("min"));
		opt->set("maxval", e.attribute("max"));
	}
	else if (type == "string")
		opt = new DrStringOption;
	else if (type == "list" || type == "bool")
	{
		if (type == "list")
			opt = new DrListOption;
		else
			opt = new DrBooleanOption;
		DrListOption	*lopt = static_cast<DrListOption*>(opt);
		QDomElement	elem = e.firstChild().toElement();
		while (!elem.isNull())
		{
			if (elem.tagName() == "value")
			{
				DrBase	*choice = new DrBase;
				choice->setName(elem.attribute("name"));
				setOptionText(choice, elem.attribute("description"));
				lopt->addChoice(choice);
			}
			elem = elem.nextSibling().toElement();
		}
	}
	else
		return 0;

	opt->setName("_kde-" + d->m_name + "-" + e.attribute("name"));
	setOptionText(opt, e.attribute("description"));
	opt->set("format", e.attribute("format"));
	opt->set("default", e.attribute("default"));
	opt->set( "persistent", e.attribute( "persistent" ) );
	opt->setValueText(opt->get("default"));

	return opt;
}

QString KXmlCommand::buildCommand(const QMap<QString,QString>& opts, bool pipein, bool pipeout)
{
	check(true);

	QString		str, cmd = d->m_command;
	QString re( "%value" ), quotedRe( "'%value'" );

	if (d->m_driver)
	{
		QMap<QString,QString>	fopts;

		d->m_driver->setOptions(opts);
		d->m_driver->getOptions(fopts, false);
		for (QMap<QString,QString>::ConstIterator it=fopts.begin(); it!=fopts.end(); ++it)
		{
			DrBase	*dopt = d->m_driver->findOption(it.key());
			if (dopt)
			{
				QString	format = dopt->get("format");
				QString value = dopt->valueText();
				if ( format.find( quotedRe ) != -1 )
				{
					if ( ( value.right( 1 ) == "'" && value.left( 1 ) == "'" )  ||
					     ( value.right( 1 ) == "\"" && value.left( 1 ) == "\"" ) )
						format.replace( quotedRe, value );
					else
						format.replace( re, value );
				}
				else
				{
					format.replace( re, KProcess::quote( dopt->valueText() ) );
				}
				str.append(format).append(" ");
			}
		}
		cmd.replace("%filterargs", str);
	}

	cmd.replace("%filterinput", d->m_io[0].m_format[(pipein?1:0)]);
	cmd.replace("%filteroutput", d->m_io[1].m_format[(pipeout?1:0)]);

	return cmd;
}

void KXmlCommand::setOptions(const QMap<QString,QString>& opts)
{
	if (opts.count() == 0)
		return;
	// force loading the driver if needed
	if (driver())
		d->m_driver->setOptions(opts);
}

void KXmlCommand::getOptions(QMap<QString,QString>& opts, bool incldef)
{
	// force loading the driver
	if (driver())
		d->m_driver->getOptions(opts, incldef);
}

void KXmlCommand::saveXml()
{
	QFile	f(locateLocal("data", "kdeprint/filters/"+name()+".xml"));
	if (!f.open(IO_WriteOnly))
		return;

	QDomDocument	doc("kprintfilter");
	QDomElement	root = doc.createElement("kprintfilter"), elem;

	root.setAttribute("name", d->m_name);
	doc.appendChild(root);

	// command
	elem = doc.createElement("filtercommand");
	elem.setAttribute("data", d->m_command);
	root.appendChild(elem);

	// options
	if (d->m_driver)
	{
		elem = createGroup(doc, d->m_driver);
		elem.setTagName("filterargs");
		root.appendChild(elem);
	}

	// IO
	if (!(elem=createIO(doc, 0, "filterinput")).isNull())
		root.appendChild(elem);
	if (!(elem=createIO(doc, 1, "filteroutput")).isNull())
		root.appendChild(elem);

	// save to file (and close it)
	QTextStream	t(&f);
	t << doc.toString();
	f.close();
}

QDomElement KXmlCommand::createIO(QDomDocument& doc, int n, const QString& tag)
{
	QDomElement	elem = doc.createElement(tag);
	if (d->m_command.find("%"+tag) != -1)
	{
		for (int i=0; i<2; i++)
		{
			QDomElement	io = doc.createElement("filterarg");
			io.setAttribute("name", (i ? "pipe" : "file"));
			io.setAttribute("format", d->m_io[n].m_format[i]);
			elem.appendChild(io);
		}
	}

	return elem;
}

QDomElement KXmlCommand::createGroup(QDomDocument& doc, DrGroup *group)
{
	QDomElement	elem = doc.createElement("filtergroup");
	elem.setAttribute("name", group->name());
	elem.setAttribute("description", group->get("text"));

	QPtrListIterator<DrGroup>	git(group->groups());
	for (; git.current(); ++git)
		elem.appendChild(createGroup(doc, git.current()));

	QPtrListIterator<DrBase>	oit(group->options());
	for (; oit.current(); ++oit)
		elem.appendChild(createElement(doc, oit.current()));

	return elem;
}

QDomElement KXmlCommand::createElement(QDomDocument& doc, DrBase *opt)
{
	QDomElement	elem = doc.createElement("filterarg");
	QString	elemName = opt->name();
	if (elemName.startsWith("_kde-"))
		elemName.replace(0, name().length()+6, "");
	elem.setAttribute("name", elemName);
	elem.setAttribute("format", opt->get("format"));
	elem.setAttribute("description", opt->get("text"));
	elem.setAttribute("default", opt->get("default"));
	elem.setAttribute( "persistent", opt->get( "persistent" ) == "1" ? "1" : "0");

	switch (opt->type())
	{
		case DrBase::String:
			elem.setAttribute("type", "string");
			break;
		case DrBase::Integer:
		case DrBase::Float:
			elem.setAttribute("type", (opt->type() == DrBase::Integer ? "int" : "float"));
			elem.setAttribute("min", opt->get("minval"));
			elem.setAttribute("max", opt->get("maxval"));
			break;
		case DrBase::Boolean:
		case DrBase::List:
			elem.setAttribute("type", (opt->type() == DrBase::List ? "list" : "bool"));
			{
				QPtrListIterator<DrBase>	it(*(static_cast<DrListOption*>(opt)->choices()));
				for (; it.current(); ++it)
				{
					QDomElement	chElem = doc.createElement("value");
					chElem.setAttribute("name", it.current()->name());
					chElem.setAttribute("description", it.current()->get("text"));
					elem.appendChild(chElem);
				}
			}
			break;
		default:
			break;
	}

	return elem;
}

//---------------------------------------------------------------------------------------------------

class KXmlCommandManager::KXmlCommandManagerPrivate
{
public:
	QStringList	m_cmdlist;
	QMap<QString, QValueList<KXmlCommand*> >	m_mimemap;
	QMap<QString, KXmlCommand*>	m_cmdmap;
};

KXmlCommandManager* KXmlCommandManager::m_self = 0;

KXmlCommandManager* KXmlCommandManager::self()
{
	if (!m_self)
	{
		m_self = new KXmlCommandManager;
		Q_CHECK_PTR(m_self);
	}
	return m_self;
}

KXmlCommandManager::KXmlCommandManager()
: QObject(KMFactory::self(), "XmlCommandManager")
{
	d = new KXmlCommandManagerPrivate;
}

KXmlCommandManager::~KXmlCommandManager()
{
	cleanUp();
	delete d;
}

KXmlCommand* KXmlCommandManager::loadCommand(const QString& xmlId, bool check)
{
	if (check)
	{
		QString	desktopFile = locate("data", "kdeprint/filters/"+xmlId+".desktop");
		if (desktopFile.isEmpty())
			return 0;
	}
	return new KXmlCommand(xmlId);
}

void KXmlCommandManager::saveCommand(KXmlCommand *xmlCmd)
{
	xmlCmd->saveDesktop();
	xmlCmd->saveXml();

	cleanUp();
}

void KXmlCommandManager::cleanUp()
{
	for (QMap<QString, KXmlCommand*>::ConstIterator it=d->m_cmdmap.begin(); it!=d->m_cmdmap.end(); ++it)
		delete (*it);
	d->m_cmdmap.clear();
	d->m_mimemap.clear();
	d->m_cmdlist.clear();
}

void KXmlCommandManager::preload()
{
	if (d->m_cmdmap.count() == 0)
	{
		commandList();
		for (QStringList::Iterator it=d->m_cmdlist.begin(); it!=d->m_cmdlist.end(); ++it)
		{
			KXmlCommand	*xmlCmd = loadCommand(*it);
			if (!xmlCmd) continue; // Error!

			QStringList	inputMime = xmlCmd->inputMimeTypes();
			for (QStringList::ConstIterator mime=inputMime.begin(); mime!=inputMime.end(); ++mime)
			{
				d->m_mimemap[*mime].append(xmlCmd);
				d->m_cmdmap[*it] = xmlCmd;
			}
		}
	}
}

QStringList KXmlCommandManager::commandList()
{
	if (d->m_cmdlist.isEmpty())
	{
		QStringList	dirs = KGlobal::dirs()->findDirs("data", "kdeprint/filters/");

		for (QStringList::ConstIterator it=dirs.begin(); it!=dirs.end(); ++it)
		{
			QStringList	list = QDir(*it).entryList("*.desktop", QDir::Files, QDir::Unsorted);
			for (QStringList::ConstIterator it2=list.begin(); it2!=list.end(); ++it2)
			{
				QString	cmdName = (*it2).left((*it2).length()-8);
				if (d->m_cmdlist.find(cmdName) == d->m_cmdlist.end())
					d->m_cmdlist.append(cmdName);
			}
		}

		d->m_cmdlist.sort();
	}

	return d->m_cmdlist;
}

QStringList KXmlCommandManager::commandListWithDescription()
{
	preload();
	QStringList	l;
	for (QMap<QString,KXmlCommand*>::ConstIterator it=d->m_cmdmap.begin(); it!=d->m_cmdmap.end(); ++it)
		l << (*it)->name() << (*it)->description();

	return l;
}

QString KXmlCommandManager::selectCommand(QWidget *parent)
{
	KLibrary *lib = KLibLoader::self()->library( "libkdeprint_management_module" );
	if ( !lib )
	{
		KMessageBox::error( parent, i18n( "Unable to load KDE print management library: %1" ).arg( KLibLoader::self()->lastErrorMessage() ) );
		return QString::null;
	}
	else
	{
		QString ( *func )( QWidget* ) = ( QString( * )( QWidget* ) )lib->symbol( "select_command" );
		if ( !func )
		{
			KMessageBox::error( parent, i18n( "Unable to find wizard object in management library." ) );
			return QString::null;
		}
		else
			return func( parent );
	}
}

KXmlCommand* KXmlCommandManager::command(const QString& xmlId) const
{
	return (d->m_cmdmap.contains(xmlId) ? d->m_cmdmap[xmlId] : 0);
}

int KXmlCommandManager::insertCommand(QStringList& list, const QString& filtername, bool defaultToStart)
{
	preload();

	int	pos(0);
	KXmlCommand	*f1 = command(filtername), *f2 = 0;
	if (f1 && f1->inputMimeTypes().count() > 0)
	{
		QString	mimetype = f1->inputMimeTypes()[0];
		for (QStringList::Iterator it=list.begin(); it!=list.end(); ++it, pos++)
		{
			f2 = command(*it);
			if (!f2)
				return -1; // Shouldn't happen

			if (f2->acceptMimeType(f1->mimeType()) && f1->acceptMimeType(mimetype))
			{
				list.insert(it, filtername);
				break;
			}
			else
			{
				mimetype = f2->mimeType();
				f2 = 0;
			}
		}
		if (pos == (int)(list.count()))
		{
			if (list.count() == 0 || f1->acceptMimeType(mimetype))
				list.append(filtername);
			else if (defaultToStart)
			{
				pos = 0;
				list.prepend(filtername);
			}
			else
				pos = -1;
		}
	}
	return pos;
}

QStringList KXmlCommandManager::autoConvert(const QString& mimesrc, const QString& mimedest)
{
	QStringList	chain;
	uint		score(0);

	preload();

	if (d->m_mimemap.contains(mimesrc))
	{
		const QValueList<KXmlCommand*>	l = d->m_mimemap[mimesrc];
		for (QValueList<KXmlCommand*>::ConstIterator it=l.begin(); it!=l.end(); ++it)
		{
			// direct filter: shortest path => return immediately
			if ((*it)->mimeType() == mimedest)
			{
				chain = QStringList((*it)->name());
				break;
			}
			// non direct filter: find the shortest way between
			// its output and mimedest (do not consider cyling filters)
			else if ((*it)->mimeType() != mimesrc)
			{
				QStringList	subchain = autoConvert((*it)->mimeType(), mimedest);
				// If chain length is 0, then there's no possible filter between those 2
				// mime types. Just discard it. If the subchain contains also the current
				// considered filter, then discard it: it denotes of a cycle in filter
				// chain.
				if (subchain.count() > 0 && subchain.findIndex((*it)->name()) == -1)
				{
					subchain.prepend((*it)->name());
					if (subchain.count() < score || score == 0)
					{
						chain = subchain;
						score = subchain.count();
					}
				}
			}
		}
	}
	// At this point, either we have the shortest path, or empty
	// list if nothing could be found
	return chain;
}

bool KXmlCommandManager::checkCommand(const QString& xmlId, int inputCheck, int outputCheck, QString *msg)
{
	KXmlCommand	*xmlCmd = command(xmlId);
	QString	errmsg;
	bool	needDestroy(false);
	//kdDebug(500) << "checking command: " << xmlId << " (" << (xmlCmd != NULL) << ")" << endl;
	if (!xmlCmd)
	{
		xmlCmd = loadCommand(xmlId, true);
		needDestroy = (xmlCmd != 0);
	}

	bool	status(true);
	if (xmlCmd)
	{
		status = (xmlCmd->isValid() && KdeprintChecker::check(xmlCmd->requirements()));
		if (!status)
			errmsg = i18n("One of the command object's requirements is not met.");
	}
	QString	cmd = (xmlCmd ? xmlCmd->command() : xmlId);
	if (status && !cmd.isEmpty() && (inputCheck > None || outputCheck > None))
	{
		if (inputCheck > None && (cmd.find("%in") == -1 || inputCheck == Advanced) && cmd.find("%filterinput") == -1)
		{
			status = false;
			errmsg = i18n("The command does not contain the required tag %1.").arg(inputCheck == Advanced ? "%filterinput" : "{%in,%filterinput}");
		}
		if (status && outputCheck > None && (cmd.find("%out") == -1 || outputCheck == Advanced) && cmd.find("filteroutput") == -1)
		{
			status = false;
			errmsg = i18n("The command does not contain the required tag %1.").arg(outputCheck == Advanced ? "%filteroutput" : "{%out,%filteroutput}");
		}
	}

	if (needDestroy)
		delete xmlCmd;

	if (msg)
		*msg = errmsg;

	return status;
}

bool KXmlCommandManager::configure(KXmlCommand *xmlCmd, QWidget *parent)
{
	if (xmlCmd->driver())
	{
		KDialogBase	dlg(parent, 0, true, xmlCmd->description(), KDialogBase::Ok);
		DriverView	view(&dlg);

		dlg.setMainWidget(&view);
		view.setDriver(xmlCmd->driver());
		dlg.resize(350,400);
		dlg.exec();

		return true;
	}
	return false;
}
