/*
 *   tdeprintfax - a small fax utility
 *   Copyright (C) 2001  Michael Goffioul
 *
 *   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

#include "faxctrl.h"
#include "tdeprintfax.h"
#include "defcmds.h"

#include <ktextedit.h>
#include <tqfile.h>
#include <tqtextstream.h>
#include <kpushbutton.h>
#include <tqlayout.h>
#include <tqregexp.h>
#include <kprinter.h>
#include <tqsimplerichtext.h>
#include <tqpainter.h>
#include <tqpaintdevicemetrics.h>
#include <tqvaluestack.h>
#include <tqstylesheet.h>

#include <tdeprocess.h>
#include <tdeglobal.h>
#include <tdeconfig.h>
#include <tdelocale.h>
#include <kdialogbase.h>
#include <kmimetype.h>
#include <tdestandarddirs.h>
#include <tdeapplication.h>
#include <twin.h>
#include <tdeemailsettings.h>
#include <kdebug.h>
#include <kstdguiitem.h>
#include <tdefiledialog.h>
#include <tdemessagebox.h>

#include <stdlib.h>
#include <stdarg.h>

#define quote(x) TDEProcess::quote(x)

/** \brief Return a string for the page size.
 *
 * @param size the page size, which is actually of type @c TQPrinter::PageSize
 * @return a pointer to a static string for the name of the page size.
 */
char const* pageSizeText(int size)
{
	switch(size)
	{
	case TQPrinter::A4:		return "a4";
	case TQPrinter::Legal:	return "legal";
	case TQPrinter::Letter:	return "letter";
	default:				return "letter";
	}
}

/** \brief Return the default page size.
 */
static TQString pageSize()
{
	TDEConfig	*conf = TDEGlobal::config();
	conf->setGroup("Fax");
	return conf->readEntry("Page", pageSizeText(TDEGlobal::locale()->pageSize()));
}

static TQString stripNumber( const TQString& s )
{
	TDEConfig *conf = TDEGlobal::config();
	conf->setGroup( "Personal" );

	// removes any non-numeric character, except ('+','*','#') (hope it's supported by faxing tools)
	TQString strip_s = s;
	strip_s.replace( TQRegExp( "[^\\d+*#]" ), "" );
	if ( strip_s.find( '+' ) != -1 && conf->readBoolEntry( "ReplaceIntChar", false ) )
		strip_s.replace( "+", conf->readEntry( "ReplaceIntCharVal" ) );
	return strip_s;
}

static TQString tagList( int n, ... )
{
	TQString t;

	va_list ap;
	va_start( ap, n );
	for ( int i=0; i<n; i++ )
	{
		TQString tag = va_arg( ap, const char* );
		tag.append( "(_(\\w|\\{[^\\}]*\\}))?" );
		if ( t.isEmpty() )
			t = tag;
		else
			t.append( "|" ).append( tag );
	}

	return t;
}

/** Process a @c %variable that has a boolean value.
 * The format is <tt>%name_{iftrue??iffalse}</tt>. Either
 * @em iftrue or @em iffalse can be empty (or both, but that would
 * be silly). For example, if the fax software uses the
 * option @c -l for low resolution, you might use <tt>%res_{??-l}</tt>,
 * and if it uses high resolution, use <tt>%res_{-h??}</tt>. As an
 * abbreviation, omit the trailing <tt>?</tt>, e.g., <tt>%res_{-h}</tt>.
 *
 * By the way, when using multiple, adjacent question marks,
 * always be careful about inadvertently using trigraphs.
 * Always escape the second and subsequent question marks, to be safe.
 * I suppose question marks are not the best string to use,
 * but I think they work well for the end-user.
 *
 * @param match the string to process
 * @param value the Boolean value to control the processing
 * @return the new value of the tag
 */
static TQString processTag( const TQString& match, bool value)
{
	TQString v;
	int p = match.find( '_' );
	if ( p != -1 && match[ p+1 ] == '{' )
	{
		// Find the ?? that separates the iftrue from the iffalse parts.
		int q = match.find( "?\?", p+2 );
		if ( q == -1 )
		{
			// No iffalse part
			if (value)
				v = match.mid( p+2 );
			// else value is false, so leave v empty.
		}
		else if ( value )
			// Extract only the iftrue part
			v = match.mid( p+2, q-p-2 );
		else
			// Extract only the iffalse part
			v = match.mid( q+2, match.length()-q-3 );
	}
	// Else the string is malformed: no _ or no { after the _
	// In either case, there isn't much the program can do,
	// so just leave the result string empty.

	return v;
}

static TQString processTag( const TQString& match, const TQString& value )
{
	TQString v;
	int p = match.find( '_' );
	if ( p != -1 )
	{
		if ( value.isEmpty() )
			v = "";
		else
		{
			if ( match[ p+1 ] == '{' )
			{
				v = match.mid( p+2, match.length()-p-3 );
				v.replace( "@@", quote( value ) );
			}
			else
				v = ( "-" + match.mid( p+1 ) + " " + quote( value ) );
		}
	}
	else
		v = quote( value );
	return v;
}

static bool isTag( const TQString& m, const TQString& t )
{
	return ( m == t || m.startsWith( t+"_" ) );
}

static TQString replaceTags( const TQString& s, const TQString& tags, KdeprintFax *fax = NULL, const KdeprintFax::FaxItem& item = KdeprintFax::FaxItem() )
{
	// unquote variables (they will be replaced with quoted values later)

	TQValueStack<bool> stack;
	TDEConfig	*conf = TDEGlobal::config();

	TQString cmd = s;

	bool issinglequote=false;
	bool isdoublequote=false;
	TQRegExp re_noquote("(\\$\\(|\\)|\\(|\"|'|\\\\|`|"+tags+")");
	TQRegExp re_singlequote("('|"+tags+")");
	TQRegExp re_doublequote("(\\$\\(|\"|\\\\|`|"+tags+")");
	for	( int i = re_noquote.search(cmd);
		i != -1;
		i = (issinglequote?re_singlequote.search(cmd,i)
			:isdoublequote?re_doublequote.search(cmd,i)
			:re_noquote.search(cmd,i))
	)
	{
		if (cmd[i]=='(') // (...)
		{
			// assert(isdoublequote == false)
			stack.push(isdoublequote);
			i++;
		}
		else if (cmd[i]=='$') // $(...)
		{
			stack.push(isdoublequote);
			isdoublequote = false;
			i+=2;
		}
		else if (cmd[i]==')') // $(...) or (...)
		{
			if (!stack.isEmpty())
				isdoublequote = stack.pop();
			else
				tqWarning("Parse error.");
			i++;
		}
		else if (cmd[i]=='\'')
		{
			issinglequote=!issinglequote;
			i++;
		}
		else if (cmd[i]=='"')
		{
			isdoublequote=!isdoublequote;
			i++;
		}
		else if (cmd[i]=='\\')
			i+=2;
		else if (cmd[i]=='`')
		{
			// Replace all `...` with safer $(...)
			cmd.replace (i, 1, "$(");
			TQRegExp re_backticks("(`|\\\\`|\\\\\\\\|\\\\\\$)");
			for (	int i2=re_backticks.search(cmd,i+2);
				i2!=-1;
				i2=re_backticks.search(cmd,i2)
			)
			{
				if (cmd[i2] == '`')
				{
					cmd.replace (i2, 1, ")");
					i2=cmd.length(); // leave loop
				}
				else
				{	// remove backslash and ignore following character
					cmd.remove (i2, 1);
					i2++;
				}
			}
			// Leave i unchanged! We need to process "$("
		}
		else
		{
			TQString match, v;

			// get match
			if (issinglequote)
				match=re_singlequote.cap();
			else if (isdoublequote)
				match=re_doublequote.cap();
			else
				match=re_noquote.cap();

			// substitute %variables
			// settings
			if ( isTag( match, "%dev" ) )
			{
				conf->setGroup("Fax");
				v = processTag( match, conf->readEntry("Device", "modem") );

			}
			else if (isTag( match, "%server" ))
			{
				conf->setGroup( "Fax" );
				v = conf->readEntry("Server");
				if (v.isEmpty())
					v = getenv("FAXSERVER");
				if (v.isEmpty())
					v = TQString::fromLatin1("localhost");
				v = processTag( match, v );
			}
			else if (isTag( match, "%page" ))
			{
				conf->setGroup( "Fax" );
				v = processTag( match, pageSize() );
			}
			else if (isTag( match, "%res" ))
			{
				conf->setGroup( "Fax" );
				v = processTag(match, conf->readEntry("Resolution", "High") == "High");
			}
			else if (isTag( match, "%user" ))
			{
				conf->setGroup("Personal");
				v = processTag(match, conf->readEntry("Name", getenv("USER")));
			}
			else if (isTag( match, "%from" ))
			{
				conf->setGroup( "Personal" );
				v = processTag(match, conf->readEntry("Number"));
			}
			else if (isTag( match, "%email" ))
			{
				KEMailSettings	e;
				v = processTag(match, e.getSetting(KEMailSettings::EmailAddress));
			}
			// arguments
			else if (isTag( match, "%number" ))
				v = processTag( match, stripNumber( item.number) );
			else if (isTag( match, "%rawnumber" ))
				v = processTag( match, item.number );
			else if (isTag( match, "%name" ))
				v = processTag(match, item.name);
			else if (isTag( match, "%comment" ))
				v = processTag(match, fax->comment());
			else if (isTag( match, "%enterprise" ))
				v = processTag(match, item.enterprise);
			else if ( isTag( match, "%time" ) )
				v = processTag( match, fax->time() );
			else if ( isTag( match, "%subject" ) )
				v = processTag( match, fax->subject() );
			else if (isTag( match, "%cover" ))
				v = processTag(match, fax->cover());

			// %variable inside of a quote?
			if (isdoublequote)
				v='"'+v+'"';
			else if (issinglequote)
				v="'"+v+"'";

			cmd.replace (i, match.length(), v);
			i+=v.length();
		}
	}

	return cmd;
}

FaxCtrl::FaxCtrl(TQWidget *parent, const char *name)
: TQObject(parent, name)
{
	m_process = new TDEProcess();
	m_process->setUseShell(true);
	connect(m_process, TQ_SIGNAL(receivedStdout(TDEProcess*,char*,int)), TQ_SLOT(slotReceivedStdout(TDEProcess*,char*,int)));
	connect(m_process, TQ_SIGNAL(receivedStderr(TDEProcess*,char*,int)), TQ_SLOT(slotReceivedStdout(TDEProcess*,char*,int)));
	connect(m_process, TQ_SIGNAL(processExited(TDEProcess*)), TQ_SLOT(slotProcessExited(TDEProcess*)));
	connect(this, TQ_SIGNAL(faxSent(bool)), TQ_SLOT(cleanTempFiles()));
	m_logview = 0;
}

FaxCtrl::~FaxCtrl()
{
	slotCloseLog();
	delete m_process;
}

bool FaxCtrl::send(KdeprintFax *f)
{
	m_command = faxCommand();
	if (m_command.isEmpty())
		return false;

	// replace tags common to all fax "operations"
	m_command = replaceTags( m_command, tagList( 11, "%dev", "%server", "%page", "%res", "%user", "%from", "%email", "%comment", "%time", "%subject", "%cover" ), f );

	m_log = TQString::null;
	m_filteredfiles.clear();
	cleanTempFiles();
	m_files = f->files();
	m_faxlist = f->faxList();

	addLogTitle( i18n( "Converting input files to PostScript" ) );
	filter();

	return true;
}

void FaxCtrl::slotReceivedStdout(TDEProcess*, char *buffer, int len)
{
	TQCString	str(buffer, len);
	kdDebug() << "Received stdout: " << str << endl;
	addLog(TQString(str));
}

void FaxCtrl::slotProcessExited(TDEProcess*)
{
	// we exited a process: if there's still entries in m_files, this was a filter
	// process, else this was the fax process
	bool	ok = (m_process->normalExit() && ((m_process->exitStatus() & (m_files.count() > 0 ? 0x1 : 0xFFFFFFFF)) == 0));
	if ( ok )
	{
		if ( m_files.count() > 0 )
		{
			// remove first element
			m_files.remove(m_files.begin());
			if (m_files.count() > 0)
				filter();
			else
				sendFax();
		}
		else if ( !m_faxlist.isEmpty() )
			sendFax();
		else
			faxSent( true );
	}
	else
	{
		emit faxSent(false);
	}
}

TQString FaxCtrl::faxCommand()
{
	TDEConfig	*conf = TDEGlobal::config();
	conf->setGroup("System");
	TQString	sys = conf->readPathEntry("System", "efax");
	TQString cmd;
	if (sys == "hylafax")
		cmd = conf->readPathEntry("HylaFax", hylafax_default_cmd);
	else if (sys == "mgetty")
		cmd = conf->readPathEntry("Mgetty", mgetty_default_cmd);
	else if ( sys == "other" )
		cmd = conf->readPathEntry( "Other", TQString::null );
	else
		cmd = conf->readPathEntry("EFax", efax_default_cmd);
	if (cmd.startsWith("%exe_"))
		cmd = defaultCommand(cmd);
	return cmd;
}

void FaxCtrl::sendFax()
{
	if ( m_command.find( "%files" ) != -1 )
	{
		// replace %files tag
		TQString	filestr;
		for (TQStringList::ConstIterator it=m_filteredfiles.begin(); it!=m_filteredfiles.end(); ++it)
			filestr += (quote(*it)+" ");
		m_command.replace("%files", filestr);
	}

	if ( !m_faxlist.isEmpty() )
	{
		KdeprintFax::FaxItem item = m_faxlist.first();
		m_faxlist.remove(m_faxlist.begin());

		addLogTitle( i18n( "Sending fax to %1 (%2)" ).arg( item.number ).arg( item.name ) );

		TQString cmd = replaceTags( m_command, tagList( 4, "%number", "%name", "%enterprise", "%rawnumber" ), NULL, item );
		m_process->clearArguments();
		*m_process << cmd;
		addLog(i18n("Sending to fax using: %1").arg(cmd));
		if (!m_process->start(TDEProcess::NotifyOnExit, TDEProcess::AllOutput))
			emit faxSent(false);
		else
			emit message(i18n("Sending fax to %1...").arg( item.number ));
	}
}

void FaxCtrl::filter()
{
	if (m_files.count() > 0)
	{
		TQString	mimeType = KMimeType::findByURL(KURL(m_files[0]), 0, true)->name();
		if (mimeType == "application/postscript" || mimeType == "image/tiff")
		{
			emit message(i18n("Skipping %1...").arg(m_files[0]));
			m_filteredfiles.prepend(m_files[0]);
			m_files.remove(m_files.begin());
			filter();
		}
		else
		{
			TQString	tmp = locateLocal("tmp","tdeprintfax_") + tdeApp->randomString(8);
			m_filteredfiles.prepend(tmp);
			m_tempfiles.append(tmp);
			m_process->clearArguments();
			*m_process << locate("data","tdeprintfax/anytops") << "-m" << TDEProcess::quote(locate("data","tdeprintfax/faxfilters"))
				<< TQString::fromLatin1("--mime=%1").arg(mimeType)
				<< "-p" << pageSize()
				<<  TDEProcess::quote(m_files[0]) << TDEProcess::quote(tmp);
			if (!m_process->start(TDEProcess::NotifyOnExit, TDEProcess::AllOutput))
				emit faxSent(false);
			else
				emit message(i18n("Filtering %1...").arg(m_files[0]));
		}
	}
	else
	{
		sendFax();
	}
}

bool FaxCtrl::abort()
{
	if (m_process->isRunning())
		return m_process->kill();
	else
		return false;
}

void FaxCtrl::viewLog(TQWidget *)
{
	if (!m_logview)
	{
		TQWidget	*topView = new TQWidget(0, "LogView", (WFlags)(WType_TopLevel|WStyle_DialogBorder|WDestructiveClose));
		m_logview = new KTextEdit(topView);
		m_logview->setTextFormat( TQt::LogText );
		m_logview->setWordWrap( TQTextEdit::WidgetWidth );
		m_logview->setPaper( TQt::white );
		//m_logview->setReadOnly(true);
		//m_logview->setWordWrap(TQTextEdit::NoWrap);
		TQPushButton	*m_clear = new KPushButton(KStdGuiItem::clear(), topView);
		TQPushButton	*m_close = new KPushButton(KStdGuiItem::close(), topView);
		TQPushButton *m_print = new KPushButton( KStdGuiItem::print(), topView );
		TQPushButton *m_save = new KPushButton( KStdGuiItem::saveAs(), topView );
		m_close->setDefault(true);
		connect(m_clear, TQ_SIGNAL(clicked()), TQ_SLOT(slotClearLog()));
		connect(m_close, TQ_SIGNAL(clicked()), TQ_SLOT(slotCloseLog()));
		connect(m_logview, TQ_SIGNAL(destroyed()), TQ_SLOT(slotCloseLog()));
		connect( m_print, TQ_SIGNAL( clicked() ), TQ_SLOT( slotPrintLog() ) );
		connect( m_save, TQ_SIGNAL( clicked() ), TQ_SLOT( slotSaveLog() ) );

		TQVBoxLayout	*l0 = new TQVBoxLayout(topView, 10, 10);
		l0->addWidget(m_logview);
		TQHBoxLayout	*l1 = new TQHBoxLayout(0, 0, 10);
		l0->addLayout(l1);
		l1->addStretch(1);
		l1->addWidget( m_save );
		l1->addWidget( m_print );
		l1->addWidget(m_clear);
		l1->addWidget(m_close);

		m_logview->setText(m_log);

		topView->resize(450, 350);
		topView->show();
	}
	else
	{
		KWin::activateWindow(m_logview->parentWidget()->winId());
	}
}

void FaxCtrl::addLogTitle( const TQString& s )
{
	TQString t( s );
	t.prepend( '\n' ).append( '\n' );
	addLog( t, true );
}

void FaxCtrl::addLog(const TQString& s, bool isTitle)
{
	TQString t = TQStyleSheet::escape(s);
	if ( isTitle )
		t.prepend( "<font color=red><b>" ).append( "</b></font>" );
	m_log.append( t + '\n' );
	if (m_logview)
		m_logview->append(t);
}

TQString FaxCtrl::faxSystem()
{
	TDEConfig	*conf = TDEGlobal::config();
	conf->setGroup("System");
	TQString	s = conf->readEntry("System", "efax");
	s[0] = s[0].upper();
	return s;
}

void FaxCtrl::cleanTempFiles()
{
	for (TQStringList::ConstIterator it=m_tempfiles.begin(); it!=m_tempfiles.end(); ++it)
		TQFile::remove(*it);
	m_tempfiles.clear();
}

void FaxCtrl::slotClearLog()
{
	m_log = TQString::null;
	if (m_logview)
		m_logview->clear();
}

void FaxCtrl::slotCloseLog()
{
	const TQObject	*obj = sender();
	if (m_logview)
	{
		TQTextEdit	*view = m_logview;
		m_logview = 0;
		if (obj && obj->inherits("TQPushButton"))
			delete view->parentWidget();
kdDebug() << "slotClose()" << endl;
	}
}

void FaxCtrl::slotPrintLog()
{
	if ( m_logview )
	{
		KPrinter printer;
		printer.setDocName( i18n( "Fax log" ) );
		printer.setDocFileName( "faxlog" );
		if ( printer.setup( m_logview->topLevelWidget(), i18n( "Fax Log" ) ) )
		{
			TQPainter painter( &printer );
			TQPaintDeviceMetrics metric( &printer );
			TQRect body( 0, 0, metric.width(), metric.height() ), view( body );
			//TQString txt = m_logview->text();
			TQString txt = m_log;

			txt.replace( '\n', "<br>" );
			txt.prepend( "<h2>" + i18n( "TDEPrint Fax Tool Log" ) + "</h2>" );

			kdDebug() << "Log: " << txt << endl;
			TQSimpleRichText richText( txt, m_logview->font() );

			richText.setWidth( &painter, body.width() );
			do
			{
				richText.draw( &painter, body.left(), body.top(), view, m_logview->colorGroup() );
				view.moveBy( 0, body.height() );
				painter.translate( 0, -body.height() );
				if ( view.top() >= richText.height() )
					break;
				printer.newPage();
			} while ( true );
		}
	}
}

void FaxCtrl::slotSaveLog()
{
	if ( m_logview )
	{
		TQString filename = KFileDialog::getSaveFileName( TQString::null, TQString::null, m_logview );
		if ( !filename.isEmpty() )
		{
			TQFile f( filename );
			if ( f.open( IO_WriteOnly ) )
			{
				TQTextStream t( &f );
				t << i18n( "TDEPrint Fax Tool Log" ) << endl;
				t << m_logview->text() << endl;
				f.close();
			}
			else
				KMessageBox::error( m_logview, i18n( "Cannot open file for writing." ) );
		}
	}
}

#include "faxctrl.moc"
