/*
 * 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 "dbgdriver.h"
#include "exprwnd.h"
#include <tqstringlist.h>
#include <ctype.h>
#include <stdlib.h>			/* strtol, atoi */
#include <algorithm>
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "mydebug.h"
#include <assert.h>


DebuggerDriver::DebuggerDriver() :
	TDEProcess(),
	m_state(DSidle),
	m_output(0),
	m_outputLen(0),
	m_outputAlloc(0),
	m_activeCmd(0),
	m_promptMinLen(0),
	m_promptLastChar('\0')
{
    m_outputAlloc = 4000;
    m_output = new char[m_outputAlloc];

    m_prompt[0] = '\0';
    // Derived classes can set either m_prompt or m_promptRE.
    // If m_promptRE is set, it must include the '$' at the end.
    // m_promptLastChar and m_promptMinLen must also be set.

    // debugger process
    connect(this, TQ_SIGNAL(receivedStdout(TDEProcess*,char*,int)),
	    TQ_SLOT(slotReceiveOutput(TDEProcess*,char*,int)));
    connect(this, TQ_SIGNAL(wroteStdin(TDEProcess*)), TQ_SLOT(slotCommandRead(TDEProcess*)));
    connect(this, TQ_SIGNAL(processExited(TDEProcess*)), TQ_SLOT(slotExited(TDEProcess*)));
}

DebuggerDriver::~DebuggerDriver()
{
    delete[] m_output;
    flushHiPriQueue();
    flushLoPriQueue();
}

int DebuggerDriver::commSetupDoneC()
{
    TRACE(__PRETTY_FUNCTION__);

    if (!TDEProcess::commSetupDoneC())
	return 0;

    close(STDERR_FILENO);
    return dup2(STDOUT_FILENO, STDERR_FILENO) != -1;
}


bool DebuggerDriver::startup(TQString cmdStr)
{
    // clear command queues
    delete m_activeCmd;
    m_activeCmd = 0;
    flushHiPriQueue();
    flushLoPriQueue();
    m_state = DSidle;

    // debugger executable
    if (cmdStr.isEmpty())
	cmdStr = defaultInvocation();

    TQStringList cmd = TQStringList::split(' ', cmdStr);
    clearArguments();
    for (TQStringList::iterator i = cmd.begin(); i != cmd.end(); ++i) {
	*this << *i;
    }

    if (!start(TDEProcess::NotifyOnExit,
	       TDEProcess::Communication(TDEProcess::Stdin|TDEProcess::Stdout))) {
	return false;
    }

    // open log file
    if (!m_logFile.isOpen() && !m_logFileName.isEmpty()) {
	m_logFile.setName(m_logFileName);
	m_logFile.open(IO_WriteOnly);
    }

    // these must be set by derived classes in their constructor
    assert(m_promptMinLen > 0);		// _must_ have a prompt
    assert(m_promptLastChar != '\0');	// last char _must_ be fixed
    assert(m_prompt[0] == '\0' || strlen(m_prompt) == m_promptMinLen);
    // either a prompt or a regexp
    assert(m_prompt[0] != '\0' && m_promptMinLen < sizeof(m_prompt) ||
	   !m_promptRE.isEmpty() && m_promptRE.isValid());

    return true;
}

void DebuggerDriver::slotExited(TDEProcess*)
{
    static const char txt[] = "\n====== debugger exited ======\n";
    if (m_logFile.isOpen()) {
	m_logFile.writeBlock(txt,sizeof(txt)-1);
    }

    // reset state
    m_state = DSidle;
    // empty buffer
    m_outputLen = 0;
    *m_output = '\0';
}


CmdQueueItem* DebuggerDriver::executeCmdString(DbgCommand cmd,
					       TQString cmdString, bool clearLow)
{
    // place a new command into the high-priority queue
    CmdQueueItem* cmdItem = new CmdQueueItem(cmd, cmdString);
    m_hipriCmdQueue.push(cmdItem);

    if (clearLow) {
	if (m_state == DSrunningLow) {
	    // take the liberty to interrupt the running command
	    m_state = DSinterrupted;
	    kill(SIGINT);
	    ASSERT(m_activeCmd != 0);
	    TRACE(TQString("interrupted the command %1").arg((m_activeCmd ? m_activeCmd->m_cmd : -1)));
	    delete m_activeCmd;
	    m_activeCmd = 0;
	}
	flushLoPriQueue();
    }
    // if gdb is idle, send it the command
    if (m_state == DSidle) {
	ASSERT(m_activeCmd == 0);
	writeCommand();
    }

    return cmdItem;
}

bool CmdQueueItem::IsEqualCmd::operator()(CmdQueueItem* cmd) const
{
    return cmd->m_cmd == m_cmd && cmd->m_cmdString == m_str;
}

CmdQueueItem* DebuggerDriver::queueCmdString(DbgCommand cmd,
					     TQString cmdString, QueueMode mode)
{
    // place a new command into the low-priority queue
    std::list<CmdQueueItem*>::iterator i;
    CmdQueueItem* cmdItem = 0;
    switch (mode) {
    case TQMoverrideMoreEqual:
    case TQMoverride:
	// check whether gdb is currently processing this command
	if (m_activeCmd != 0 &&
	    m_activeCmd->m_cmd == cmd && m_activeCmd->m_cmdString == cmdString)
	{
	    return m_activeCmd;
	}
	// check whether there is already the same command in the queue
	i = find_if(m_lopriCmdQueue.begin(), m_lopriCmdQueue.end(), CmdQueueItem::IsEqualCmd(cmd, cmdString));
	if (i != m_lopriCmdQueue.end()) {
	    // found one
	    cmdItem = *i;
	    if (mode == TQMoverrideMoreEqual) {
		// All commands are equal, but some are more equal than others...
		// put this command in front of all others
		m_lopriCmdQueue.erase(i);
		m_lopriCmdQueue.push_front(cmdItem);
	    }
	    break;
	} // else none found, so add it
	// drop through
    case TQMnormal:
	cmdItem = new CmdQueueItem(cmd, cmdString);
	m_lopriCmdQueue.push_back(cmdItem);
    }

    // if gdb is idle, send it the command
    if (m_state == DSidle) {
	ASSERT(m_activeCmd == 0);
	writeCommand();
    }

    return cmdItem;
}

// dequeue a pending command, make it the active one and send it to gdb
void DebuggerDriver::writeCommand()
{
//    ASSERT(m_activeCmd == 0);
    assert(m_activeCmd == 0);

    // first check the high-priority queue - only if it is empty
    // use a low-priority command.
    CmdQueueItem* cmd;
    DebuggerState newState = DScommandSent;
    if (!m_hipriCmdQueue.empty()) {
	cmd = m_hipriCmdQueue.front();
	m_hipriCmdQueue.pop();
    } else if (!m_lopriCmdQueue.empty()) {
	cmd = m_lopriCmdQueue.front();
	m_lopriCmdQueue.pop_front();
	newState = DScommandSentLow;
    } else {
	// nothing to do
	m_state = DSidle;		/* is necessary if command was interrupted earlier */
	return;
    }

    m_activeCmd = cmd;
    TRACE("in writeCommand: " + cmd->m_cmdString);

    const char* str = cmd->m_cmdString.local8Bit().data();
    size_t cmdStrLen = strlen(str);
    writeStdin(str, cmdStrLen);

    // write also to log file
    if (m_logFile.isOpen()) {
	m_logFile.writeBlock(str, cmdStrLen);
	m_logFile.flush();
    }

    m_state = newState;
}

void DebuggerDriver::flushLoPriQueue()
{
    while (!m_lopriCmdQueue.empty()) {
	delete m_lopriCmdQueue.back();
	m_lopriCmdQueue.pop_back();
    }
}

void DebuggerDriver::flushHiPriQueue()
{
    while (!m_hipriCmdQueue.empty()) {
	delete m_hipriCmdQueue.front();
	m_hipriCmdQueue.pop();
    }
}

void DebuggerDriver::flushCommands(bool hipriOnly)
{
    flushHiPriQueue();
    if (!hipriOnly) {
	flushLoPriQueue();
    }
}

void DebuggerDriver::slotCommandRead(TDEProcess*)
{
    TRACE(__PRETTY_FUNCTION__);

    // there must be an active command which is not yet commited
    ASSERT(m_state == DScommandSent || m_state == DScommandSentLow);
    ASSERT(m_activeCmd != 0);
    ASSERT(!m_activeCmd->m_committed);

    // commit the command
    m_activeCmd->m_committed = true;

    // now the debugger is officially working on the command
    m_state = m_state == DScommandSent ? DSrunning : DSrunningLow;

    // set the flag that reflects whether the program is really running
    switch (m_activeCmd->m_cmd) {
    case DCrun:	case DCcont: case DCnext: case DCstep: case DCfinish: case DCuntil:
	emit inferiorRunning();
	break;
    default:
	break;
    }

    // re-receive delayed output
    while (!m_delayedOutput.empty()) {
	TQByteArray delayed = m_delayedOutput.front();
	m_delayedOutput.pop();
	slotReceiveOutput(0, const_cast<char*>(delayed.data()), delayed.size());
    }
}

void DebuggerDriver::slotReceiveOutput(TDEProcess*, char* buffer, int buflen)
{
    /*
     * The debugger should be running (processing a command) at this point.
     * If it is not, it is still idle because we haven't received the
     * wroteStdin signal yet, in which case there must be an active command
     * which is not commited.
     */
    if (m_state == DScommandSent || m_state == DScommandSentLow) {
	ASSERT(m_activeCmd != 0);
	ASSERT(!m_activeCmd->m_committed);
	/*
	 * We received output before we got signal wroteStdin. Collect this
	 * output, it will be re-sent by commandRead when it gets the
	 * acknowledgment for the uncommitted command.
	 */
	m_delayedOutput.push(TQByteArray().duplicate(buffer, buflen));
	return;
    }
    // write to log file (do not log delayed output - it would appear twice)
    if (m_logFile.isOpen()) {
	m_logFile.writeBlock(buffer, buflen);
	m_logFile.flush();
    }
    
    /*
     * gdb sometimes produces stray output while it's idle. This happens if
     * it receives a signal, most prominently a SIGCONT after a SIGTSTP:
     * The user haltet kdbg with Ctrl-Z, then continues it with "fg", which
     * also continues gdb, which repeats the prompt!
     */
    if (m_activeCmd == 0 && m_state != DSinterrupted) {
	// ignore the output
	TRACE("ignoring stray output: " + TQString::fromLatin1(buffer, buflen));
	return;
    }
    ASSERT(m_state == DSrunning || m_state == DSrunningLow || m_state == DSinterrupted);
    ASSERT(m_activeCmd != 0 || m_state == DSinterrupted);

    // collect output until next prompt string is found
    
    // accumulate it
    if (m_outputLen + buflen >= m_outputAlloc) {
	/*
	 * Must enlarge m_output: double it. Note: That particular
	 * sequence of commandes here ensures exception safety.
	 */
	int newSize = m_outputAlloc * 2;
	char* newBuf = new char[newSize];
	memcpy(newBuf, m_output, m_outputLen);
	delete[] m_output;
	m_output = newBuf;
	m_outputAlloc = newSize;
    }
    memcpy(m_output+m_outputLen, buffer, buflen);
    m_outputLen += buflen;
    m_output[m_outputLen] = '\0';

    /*
     * If there's a prompt string in the collected output, it must be at
     * the very end. In order to quickly find out whether there is a prompt
     * string, we check whether the last character of m_output is identical
     * to the last character of the prompt string. Only if it is, we check
     * for the full prompt string.
     * 
     * Note: Using the regular expression here is most expensive, because
     * it translates m_output to a TQString each time.
     * 
     * Note: It could nevertheless happen that a character sequence that is
     * equal to the prompt string appears at the end of the output,
     * although it is very, very unlikely (namely as part of a string that
     * lingered in gdb's output buffer due to some timing/heavy load
     * conditions for a very long time such that that buffer overflowed
     * exactly at the end of the prompt string look-a-like).
     */
    int promptStart = -1;
    if (m_output[m_outputLen-1] == m_promptLastChar &&
	m_outputLen >= m_promptMinLen)
    {
	// this is a candidate for a prompt at the end,
	// now see if there really is
	if (m_prompt[0] != '\0') {
	    if (strncmp(m_output+m_outputLen-m_promptMinLen,
			m_prompt, m_promptMinLen) == 0)
	    {
		promptStart = m_outputLen-m_promptMinLen;
	    }
	} else {
	    TQString output = TQString::fromLatin1(m_output, m_outputLen);
#if TQT_VERSION >= 300
	    promptStart = m_promptRE.search(output);
#else
	    promptStart = m_promptRE.match(output);
#endif
	}
    }
    if (promptStart >= 0)
    {
	// found prompt!

	// terminate output before the prompt
	m_output[promptStart] = '\0';

	/*
	 * We've got output for the active command. But if it was
	 * interrupted, ignore it.
	 */
	if (m_state != DSinterrupted) {
	    /*
	     * m_state shouldn't be DSidle while we are parsing the output
	     * so that all commands produced by parse() go into the queue
	     * instead of being written to gdb immediately.
	     */
	    ASSERT(m_state != DSidle);
	    CmdQueueItem* cmd = m_activeCmd;
	    m_activeCmd = 0;
	    commandFinished(cmd);
	    delete cmd;
	}

	// empty buffer
	m_outputLen = 0;
	*m_output = '\0';
	// also clear delayed output if interrupted
	if (m_state == DSinterrupted) {
	    m_delayedOutput = std::queue<TQByteArray>();
	}

	/*
	 * We parsed some output successfully. Unless there's more delayed
	 * output, the debugger must be idle now, so send down the next
	 * command.
	 */
	if (m_delayedOutput.empty()) {
	    if (m_hipriCmdQueue.empty() && m_lopriCmdQueue.empty()) {
		// no pending commands
		m_state = DSidle;
		emit enterIdleState();
	    } else {
		writeCommand();
	    }
	}
    }
}

void DebuggerDriver::dequeueCmdByVar(VarTree* var)
{
    if (var == 0)
	return;

    std::list<CmdQueueItem*>::iterator i = m_lopriCmdQueue.begin();
    while (i != m_lopriCmdQueue.end()) {
	if ((*i)->m_expr != 0 && var->isAncestorEq((*i)->m_expr)) {
	    // this is indeed a critical command; delete it
	    TRACE("removing critical lopri-cmd: " + (*i)->m_cmdString);
	    delete *i;
	    m_lopriCmdQueue.erase(i++);
	} else
	    ++i;
    }
}


TQString DebuggerDriver::editableValue(VarTree* value)
{
    // by default, let the user edit what is visible
    return value->value();
}


StackFrame::~StackFrame()
{
    delete var;
}


DbgAddr::DbgAddr(const TQString& aa) :
	a(aa)
{
    cleanAddr();
}

/*
 * We strip off the leading 0x and any leading zeros.
 */
void DbgAddr::cleanAddr()
{
    if (a.isEmpty())
	return;

    while (a[0] == '0' || a[0] == 'x') {
	a.remove(0, 1);
    }
}

void DbgAddr::operator=(const TQString& aa)
{
    a = aa;
    fnoffs = TQString();
    cleanAddr();
}

/* Re-attach 0x in front of the address */
TQString DbgAddr::asString() const
{
    if (a.isEmpty())
	return TQString();
    else
	return "0x" + a;
}

bool operator==(const DbgAddr& a1, const DbgAddr& a2)
{
    return TQString::compare(a1.a, a2.a) == 0;
}

bool operator>(const DbgAddr& a1, const DbgAddr& a2)
{
    if (a1.a.length() > a2.a.length())
	return true;
    if (a1.a.length() < a2.a.length())
	return false;
    return TQString::compare(a1.a, a2.a) > 0;
}


Breakpoint::Breakpoint() :
	id(0),
	type(breakpoint),
	temporary(false),
	enabled(true),
	ignoreCount(0),
	hitCount(0),
	lineNo(0)
{ }

#include "dbgdriver.moc"
