/* minicli.cpp
*
* Copyright (C) 1997 Matthias Ettrich <ettrich@kde.org>
* Copyright (c) 1999 Preston Brown <pbrown@kde.org>
* Complete re-write:
* Copyright (C) 1999 - 2000 Dawit Alemayehu <adawit@kde.org>
* Copyright (C) 2000 Malte Starostik <starosti@zedat.fu-berlin.de>
* Copyright (C) 2000 Geert Jansen <jansen@kde.org>
*/

#include <pwd.h>
#include <string.h>
#include <errno.h>

#include <qlabel.h>
#include <qcheckbox.h>
#include <qpushbutton.h>
#include <qlayout.h>
#include <qtimer.h>
#include <qvbox.h>
#include <qdir.h>
#include <qslider.h>
#include <qwhatsthis.h>
#include <qcstring.h>

#include <kdialog.h>
#include <klocale.h>
#include <kconfig.h>
#include <kiconloader.h>
#include <kbuttonbox.h>
#include <kmessagebox.h>
#include <kmimetype.h>
#include <kservice.h>
#include <kprocess.h>
#include <kcombobox.h>
#include <kcompletion.h>
#include <knuminput.h>
#include <klineedit.h>
#include <kwm.h>
#include <kapp.h>
#include <kdebug.h>
#include <kpassdlg.h>
#include <krun.h>
#include <kdesu/stub.h>
#include <kdesu/su.h>

#include "minicli.moc"

#define ERR strerror(errno)

Minicli::Minicli( QWidget *parent, const char *name)
        :QDialog( parent, name )
{
    m_pCompletion = new KCompletion();
    m_strCWD = QString::null;
    loadConfig();
    // TODO: Should be invoked from the loadConfig.  Need to
    // make it a DCOP client as well so that GUI changes from
    // control panel can be reflected immediately!!
    loadStandardGUI();
}

Minicli::~Minicli()
{
    delete m_pCompletion;
    m_pCompletion = 0; // Set it to proper NULL pointer! We play nice :))
}

void Minicli::loadStandardGUI()
{
    QVBoxLayout* vbox = new QVBoxLayout( this );
    setIcon(SmallIcon("run"));

    setCaption( i18n("Run Command") );
    vbox->setResizeMode( QLayout::Fixed );

    vbox->setSpacing( KDialog::spacingHint() );
    vbox->setMargin( KDialog::marginHint() );

    QHBox *hBox = new QHBox( this );
    CHECK_PTR(hBox);
    vbox->addWidget( hBox );
    hBox->setSpacing( KDialog::marginHint() );

    m_runIcon = new QLabel( hBox );
    CHECK_PTR(m_runIcon);
    m_runIcon->setPixmap(DesktopIcon("go"));
    m_runIcon->setFixedSize(m_runIcon->sizeHint());

    QLabel *label = new QLabel( i18n("Enter the name of the application you "
	    "want to run or\nthe URL you want to view."), hBox);
    CHECK_PTR(label);

    hBox = new QHBox( this );
    vbox->addWidget( hBox );
    CHECK_PTR(hBox);
    hBox->setSpacing( KDialog::marginHint() );

    label = new QLabel(i18n("&Command:"), hBox);
    CHECK_PTR(label);
    label->setFixedSize(label->sizeHint());

    m_runCombo = new KComboBox( true, hBox );
    QWhatsThis::add(m_runCombo, i18n("Here you can enter the command you wish to run."));
    CHECK_PTR(m_runCombo);
    if( m_pCompletion )
    {
        m_runCombo->setCompletionObject( m_pCompletion );
        m_runCombo->setCompletionMode( m_pCompletion->completionMode() ); 
        m_runCombo->insertStringList( m_pCompletion->items() ); 
    }
    m_runCombo->setDuplicatesEnabled( false );
    m_runCombo->setInsertionPolicy( QComboBox::AtTop );
    connect( m_runCombo, SIGNAL( textChanged( const QString& ) ), 
	    SLOT( slotCmdChanged( const QString& ) ) );
    label->setBuddy(m_runCombo);

    m_parseTimer = new QTimer(this);
    CHECK_PTR(m_parseTimer);
    connect(m_parseTimer, SIGNAL(timeout()), SLOT(slotParseTimer()));

    mbAdvanced = false;
    mpAdvanced = new MinicliAdvanced(this);
    mpAdvanced->setMaximumSize(0, 0);
    vbox->addWidget(mpAdvanced, AlignLeft);

    hBox = new QHBox(this);
    CHECK_PTR(hBox);
    vbox->addWidget( hBox );
    hBox->setSpacing( KDialog::marginHint() );

    KButtonBox *bbox = new KButtonBox(hBox);
    CHECK_PTR(bbox);
    mpAdvButton = bbox->addButton(i18n("&Options >>"), false);
    CHECK_PTR(mpAdvButton);
    connect(mpAdvButton, SIGNAL(clicked()), SLOT(slotAdvanced()));
    bbox->addStretch();
    QPushButton *runButton = bbox->addButton(i18n("&Run"), false);
    CHECK_PTR(runButton);
    connect(runButton, SIGNAL(clicked()), this, SLOT(accept()));

    QPushButton *cancelButton = bbox->addButton(i18n("&Cancel"), false);
    CHECK_PTR(cancelButton);
    connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));

    m_runCombo->clearEdit();
    m_runCombo->setFocus();
    bbox->layout();
    move( QApplication::desktop()->width()/2 - width()/2,
          QApplication::desktop()->height()/2 - height()/2);
}

void Minicli::loadConfig()
{
    KConfig *config = KGlobal::config();
    config->setGroup("MiniCli");
    m_History = config->readListEntry("History");
    m_iMaxHistory = config->readNumEntry("HistoryLength", 50);
    if ( m_History.count() > m_iMaxHistory )
    {
	QStringList::Iterator it = m_History.at( m_iMaxHistory );
	while ( it != m_History.end() )
	    it = m_History.remove( it );
    }
    m_pCompletion->setItems( m_History );
    KGlobalSettings::Completion comp = (KGlobalSettings::Completion) 
	    config->readNumEntry("CompletionMode", KGlobalSettings::completionMode());
    m_pCompletion->setCompletionMode( comp );
}

void Minicli::saveConfig()
{
    if( m_pCompletion )
    {
        // save history list (M.H.)
        KConfig *config = KGlobal::config();
        config->setGroup("MiniCli");
        config->writeEntry( "History", m_History );
        config->writeEntry( "CompletionMode", m_pCompletion->completionMode() );
        config->sync();
    }
}

bool Minicli::setCWD( const QString& cwd )
{
    QDir d( cwd );
    if( d.exists() )
    {
        m_strCWD = cwd;
        if( m_strCWD.left(1) != '/' ) m_strCWD += '/';
        return true;
    }
    return false;
}

void Minicli::keyPressEvent(QKeyEvent *kev)
{
    int a = ((QKeyEvent*)kev)->ascii();
    // ^C, EOF, ESC
    if (a == 3 || a == 4 || a == 27)
    {
        kev->accept();
	reject();
        return;
    } 
    if (kev->key() == Key_Return || kev->key() == Key_Enter)
    {
        kev->accept();
        accept();
        return;
    }
    kev->ignore();
}

void Minicli::accept()
{
    QString text = m_runCombo->currentText();
    int ret = run_command();
    if (ret < 0)
	QDialog::accept();
    else if (ret > 0)
	return;
    if (!text.isEmpty() && !m_History.contains(text))
	m_History.prepend(text);
    reset();
    saveConfig();
    QDialog::accept();
}

void Minicli::reject()
{
    reset();
    saveConfig();
    QDialog::reject();
}

void Minicli::reset()
{
    m_runCombo->clearEdit();
    if (mbAdvanced)
	slotAdvanced();
    mpAdvanced->reset();
    m_runIcon->setPixmap(DesktopIcon("go"));
    m_runCombo->setFocus();
}

int Minicli::run_command()
{
    kapp->propagateSessionManager();
    slotParseTimer(); // make sure m_IconName is set

    QString s;
    if (m_strCWD.isNull())  
	s = m_runCombo->currentText().simplifyWhiteSpace();
    else 
	s = m_strCWD + m_runCombo->currentText().simplifyWhiteSpace();

    if (s.isEmpty())
    {
	return 0;
    } 
    else if (s == "logout")
    {
        bool shutdown = kapp->requestShutDown();
        if( !shutdown )
            KMessageBox::error( 0, i18n("Could not logout properly.  The sesion manager cannot\n"
		    "be contacted.  You can try to force a shutdown by pressing\n"
                    "the CTRL, SHIFT and BACKSPACE keys at the same time.  Note\n"
                    "however that your current session will not be saved with a\n"
                     "forced shutdown." ) );
	return 0;
    } 
    else if (mpAdvanced->needsKDEsu())
    {
	SuProcess proc;
	struct passwd *pw;
	if (mpAdvanced->changeUid())
	{
	    pw = getpwnam(mpAdvanced->username().latin1());
	    if (pw == 0L)
	    {
		KMessageBox::sorry( 0L, i18n("The user %1 does not exist on "
			"this system.\n").arg(mpAdvanced->username()));
		return 1;
	    }
	    QCString u = mpAdvanced->username().latin1();
	    proc.setUser(u);
	}
	else
	{
	    pw = getpwuid(getuid());
	    if (pw == 0L)
	    {
		KMessageBox::error( 0L, i18n("You don't exist!\n"));
		return -1;
	    }
	    proc.setUser(pw->pw_name);
	}
	if (mpAdvanced->changeScheduler())
	{
	    proc.setPriority(mpAdvanced->priority());
	    proc.setScheduler(mpAdvanced->scheduler());
	}
	QCString command = s.latin1();
	if (mpAdvanced->terminal())
	    command.prepend("konsole -e /bin/sh -- -c ");
	proc.setCommand(command);
	if (proc.checkInstall(mpAdvanced->password()) < 0)
	{
	    KMessageBox::sorry(0L, i18n("Incorrect password! Please try again."));
	    return 1;
	}
	int pid = fork();
	if (pid < 0)
	{
	    kdError(1207) << "fork(): " << ERR << "\n";
	    return -1;
	}
	if (pid > 0)
	    return 0;
	
	// Block SIGCHLD because SuProcess::exec() uses waitpid()
	sigset_t sset;
	sigemptyset(&sset);
	sigaddset(&sset, SIGCHLD);
	sigprocmask(SIG_BLOCK, &sset, 0L);

	proc.setTerminal(true);
	proc.setErase(true);
	int ret = proc.exec(mpAdvanced->password());
	_exit(ret);
    } 
    else
    {
	QString cmd, name;

        if (mpAdvanced->terminal())
	{
	    cmd = QString::fromLatin1("konsole -e /bin/sh -- -c \"%1\"").arg(s);
	    name = s;
	} 
	else
        {
            KURIFilterData data ( s );
            KURIFilter::self()->filterURI( data );
            kdDebug(1207) << "Original url: " << s << " filtered url: " 
		    << data.uri().url() << "\n";
            switch( data.uriType() )
            {
                case KURIFilterData::LOCAL_FILE:
                case KURIFilterData::LOCAL_DIR:
                case KURIFilterData::NET_PROTOCOL:
                    cmd = QString::fromLatin1("konqueror \"%1\"").arg(data.uri().url());
		    name = data.uri().url();
                    break;
                case KURIFilterData::EXECUTABLE:
                case KURIFilterData::SHELL:
		    cmd = data.uri().url();
		    name = cmd;
		    break;
                case KURIFilterData::HELP:
                    // FIXME parent should be this but somehow that screws our layout (malte)
                    KMessageBox::sorry( 0, i18n("This is currently not supported") );
                    return -1;
                case KURIFilterData::ERROR:
                    KMessageBox::error( 0, data.uri().url() );
                    return -1;
                default:
                    KMessageBox::sorry( 0, i18n("The program name or command: %1\n"
			    "cannot be found.").arg( data.uri().url() ) );
                    return -1;
            }
        }

	KURL::List empty;
	KRun::run(cmd, empty, name, m_IconName, m_IconName);
	return 0;
    }

    return 0;
}

void Minicli::slotCmdChanged( const QString& )
{
    m_parseTimer->start(250, true);
}

void Minicli::slotAdvanced()
{
    mbAdvanced = !mbAdvanced;
    if (mbAdvanced)
    {
	mpAdvButton->setText(i18n("&Options <<"));
	mpAdvanced->setMaximumSize(1000, 1000);
	mpAdvanced->adjustSize();
    } else
    {
	mpAdvButton->setText(i18n("&Options >>"));
	mpAdvanced->setMaximumSize(0, 0);
	mpAdvanced->adjustSize();
    }
}

void Minicli::slotParseTimer()
{
    // Change the icon according to the command type
    QString cmd = m_runCombo->currentText().simplifyWhiteSpace();
    if (cmd.isEmpty())
	return;

    QPixmap icon;
    QString name;
    if (mpAdvanced->terminal()) 
    {
	icon = DesktopIcon("konsole");
	name = QString::fromLatin1("konsole");
    }

    KURIFilterData data( cmd );	    
    KURIFilter::self()->filterURI( data );
    kdDebug(1207) << "Original url: " << cmd << " filtered url: " 
	    << data.uri().url() << "\n";
    if( data.hasBeenFiltered() ) cmd = data.uri().url(); // get the new URL
    switch ( data.uriType() )
    {
	case KURIFilterData::LOCAL_FILE:
	case KURIFilterData::LOCAL_DIR:
	case KURIFilterData::NET_PROTOCOL:
	{
	    KMimeType::Ptr mimetype = KMimeType::findByURL(cmd);
	    if (mimetype)
	    {
		icon = mimetype->pixmap(cmd, KIcon::Desktop);
		name = mimetype->icon(cmd, false);
	    }
	    break;
	}
	case KURIFilterData::EXECUTABLE:
	{
	    KService::Ptr service = KService::serviceByDesktopName(cmd);
	    if (service)
	    {
		icon = service->pixmap(KIcon::Desktop);
		name = service->icon();
	    } else 
	    {
		icon = DesktopIcon("exec");
		name = QString::fromLatin1("exec");
	    }
	    break;
	}
	case KURIFilterData::HELP:
	    icon = DesktopIcon("khelpcenter");
	    name = QString::fromLatin1("khelpcenter");
	    break;
	case KURIFilterData::ERROR:
	    icon = DesktopIcon("error");
	    name = QString::fromLatin1("error");
	default:
	    break;
    }

    if (icon.isNull())
    {
	m_runIcon->setPixmap(DesktopIcon("go"));
	m_IconName = QString::fromLatin1("go");
    } else
    {
	m_runIcon->setPixmap(icon);
	m_IconName = name;
    }
}


MinicliAdvanced::MinicliAdvanced(QWidget *parent, const char *name)
    : QGroupBox(parent, name)
{
    mbTerminal = mbChangeUid = mbChangeScheduler = false;
    mScheduler = 0;
    
    setTitle("Advanced settings");

    QBoxLayout *top = new QVBoxLayout(this, KDialog::marginHint(), KDialog::spacingHint());
    top->addSpacing(20);

    mpCBTerm = new QCheckBox(i18n("Run in &terminal"), this);
    QWhatsThis::add(mpCBTerm, i18n("Check this option if the application you want "
	    "to run is a text mode application. The application will then be "
	    "run in a terminal emulator window."));
    connect(mpCBTerm, SIGNAL(toggled(bool)), SLOT(slotTerminal(bool)));
    top->addWidget(mpCBTerm, AlignLeft);
    mpCBUser = new QCheckBox(i18n("Run as a different &user"), this);
    QWhatsThis::add(mpCBUser, i18n("Check this option if you want to run the "
	    "application with a different user id. Every process has a "
	    "user id associated with it. This id code determines file "
	    "access and other permissions. The password of the user is "
	    "required to do this."));
    connect(mpCBUser, SIGNAL(toggled(bool)), SLOT(slotChangeUid(bool)));
    top->addWidget(mpCBUser, AlignLeft);
    QBoxLayout *hbox = new QHBoxLayout(0L, KDialog::marginHint(), KDialog::spacingHint());
    top->addLayout(hbox);
    hbox->addSpacing(20);
    QLabel *lbl = new QLabel(i18n("&username:"), this);
    hbox->addWidget(lbl);
    mpEdit = new KLineEdit(this);
    QWhatsThis::add(mpEdit, i18n("Enter the user as who you want to run "
	    "the application here."));
    lbl->setBuddy(mpEdit);
    connect(mpEdit, SIGNAL(textChanged(const QString &)),
	    SLOT(slotUsername(const QString &)));
    hbox->addWidget(mpEdit);
    hbox->addStretch();
    mpCBPrio = new QCheckBox(i18n("Run with a different &priority"), this);
    QWhatsThis::add(mpCBPrio, i18n("Check this option if you want to run the "
	    "application with a different priority. A higher priority "
	    "tells the operating system to give more processing time to your "
	    "application."));
    top->addWidget(mpCBPrio, AlignLeft);
    connect(mpCBPrio, SIGNAL(toggled(bool)), SLOT(slotChangeScheduler(bool)));

    hbox = new QHBoxLayout(0L, KDialog::marginHint(), KDialog::spacingHint());
    top->addLayout(hbox);
    hbox->addSpacing(20);
    lbl = new QLabel(i18n("pr&iority:"), this);
    hbox->addWidget(lbl);
    mpSlider = new QSlider(0, 100, 10, 50, QSlider::Horizontal, this);
    QWhatsThis::add(mpSlider, i18n("The priority can be set here. From left "
	    "to right, it goes from low to high. The center position is the "
	    "default value. For priorities higher than the default, you will "
	    "need root's password."));
    lbl->setBuddy(mpSlider);
    mpSlider->setLineStep(5);
    connect(mpSlider, SIGNAL(valueChanged(int)), SLOT(slotPriority(int)));
    hbox->addWidget(mpSlider);
    lbl = new QLabel(i18n("high"), this);
    hbox->addWidget(lbl);
    hbox->addStretch();

    hbox = new QHBoxLayout(0L, KDialog::marginHint(), KDialog::spacingHint());
    top->addLayout(hbox);
    hbox->addSpacing(20);
    lbl = new QLabel(i18n("&scheduler:"), this);
    hbox->addWidget(lbl);
    mpCombo = new KComboBox(this);
    QWhatsThis::add(mpCombo, i18n("Here you can select which scheduler to use "
	    "for the application. The scheduler is that part of the operating "
	    "system which decides what process will run and which will have "
	    "to wait. Two schedulers are available:"
	    "<ul><li><em>Normal:</em> This is the standard, timesharing "
	    "scheduler. It will fairly divide the available processing time "
	    "over all processes. </li>"
	    "<li><em>Realtime:</em> This scheduler will run your application "
	    "uninterrupted until it gives up the processor. This can be "
	    "dangerous. An application that does not give up the processor "
	    "might hang the system. You need root's password to use this "
	    "scheduler."));
    lbl->setBuddy(mpCombo);
    connect(mpCombo, SIGNAL(activated(int)), SLOT(slotScheduler(int)));
    hbox->addWidget(mpCombo);
    hbox->addStretch();

    mpAuthLabel = new QLabel(this);
    top->addWidget(mpAuthLabel);
    hbox = new QHBoxLayout(0L, KDialog::marginHint(), KDialog::spacingHint());
    hbox->addSpacing(20);
    top->addLayout(hbox);
    lbl = new QLabel(i18n("pass&word:"), this);
    hbox->addWidget(lbl);
    mpPassword = new KPasswordEdit(this);
    QWhatsThis::add(mpPassword, i18n("Enter the requested password here."));
    lbl->setBuddy(mpPassword);
    hbox->addWidget(mpPassword);
    hbox->addStretch();

    // Provide username completion up to 1000 users.
    KCompletion *completion = new KCompletion;
    completion->setOrder(KCompletion::Sorted);
    struct passwd *pw;
    int i, maxEntries = 1000;
    setpwent();
    for (i=0; ((pw = getpwent()) != 0L) && (i < maxEntries); i++)
	completion->addItem(QString::fromLatin1(pw->pw_name));
    endpwent();
    if (i < maxEntries)
    {
	mpEdit->setCompletionObject(completion, true);
	mpEdit->setCompletionMode(KGlobalSettings::completionMode());
    }
    else
	delete completion;

    mpCombo->insertItem(i18n("Normal"), StubProcess::SchedNormal);
    mpCombo->insertItem(i18n("Realtime"), StubProcess::SchedRealtime);

    mpEdit->setEnabled(false);
    mpCombo->setEnabled(false);
    mpSlider->setEnabled(false);

    updateAuthLabel();
}

MinicliAdvanced::~MinicliAdvanced()
{
}

void MinicliAdvanced::updateAuthLabel()
{
    QString authUser;
    if (mbChangeScheduler && (mPriority > 50) || (mScheduler != StubProcess::SchedNormal))
    {
	authUser = QString::fromLatin1("root");
	mpPassword->setEnabled(true);
    } else if (mbChangeUid && !mUsername.isEmpty()) 
    {
	authUser = mpEdit->text();
	mpPassword->setEnabled(true);
    } else
    {
	authUser = QString::fromLatin1("none");
	mpPassword->setEnabled(false);
    }
    mpAuthLabel->setText(i18n("Password required: %1").arg(authUser));
}
	
void MinicliAdvanced::slotTerminal(bool ena)
{
    mbTerminal = ena;
}

void MinicliAdvanced::slotChangeUid(bool ena)
{
    mbChangeUid = ena;
    mpEdit->setEnabled(ena);
    updateAuthLabel();
}

void MinicliAdvanced::slotUsername(const QString &name)
{
    kdDebug(1207) << "text: " << mpEdit->text() << "\n";
    mUsername = name;
    updateAuthLabel();
}

void MinicliAdvanced::slotChangeScheduler(bool ena)
{
    mbChangeScheduler = ena;
    mpCombo->setEnabled(ena);
    mpSlider->setEnabled(ena);
    updateAuthLabel();
}

bool MinicliAdvanced::needsKDEsu()
{
    return ((mbChangeScheduler && ((mPriority != 50) || (mScheduler != StubProcess::SchedNormal))) 
	    || (mbChangeUid && !mUsername.isEmpty()));
}

void MinicliAdvanced::slotScheduler(int scheduler)
{
    mScheduler = scheduler;
    if (mScheduler == StubProcess::SchedRealtime)
    {
	if (KMessageBox::warningContinueCancel(this, 
		i18n("Running a realtime application can be very dangerous.\n"
		     "If the application misbehaves, the system might hang\n"
		     "unrecoverably.\n\nAre you sure you want to continue?"),
		i18n("Danger, Will Robinson!"), i18n("Continue"))
		!= KMessageBox::Continue
	   )
	{
	    mScheduler = StubProcess::SchedNormal;
	    mpCombo->setCurrentItem(mScheduler);
	}
    }
    updateAuthLabel();
}

void MinicliAdvanced::slotPriority(int priority)
{
    // Provide a way to easily return to the default priority
    if ((priority > 40) && (priority < 60))
    {
	priority = 50;
	mpSlider->setValue(50);
    }
    mPriority = priority;
    updateAuthLabel();
}

const char *MinicliAdvanced::password()
{
    return mpPassword->password();
}

void MinicliAdvanced::reset()
{
    mbTerminal = false;
    mpCBTerm->setChecked(false);
    mbChangeUid = false;
    mpCBUser->setChecked(false);
    mpEdit->clear();
    mbChangeScheduler = false;
    mpCBPrio->setChecked(false);
    mPriority = 50;
    mpSlider->setValue(mPriority);
    mScheduler = StubProcess::SchedNormal;
    mpCombo->setCurrentItem(mScheduler);
    mpPassword->erase();
    updateAuthLabel();
}
