/*
 * Copyright Johannes Sixt
 * This file is licensed under the GNU General Public License Version 2.
 * See the file COPYING in the toplevel directory of the source directory.
 */

#include <tdeapplication.h>
#include <tdelocale.h>			/* i18n */
#include <tdeconfig.h>
#include <tdemessagebox.h>
#include <kstatusbar.h>
#include <tdefiledialog.h>
#include <tqtabdialog.h>
#include <tqfile.h>
#include <tqdragobject.h>
#include "mainwndbase.h"
#include "debugger.h"
#include "gdbdriver.h"
#include "xsldbgdriver.h"
#include "prefdebugger.h"
#include "prefmisc.h"
#include "ttywnd.h"
#include "commandids.h"
#ifdef HAVE_CONFIG
#include "config.h"
#endif
#include "mydebug.h"
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>			/* mknod(2) */
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>			/* open(2) */
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>			/* getpid, unlink etc. */
#endif

#define MAX_RECENT_FILES 4

WatchWindow::WatchWindow(TQWidget* parent, const char* name, WFlags f) :
	TQWidget(parent, name, f),
	m_watchEdit(this, "watch_edit"),
	m_watchAdd(i18n(" Add "), this, "watch_add"),
	m_watchDelete(i18n(" Del "), this, "watch_delete"),
	m_watchVariables(this, i18n("Expression"), "watch_variables"),
	m_watchV(this, 0),
	m_watchH(0)
{
    // setup the layout
    m_watchAdd.setMinimumSize(m_watchAdd.sizeHint());
    m_watchDelete.setMinimumSize(m_watchDelete.sizeHint());
    m_watchV.addLayout(&m_watchH, 0);
    m_watchV.addWidget(&m_watchVariables, 10);
    m_watchH.addWidget(&m_watchEdit, 10);
    m_watchH.addWidget(&m_watchAdd, 0);
    m_watchH.addWidget(&m_watchDelete, 0);

    connect(&m_watchEdit, TQ_SIGNAL(returnPressed()), TQ_SIGNAL(addWatch()));
    connect(&m_watchAdd, TQ_SIGNAL(clicked()), TQ_SIGNAL(addWatch()));
    connect(&m_watchDelete, TQ_SIGNAL(clicked()), TQ_SIGNAL(deleteWatch()));
    connect(&m_watchVariables, TQ_SIGNAL(currentChanged(TQListViewItem*)),
	    TQ_SLOT(slotWatchHighlighted()));

    m_watchVariables.installEventFilter(this);
    setAcceptDrops(true);
}

WatchWindow::~WatchWindow()
{
}

bool WatchWindow::eventFilter(TQObject*, TQEvent* ev)
{
    if (ev->type() == TQEvent::KeyPress)
    {
	TQKeyEvent* kev = static_cast<TQKeyEvent*>(ev);
	if (kev->key() == Key_Delete) {
	    emit deleteWatch();
	    return true;
	}
    }
    return false;
}

void WatchWindow::dragEnterEvent(TQDragEnterEvent* event)
{
    event->accept(TQTextDrag::canDecode(event));
}

void WatchWindow::dropEvent(TQDropEvent* event)
{
    TQString text;
    if (TQTextDrag::decode(event, text))
    {
	// pick only the first line
	text = text.stripWhiteSpace();
	int pos = text.find('\n');
	if (pos > 0)
	    text.truncate(pos);
	text = text.stripWhiteSpace();
	if (!text.isEmpty())
	    emit textDropped(text);
    }
}


// place the text of the hightlighted watch expr in the edit field
void WatchWindow::slotWatchHighlighted()
{
    VarTree* expr = m_watchVariables.selectedItem();
    TQString text = expr ? expr->computeExpr() : TQString();
    m_watchEdit.setText(text);
}


const char defaultTermCmdStr[] = "xterm -name kdbgio -title %T -e sh -c %C";
const char defaultSourceFilter[] = "*.c *.cc *.cpp *.c++ *.C *.CC";
const char defaultHeaderFilter[] = "*.h *.hh *.hpp *.h++";


DebuggerMainWndBase::DebuggerMainWndBase() :
	m_outputTermCmdStr(defaultTermCmdStr),
	m_outputTermProc(0),
	m_ttyLevel(-1),			/* no tty yet */
#ifdef GDB_TRANSCRIPT
	m_transcriptFile(GDB_TRANSCRIPT),
#endif
	m_popForeground(false),
	m_backTimeout(1000),
	m_tabWidth(0),
	m_sourceFilter(defaultSourceFilter),
	m_headerFilter(defaultHeaderFilter),
	m_debugger(0)
{
    m_statusActive = i18n("active");
}

DebuggerMainWndBase::~DebuggerMainWndBase()
{
    delete m_debugger;

    // if the output window is open, close it
    if (m_outputTermProc != 0) {
	m_outputTermProc->disconnect();	/* ignore signals */
	m_outputTermProc->kill();
	shutdownTermWindow();
    }
}

void DebuggerMainWndBase::setupDebugger(TQWidget* parent,
					ExprWnd* localVars,
					ExprWnd* watchVars,
					TQListBox* backtrace)
{
    m_debugger = new KDebugger(parent, localVars, watchVars, backtrace);

    TQObject::connect(m_debugger, TQ_SIGNAL(updateStatusMessage()),
		     parent, TQ_SLOT(slotNewStatusMsg()));
    TQObject::connect(m_debugger, TQ_SIGNAL(updateUI()),
		     parent, TQ_SLOT(updateUI()));

    TQObject::connect(m_debugger, TQ_SIGNAL(breakpointsChanged()),
		     parent, TQ_SLOT(updateLineItems()));
    
    TQObject::connect(m_debugger, TQ_SIGNAL(debuggerStarting()),
		     parent, TQ_SLOT(slotDebuggerStarting()));
}


void DebuggerMainWndBase::setCoreFile(const TQString& corefile)
{
    assert(m_debugger != 0);
    m_debugger->useCoreFile(corefile, true);
}

void DebuggerMainWndBase::setRemoteDevice(const TQString& remoteDevice)
{
    if (m_debugger != 0) {
	m_debugger->setRemoteDevice(remoteDevice);
    }
}

void DebuggerMainWndBase::overrideProgramArguments(const TQString& args)
{
    assert(m_debugger != 0);
    m_debugger->overrideProgramArguments(args);
}

void DebuggerMainWndBase::setTranscript(const TQString& name)
{
    m_transcriptFile = name;
    if (m_debugger != 0 && m_debugger->driver() != 0)
	m_debugger->driver()->setLogFileName(m_transcriptFile);
}

const char OutputWindowGroup[] = "OutputWindow";
const char TermCmdStr[] = "TermCmdStr";
const char KeepScript[] = "KeepScript";
const char DebuggerGroup[] = "Debugger";
const char DebuggerCmdStr[] = "DebuggerCmdStr";
const char PreferencesGroup[] = "Preferences";
const char PopForeground[] = "PopForeground";
const char BackTimeout[] = "BackTimeout";
const char TabWidth[] = "TabWidth";
const char FilesGroup[] = "Files";
const char SourceFileFilter[] = "SourceFileFilter";
const char HeaderFileFilter[] = "HeaderFileFilter";
const char GeneralGroup[] = "General";

void DebuggerMainWndBase::saveSettings(TDEConfig* config)
{
    if (m_debugger != 0) {
	m_debugger->saveSettings(config);
    }

    TDEConfigGroupSaver g(config, OutputWindowGroup);
    config->writeEntry(TermCmdStr, m_outputTermCmdStr);

    config->setGroup(DebuggerGroup);
    config->writeEntry(DebuggerCmdStr, m_debuggerCmdStr);

    config->setGroup(PreferencesGroup);
    config->writeEntry(PopForeground, m_popForeground);
    config->writeEntry(BackTimeout, m_backTimeout);
    config->writeEntry(TabWidth, m_tabWidth);
    config->writeEntry(SourceFileFilter, m_sourceFilter);
    config->writeEntry(HeaderFileFilter, m_headerFilter);
}

void DebuggerMainWndBase::restoreSettings(TDEConfig* config)
{
    if (m_debugger != 0) {
	m_debugger->restoreSettings(config);
    }

    TDEConfigGroupSaver g(config, OutputWindowGroup);
    /*
     * For debugging and emergency purposes, let the config file override
     * the shell script that is used to keep the output window open. This
     * string must have EXACTLY 1 %s sequence in it.
     */
    setTerminalCmd(config->readEntry(TermCmdStr, defaultTermCmdStr));
    m_outputTermKeepScript = config->readEntry(KeepScript);

    config->setGroup(DebuggerGroup);
    setDebuggerCmdStr(config->readEntry(DebuggerCmdStr));

    config->setGroup(PreferencesGroup);
    m_popForeground = config->readBoolEntry(PopForeground, false);
    m_backTimeout = config->readNumEntry(BackTimeout, 1000);
    m_tabWidth = config->readNumEntry(TabWidth, 0);
    m_sourceFilter = config->readEntry(SourceFileFilter, m_sourceFilter);
    m_headerFilter = config->readEntry(HeaderFileFilter, m_headerFilter);
}

void DebuggerMainWndBase::setAttachPid(const TQString& pid)
{
    assert(m_debugger != 0);
    m_debugger->setAttachPid(pid);
}

bool DebuggerMainWndBase::debugProgram(const TQString& executable,
				       TQString lang, TQWidget* parent)
{
    assert(m_debugger != 0);

    TRACE(TQString("trying language '%1'...").arg(lang));
    DebuggerDriver* driver = driverFromLang(lang);

    if (driver == 0)
    {
	// see if there is a language in the per-program config file
	TQString configName = m_debugger->getConfigForExe(executable);
	if (TQFile::exists(configName))
	{
	    TDESimpleConfig c(configName, true);	// read-only
	    c.setGroup(GeneralGroup);

	    // Using "GDB" as default here is for backwards compatibility:
	    // The config file exists but doesn't have an entry,
	    // so it must have been created by an old version of KDbg
	    // that had only the GDB driver.
	    lang = c.readEntry(KDebugger::DriverNameEntry, "GDB");

	    TRACE(TQString("...bad, trying config driver %1...").arg(lang));
	    driver = driverFromLang(lang);
	}

    }
    if (driver == 0)
    {
	TQString name = driverNameFromFile(executable);

	TRACE(TQString("...no luck, trying %1 derived"
		" from file contents").arg(name));
	driver = driverFromLang(name);
    }
    if (driver == 0)
    {
	// oops
	TQString msg = i18n("Don't know how to debug language `%1'");
	KMessageBox::sorry(parent, msg.arg(lang));
	return false;
    }

    driver->setLogFileName(m_transcriptFile);

    bool success = m_debugger->debugProgram(executable, driver);

    if (!success)
    {
	delete driver;

	TQString msg = i18n("Could not start the debugger process.\n"
			   "Please shut down KDbg and resolve the problem.");
	KMessageBox::sorry(parent, msg);
    }

    return success;
}

// derive driver from language
DebuggerDriver* DebuggerMainWndBase::driverFromLang(TQString lang)
{
    // lang is needed in all lowercase
    lang = lang.lower();

    // The following table relates languages and debugger drivers
    static const struct L {
	const char* shortest;	// abbreviated to this is still unique
	const char* full;	// full name of language
	int driver;
    } langs[] = {
	{ "c",       "c++",     1 },
	{ "f",       "fortran", 1 },
	{ "p",       "python",  3 },
	{ "x",       "xslt",    2 },
	// the following are actually driver names
	{ "gdb",     "gdb",     1 },
	{ "xsldbg",  "xsldbg",  2 },
    };
    const int N = sizeof(langs)/sizeof(langs[0]);

    // lookup the language name
    int driverID = 0;
    for (int i = 0; i < N; i++)
    {
	const L& l = langs[i];

	// shortest must match
	if (!lang.startsWith(l.shortest))
	    continue;

	// lang must not be longer than the full name, and it must match
	if (TQString(l.full).startsWith(lang))
	{
	    driverID = l.driver;
	    break;
	}
    }
    DebuggerDriver* driver = 0;
    switch (driverID) {
    case 1:
	{
	    GdbDriver* gdb = new GdbDriver;
	    gdb->setDefaultInvocation(m_debuggerCmdStr);
	    driver = gdb;
	}
	break;
    case 2:
	driver = new XsldbgDriver;
	break;
    default:
	// unknown language
	break;
    }
    return driver;
}

/**
 * Try to guess the language to use from the contents of the file.
 */
TQString DebuggerMainWndBase::driverNameFromFile(const TQString& exe)
{
    /* Inprecise but simple test to see if file is in XSLT language */
    if (exe.right(4).lower() == ".xsl")
	return "XSLT";

    return "GDB";
}

// helper that gets a file name (it only differs in the caption of the dialog)
TQString DebuggerMainWndBase::myGetFileName(TQString caption,
			   TQString dir, TQString filter,
			   TQWidget* parent)
{
    TQString filename;
    KFileDialog dlg(dir, filter, parent, "filedialog", true);

    dlg.setCaption(caption);

    if (dlg.exec() == TQDialog::Accepted)
	filename = dlg.selectedFile();

    return filename;
}

void DebuggerMainWndBase::newStatusMsg(KStatusBar* statusbar)
{
    TQString msg = m_debugger->statusMessage();
    statusbar->changeItem(msg, ID_STATUS_MSG);
}

void DebuggerMainWndBase::doGlobalOptions(TQWidget* parent)
{
    TQTabDialog dlg(parent, "global_options", true);
    TQString title = tdeApp->caption();
    title += i18n(": Global options");
    dlg.setCaption(title);
    dlg.setCancelButton(i18n("Cancel"));
    dlg.setOKButton(i18n("OK"));

    PrefDebugger prefDebugger(&dlg);
    prefDebugger.setDebuggerCmd(m_debuggerCmdStr.isEmpty()  ?
				GdbDriver::defaultGdb()  :  m_debuggerCmdStr);
    prefDebugger.setTerminal(m_outputTermCmdStr);

    PrefMisc prefMisc(&dlg);
    prefMisc.setPopIntoForeground(m_popForeground);
    prefMisc.setBackTimeout(m_backTimeout);
    prefMisc.setTabWidth(m_tabWidth);
    prefMisc.setSourceFilter(m_sourceFilter);
    prefMisc.setHeaderFilter(m_headerFilter);

    dlg.addTab(&prefDebugger, i18n("&Debugger"));
    dlg.addTab(&prefMisc, i18n("&Miscellaneous"));
    if (dlg.exec() == TQDialog::Accepted)
    {
	setDebuggerCmdStr(prefDebugger.debuggerCmd());
	setTerminalCmd(prefDebugger.terminal());
	m_popForeground = prefMisc.popIntoForeground();
	m_backTimeout = prefMisc.backTimeout();
	m_tabWidth = prefMisc.tabWidth();
	m_sourceFilter = prefMisc.sourceFilter();
	if (m_sourceFilter.isEmpty())
	    m_sourceFilter = defaultSourceFilter;
	m_headerFilter = prefMisc.headerFilter();
	if (m_headerFilter.isEmpty())
	    m_headerFilter = defaultHeaderFilter;
    }
}

const char fifoNameBase[] = "/tmp/kdbgttywin%05d";

/*
 * We use the scope operator :: in this function, so that we don't
 * accidentally use the wrong close() function (I've been bitten ;-),
 * outch!) (We use it for all the libc functions, to be consistent...)
 */
TQString DebuggerMainWndBase::createOutputWindow()
{
    // create a name for a fifo
    TQString fifoName;
    fifoName.sprintf(fifoNameBase, ::getpid());

    // create a fifo that will pass in the tty name
    TQFile::remove(fifoName);		// remove remnants
#ifdef HAVE_MKFIFO
    if (::mkfifo(fifoName.local8Bit(), S_IRUSR|S_IWUSR) < 0) {
	// failed
	TRACE("mkfifo " + fifoName + " failed");
	return TQString();
    }
#else
    if (::mknod(fifoName.local8Bit(), S_IFIFO | S_IRUSR|S_IWUSR, 0) < 0) {
	// failed
	TRACE("mknod " + fifoName + " failed");
	return TQString();
    }
#endif

    m_outputTermProc = new TDEProcess;

    {
	/*
	 * Spawn an xterm that in turn runs a shell script that passes us
	 * back the terminal name and then only sits and waits.
	 */
	static const char shellScriptFmt[] =
	    "tty>%s;"
	    "trap \"\" INT QUIT TSTP;"	/* ignore various signals */
	    "exec<&-;exec>&-;"		/* close stdin and stdout */
	    "while :;do sleep 3600;done";
	// let config file override this script
	TQString shellScript;
	if (!m_outputTermKeepScript.isEmpty()) {
	    shellScript = m_outputTermKeepScript;
	} else {
	    shellScript = shellScriptFmt;
	}

	shellScript.replace("%s", fifoName);
	TRACE("output window script is " + shellScript);

	TQString title = tdeApp->caption();
	title += i18n(": Program output");

	// parse the command line specified in the preferences
	TQStringList cmdParts = TQStringList::split(' ', m_outputTermCmdStr);

	/*
	 * Build the argv array. Thereby substitute special sequences:
	 */
	struct {
	    char seq[4];
	    TQString replace;
	} substitute[] = {
	    { "%T", title },
	    { "%C", shellScript }
	};

	for (TQStringList::iterator i = cmdParts.begin(); i != cmdParts.end(); ++i)
	{
	    TQString& str = *i;
	    for (int j = sizeof(substitute)/sizeof(substitute[0])-1; j >= 0; j--) {
		int pos = str.find(substitute[j].seq);
		if (pos >= 0) {
		    str.replace(pos, 2, substitute[j].replace);
		    break;		/* substitute only one sequence */
		}
	    }
	    *m_outputTermProc << str;
	}

    }

    if (m_outputTermProc->start())
    {
	TQString tty;

	// read the ttyname from the fifo
	TQFile f(fifoName);
	if (f.open(IO_ReadOnly))
	{
	    TQByteArray t = f.readAll();
	    tty = TQString::fromLocal8Bit(t, t.size());
	    f.close();
	}
	f.remove();

	// remove whitespace
	tty = tty.stripWhiteSpace();
	TRACE("tty=" + tty);
	return tty;
    }
    else
    {
	// error, could not start xterm
	TRACE("fork failed for fifo " + fifoName);
	TQFile::remove(fifoName);
	shutdownTermWindow();
	return TQString();
    }
}

void DebuggerMainWndBase::shutdownTermWindow()
{
    delete m_outputTermProc;
    m_outputTermProc = 0;
}

void DebuggerMainWndBase::setTerminalCmd(const TQString& cmd)
{
    m_outputTermCmdStr = cmd;
    // revert to default if empty
    if (m_outputTermCmdStr.isEmpty()) {
	m_outputTermCmdStr = defaultTermCmdStr;
    }
}

void DebuggerMainWndBase::slotDebuggerStarting()
{
    if (m_debugger == 0)		/* paranoia check */
	return;

    if (m_ttyLevel == m_debugger->ttyLevel())
    {
    }
    else
    {
	// shut down terminal emulations we will not need
	switch (m_ttyLevel) {
	case KDebugger::ttySimpleOutputOnly:
	    ttyWindow()->deactivate();
	    break;
	case KDebugger::ttyFull:
	    if (m_outputTermProc != 0) {
		m_outputTermProc->kill();
		// will be deleted in slot
	    }
	    break;
	default: break;
	}

	m_ttyLevel = m_debugger->ttyLevel();

	TQString ttyName;
	switch (m_ttyLevel) {
	case KDebugger::ttySimpleOutputOnly:
	    ttyName = ttyWindow()->activate();
	    break;
	case KDebugger::ttyFull:
	    if (m_outputTermProc == 0) {
		// create an output window
		ttyName = createOutputWindow();
		TRACE(ttyName.isEmpty() ?
		      "createOuputWindow failed" : "successfully created output window");
	    }
	    break;
	default: break;
	}

	m_debugger->setTerminal(ttyName);
    }
}

void DebuggerMainWndBase::setDebuggerCmdStr(const TQString& cmd)
{
    m_debuggerCmdStr = cmd;
    // make empty if it is the default
    if (m_debuggerCmdStr == GdbDriver::defaultGdb()) {
	m_debuggerCmdStr = TQString();
    }
}


#include "mainwndbase.moc"
