/***********************************************************************************
* Run Command: Simple plasmoid to run commands with support for runners.
* Copyright (C) 2008 - 2012 Michal Dutkiewicz aka Emdek <emdeck@gmail.com>
*
* 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 "RunCommandApplet.h"
#include "RunCommandItem.h"

#include <QtCore/QDir>
#include <QtCore/QRegExp>
#include <QtCore/QFileInfo>
#include <QtGui/QMenu>
#include <QtGui/QScrollBar>
#include <QtGui/QGraphicsView>
#include <QtGui/QGraphicsLinearLayout>
#include <QtScript/QScriptEngine>

#include <KRun>
#include <KUrl>
#include <KIcon>
#include <KShell>
#include <KService>
#include <KAuthorized>
#include <KCompletion>
#include <KWindowSystem>
#include <KStandardDirs>
#include <KProtocolInfo>
#include <KGlobalSettings>
#include <KToolInvocation>
#include <KServiceTypeTrader>
#include <kworkspace/kworkspace.h>
#include <kworkspace/kdisplaymanager.h>

#include <Plasma/Theme>
#include <Plasma/Corona>
#include <Plasma/Containment>
#include <Plasma/ToolTipManager>

K_EXPORT_PLASMA_APPLET(runcommand, RunCommandApplet)

RunCommandApplet::RunCommandApplet(QObject *parent, const QVariantList &args) : Plasma::Applet(parent, args),
    m_dialog(NULL),
    m_runnerManager(NULL),
    m_comboBox(new KHistoryComboBox(false)),
    m_initialWidth(0)
{
    setAspectRatioMode(Plasma::IgnoreAspectRatio);

    resize(200, 50);
}

void RunCommandApplet::init()
{
    KConfig krunnerConfiguration("krunnerrc", KConfig::SimpleConfig);
    QStringList commands = KConfigGroup(&krunnerConfiguration, "General").readEntry("PastQueries", QStringList());
    KCompletion completion;
    completion.insertItems(commands);
    completion.setOrder(KCompletion::Sorted);

    m_comboBox->setFocusPolicy(Qt::StrongFocus);
    m_comboBox->setAttribute(Qt::WA_NoSystemBackground);
    m_comboBox->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength);
    m_comboBox->setHistoryItems(commands);
    m_comboBox->setEditable(true);
    m_comboBox->clearEditText();
    m_comboBox->setWindowFlags(m_comboBox->windowFlags() | Qt::BypassGraphicsProxyWidget);
    m_comboBox->view()->installEventFilter(this);
    m_comboBox->setCompletionMode(static_cast<KGlobalSettings::Completion>(config().readEntry("completionMode", static_cast<int>(KGlobalSettings::completionMode()))));
    m_comboBox->setCompletionObject(&completion);

    Plasma::ComboBox *comboBox = new Plasma::ComboBox(this);
    comboBox->setNativeWidget(m_comboBox);

    QGraphicsLinearLayout *layout = new QGraphicsLinearLayout;
    layout->addItem(comboBox);
    layout->setSpacing(0);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));

    setLayout(layout);
    setPreferredWidth(QWIDGETSIZE_MAX);
    constraintsEvent(Plasma::FormFactorConstraint | Plasma::ImmutableConstraint);

    Plasma::ToolTipManager::self()->setContent(this, Plasma::ToolTipContent(i18n("Run Command"), i18n("Launch command without terminal."), KIcon("system-run").pixmap(IconSize(KIconLoader::Desktop))));

    QTimer::singleShot(250, this, SLOT(configChanged()));

    connect(this, SIGNAL(activate()), this, SLOT(focusWidget()));
    connect(containment(), SIGNAL(toolBoxToggled()), this, SLOT(constraintsEvent()));
    connect(m_comboBox, SIGNAL(cleared()), this, SLOT(clearHistory()));
    connect(m_comboBox, SIGNAL(returnPressed()), this, SLOT(getCommand()));
    connect(m_comboBox, SIGNAL(activated(QString)), this, SLOT(runCommand(QString)));
    connect(m_comboBox, SIGNAL(completionModeChanged(KGlobalSettings::Completion)), this, SLOT(completionModeChanged(KGlobalSettings::Completion)));
    connect(m_comboBox, SIGNAL(editTextChanged(QString)), this, SLOT(queryChanged(QString)));
    connect(m_comboBox, SIGNAL(aboutToShowContextMenu(QMenu*)), this, SLOT(extendContextMenu(QMenu*)));
}

void RunCommandApplet::constraintsEvent(Plasma::Constraints constraints)
{
    if (constraints & Plasma::FormFactorConstraint)
    {
        if (formFactor() == Plasma::Horizontal || formFactor() == Plasma::Vertical)
        {
            m_comboBox->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
        }
        else
        {
            m_comboBox->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed));
        }

        if (config().readEntry("width", -1) == -1)
        {
            setMinimumWidth(m_comboBox->sizeHint().width() + 25);
        }
    }

    if (constraints & Plasma::ImmutableConstraint)
    {
        if (immutability() == Plasma::Mutable && !containment()->isToolBoxOpen())
        {
            layout()->setContentsMargins(5, 0, 5, 0);

            setCursor(Qt::SizeHorCursor);
        }
        else
        {
            layout()->setContentsMargins(0, 0, 0, 0);

            setCursor(Qt::ArrowCursor);
        }
    }
}

void RunCommandApplet::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    m_initialWidth = size().width();

    event->accept();
}

void RunCommandApplet::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if (immutability() != Plasma::Mutable || !(event->buttons() & Qt::LeftButton))
    {
        return;
    }

    const int width = (m_initialWidth + ((event->screenPos().x() - event->buttonDownScreenPos(Qt::LeftButton).x()) * ((event->buttonDownPos(Qt::LeftButton).x() > (m_initialWidth / 2))?1:-1)));

    setMaximumWidth(width);
    setMinimumWidth(width);

    config().writeEntry("width", width);

    emit configNeedsSaving();
}

void RunCommandApplet::configChanged()
{
    if (config().readEntry("width", -1) > -1)
    {
        setMaximumWidth(config().readEntry("width", -1));
        setMinimumWidth(config().readEntry("width", -1));
    }
    else
    {
        setMaximumWidth(-1);
        setMinimumWidth(m_comboBox->sizeHint().width() + 25);
    }
}

void RunCommandApplet::focusWidget()
{
    if (scene())
    {
        QGraphicsView *parentView = NULL;
        QGraphicsView *possibleParentView = NULL;

        foreach (QGraphicsView *view, scene()->views())
        {
            if (view->sceneRect().intersects(sceneBoundingRect()) || view->sceneRect().contains(scenePos()))
            {
                if (view->isActiveWindow())
                {
                    parentView = view;

                    break;
                }
                else
                {
                    possibleParentView = view;
                }
            }
        }

        if (!parentView)
        {
            parentView = possibleParentView;
        }

        if (parentView)
        {
            KWindowSystem::forceActiveWindow(parentView->winId());
        }
    }

    raise();

    m_comboBox->setFocus();

    QTimer::singleShot(250, m_comboBox, SLOT(setFocus()));
}

void RunCommandApplet::clearWidth()
{
    config().deleteEntry("width");

    configChanged();

    emit configNeedsSaving();
}

void RunCommandApplet::clearHistory()
{
    KConfig krunnerConfiguration("krunnerrc", KConfig::SimpleConfig);
    KConfigGroup configuration = KConfigGroup(&krunnerConfiguration, "General");

    configuration.deleteEntry("PastQueries");
    configuration.sync();

    m_comboBox->clearHistory();
}

void RunCommandApplet::resetColor()
{
    m_comboBox->setStyleSheet("QComboBox {color: auto;}");
}

void RunCommandApplet::queryChanged(const QString &query)
{
    Plasma::ToolTipManager::self()->hide(this);

    if (config().readEntry("enableRunners", true))
    {
        if (!m_runnerManager)
        {
            m_runnerManager = new Plasma::RunnerManager(this);

            connect(m_runnerManager, SIGNAL(matchesChanged(QList<Plasma::QueryMatch>)), this, SLOT(resultsChanged(QList<Plasma::QueryMatch>)));
        }

        if (query.length() > 1)
        {
            m_runnerManager->launchQuery(query);
        }
        else
        {
            resultsChanged(QList<Plasma::QueryMatch>());
        }
    }
    else if (m_dialog && m_dialog->isVisible())
    {
        m_dialog->hide();
        m_dialog->deleteLater();
        m_dialog = NULL;
    }
}

void RunCommandApplet::resultsChanged(const QList<Plasma::QueryMatch> &matches)
{
    if (!m_dialog)
    {
        m_dialog = new Plasma::Dialog(NULL, Qt::Tool | Qt::FramelessWindowHint);
        m_dialog->setResizeHandleCorners(Plasma::Dialog::All);
        m_dialog->installEventFilter(this);

        m_resultsUi.setupUi(m_dialog);
        m_resultsUi.resultsLayout->setAlignment(Qt::AlignCenter);
        m_resultsUi.moveUpButton->setIcon(KIcon("arrow-up"));
        m_resultsUi.moveDownButton->setIcon(KIcon("arrow-down"));

        updateTheme();

        connect(m_dialog, SIGNAL(dialogResized()), this, SLOT(dialogResized()));
        connect(m_resultsUi.moveUpButton, SIGNAL(clicked()), this, SLOT(moveListUp()));
        connect(m_resultsUi.moveDownButton, SIGNAL(clicked()), this, SLOT(moveListDown()));
        connect(m_resultsUi.scrollArea->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(updateButtons()));
        connect(this, SIGNAL(destroyed()), m_dialog, SLOT(deleteLater()));
        connect(Plasma::Theme::defaultTheme(), SIGNAL(themeChanged()), this, SLOT(updateTheme()));
    }

    for (int i = (m_resultsUi.resultsLayout->count() - 1); i >= 0; --i)
    {
        m_resultsUi.resultsLayout->takeAt(i)->widget()->deleteLater();
        m_resultsUi.resultsLayout->removeItem(m_resultsUi.resultsLayout->itemAt(i));
    }

    if (!matches.count() || m_comboBox->currentText().isEmpty())
    {
        m_dialog->hide();

        focusWidget();

        return;
    }

    for (int i = 0; i < matches.count(); ++i)
    {
        RunCommandItem *item = new RunCommandItem(matches.at(i), m_runnerManager->actionsForMatch(matches.at(i)), m_dialog);

        m_resultsUi.resultsLayout->addWidget(item);

        connect(item, SIGNAL(sizeChanged()), this, SLOT(updateButtons()));
        connect(item, SIGNAL(run(Plasma::QueryMatch)), this, SLOT(runMatch(Plasma::QueryMatch)));
    }

    const QSize size = config().readEntry("resultsDialogSize", QSize(300, 300));

    m_dialog->move(containment()->corona()->popupPosition(this, size, Qt::AlignCenter));
    m_dialog->show();
    m_dialog->resize(size);

    updateButtons();

    KWindowSystem::forceActiveWindow(m_dialog->winId());

    QTimer::singleShot(50, m_resultsUi.resultsLayout->itemAt(0)->widget(), SLOT(setFocus()));
}

void RunCommandApplet::dialogResized()
{
    config().writeEntry("resultsDialogSize", m_dialog->size());

    updateButtons();

    emit configNeedsSaving();
}

void RunCommandApplet::moveListUp()
{
    if (m_dialog)
    {
        QScrollBar *scrollBar = m_resultsUi.scrollArea->verticalScrollBar();
        scrollBar->setValue(scrollBar->value() - scrollBar->pageStep());
    }
}

void RunCommandApplet::moveListDown()
{
    if (m_dialog)
    {
        QScrollBar *scrollBar = m_resultsUi.scrollArea->verticalScrollBar();
        scrollBar->setValue(scrollBar->value() + scrollBar->pageStep());
    }
}

void RunCommandApplet::getCommand()
{
    if (m_comboBox->currentText().isEmpty())
    {
        return;
    }

    runCommand(m_comboBox->currentText());
}

void RunCommandApplet::runCommand(QString command)
{
    KDisplayManager displayManager;
    KConfig krunnerConfiguration("krunnerrc", KConfig::SimpleConfig);
    KConfigGroup configuration = KConfigGroup(&krunnerConfiguration, "General");
    KConfig uriConfiguration("kuriikwsfilterrc", KConfig::NoGlobals);
    KConfigGroup uriGeneralGroup(&uriConfiguration, "General");
    QString delimiter = uriGeneralGroup.readPathEntry("KeywordDelimiter", QString(":"));
    QString path = QDir::cleanPath(KShell::tildeExpand(command));
    QRegExp webAddress("^((ht|f)tps?://|www\\.).+");
    int index;
    bool commandStatus = true;
    bool addToHistory = true;

    if (command.length() > 3 && command.contains(delimiter))
    {
        index = command.indexOf(delimiter);

        if (index != (command.length() - 1))
        {
            KService::List offers = KServiceTypeTrader::self()->query("SearchProvider", QString("'%1' in Keys").arg(command.left(index)));

            if (!offers.isEmpty())
            {
                QString query = offers.at(0)->property("Query").toString();

                command = query.replace("\\{@}", command.right(command.length() - index - 1));
            }
        }
    }

    KUrl url(command);

    if (command.startsWith('=') || command.startsWith(QString("bin=")) || command.startsWith(QString("oct=")) || command.startsWith(QString("dec=")) || command.startsWith(QString("hex=")))
    {
        int base = 10;
        QString result;

        addToHistory = false;

        if (!command.startsWith('='))
        {
            if (command.startsWith(QString("bin=")))
            {
                base = 2;
            }
            else if (command.startsWith(QString("oct=")))
            {
                base = 8;
            }
            else if (command.startsWith(QString("hex=")))
            {
                base = 16;
            }

            command.remove(0, 3);
        }

        command.remove(0, 1).remove(' ');

        if (command.contains(KGlobal::locale()->decimalSymbol(), Qt::CaseInsensitive))
        {
            command.replace(KGlobal::locale()->decimalSymbol(), QChar('.'), Qt::CaseInsensitive);
        }

        if (command.contains("0x"))
        {
            int position = 0;
            QString hex;

            while ((position = command.indexOf("0x")) > -1)
            {
                hex.clear();

                for (int i = (position + 2); i < command.size(); ++i)
                {
                    if (((command[i] <= '9') && (command[i] >= '0')) || ((command[i] <= 'f') && (command[i] >= 'a')) || ((command[i] <= 'F') && (command[i] >= 'A')))
                    {
                        hex.append(command[i]);
                    }
                    else
                    {
                        break;
                    }
                }

                command.replace("0x" + hex, QString::number(hex.toInt(NULL, 16)));
            }
        }

        command.replace(QRegExp("([a-zA-Z]+)"), "Math.\\1");

        QScriptEngine engine;
        QScriptValue value = engine.evaluate(command);

        if (value.isError())
        {
            commandStatus = false;
        }
        else
        {
            result = (((base == 16)?"0x":"") + QString::number(value.toInt32(), base).toUpper());

            m_comboBox->setEditText(result);
        }
    }
    else if (QFileInfo(path).exists())
    {
        command = path;

        m_comboBox->clearFocus();

        new KRun(path, NULL);
    }
    else if ((url.protocol() != "file" && KProtocolInfo::isKnownProtocol(url.protocol())) || webAddress.exactMatch(command))
    {
        if (url.protocol().isEmpty())
        {
            index = command.indexOf('/');

            url.clear();
            url.setHost(command.left(index));

            if (index != -1)
            {
                url.setPath(command.mid(index));
            }

            url.setProtocol("http");

            command.prepend("http://");
        }

        m_comboBox->clearFocus();

        KToolInvocation::invokeBrowser(url.url());
    }
    else if (command.startsWith(QString("mailto:")))
    {
        KToolInvocation::invokeMailer(command.remove(0, 7), NULL);
    }
    else if (command == "logout")
    {
        KWorkSpace::requestShutDown(KWorkSpace::ShutdownConfirmDefault, KWorkSpace::ShutdownTypeNone);
    }
    else if (command == "shutdown")
    {
        KWorkSpace::requestShutDown(KWorkSpace::ShutdownConfirmDefault, KWorkSpace::ShutdownTypeHalt);
    }
    else if (command == "restart" || command == "reboot")
    {
        KWorkSpace::requestShutDown(KWorkSpace::ShutdownConfirmDefault, KWorkSpace::ShutdownTypeReboot);
    }
    else if (command == "switch" && KAuthorized::authorizeKAction("start_new_session") && displayManager.isSwitchable() && displayManager.numReserve() > 0)
    {
        displayManager.startReserve();
    }
    else if (command == "lock")
    {
        KRun::runCommand("dbus-send --print-reply --dest=org.freedesktop.ScreenSaver /ScreenSaver org.freedesktop.ScreenSaver.Lock", NULL);
    }
    else
    {
        if (command.startsWith('$'))
        {
            m_comboBox->clearFocus();

            KToolInvocation::invokeTerminal(command.remove(0, 1));
        }
        else
        {
            QString binaryName = KRun::binaryName(command, true);

            if (!QFile(binaryName).exists() && KStandardDirs::findExe(binaryName).isEmpty())
            {
                commandStatus = false;
            }
            else
            {
                m_comboBox->clearFocus();

                commandStatus = KRun::runCommand(command, NULL);
            }
        }
    }

    if (commandStatus)
    {
        if (addToHistory)
        {
            m_comboBox->addToHistory(command);
            m_comboBox->clearEditText();

            configuration.writeEntry("PastQueries", m_comboBox->historyItems());
            configuration.sync();
        }
    }
    else
    {
        m_comboBox->setStyleSheet("QComboBox {color: red;}");

        QTimer::singleShot(1500, this, SLOT(resetColor()));
    }
}

void RunCommandApplet::runMatch(const Plasma::QueryMatch &match)
{
    m_runnerManager->run(match);

    m_comboBox->clearFocus();
    m_comboBox->clearEditText();
}

void RunCommandApplet::extendContextMenu(QMenu *menu)
{
    menu->addSeparator();

    QMenu *configurationMenu = menu->addMenu(KIcon("configure"), i18n("Configuration"));
    configurationMenu->addAction(KIcon("configure"), QString(i18n("Configure: %1")).arg(name()), this, SLOT(showConfigurationInterface()));

    QAction *enableRunners = configurationMenu->addAction(i18n("Enable Runners"));
    enableRunners->setCheckable(true);
    enableRunners->setChecked(config().readEntry("enableRunners", true));

    connect(enableRunners, SIGNAL(toggled(bool)), this, SLOT(setRunnersEnabled(bool)));

    if (config().readEntry("width", -1) > -1)
    {
        configurationMenu->addAction(i18n("Set flexible width"), this, SLOT(clearWidth()));
    }
}

void RunCommandApplet::completionModeChanged(KGlobalSettings::Completion mode)
{
    config().writeEntry("completionMode", static_cast<int>(mode));

    emit configNeedsSaving();
}

void RunCommandApplet::setRunnersEnabled(bool enable)
{
    config().writeEntry("enableRunners", enable);

    if (!m_comboBox->currentText().isEmpty())
    {
        queryChanged(m_comboBox->currentText());
    }

    emit configNeedsSaving();
}

void RunCommandApplet::updateButtons()
{
    QScrollBar *scrollBar = m_resultsUi.scrollArea->verticalScrollBar();
    const bool overflow = (m_resultsUi.scrollArea->widget()->height() > m_resultsUi.scrollArea->viewport()->height());

    m_resultsUi.moveUpButton->setEnabled(scrollBar->value() != scrollBar->minimum());
    m_resultsUi.moveUpButton->setVisible(overflow);
    m_resultsUi.moveDownButton->setEnabled(scrollBar->value() != scrollBar->maximum());
    m_resultsUi.moveDownButton->setVisible(overflow);
}

void RunCommandApplet::updateTheme()
{
    QPalette palette = m_dialog->palette();
    palette.setColor(QPalette::WindowText, Plasma::Theme::defaultTheme()->color(Plasma::Theme::TextColor));
    palette.setColor(QPalette::ButtonText, Plasma::Theme::defaultTheme()->color(Plasma::Theme::ButtonTextColor));
    palette.setColor(QPalette::Background, Plasma::Theme::defaultTheme()->color(Plasma::Theme::BackgroundColor));
    palette.setColor(QPalette::Button, palette.color(QPalette::Background).lighter(250));

    m_dialog->setPalette(palette);
}

bool RunCommandApplet::eventFilter(QObject *object, QEvent *event)
{
    if (event->type() == QEvent::Show && object == m_comboBox->view())
    {
        m_comboBox->view()->nativeParentWidget()->move(containment()->corona()->popupPosition(dynamic_cast<QGraphicsWidget*>(layout()->itemAt(0)), m_comboBox->view()->nativeParentWidget()->size()));
    }
    else if (event->type() == QEvent::KeyPress && object == m_dialog)
    {
        QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);

        if (keyEvent->key() != Qt::Key_Tab && keyEvent->key() != Qt::Key_Backtab && keyEvent->key() != Qt::Key_Escape)
        {
            QApplication::sendEvent(m_comboBox, event);

            return true;
        }
    }
    else if (event->type() == QEvent::Resize && object == m_dialog)
    {
        updateButtons();
    }

    return QObject::eventFilter(object, event);
}
