/*
    Copyright 2006-2008 by Robert Knight <robertknight@gmail.com>
    Copyright 2009 by Thomas Dreibholz <dreibh@iem.uni-due.de>

    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.
*/

// Own
#include "SessionController.h"

// Qt
#include <QtGui/QApplication>
#include <QMenu>

// KDE
#include <KAction>
#include <KDebug>
#include <KIcon>
#include <KInputDialog>
#include <KLocale>
#include <KMenu>
#include <KMessageBox>
#include <KRun>
#include <kshell.h>
#include <KStandardDirs>
#include <KToggleAction>
#include <KUrl>
#include <KXmlGuiWindow>
#include <KXMLGUIFactory>
#include <KXMLGUIBuilder>
#include <kdebug.h>
#include <kcodecaction.h>
#include <kdeversion.h>

// Konsole
#include "EditProfileDialog.h"
#include "CopyInputDialog.h"
#include "Emulation.h"
#include "Filter.h"
#include "History.h"
#include "IncrementalSearchBar.h"
#include "ScreenWindow.h"
#include "Session.h"
#include "ProfileList.h"
#include "TerminalDisplay.h"
#include "SessionManager.h"

// for SaveHistoryTask
#include <KFileDialog>
#include <KIO/Job>
#include <KJob>
#include "TerminalCharacterDecoder.h"


using namespace Konsole;

KIcon SessionController::_activityIcon;
KIcon SessionController::_silenceIcon;
QSet<SessionController*> SessionController::_allControllers;
QPointer<SearchHistoryThread> SearchHistoryTask::_thread;
int SessionController::_lastControllerId;

SessionController::SessionController(Session* session , TerminalDisplay* view, QObject* parent)
    : ViewProperties(parent)
    , KXMLGUIClient()
    , _session(session)
    , _view(view)
    , _copyToGroup(0)
    , _profileList(0)
    , _previousState(-1)
    , _viewUrlFilter(0)
    , _searchFilter(0)
    , _searchToggleAction(0)
    , _findNextAction(0)
    , _findPreviousAction(0)
    , _urlFilterUpdateRequired(false)
    , _codecAction(0)
    , _changeProfileMenu(0)
    , _listenForScreenWindowUpdates(false)
    , _preventClose(false)
{
    _allControllers.insert(this);

    Q_ASSERT( session );
    Q_ASSERT( view );

    // handle user interface related to session (menus etc.)
    if (isKonsolePart())
        setXMLFile("konsole/partui.rc");
    else
        setXMLFile("konsole/sessionui.rc");

    setupActions();
    actionCollection()->addAssociatedWidget(view);
    foreach (QAction* action, actionCollection()->actions())
        action->setShortcutContext(Qt::WidgetWithChildrenShortcut);

    setIdentifier(++_lastControllerId);
    sessionTitleChanged();

    view->installEventFilter(this);

    // listen for session resize requests
    connect( _session , SIGNAL(resizeRequest(const QSize&)) , this ,
            SLOT(sessionResizeRequest(const QSize&)) );

    // listen for popup menu requests
    connect( _view , SIGNAL(configureRequest(QPoint)) , this,
            SLOT(showDisplayContextMenu(QPoint)) );

    // move view to newest output when keystrokes occur
    connect( _view , SIGNAL(keyPressedSignal(QKeyEvent*)) , this ,
            SLOT(trackOutput(QKeyEvent*)) );

    // listen to activity / silence notifications from session
    connect( _session , SIGNAL(stateChanged(int)) , this ,
            SLOT(sessionStateChanged(int) ));
    // listen to title and icon changes
    connect( _session , SIGNAL(titleChanged()) , this , SLOT(sessionTitleChanged()) );

    // listen for color changes
    connect( _session , SIGNAL(changeBackgroundColorRequest(QColor)) , _view , SLOT(setBackgroundColor(QColor)) );
    connect( _session , SIGNAL(changeForegroundColorRequest(QColor)) , _view , SLOT(setForegroundColor(QColor)) );

    // update the title when the session starts
    connect( _session , SIGNAL(started()) , this , SLOT(snapshot()) ); 

    // listen for output changes to set activity flag
    connect( _session->emulation() , SIGNAL(outputChanged()) , this ,
            SLOT(fireActivity()) );

    // listen for detection of ZModem transfer
    connect( _session , SIGNAL(zmodemDetected()) , this , SLOT(zmodemDownload()) ); 

    // listen for flow control status changes
    connect( _session , SIGNAL(flowControlEnabledChanged(bool)) , _view ,
        SLOT(setFlowControlWarningEnabled(bool)) );
    _view->setFlowControlWarningEnabled(_session->flowControlEnabled());

    // take a snapshot of the session state every so often when
    // user activity occurs
    //
    // the timer is owned by the session so that it will be destroyed along
    // with the session
    QTimer* activityTimer = new QTimer(_session);
    activityTimer->setSingleShot(true);
    activityTimer->setInterval(2000);
    connect( _view , SIGNAL(keyPressedSignal(QKeyEvent*)) , activityTimer , SLOT(start()) );
    connect( activityTimer , SIGNAL(timeout()) , this , SLOT(snapshot()) );
}

void SessionController::updateSearchFilter()
{
    if ( _searchFilter ) 
    {
        Q_ASSERT( searchBar() && searchBar()->isVisible() );

        _view->processFilters();
    }
}

SessionController::~SessionController()
{
   if ( _view )
      _view->setScreenWindow(0);

   _allControllers.remove(this);
}
void SessionController::trackOutput(QKeyEvent* event)
{
    Q_ASSERT( _view->screenWindow() );

    // jump to the end of the history buffer unless the key pressed
    // is one of the three main modifiers, as these are used to select
    // the selection mode (eg. Ctrl+Alt+<Left Click> for column/block selection)
    switch (event->key())
    {
        case Qt::Key_Shift:
        case Qt::Key_Control:
        case Qt::Key_Alt:
            break;
        default:
            _view->screenWindow()->setTrackOutput(true);
    }
}
void SessionController::requireUrlFilterUpdate()
{
    // this method is called every time the screen window's output changes, so do not
    // do anything expensive here.

    _urlFilterUpdateRequired = true;
}
void SessionController::snapshot()
{
    Q_ASSERT( _session != 0 );

    QString title = _session->getDynamicTitle();    
    title         = title.simplified();

    // Visualize that the session is broadcasting to others
    if (_copyToGroup && _copyToGroup->sessions().count() > 1) {
        title.append('*');
    }
    updateSessionIcon();

    // apply new title
    if ( !title.isEmpty() )
        _session->setTitle(Session::DisplayedTitleRole,title);
    else
        _session->setTitle(Session::DisplayedTitleRole,_session->title(Session::NameRole));
}

QString SessionController::currentDir() const
{
    return _session->currentWorkingDirectory();
}

KUrl SessionController::url() const
{
    return _session->getUrl();
}

void SessionController::rename()
{
    renameSession();
}

void SessionController::openUrl( const KUrl& url )
{
    // handle local paths
    if ( url.isLocalFile() )
    {
        QString path = url.toLocalFile();
        _session->emulation()->sendText("cd " + KShell::quoteArg(path) + '\r');
    }
    else if ( url.protocol() == "ssh" )
    {
        _session->emulation()->sendText("ssh ");

        if ( url.port() > -1 )
            _session->emulation()->sendText("-p " + QString::number(url.port()) + ' ' );
        if ( url.hasUser() )
            _session->emulation()->sendText(url.user() + '@');
        if ( url.hasHost() )
            _session->emulation()->sendText(url.host() + '\r');
    }
    else if ( url.protocol() == "telnet" )
    {
        _session->emulation()->sendText("telnet ");

        if ( url.hasUser() )
            _session->emulation()->sendText("-l " + url.user() + ' ');
        if ( url.hasHost() )
            _session->emulation()->sendText(url.host() + ' ');
        if ( url.port() > -1 )
            _session->emulation()->sendText(QString::number(url.port()));
         _session->emulation()->sendText("\r");
    }
    else
    {
        //TODO Implement handling for other Url types

        KMessageBox::sorry(_view->window(),
                           i18n("Konsole does not know how to open the bookmark: ") +
                           url.prettyUrl());

        kWarning(1211) << "Unable to open bookmark at url" << url << ", I do not know"
           << " how to handle the protocol " << url.protocol();
    }
}

bool SessionController::eventFilter(QObject* watched , QEvent* event)
{
    if ( watched == _view )
    {
        if ( event->type() == QEvent::FocusIn )
        {
            // notify the world that the view associated with this session has been focused
            // used by the view manager to update the title of the MainWindow widget containing the view
            emit focused(this);

            // when the view is focused, set bell events from the associated session to be delivered
            // by the focused view

            // first, disconnect any other views which are listening for bell signals from the session
            disconnect( _session , SIGNAL(bellRequest(const QString&)) , 0 , 0 );
            // second, connect the newly focused view to listen for the session's bell signal
            connect( _session , SIGNAL(bellRequest(const QString&)) ,
                    _view , SLOT(bell(const QString&)) );
                    
            if(_copyToAllTabsAction->isChecked()) {
                // A session with "Copy To All Tabs" has come into focus:
                // Ensure that newly created sessions are included in _copyToGroup!
                copyInputToAllTabs();
            }
        }
        // when a mouse move is received, create the URL filter and listen for output changes if
        // it has not already been created.  If it already exists, then update only if the output
        // has changed since the last update ( _urlFilterUpdateRequired == true )
        //
        // also check that no mouse buttons are pressed since the URL filter only applies when
        // the mouse is hovering over the view
        if ( event->type() == QEvent::MouseMove &&
            (!_viewUrlFilter || _urlFilterUpdateRequired) &&
            ((QMouseEvent*)event)->buttons() == Qt::NoButton )
        {
            if ( _view->screenWindow() && !_viewUrlFilter )
            {
                connect( _view->screenWindow() , SIGNAL(scrolled(int)) , this ,
                        SLOT(requireUrlFilterUpdate()) );
                connect( _view->screenWindow() , SIGNAL(outputChanged()) , this ,
                         SLOT(requireUrlFilterUpdate()) );

                // install filter on the view to highlight URLs
                _viewUrlFilter = new UrlFilter();
                _view->filterChain()->addFilter( _viewUrlFilter );
            }

            _view->processFilters();
            _urlFilterUpdateRequired = false;
        }
    }

    return false;
}

void SessionController::removeSearchFilter()
{
    if (!_searchFilter)
        return;

    _view->filterChain()->removeFilter(_searchFilter);
    delete _searchFilter;
    _searchFilter = 0;
}

void SessionController::setSearchBar(IncrementalSearchBar* searchBar)
{
    // disconnect the existing search bar
    if ( _searchBar )
    {
        disconnect( this , 0 , _searchBar , 0 );
        disconnect( _searchBar , 0 , this , 0 );
    }

    // remove any existing search filter
    removeSearchFilter();

    // connect new search bar
    _searchBar = searchBar;
    if ( _searchBar )
    {
        connect( _searchBar , SIGNAL(closeClicked()) , this , SLOT(searchClosed()) );
        connect( _searchBar , SIGNAL(findNextClicked()) , this , SLOT(findNextInHistory()) );
        connect( _searchBar , SIGNAL(findPreviousClicked()) , this , SLOT(findPreviousInHistory()) );
        connect( _searchBar , SIGNAL(highlightMatchesToggled(bool)) , this , SLOT(highlightMatches(bool)) );

        // if the search bar was previously active
        // then re-enter search mode
        searchHistory( _searchToggleAction->isChecked() );
    }
}
IncrementalSearchBar* SessionController::searchBar() const
{
    return _searchBar;
}

void SessionController::setShowMenuAction(QAction* action)
{
    actionCollection()->addAction("show-menubar",action);
}

void SessionController::setupActions()
{
    KAction* action = 0;
    KToggleAction* toggleAction = 0;
    KActionCollection* collection = actionCollection();

    // Close Session
    action = collection->addAction("close-session", this, SLOT(closeSession()));
    action->setIcon(KIcon("tab-close"));
    action->setText(i18n("&Close Tab"));
    action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_W));

    // Open Browser
    action = collection->addAction("open-browser", this, SLOT(openBrowser()));
    action->setText(i18n("Open File Manager"));
    action->setIcon(KIcon("system-file-manager"));

    // Copy and Paste
    action = KStandardAction::copy(this, SLOT(copy()), collection);
    action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_C));

    action = KStandardAction::paste(this, SLOT(paste()), collection);
    KShortcut pasteShortcut = action->shortcut();
    pasteShortcut.setPrimary(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_V));
    pasteShortcut.setAlternate(QKeySequence(Qt::SHIFT + Qt::Key_Insert));
    action->setShortcut(pasteShortcut);

    action = collection->addAction("paste-selection", this, SLOT(pasteSelection()));
    action->setText(i18n("Paste Selection"));
    action->setShortcut(QKeySequence(Qt::CTRL+Qt::SHIFT+Qt::Key_Insert));

    // Rename Session
    action = collection->addAction("rename-session", this, SLOT(renameSession()));
    action->setText( i18n("&Rename Tab...") );
    action->setShortcut( QKeySequence(Qt::CTRL+Qt::ALT+Qt::Key_S) );

    // Copy Input To -> All Tabs in Current Window
    _copyToAllTabsAction = collection->addAction("copy-input-to-all-tabs", this, SLOT(copyInputToAllTabs()));
    _copyToAllTabsAction->setText(i18n("&All Tabs in Current Window"));
    _copyToAllTabsAction->setCheckable(true);

    // Copy Input To -> Select Tabs
    _copyToSelectedAction = collection->addAction("copy-input-to-selected-tabs", this, SLOT(copyInputToSelectedTabs()));
    _copyToSelectedAction->setShortcut( QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Period) );
    _copyToSelectedAction->setText(i18n("&Select Tabs..."));
    _copyToSelectedAction->setCheckable(true);

    // Copy Input To -> None
    _copyToNoneAction = collection->addAction("copy-input-to-none", this, SLOT(copyInputToNone()));
    _copyToNoneAction->setText(i18n("&None"));
    _copyToNoneAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Slash));
    _copyToNoneAction->setCheckable(true);
    _copyToNoneAction->setChecked(true);

    action = collection->addAction("zmodem-upload", this, SLOT(zmodemUpload()));
    action->setText(i18n("&ZModem Upload..."));
    action->setIcon(KIcon("document-open"));
    action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_U));

    // Monitor
    toggleAction = new KToggleAction(i18n("Monitor for &Activity"),this);
    toggleAction->setShortcut(QKeySequence(Qt::CTRL+Qt::SHIFT+Qt::Key_A));
    action = collection->addAction("monitor-activity", toggleAction);
    connect(action, SIGNAL(toggled(bool)), this, SLOT(monitorActivity(bool)));

    toggleAction = new KToggleAction(i18n("Monitor for &Silence"),this);
    toggleAction->setShortcut(QKeySequence(Qt::CTRL+Qt::SHIFT+Qt::Key_I));
    action = collection->addAction("monitor-silence", toggleAction);
    connect(action, SIGNAL(toggled(bool)), this, SLOT(monitorSilence(bool)));

    // Character Encoding
    _codecAction = new KCodecAction(i18n("Set &Encoding"),this);
    _codecAction->setIcon(KIcon("character-set"));
    collection->addAction("set-encoding", _codecAction);
    connect(_codecAction->menu(), SIGNAL(aboutToShow()), this, SLOT(updateCodecAction()));
    connect(_codecAction, SIGNAL(triggered(QTextCodec*)), this, SLOT(changeCodec(QTextCodec*)));

    // Text Size
    action = collection->addAction("enlarge-font", this, SLOT(increaseTextSize()));
    action->setText(i18n("Enlarge Font"));
    action->setIcon(KIcon("format-font-size-more"));
    action->setShortcut(QKeySequence(Qt::CTRL+Qt::SHIFT+Qt::Key_Plus));

    action = collection->addAction("shrink-font", this, SLOT(decreaseTextSize()));
    action->setText(i18n("Shrink Font"));
    action->setIcon(KIcon("format-font-size-less"));
    action->setShortcut(QKeySequence(Qt::CTRL+Qt::SHIFT+Qt::Key_Minus));

    // History
    _searchToggleAction = KStandardAction::find(this, NULL, collection);
    _searchToggleAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_F));
    _searchToggleAction->setCheckable(true);
    connect(_searchToggleAction, SIGNAL(toggled(bool)), this, SLOT(searchHistory(bool)));

    _findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection);
    _findNextAction->setEnabled(false);

    _findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection);
    _findPreviousAction->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_F3));
    _findPreviousAction->setEnabled(false);

    action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection);
    action->setText(i18n("Save Output &As..."));

    action = collection->addAction("configure-history", this, SLOT(showHistoryOptions()));
    action->setText(i18n("Configure Scrollback..."));
    action->setIcon(KIcon("configure"));

    action = collection->addAction("clear-history", this, SLOT(clearHistory()));
    action->setText(i18n("Clear Scrollback"));
    action->setIcon(KIcon("edit-clear-history"));

    action = collection->addAction("clear-history-and-reset", this, SLOT(clearHistoryAndReset()));
    action->setText(i18n("Clear Scrollback and Reset"));
    action->setIcon(KIcon("edit-clear-history"));
    action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_X));

    // Profile Options
    action = collection->addAction("edit-current-profile", this, SLOT(editCurrentProfile()));
    action->setText(i18n("Configure Current Profile..."));
    action->setIcon(KIcon("document-properties") );

    _changeProfileMenu = new KActionMenu(i18n("Change Profile"), _view);
    collection->addAction("change-profile", _changeProfileMenu);
    connect(_changeProfileMenu->menu(), SIGNAL(aboutToShow()), this, SLOT(prepareChangeProfileMenu()));
}

void SessionController::changeProfile(Profile::Ptr profile)
{
    SessionManager::instance()->setSessionProfile(_session,profile);
}

void SessionController::prepareChangeProfileMenu()
{
    if (_changeProfileMenu->menu()->isEmpty()) {
        _profileList = new ProfileList(false,this);
        connect(_profileList, SIGNAL(profileSelected(Profile::Ptr)), this, SLOT(changeProfile(Profile::Ptr)));
    }

    _changeProfileMenu->menu()->clear();
    _changeProfileMenu->menu()->addActions(_profileList->actions());
}
void SessionController::updateCodecAction()
{
    _codecAction->setCurrentCodec(QString(_session->emulation()->codec()->name()));
}

void SessionController::changeCodec(QTextCodec* codec)
{
    _session->setCodec(codec);
}

void SessionController::editCurrentProfile()
{
    EditProfileDialog* dialog = new EditProfileDialog( QApplication::activeWindow() );

    dialog->setProfile(SessionManager::instance()->sessionProfile(_session));
    dialog->show();
}

void SessionController::renameSession()
{
    QPointer<Session> guard(_session);
    bool ok = false;
    const QString& text = KInputDialog::getText( i18n("Rename Tab") ,
                                                 i18n("Enter new tab text:") ,
                                                 _session->tabTitleFormat(Session::LocalTabTitle) ,
                                                 &ok, QApplication::activeWindow() );
    if (!guard)
        return;

    if ( ok )
    {
        // renaming changes both the local and remote tab title formats, to save confusion over
        // the tab title not changing if renaming the tab whilst the remote tab title format is 
        // being displayed
        //
        // The downside of this approach is that after renaming a tab manually, the ability to 
        // have separate formats for local and remote activities is lost
        _session->setTabTitleFormat(Session::LocalTabTitle,text);
        _session->setTabTitleFormat(Session::RemoteTabTitle,text);

        // trigger an update of the tab text
        snapshot();
    }
}
void SessionController::saveSession()
{
    Q_ASSERT(0); // not implemented yet

    //SaveSessionDialog dialog(_view);
    //int result = dialog.exec();
}
bool SessionController::confirmClose() const
{
    if (_session->isForegroundProcessActive())
    {
        QString title = _session->foregroundProcessName();
      
        // hard coded for now.  In future make it possible for the user to specify which programs
        // are ignored when considering whether to display a confirmation
        QStringList ignoreList; 
        ignoreList << QString(qgetenv("SHELL")).section('/',-1);
        if (ignoreList.contains(title))
            return true;

        QString question;
        if (title.isEmpty())
            question = i18n("A program is currently running in this session."
                            "  Are you sure you want to close it?");
        else
            question = i18n("The program '%1' is currently running in this session."  
                            "  Are you sure you want to close it?",title);

        int result = KMessageBox::warningYesNo(_view->window(),question,i18n("Confirm Close"));
        return (result == KMessageBox::Yes) ? true : false; 
    }
    return true;
}
void SessionController::closeSession()
{
    if (_preventClose)
        return;

    if (confirmClose())
        _session->close();
}

void SessionController::openBrowser()
{
    new KRun(url(), QApplication::activeWindow());
}

void SessionController::copy()
{
    _view->copyClipboard();
}

void SessionController::paste()
{
    _view->pasteClipboard();
}
void SessionController::pasteSelection()
{
    _view->pasteSelection();
}
static const KXmlGuiWindow* findWindow(const QObject* object)
{
    // Walk up the QObject hierarchy to find a KXmlGuiWindow.
    while(object != NULL) {
        const KXmlGuiWindow* window = dynamic_cast<const KXmlGuiWindow*>(object);
        if(window != NULL) {
            return(window);
        }
        object = object->parent();
    }
    return(NULL);
}

static bool hasTerminalDisplayInSameWindow(const Session* session, const KXmlGuiWindow* window)
{
    // Iterate all TerminalDisplays of this Session ...
    QListIterator<TerminalDisplay*> terminalDisplayIterator(session->views());
    while(terminalDisplayIterator.hasNext()) {
        const TerminalDisplay* terminalDisplay = terminalDisplayIterator.next();
        // ... and check whether a TerminalDisplay has the same
        // window as given in the parameter
        if(window == findWindow(terminalDisplay)) {
            return(true);    
        }
    }
    return(false);
}

void SessionController::copyInputToAllTabs()
{
    if(!_copyToGroup) {
        _copyToGroup = new SessionGroup(this);
    }

    // Find our window ...
    const KXmlGuiWindow* myWindow = findWindow(_view);

    QSet<Session*> group =
       QSet<Session*>::fromList(SessionManager::instance()->sessions());
    for(QSet<Session*>::iterator iterator = group.begin();
        iterator != group.end(); ++iterator) {
        Session* session = *iterator;
 
        // First, ensure that the session is removed
        // (necessary to avoid duplicates on addSession()!)
        _copyToGroup->removeSession(session);

        // Add current session if it is displayed our window
        if(hasTerminalDisplayInSameWindow(session, myWindow)) {
            _copyToGroup->addSession(session);
        }
    }
    _copyToGroup->setMasterStatus(_session, true);
    _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
    
    snapshot();
    _copyToAllTabsAction->setChecked(true);
    _copyToSelectedAction->setChecked(false);
    _copyToNoneAction->setChecked(false);
}

void SessionController::copyInputToSelectedTabs()
{
    if (!_copyToGroup)
    {
        _copyToGroup = new SessionGroup(this);
        _copyToGroup->addSession(_session);
        _copyToGroup->setMasterStatus(_session,true);
        _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
    }

    CopyInputDialog* dialog = new CopyInputDialog(_view);
    dialog->setMasterSession(_session);
    
    QSet<Session*> currentGroup = QSet<Session*>::fromList(_copyToGroup->sessions());
    currentGroup.remove(_session);
    
    dialog->setChosenSessions(currentGroup);

    QPointer<Session> guard(_session);
    int result = dialog->exec();
    if (!guard)
        return;

    if (result)
    {
        QSet<Session*> newGroup = dialog->chosenSessions();
        newGroup.remove(_session);
    
        QSet<Session*> completeGroup = newGroup | currentGroup;
        foreach(Session* session, completeGroup)
        {
            if (newGroup.contains(session) && !currentGroup.contains(session))
                _copyToGroup->addSession(session);
            else if (!newGroup.contains(session) && currentGroup.contains(session))
                _copyToGroup->removeSession(session);
        }

        _copyToGroup->setMasterStatus(_session, true);
        _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
        snapshot();        
    }

    delete dialog;
    _copyToAllTabsAction->setChecked(false);
    _copyToSelectedAction->setChecked(true);
    _copyToNoneAction->setChecked(false);
}

void SessionController::copyInputToNone()
{
    if (!_copyToGroup)      // No 'Copy To' is active
        return;

    QSet<Session*> group =
       QSet<Session*>::fromList(SessionManager::instance()->sessions());
    for(QSet<Session*>::iterator iterator = group.begin();
        iterator != group.end(); ++iterator) {
        Session* session = *iterator;
 
        if(session != _session) {
            _copyToGroup->removeSession(*iterator);
        }
    }
    delete _copyToGroup;
    _copyToGroup = NULL;
    snapshot();
    
    _copyToAllTabsAction->setChecked(false);
    _copyToSelectedAction->setChecked(false);
    _copyToNoneAction->setChecked(true);
}

void SessionController::searchClosed()
{
    _searchToggleAction->toggle();
}

#if 0
void SessionController::searchHistory()
{
    searchHistory(true);
}
#endif

void SessionController::listenForScreenWindowUpdates()
{
    if (_listenForScreenWindowUpdates)
        return;

    connect( _view->screenWindow() , SIGNAL(outputChanged()) , this , 
            SLOT(updateSearchFilter()) );
    connect( _view->screenWindow() , SIGNAL(scrolled(int)) , this , 
            SLOT(updateSearchFilter()) );

    _listenForScreenWindowUpdates = true;
}

// searchHistory() may be called either as a result of clicking a menu item or
// as a result of changing the search bar widget
void SessionController::searchHistory(bool showSearchBar)
{
    if ( _searchBar )
    {
        _searchBar->setVisible(showSearchBar);

        if (showSearchBar)
        {
            removeSearchFilter();

            listenForScreenWindowUpdates();
            
            _searchFilter = new RegExpFilter();
            _view->filterChain()->addFilter(_searchFilter);
            connect( _searchBar , SIGNAL(searchChanged(const QString&)) , this ,
                    SLOT(searchTextChanged(const QString&)) );

            // invoke search for matches for the current search text
            const QString& currentSearchText = _searchBar->searchText();
            if (!currentSearchText.isEmpty())
            {
                searchTextChanged(currentSearchText);
            }

            setFindNextPrevEnabled(true);
        }
        else
        {
            setFindNextPrevEnabled(false);

            disconnect( _searchBar , SIGNAL(searchChanged(const QString&)) , this ,
                    SLOT(searchTextChanged(const QString&)) );

            removeSearchFilter();

            _view->setFocus( Qt::ActiveWindowFocusReason );
        }
    }
}
void SessionController::setFindNextPrevEnabled(bool enabled)
{
    _findNextAction->setEnabled(enabled);
    _findPreviousAction->setEnabled(enabled);
}
void SessionController::searchTextChanged(const QString& text)
{
    Q_ASSERT( _view->screenWindow() );

    if ( text.isEmpty() )
        _view->screenWindow()->clearSelection();

    // update search.  this is called even when the text is
    // empty to clear the view's filters
    beginSearch(text , SearchHistoryTask::ForwardsSearch);
}
void SessionController::searchCompleted(bool success)
{
    if ( _searchBar )
        _searchBar->setFoundMatch(success);
}

void SessionController::beginSearch(const QString& text , int direction)
{
    Q_ASSERT( _searchBar );
    Q_ASSERT( _searchFilter );

    Qt::CaseSensitivity caseHandling = _searchBar->matchCase() ? Qt::CaseSensitive : Qt::CaseInsensitive;
    QRegExp::PatternSyntax syntax = _searchBar->matchRegExp() ? QRegExp::RegExp : QRegExp::FixedString;

    QRegExp regExp( text.trimmed() ,  caseHandling , syntax );
    _searchFilter->setRegExp(regExp);

    if ( !regExp.isEmpty() )
    {
        SearchHistoryTask* task = new SearchHistoryTask(this);

        connect( task , SIGNAL(completed(bool)) , this , SLOT(searchCompleted(bool)) );

        task->setRegExp(regExp);
        task->setSearchDirection( (SearchHistoryTask::SearchDirection)direction );
        task->setAutoDelete(true);
        task->addScreenWindow( _session , _view->screenWindow() );
        task->execute();
    }

    _view->processFilters();
}
void SessionController::highlightMatches(bool highlight)
{
    if ( highlight )
    {
        _view->filterChain()->addFilter(_searchFilter);
        _view->processFilters();
    }
    else
    {
        _view->filterChain()->removeFilter(_searchFilter);
    }

    _view->update();
}
void SessionController::findNextInHistory()
{
    Q_ASSERT( _searchBar );
    Q_ASSERT( _searchFilter );

    beginSearch(_searchBar->searchText(),SearchHistoryTask::ForwardsSearch);
}
void SessionController::findPreviousInHistory()
{
    Q_ASSERT( _searchBar );
    Q_ASSERT( _searchFilter );

    beginSearch(_searchBar->searchText(),SearchHistoryTask::BackwardsSearch);
}
void SessionController::showHistoryOptions()
{
    HistorySizeDialog* dialog = new HistorySizeDialog( QApplication::activeWindow() );
    const HistoryType& currentHistory = _session->historyType();

    if ( currentHistory.isEnabled() )
    {
        if ( currentHistory.isUnlimited() )
            dialog->setMode( HistorySizeDialog::UnlimitedHistory );
        else
        {
            dialog->setMode( HistorySizeDialog::FixedSizeHistory );
            dialog->setLineCount( currentHistory.maximumLineCount() );
        }
    }
    else
        dialog->setMode( HistorySizeDialog::NoHistory );

    connect( dialog , SIGNAL(optionsChanged(int,int,bool)) ,
             this , SLOT(scrollBackOptionsChanged(int,int,bool)) );

    dialog->show();
}
void SessionController::sessionResizeRequest(const QSize& size)
{
    //kDebug(1211) << "View resize requested to " << size;
    _view->setSize(size.width(),size.height());
}
void SessionController::scrollBackOptionsChanged(int mode, int lines, bool saveToCurrentProfile )
{
    switch (mode)
    {
        case HistorySizeDialog::NoHistory:
            _session->setHistoryType( HistoryTypeNone() );
            break;
         case HistorySizeDialog::FixedSizeHistory:
            _session->setHistoryType( CompactHistoryType(lines) );
            break;
         case HistorySizeDialog::UnlimitedHistory:
             _session->setHistoryType( HistoryTypeFile() );
            break;
    }
    if (saveToCurrentProfile)
    {
        Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session);

        switch (mode)
        {
            case HistorySizeDialog::NoHistory:
                profile->setProperty(Profile::HistoryMode , Profile::DisableHistory);
                break;
            case HistorySizeDialog::FixedSizeHistory:
                profile->setProperty(Profile::HistoryMode , Profile::FixedSizeHistory);
                profile->setProperty(Profile::HistorySize , lines);
                break;
            case HistorySizeDialog::UnlimitedHistory:
                profile->setProperty(Profile::HistoryMode , Profile::UnlimitedHistory);
                break;
        }
        SessionManager::instance()->changeProfile(profile, profile->setProperties());
    }
}

void SessionController::saveHistory()
{
    SessionTask* task = new SaveHistoryTask(this);
    task->setAutoDelete(true);
    task->addSession( _session );
    task->execute();
}

void SessionController::clearHistory()
{
    _session->clearHistory();
    _view->updateImage();   // To reset view scrollbar
}

void SessionController::clearHistoryAndReset()
{
    Emulation* emulation = _session->emulation();
    emulation->reset();
    //_session->refresh();
    clearHistory();
}

void SessionController::increaseTextSize()
{
    QFont font = _view->getVTFont();
    font.setPointSizeF(font.pointSizeF()+1);
    _view->setVTFont(font);

    //TODO - Save this setting as a session default
}

void SessionController::decreaseTextSize()
{
    static const qreal MinimumFontSize = 6;

    QFont font = _view->getVTFont();
    font.setPointSizeF( qMax(font.pointSizeF()-1,MinimumFontSize) );
    _view->setVTFont(font);

    //TODO - Save this setting as a session default
}

void SessionController::monitorActivity(bool monitor)
{
    _session->setMonitorActivity(monitor);
}
void SessionController::monitorSilence(bool monitor)
{
    _session->setMonitorSilence(monitor);
}
void SessionController::updateSessionIcon()
{
    // Visualize that the session is broadcasting to others
    if (_copyToGroup && _copyToGroup->sessions().count() > 1) {
        // Master Mode: set different icon, to warn the user to be careful
        setIcon(KIcon("emblem-important"));
    }
    else {
        // Not in Master Mode: use normal icon
        setIcon( _sessionIcon );
    }
}
void SessionController::sessionTitleChanged()
{
        if ( _sessionIconName != _session->iconName() )
        {
            _sessionIconName = _session->iconName();
            _sessionIcon = KIcon( _sessionIconName );
            updateSessionIcon();
        }

        QString title = _session->title(Session::DisplayedTitleRole);

        // special handling for the "%w" marker which is replaced with the
        // window title set by the shell
        title.replace("%w",_session->userTitle());
        // special handling for the "%#" marker which is replaced with the 
        // number of the shell
        title.replace("%#",QString::number(_session->sessionId()));

       if ( title.isEmpty() )
          title = _session->title(Session::NameRole);

       setTitle( title );
}

void SessionController::showDisplayContextMenu(const QPoint& position)
{
    // needed to make sure the popup menu is available, even if a hosting
    // application did not merge our GUI.
    if (!factory()) 
    {
        if (!clientBuilder()) 
            setClientBuilder(new KXMLGUIBuilder(_view));
        
        KXMLGUIFactory* factory = new KXMLGUIFactory(clientBuilder(), this);
        factory->addClient(this);
    }

    QMenu* popup = qobject_cast<QMenu*>(factory()->container("session-popup-menu",this));
    if (popup)
    {
        // prepend content-specific actions such as "Open Link", "Copy Email Address" etc.
        QList<QAction*> contentActions = _view->filterActions(position);
        QAction* contentSeparator = new QAction(popup);
        contentSeparator->setSeparator(true);
        contentActions << contentSeparator;

        _preventClose = true;

        popup->insertActions(popup->actions().value(0,0),contentActions);
        QAction* chosen = popup->exec( _view->mapToGlobal(position) );

        // remove content-specific actions, unless the close action was chosen
        // in which case the popup menu will be partially destroyed at this point
           foreach(QAction* action,contentActions)
            popup->removeAction(action);
        delete contentSeparator;

        _preventClose = false;

        if (chosen && chosen->objectName() == "close-session")
            chosen->trigger();
    }
    else
    {
        kWarning() << "Unable to display popup menu for session"
                   << _session->title(Session::NameRole)
                   << ", no GUI factory available to build the popup.";
    }
}

void SessionController::sessionStateChanged(int state)
{
    if ( state == _previousState )
        return;

    _previousState = state;

    // TODO - Replace the icon choices below when suitable icons for silence and activity
    // are available
    if ( state == NOTIFYACTIVITY )
    {
        if (_activityIcon.isNull())
        {
            _activityIcon = KIcon("dialog-information");
        }

        setIcon(_activityIcon);
    }
    else if ( state == NOTIFYSILENCE )
    {
        if (_silenceIcon.isNull())
        {
            _silenceIcon = KIcon("dialog-information");
        }

        setIcon(_silenceIcon);
    }
    else if ( state == NOTIFYNORMAL )
    {
        if ( _sessionIconName != _session->iconName() )
        {
            _sessionIconName = _session->iconName();
            _sessionIcon = KIcon( _sessionIconName );
        }

        updateSessionIcon();
    }
}

void SessionController::zmodemDownload()
{
    QString zmodem = KGlobal::dirs()->findExe("rz");
    if(zmodem.isEmpty()) {
       zmodem = KGlobal::dirs()->findExe("lrz");
    }
    if(!zmodem.isEmpty()) {
        const QString path = KFileDialog::getExistingDirectory(
                                QString(), _view,
                                i18n("Save ZModem Download to..."));

        if(!path.isEmpty()) {
            _session->startZModem(zmodem, path, QStringList());
            return;
        }
    }
    else {
        KMessageBox::error(_view,
          i18n("<p>A ZModem file transfer attempt has been detected, "
               "but no suitable ZModem software was found on this system.</p>"
               "<p>You may wish to install the 'rzsz' or 'lrzsz' package.</p>"));
    }
    _session->cancelZModem();
    return;
}

void SessionController::zmodemUpload()
{
    if(_session->isZModemBusy()) {
      KMessageBox::sorry(_view,
         i18n("<p>The current session already has a ZModem file transfer in progress.</p>"));
      return;
    }
    QString zmodem = KGlobal::dirs()->findExe("sz");
    if(zmodem.isEmpty()) {
       zmodem = KGlobal::dirs()->findExe("lsz");
    }
    if(zmodem.isEmpty()) {
        KMessageBox::sorry(_view,
           i18n("<p>No suitable ZModem software was found on this system.</p>"
                "<p>You may wish to install the 'rzsz' or 'lrzsz' package.</p>"));
        return;
    }

    QStringList files = KFileDialog::getOpenFileNames(KUrl(), QString(), _view,
                           i18n("Select Files for ZModem Upload"));
    if(!files.isEmpty()) {
        _session->startZModem(zmodem, QString(), files);
    }
}

bool SessionController::isKonsolePart() const
{
    // Check to see if we are being called from Konsole or a KPart
    if (QString(qApp->metaObject()->className()) == "Konsole::Application")
        return false;
    else
        return true;
}

SessionTask::SessionTask(QObject* parent)
    :  QObject(parent)
    ,  _autoDelete(false)
{
}
void SessionTask::setAutoDelete(bool enable)
{
    _autoDelete = enable;
}
bool SessionTask::autoDelete() const
{
    return _autoDelete;
}
void SessionTask::addSession(Session* session)
{
    _sessions << session;
}
QList<SessionPtr> SessionTask::sessions() const
{
    return _sessions;
}

SaveHistoryTask::SaveHistoryTask(QObject* parent)
    : SessionTask(parent)
{
}
SaveHistoryTask::~SaveHistoryTask()
{
}

void SaveHistoryTask::execute()
{
    QListIterator<SessionPtr> iter(sessions());

    // TODO - think about the UI when saving multiple history sessions, if there are more than two or
    //        three then providing a URL for each one will be tedious

    // TODO - show a warning ( preferably passive ) if saving the history output fails
    //

     KFileDialog* dialog = new KFileDialog( QString(":konsole") /* check this */,
                                               QString(), QApplication::activeWindow() );
     dialog->setOperationMode(KFileDialog::Saving);
     dialog->setConfirmOverwrite(true);

     QStringList mimeTypes;
     mimeTypes << "text/plain";
     mimeTypes << "text/html";
     dialog->setMimeFilter(mimeTypes,"text/plain");

     // iterate over each session in the task and display a dialog to allow the user to choose where
     // to save that session's history.
     // then start a KIO job to transfer the data from the history to the chosen URL
    while ( iter.hasNext() )
    {
        SessionPtr session = iter.next();

        dialog->setCaption( i18n("Save Output From %1",session->title(Session::NameRole)) );

        int result = dialog->exec();

        if ( result != QDialog::Accepted )
            continue;

        KUrl url = dialog->selectedUrl();

        if ( !url.isValid() )
        { // UI:  Can we make this friendlier?
            KMessageBox::sorry( 0 , i18n("%1 is an invalid URL, the output could not be saved.",url.url()) );
            continue;
        }

        KIO::TransferJob* job = KIO::put( url,
                                          -1,   // no special permissions
                                          // overwrite existing files
                                          // do not resume an existing transfer
                                          // show progress information only for remote
                                          // URLs
                                          KIO::Overwrite | (url.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags)
                                                             // a better solution would be to show progress
                                                             // information after a certain period of time
                                                             // instead, since the overall speed of transfer
                                                             // depends on factors other than just the protocol
                                                             // used
                                        );


        SaveJob jobInfo;
        jobInfo.session = session;
        jobInfo.lastLineFetched = -1;  // when each request for data comes in from the KIO subsystem
                                       // lastLineFetched is used to keep track of how much of the history
                                       // has already been sent, and where the next request should continue
                                       // from.
                                       // this is set to -1 to indicate the job has just been started

        if ( dialog->currentMimeFilter() == "text/html" )
           jobInfo.decoder = new HTMLDecoder();
        else
           jobInfo.decoder = new PlainTextDecoder();

        _jobSession.insert(job,jobInfo);

        connect( job , SIGNAL(dataReq(KIO::Job*,QByteArray&)),
                 this, SLOT(jobDataRequested(KIO::Job*,QByteArray&)) );
        connect( job , SIGNAL(result(KJob*)),
                 this, SLOT(jobResult(KJob*)) );
    }

    dialog->deleteLater();
}
void SaveHistoryTask::jobDataRequested(KIO::Job* job , QByteArray& data)
{
    // TODO - Report progress information for the job

    // PERFORMANCE:  Do some tests and tweak this value to get faster saving
    const int LINES_PER_REQUEST = 500;

    SaveJob& info = _jobSession[job];

    // transfer LINES_PER_REQUEST lines from the session's history
    // to the save location
    if ( info.session )
    {
        // note:  when retrieving lines from the emulation,
        // the first line is at index 0.

        int sessionLines = info.session->emulation()->lineCount();

        if ( sessionLines-1 == info.lastLineFetched )
            return; // if there is no more data to transfer then stop the job

        int copyUpToLine = qMin( info.lastLineFetched + LINES_PER_REQUEST ,
                                 sessionLines-1 );

        QTextStream stream(&data,QIODevice::ReadWrite);
        info.decoder->begin(&stream);
        info.session->emulation()->writeToStream( info.decoder , info.lastLineFetched+1 , copyUpToLine );
        info.decoder->end();

        // if there are still more lines to process after this request
        // then insert a new line character
        // to ensure that the next block of lines begins on a new line
        //
        // FIXME - There is still an extra new-line at the end of the save data.
        if ( copyUpToLine <= sessionLines-1 )
        {
            stream << '\n';
        }


        info.lastLineFetched = copyUpToLine;
    }
}
void SaveHistoryTask::jobResult(KJob* job)
{
    if ( job->error() )
    {
        KMessageBox::sorry( 0 , i18n("A problem occurred when saving the output.\n%1",job->errorString()) );
    }

    TerminalCharacterDecoder * decoder = _jobSession[job].decoder;

    _jobSession.remove(job);

    delete decoder;

    // notify the world that the task is done
    emit completed(true);

    if ( autoDelete() )
        deleteLater();
}
void SearchHistoryTask::addScreenWindow( Session* session , ScreenWindow* searchWindow )
{
   _windows.insert(session,searchWindow);
}
void SearchHistoryTask::execute()
{
    QMapIterator< SessionPtr , ScreenWindowPtr > iter(_windows);

    while ( iter.hasNext() )
    {
        iter.next();
        executeOnScreenWindow( iter.key() , iter.value() );
    }
}

void SearchHistoryTask::executeOnScreenWindow( SessionPtr session , ScreenWindowPtr window )
{
    Q_ASSERT( session );
    Q_ASSERT( window );

    Emulation* emulation = session->emulation();

    int selectionColumn = 0;
    int selectionLine = 0;

    window->getSelectionEnd(selectionColumn , selectionLine);

    if ( !_regExp.isEmpty() )
    {
        int pos = -1;
        const bool forwards = ( _direction == ForwardsSearch );
        int startLine = selectionLine + window->currentLine() + ( forwards ? 1 : -1 );
        // Temporary fix for #205495
        if (startLine < 0) startLine = 0;
        const int lastLine = window->lineCount() - 1;
        QString string;

        //text stream to read history into string for pattern or regular expression searching
        QTextStream searchStream(&string);

        PlainTextDecoder decoder;
        decoder.setRecordLinePositions(true);

        //setup first and last lines depending on search direction
        int line = startLine;

        //read through and search history in blocks of 10K lines.
        //this balances the need to retrieve lots of data from the history each time
        //(for efficient searching)
        //without using silly amounts of memory if the history is very large.
        const int maxDelta = qMin(window->lineCount(),10000);
        int delta = forwards ? maxDelta : -maxDelta;

        int endLine = line;
        bool hasWrapped = false;  // set to true when we reach the top/bottom
                                  // of the output and continue from the other
                                  // end

        //loop through history in blocks of <delta> lines.
        do
        {
            // ensure that application does not appear to hang
            // if searching through a lengthy output
            QApplication::processEvents();

            // calculate lines to search in this iteration
            if ( hasWrapped )
            {
                if ( endLine == lastLine )
                    line = 0;
                else if ( endLine == 0 )
                    line = lastLine;

                endLine += delta;

                if ( forwards )
                   endLine = qMin( startLine , endLine );
                else
                   endLine = qMax( startLine , endLine );
            }
            else
            {
                endLine += delta;

                if ( endLine > lastLine )
                {
                    hasWrapped = true;
                    endLine = lastLine;
                } else if ( endLine < 0 )
                {
                    hasWrapped = true;
                    endLine = 0;
                }
            }

            decoder.begin(&searchStream);
            emulation->writeToStream(&decoder, qMin(endLine,line) , qMax(endLine,line) );
            decoder.end();

            // line number search below assumes that the buffer ends with a new-line 
            string.append('\n');

            pos = -1;
            if (forwards)
                pos = string.indexOf(_regExp);
            else
                pos = string.lastIndexOf(_regExp);

            //if a match is found, position the cursor on that line and update the screen
            if ( pos != -1 )
            {
                int newLines = 0;
                QList<int> linePositions = decoder.linePositions();
                while (newLines < linePositions.count() && linePositions[newLines] <= pos)
                    newLines++;

                // ignore the new line at the start of the buffer
                newLines--;

                int findPos = qMin(line,endLine) + newLines;

                highlightResult(window,findPos);

                emit completed(true);

                return;
            }

            //clear the current block of text and move to the next one
            string.clear();
            line = endLine;

        } while ( startLine != endLine );

        // if no match was found, clear selection to indicate this
        window->clearSelection();
        window->notifyOutputChanged();
    }

    emit completed(false);
}
void SearchHistoryTask::highlightResult(ScreenWindowPtr window , int findPos)
{
     //work out how many lines into the current block of text the search result was found
     //- looks a little painful, but it only has to be done once per search.

     //kDebug(1211) << "Found result at line " << findPos;

     //update display to show area of history containing selection
     window->scrollTo(findPos);
     window->setSelectionStart( 0 , findPos - window->currentLine() , false );
     window->setSelectionEnd( window->columnCount() , findPos - window->currentLine() );
     window->setTrackOutput(false);
     window->notifyOutputChanged();
}

SearchHistoryTask::SearchHistoryTask(QObject* parent)
    : SessionTask(parent)
    , _direction(ForwardsSearch)
{

}
void SearchHistoryTask::setSearchDirection( SearchDirection direction )
{
    _direction = direction;
}
SearchHistoryTask::SearchDirection SearchHistoryTask::searchDirection() const
{
    return _direction;
}
void SearchHistoryTask::setRegExp(const QRegExp& expression)
{
    _regExp = expression;
}
QRegExp SearchHistoryTask::regExp() const
{
    return _regExp;
}

#include "SessionController.moc"

