/* Copyright (C) 2002 Philippe Fremy <pfremy@kde.org>

   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; see the file COPYING.  If not, write to
   the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
*/


#include <ktempfile.h>
#include <kstddirs.h>
#include <kapp.h>
#include <kmessagebox.h>
#include <kdebug.h>
#include <kconfig.h>

#include <qtimer.h>
#include <qregexp.h>
#include <qtextstream.h>
#include <qlayout.h>
#include <qmultilineedit.h>
#include <qfileinfo.h>
#include <X11/Xlib.h>

#include "vimwidget.h"

VimWidget::VimWidget( QWidget * parent, const char * name, WFlags f )
    : QXEmbed( parent, name, f )
{
	QString fileName, serverName;

    setAutoDelete( false );
    _vimReady = false;
	_fallbackMode = false;
    _readWrite = true;
    _cmdRunning = false;
	_beingClosed = false;
	_vimProcess = 0L;
	counter=0;
	bool ret = setExecutable();

	if (ret == true) {
		KTempFile f( locateLocal("tmp", "EmbeddedVim") );
		f.close();
		serverName = f.name().replace( QRegExp("^.*EmbeddedVim"), "EmbeddedVimServer");
		fileName = f.name();

		startVim( serverName, fileName );

		// Enable this for fun!
//		QTimer::singleShot( 3000, this, SLOT(someTests()) );
	} else {
		fallbackMode();
	}
}

void VimWidget::fallbackMode()
{
	// Fallback editing
	KMessageBox::information( this, QString("Falling back to normal edit control") );
	_fallbackMode = true;
	QVBoxLayout * ly = new QVBoxLayout( this );
	_mle = new QMultiLineEdit( this );
	_mle->setReadOnly( false );
	_mle->setEnabled( true );
	_mle->setFocus();
	ly->addWidget( _mle );
	ly->activate();
	_mle->show();
	show();
	kdDebug() << "VimWidget::fallbackMode() - done" << endl;
}

bool VimWidget::setExecutable()
{
	QString vimExecutable;
	KConfig config( "vimwidgetrc" );
	config.setGroup("Vim");
	vimExecutable = config.readEntry("executable", "");

	QString msg = "\nUnable to start the Vim editing control. The vim executable set at install time does not work. Try to reinstall the component and check for the goodVim file.\n";


	if (vimExecutable.isEmpty() ) {
		KMessageBox::sorry( this, "Could not read Vim execuatble path from settings. The vimwidgetrc is probably missing\n" + msg, "Vim error" );
		return false;

	}

	QFileInfo fi( vimExecutable );

	if (fi.exists() == false) {
		KMessageBox::sorry( this, QString("Vim executable '%1' does not exist or is not accessible.\n" + msg).arg( vimExecutable ), "Vim error" );
		return false;
	}

	if (fi.isExecutable() == false) {
		KMessageBox::sorry( this, QString("Vim executable '%1' is not execuable.\n" + msg).arg( vimExecutable ), "Vim error" );
		return false;
	}

	_vimExecutable = vimExecutable;
	return true;
}

void VimWidget::startVim( QString serverName, QString fileName )
{
	_fileName = fileName;
	_serverName = serverName;

    kdDebug() << "Vim filename : " << _fileName << endl;
    kdDebug() << "Vim servername : " << _serverName << endl;

	if (_vimProcess != 0L) {
		delete _vimProcess;
	}

    _vimProcess = new KProcess;

    (*_vimProcess) << _vimExecutable
        << "-gf"  // run in forground and in graphic version
//        << "--debug-server"   // enable to display vim server's processing
        << "--servername" << _serverName       // server name
        <<  "--echo-wid"                 // tell to ouput window id
        << _fileName                         // file to edit
        ;

    if (1 /* withMenuBar == false */ ) {
        // must be set twice to avoid at maximum the flicker. Gvim actually reads
        // --cmd args (disable the menu), then read /usr/share/vim/menu.vim so enables
        // the menu, then disables it again with -c args. I couldn't find a better way.
        (*_vimProcess) << "--cmd" << ":set guioptions-=m"     // disable menus
                        << "-c" << ":set guioptions-=m"     // disable menus
        ;
    }

    if (1 /* withToolbar == false */ ) {
        (*_vimProcess) << "--cmd" << ":set guioptions-=T"     // disable toolbar
                        << "-c" << ":set guioptions-=T"     // disable toolbar
        ;
    }

    connect( _vimProcess, SIGNAL(processExited(KProcess *)), 
                SLOT(vimProcessExited(KProcess *)) );
    connect( _vimProcess, SIGNAL(receivedStdout(KProcess *, char *, int)), 
                   SLOT(vimProcessStdout(KProcess *, char *, int)) );
    connect( _vimProcess, SIGNAL(receivedStderr(KProcess *, char *, int)), 
                    SLOT(vimProcessStderr(KProcess *, char *, int)) );

    connect( this, SIGNAL(embeddedId(int)), SLOT(embedVimWid(int)) );

    bool ret =  _vimProcess->start( KProcess::NotifyOnExit, KProcess::AllOutput );

    if (ret != true) {
        kdDebug() << "Vim process not started!\n" << endl;
    }
}


bool VimWidget::close( bool alsoDelete )
{
    //qDebug("VimWidget::close()");
	if (_fallbackMode == false ) {
		closeVim();
	}
    return QXEmbed::close( alsoDelete );
}

void VimWidget::closeVim()
{
	_beingClosed = true;
    //qDebug("VimWidget::closeVim()");
    if (_vimProcess->isRunning() == false) return;

    KProcess * tmpProc = new KProcess;
    (*tmpProc) << "gvim" << "-f" << "--servername" << _serverName 
        << "--remote-send" << "<C-\\><C-N>ZZ\n";
    tmpProc->start( KProcess::Block );

    //qDebug("wait for vim to stop!");
    ASSERT( _vimProcess );
    /*
    while( _vimProcess->isRunning() == true) {
        qApp->processEvents();
    }
    */
    _vimProcess->detach();

    //qDebug("send XDestroyWindow event!");
    // XXX Fix for QXEmbed: XDestroyEvent doesn't seem to be generated, 
    // so I generate it manually
    XDestroyWindowEvent * dwe = new XDestroyWindowEvent;
    dwe->type = DestroyNotify;
    dwe->window = embeddedWinId();

    x11Event( (XEvent *) dwe );

    // safe ?
    delete dwe;
    //kdDebug() << "VimWidget::close() - done." << endl;
}

VimWidget::~VimWidget()
{
    //qDebug("VimWidget::~VimWidget() - destroying widget");

	if (_fallbackMode == false) {
		closeVim();

		if (_vimProcess != 0L) delete _vimProcess;
		_vimProcess = 0;
	}
}

void VimWidget::embedVimWid( int wid )
{
    //qDebug("VimWidget::embedVimWid( %d )", wid);
    embed( wid );

    // This is empirical. It could certainly be improved. For example, there is 
    // certainly a way to know if a given window has been realised. To improve later!
//    QTimer::singleShot( 5000, this, SLOT(vimRealised()) );
	testTimerFinished();
}

void VimWidget::testTimerFinished()
{
	kdDebug() << "Starting test ..." << endl;    
	KProcess * tmpProc = new KProcess;
    (*tmpProc) << _vimExecutable << "--serverlist";
    connect( tmpProc, SIGNAL(processExited(KProcess *)), SLOT(testProcessExited(KProcess *)) );
    connect( tmpProc, SIGNAL(receivedStdout(KProcess *, char *, int)), 
                    SLOT(vimProcessStdout(KProcess *, char *, int)) );

    tmpProc->start( KProcess::NotifyOnExit, KProcess::AllOutput);
}

void VimWidget::testProcessExited(KProcess *) 
{
	counter++; // don't do it infinitely :)	
	if (!_vimReady && counter < 10) {
			testTimerFinished();
			return;
	}
	else if (!_vimReady) { 
		KMessageBox::sorry( this, QString("The vim executable you tried does not appear to support the client-server feature.\n"), "Vim error" );
		fallbackMode();
	}
}

void VimWidget::vimRealised()
{
    _vimReady = true;

    processCmd();
}

static QString buf2string( char * buf, int buflen )
{
    char * s  = new char[ buflen + 10 ];
    strncpy( s, buf, buflen );
    s[buflen] = '\0';

    QString qs = s;
    delete s;
    return qs;
}

void VimWidget::vimProcessStdout(KProcess * , char * buf, int buflen )
{
    char * s  = new char[ buflen + 10 ];
    strncpy( s, buf, buflen );
    s[buflen] = '\0';
    if (buflen > 1 && s[buflen-1] == '\n') {
        s[buflen-1] = '\0';
    }

    // get the window id of gvim thank to a small patch
    if (strncmp(s, "WID:", 4) == 0) {
        int wid;
        wid = QString(s).mid(4).toInt();
        //qDebug("WID: %d", wid);
        emit embeddedId( wid );
		resize( size() );
    } else if (strstr(s,_serverName.upper())) {
    	kdDebug() << "Found server in list !" << endl;
		if (!_vimReady) vimRealised();
	}


    kdDebug() << "Embedded stdout: " << s << endl;
    delete s;
}


void VimWidget::vimProcessStderr(KProcess * , char * buf, int buflen )
{
    char * s  = new char[ buflen + 10 ];
    strncpy( s, buf, buflen );
    s[buflen] = '\0';
    if (buflen > 1 && s[buflen-1] == '\n') {
        s[buflen-1] = '\0';
    }
    //kdDebug() << "Embedded stderr: " << s << endl;
    // get the window id of gvim thank to a small patch
    if (strncmp(s, "WID:", 4) == 0) {
        int wid;
        wid = QString(s).mid(4).toInt();
        //qDebug("WID: %d", wid);
        emit embeddedId( wid );
		resize( size() );
    }

    kdDebug() << "Embedded stderr: " << s << endl;
    delete s;
}

void VimWidget::vimProcessExited(KProcess *  proc)
{ 
    kdDebug() << "Vim process exited, with status " << proc->exitStatus(); 
    embed( 0 );

	if (_vimReady == false ) {
		// Vim was not started
		KMessageBox::sorry( this, QString("The Vim process has died.\nUnable to start the Vim editing control."), "Vim error" );
		fallbackMode();
		delete _vimProcess;
		_vimProcess = 0L;
		return;
	}

	_vimReady = false;

	if (_beingClosed == true) return;

	_pendingCmd.clear();

	// change servername to get rid of previous pending commands:
	char c = (char) (_serverName[0].latin1() + 1);
	if (c < 32) c += 40;
	_serverName = c + _serverName.mid(1);

	startVim( _serverName, _fileName );
	setReadWrite( _readWrite );
}

// Only for debug purpose
void VimWidget::cmdProcessOutput(KProcess * , char * buf, int buflen )
{
    char * s  = new char[ buflen + 10 ];
    strncpy( s, buf, buflen );
    s[buflen] = '\0';

    _cmdProcOutput.append( s );
    if (_cmdProcOutput.contains('\n')) {
        QString qs = _cmdProcOutput.left( _cmdProcOutput.find( '\n' ) );
        kdDebug() << "cmd output: " << qs << endl;
        _cmdProcOutput = _cmdProcOutput.mid( _cmdProcOutput.find( '\n' )+1 );
    }
    
    delete s;
}

// Only for debug purpose
void VimWidget::cmdProcessExited(KProcess * proc)
{ 
    //qDebug("cmd process exited : %d!", proc->exitStatus()); 

    _cmdRunning = false;

    delete proc;

    // process next pending cmd if any:
    processCmd();
}

void VimWidget::sendNormalCmd( const QString & cmd )
{
    sendRawCmd( "<C-\\><C-N>" + cmd );
}

void VimWidget::sendInsertCmd( const QString & cmd, bool leaveInInsertMode )
{
    QString s = cmd;
    if (leaveInInsertMode == false) {
        s += "<C-\\><C-N>";
    }
    sendRawCmd( "<C-\\><C-N>i" + s );
}


void VimWidget::sendCmdLineCmd( const QString & cmd )
{
    sendRawCmd( "<C-\\><C-N>:" + cmd + "<C-M>" );
}

void VimWidget::sendRawCmd( const QString & cmd )
{
	processCmd( cmd );

    /*
    kdDebug() << "VimWidget::sendCmdLineCmd() - Talking to server '" 
            << _serverName << "'" << endl;
    */
}

void VimWidget::processCmd( QString cmd )
{
    if (cmd.isEmpty() == false) {
        _pendingCmd << cmd;
    }

    if (_vimReady == true && _cmdRunning == false && _pendingCmd.isEmpty() == false) {
        QString s = _pendingCmd.first();
        _pendingCmd.remove( s );

        /*
        // Qt3 syntax:
        QString s = _pendingCmd.front() 
        _pendingCmd.pop_front();
        */
        sendCmd( s );
    }

    // The function will be called back when the current cmd finishes.
}

void VimWidget::sendCmd( QString cmd )
{
	if (_fallbackMode == true) return;

    //kdDebug() << "VimWidget::sendCmd( " << cmd << " ) " << endl;
    KProcess * tmpProc = new KProcess;
    (*tmpProc) << "gvim" << "-f" << "--servername" << _serverName 
        << "--remote-send" << cmd;
    connect( tmpProc, SIGNAL(processExited(KProcess *)), SLOT(cmdProcessExited(KProcess *)) );
    connect( tmpProc, SIGNAL(receivedStdout(KProcess *, char *, int)), 
                    SLOT(cmdProcessOutput(KProcess *, char *, int)) );
    connect( tmpProc, SIGNAL(receivedStderr(KProcess *, char *, int)), 
                    SLOT(cmdProcessOutput(KProcess *, char *, int)) );

    _cmdRunning = true;
    bool ret = tmpProc->start( KProcess::NotifyOnExit, KProcess::AllOutput);
    ASSERT( ret == true );
}

void VimWidget::setReadWrite( bool rw )
{
	if (_fallbackMode == true) {
		_mle->setReadOnly( ! rw );
		return;
	}

    QString vimReadonlyVar = "readonly";
    if (rw == true) {
        vimReadonlyVar.prepend("no");
    }
    setVimVariable(vimReadonlyVar);
    _readWrite = rw;
}

bool VimWidget::isReadWrite()
{
	if (_fallbackMode == true) {
		return ! _mle->isReadOnly();
	}

	QString s = evalExpr( "&readonly" );
	_readWrite = (s[0] != '1');
	//kdDebug() << "isReadWrite() " << _readWrite << endl;
	return _readWrite;
}

void VimWidget::openFile(QString file)
{
	if (_fallbackMode == true) {
		_fileName = file;
		QFile f( file );
		f.open(IO_ReadOnly);
		_mle->setText( QTextStream( &f ).read() );
		f.close();
		return;
	}

    //kdDebug() << "VimWidget::openFile() - opening " << file << endl;

    _fileName = file;
    saveFile();
    sendCmdLineCmd("bd!|e " + file );
	if (_readWrite == false) {
		setReadWrite( false );
	}
}

void VimWidget::saveFile()
{
	if (_fallbackMode == true) {
		QFile f( _fileName );
		f.open(IO_WriteOnly);
		QTextStream( &f ) << _mle->text();
		f.close();
		return;
	}
    //kdDebug() << "VimWidget::saveFile()" << endl;
    if ( isReadWrite() ) {
        sendCmdLineCmd("wa");
    }
}

QString VimWidget::evalExpr( QString expr )
{
	if (_fallbackMode == true) return "";
    //kdDebug() << "VimWidget::evalExpr( '" << expr << "' ) " << endl;

	// We must wait for vim to popup
	while( _vimReady == false ) {
        qApp->processEvents( 100 ); // process events during 0.1 s
	}

	// wait until there are no more commands
	while( _pendingCmd.isEmpty() == false) {
        qApp->processEvents( 100 ); // process events during 0.1 s
	}

	// wait until last command exits 
	while( _cmdRunning == true) { 
        qApp->processEvents( 100 ); // process events during 0.1 s
	}

    KProcess * tmpProc = new KProcess;
    (*tmpProc) << "gvim" << "-f" << "--servername" << _serverName 
        << "--remote-expr" << expr;

    connect( tmpProc, SIGNAL(processExited(KProcess *)), 
                    SLOT(exprProcessExited(KProcess *)) );
    connect( tmpProc, SIGNAL(receivedStdout(KProcess *, char *, int)), 
                    SLOT(exprProcessStdout(KProcess *, char *, int)) );
    connect( tmpProc, SIGNAL(receivedStderr(KProcess *, char *, int)), 
                    SLOT(exprProcessStderr(KProcess *, char *, int)) );

    bool ret = tmpProc->start( KProcess::NotifyOnExit, KProcess::AllOutput);
    if (ret != true) {
        kdDebug() << "Problem when starting client vim." << endl;
    }

    while( tmpProc->isRunning() ) {
        qApp->processEvents( 100 ); // process events during 0.1 s
    }

    QString result = _exprProcOutput;
    if (result[ result.length()-1] == '\n') {
        result = result.left( result.length()-1 );
    }

    _exprProcOutput = "";
    delete tmpProc;

    return result;
}


void VimWidget::exprProcessStdout(KProcess * , char * buf, int buflen )
{
    QString s = buf2string( buf, buflen );
    //kdDebug() << "Eval output : " << s << endl;
    _exprProcOutput.append( s );
}

void VimWidget::exprProcessStderr(KProcess * , char * buf, int buflen )
{
    _exprProcErr.append( buf2string( buf, buflen ) );

    if (_exprProcErr.contains('\n')) {
        QString qs = _exprProcErr.left( _exprProcErr.find( '\n' ) );
        kdDebug() << "Expr stderr: " << qs << endl;
        _exprProcErr = _exprProcErr.mid( _exprProcErr.find( '\n' )+1 );
    }
}


void VimWidget::exprProcessExited(KProcess * /* proc */)
{ 
    //kdDebug() << "Expr process exited with " << proc->exitStatus() << endl; 
}

QString VimWidget::text()
{
	if (_fallbackMode == true) {
		return _mle->text();
	}

	sendNormalCmd("gg\"KyG''");
	return evalExpr( "@K" );
}

void VimWidget::setText( QString s )
{
	if (_fallbackMode == true) {
		_mle->setText( s );
		return;
	}

	kdDebug() << "VimWidget::setText( " + s  + " )" << endl;
	// I am afraid that we could fill up some buffer with long text
	// and get either a segfault (unprobable) or a truncated text. But it works
	// with 3000 lines.
	sendNormalCmd("ggdGi" + s + "<C-\\><C-N>" );
}

void VimWidget::setCursorPos( int raw, int col )
{
	if (_fallbackMode == true) {
		_mle->setCursorPosition( raw, col );
		return;
	}

	QString s;
   	s = QString::number(raw) + "G";
	if (col > 0) {
		s += QString::number(col) + "l";
	}
	sendNormalCmd( s );
}

void VimWidget::insertText( uint line, uint col, QString text )
{
	if (_fallbackMode == true) {
		_mle->insertAt( text, line, col );
		return;
	}
	QString s;
   	s = QString::number(line) + "G";
	if (col > 0) {
		s += QString::number(col) + "l";
	}
	s += "i" + text;
	s += "<C-\\><C-N>"; 
	sendNormalCmd( s );
}

void VimWidget::someTests()
{
	/*
    static int i=0;
    i++;
    sendInsertCmd(QString("Coucou c'est moi moi : %1!\n").arg(i), false);
    sendNormalCmd(QString("iCoucou c'est moi : %1!\n<esc>").arg(i));
    sendNormalCmd("gg");
    sendCmdLineCmd("s/moi/hop/");
    sendNormalCmd("n");
	*/


    //kdDebug() << "Readonly : " << (isReadWrite() ? "false" : "true" ) << endl;
    //kdDebug() << "Text : " << evalExpr( "\"coucou\"" ) << endl;
    //kdDebug() << "Line 3 : '" << textline(3) << "'" << endl;
    //kdDebug() << "Text : <<" << text() << ">>" << endl;
	//kdDebug() << "Setting text to 'coucou'" << endl;
	//setText("coucou\nhop\nhop\n");
	//setCursorPos( 7, 5 );
    //kdDebug() << "cursor : " << cursorRaw() << " - " << cursorCol() << endl;
	//insertText( 7, 5, "coucou" );
}

