/***************************************************************************
   Copyright 2006-2009 David Nolden <david.nolden.kdevelop@art-master.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.                                   *
*                                                                         *
***************************************************************************/

#include "patchreviewtoolview.h"
#include "localpatchsource.h"
#include "patchreview.h"
#include "standardpatchexport.h"
#include "libdiff2/diffmodellist.h"
#include "libdiff2/komparemodellist.h"
#include <interfaces/icore.h>
#include <interfaces/idocumentcontroller.h>
#include <vcs/models/vcsfilechangesmodel.h>
#include <interfaces/iplugincontroller.h>
#include <interfaces/itestcontroller.h>
#include <interfaces/itestsuite.h>
#include <interfaces/iruncontroller.h>
#include <interfaces/context.h>
#include <interfaces/contextmenuextension.h>
#include <interfaces/iprojectcontroller.h>
#include <interfaces/iuicontroller.h>
#include <util/projecttestjob.h>
#include <sublime/area.h>
#include <sublime/view.h>
#include <sublime/document.h>
#include <KLineEdit>
#include <KTextEditor/Document>
#include <QMenu>

using namespace KDevelop;

class PatchFilesModel : public VcsFileChangesModel
{
public:
    PatchFilesModel( QObject *parent, bool allowSelection ) : VcsFileChangesModel( parent, allowSelection ) { };
    enum ItemRoles { HunksNumberRole = VcsStatusInfoRole+1 };

public slots:
    void updateState( const KDevelop::VcsStatusInfo &status, unsigned hunksNum ) {
        int row = VcsFileChangesModel::updateState( invisibleRootItem(), status );
        if ( row == -1 )
            return;

        QStandardItem *item = invisibleRootItem()->child( row, 0 );
        setFileInfo( item, hunksNum );
        item->setData( QVariant( hunksNum ), HunksNumberRole );
    }

    void updateState( const KDevelop::VcsStatusInfo &status ) {
        int row = VcsFileChangesModel::updateState( invisibleRootItem(), status );
        if ( row == -1 )
            return;

        QStandardItem *item = invisibleRootItem()->child( row, 0 );
        setFileInfo( invisibleRootItem()->child( row, 0 ), item->data( HunksNumberRole ).toUInt() );
    }

private:
    void setFileInfo( QStandardItem *item, unsigned int hunksNum ) {
        QString newText = i18ncp( "%1: number of changed hunks, %2: file name",
            "%2 (1 hunk)", "%2 (%1 hunks)", hunksNum,
            item->data(VcsFileChangesModel::VcsStatusInfoRole).value<VcsStatusInfo>().url().pathOrUrl() );
        item->setText( newText );
    }
};

PatchReviewToolView::PatchReviewToolView( QWidget* parent, PatchReviewPlugin* plugin )
    : QWidget( parent ),
    m_resetCheckedUrls( true ),
    m_plugin( plugin ) {
    connect( plugin, SIGNAL( patchChanged() ), SLOT( patchChanged() ) );
    connect( plugin, SIGNAL( startingNewReview() ), SLOT( startingNewReview() ) );
    connect( ICore::self()->documentController(), SIGNAL( documentActivated( KDevelop::IDocument* ) ), this, SLOT( documentActivated( KDevelop::IDocument* ) ) );

    showEditDialog();
    patchChanged();
}

void PatchReviewToolView::startingNewReview()
{
    m_resetCheckedUrls = true;
}

void PatchReviewToolView::patchChanged() {
    fillEditFromPatch();
    kompareModelChanged();
}

PatchReviewToolView::~PatchReviewToolView() {
}

LocalPatchSource* PatchReviewToolView::GetLocalPatchSource() {
    IPatchSource::Ptr ips = m_plugin->patch();

    if ( !ips )
        return 0;
    return dynamic_cast<LocalPatchSource*>( ips.data() );
}

void PatchReviewToolView::updatePatchFromEdit() {
    LocalPatchSource* lpatch = GetLocalPatchSource();
    if( !lpatch )
        return;

    lpatch->setCommand(m_editPatch.command->text());
    lpatch->setFilename(m_editPatch.filename->url());
    lpatch->setBaseDir(m_editPatch.baseDir->url());
    lpatch->setAlreadyApplied( m_editPatch.applied->checkState() == Qt::Checked );

    m_plugin->notifyPatchChanged();
}

void PatchReviewToolView::fillEditFromPatch() {
    IPatchSource::Ptr ipatch = m_plugin->patch();
    if ( !ipatch )
        return;

    disconnect( m_editPatch.patchSelection, SIGNAL( currentIndexChanged( int ) ), this, SLOT( patchSelectionChanged( int ) ) );

    m_editPatch.patchSelection->clear();
    foreach( IPatchSource::Ptr patch, m_plugin->knownPatches() )
    {
        if( !patch )
            continue;
        m_editPatch.patchSelection->addItem( patch->icon(), patch->name() );
        if( patch == ipatch )
            m_editPatch.patchSelection->setCurrentIndex( m_editPatch.patchSelection->count()-1 );
    }

    connect( m_editPatch.patchSelection, SIGNAL( currentIndexChanged( int ) ), this, SLOT( patchSelectionChanged( int ) ) );

    m_editPatch.cancelReview->setVisible( ipatch->canCancel() );

    QString finishText = i18n( "Finish Review" );
    if( !ipatch->finishReviewCustomText().isEmpty() )
        finishText = ipatch->finishReviewCustomText();
    kDebug() << "finish-text: " << finishText;
    m_editPatch.finishReview->setText( finishText );
    m_fileModel->setIsCheckbable( m_plugin->patch()->canSelectFiles() );

    if( m_customWidget ) {
        kDebug() << "removing custom widget";
        m_customWidget->hide();
        m_editPatch.customWidgetsLayout->removeWidget( m_customWidget );
    }

    m_customWidget = ipatch->customWidget();
    if( m_customWidget ) {
        m_editPatch.customWidgetsLayout->insertWidget( 0, m_customWidget );
        m_customWidget->show();
        kDebug() << "got custom widget";
    }

    bool showTests = false;
    IProject* project = 0;
    QMap<KUrl, VcsStatusInfo::State> files = ipatch->additionalSelectableFiles();
    QMap<KUrl, VcsStatusInfo::State>::const_iterator it = files.constBegin();

    for (; it != files.constEnd(); ++it) {
        project = ICore::self()->projectController()->findProjectForUrl(it.key());
        if (project && !ICore::self()->testController()->testSuitesForProject(project).isEmpty()) {
            showTests = true;
            break;
        }
    }

    m_editPatch.testsButton->setVisible(showTests);
    m_editPatch.testProgressBar->hide();

    LocalPatchSource* lpatch = dynamic_cast<LocalPatchSource*>( ipatch.data() );
    m_editPatch.localPatchOptions->setVisible( lpatch );
    if( !lpatch )
        return;

    m_editPatch.command->setText( lpatch->command());
    m_editPatch.filename->setUrl( lpatch->file() );
    m_editPatch.baseDir->setUrl( lpatch->baseDir() );
    m_editPatch.applied->setCheckState( lpatch->isAlreadyApplied() ? Qt::Checked : Qt::Unchecked );

    if ( lpatch->command().isEmpty() )
        m_editPatch.tabWidget->setCurrentIndex( m_editPatch.tabWidget->indexOf( m_editPatch.fileTab ) );
    else
        m_editPatch.tabWidget->setCurrentIndex( m_editPatch.tabWidget->indexOf( m_editPatch.commandTab ) );
}

void PatchReviewToolView::patchSelectionChanged( int selection ) {
    m_fileModel->removeRows( 0, m_fileModel->rowCount() );
    if( selection >= 0 && selection < m_plugin->knownPatches().size() ) {
        m_plugin->setPatch( m_plugin->knownPatches()[selection] );
    }
}

void PatchReviewToolView::slotAppliedChanged( int newState ) {
    if ( LocalPatchSource* lpatch = GetLocalPatchSource() ) {
        lpatch->setAlreadyApplied( newState == Qt::Checked );
        m_plugin->notifyPatchChanged();
    }
}

void PatchReviewToolView::showEditDialog() {
    m_editPatch.setupUi( this );

    bool allowSelection = m_plugin->patch() && m_plugin->patch()->canSelectFiles();
    m_fileModel = new PatchFilesModel( this, allowSelection );
    m_editPatch.filesList->setModel( m_fileModel );
    m_editPatch.filesList->header()->hide();
    m_editPatch.filesList->setRootIsDecorated( false );
    m_editPatch.filesList->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(m_editPatch.filesList, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customContextMenuRequested(QPoint)));
    connect(m_fileModel, SIGNAL(itemChanged(QStandardItem*)), SLOT(fileItemChanged(QStandardItem*)));

    m_editPatch.previousFile->setIcon( KIcon( "arrow-left" ) );
    m_editPatch.previousHunk->setIcon( KIcon( "arrow-up" ) );
    m_editPatch.nextHunk->setIcon( KIcon( "arrow-down" ) );
    m_editPatch.nextFile->setIcon( KIcon( "arrow-right" ) );
    m_editPatch.cancelReview->setIcon( KIcon( "dialog-cancel" ) );
    m_editPatch.finishReview->setIcon( KIcon( "dialog-ok" ) );
    m_editPatch.updateButton->setIcon( KIcon( "view-refresh" ) );
    m_editPatch.testsButton->setIcon( KIcon( "preflight-verifier" ) );

    QMenu* exportMenu = new QMenu( m_editPatch.exportReview );
    StandardPatchExport* stdactions = new StandardPatchExport( m_plugin, this );
    stdactions->addActions( exportMenu );
    connect( exportMenu, SIGNAL( triggered( QAction* ) ), m_plugin, SLOT( exporterSelected( QAction* ) ) );

    IPluginController* pluginManager = ICore::self()->pluginController();
    foreach( IPlugin* p, pluginManager->allPluginsForExtension( "org.kdevelop.IPatchExporter" ) )
    {
        KPluginInfo info = pluginManager->pluginInfo( p );
        QAction* action = exportMenu->addAction( KIcon( info.icon() ), info.name() );
        action->setData( qVariantFromValue<QObject*>( p ) );
    }

    m_editPatch.exportReview->setMenu( exportMenu );

    connect( m_editPatch.previousHunk, SIGNAL( clicked( bool ) ), this, SLOT( prevHunk() ) );
    connect( m_editPatch.nextHunk, SIGNAL( clicked( bool ) ), this, SLOT( nextHunk() ) );
    connect( m_editPatch.previousFile, SIGNAL( clicked( bool ) ), this, SLOT( prevFile() ) );
    connect( m_editPatch.nextFile, SIGNAL( clicked( bool ) ), this, SLOT( nextFile() ) );
    connect( m_editPatch.filesList, SIGNAL( activated ( QModelIndex ) ), this, SLOT( fileDoubleClicked( QModelIndex ) ) );

    connect( m_editPatch.cancelReview, SIGNAL( clicked( bool ) ), m_plugin, SLOT( cancelReview() ) );
    connect( m_editPatch.finishReview, SIGNAL( clicked( bool ) ), this, SLOT( finishReview() ) );
    //connect( m_editPatch.cancelButton, SIGNAL(pressed()), this, SLOT(slotEditCancel()) );

    //connect( this, SIGNAL(finished(int)), this, SLOT(slotEditDialogFinished(int)) );

    connect( m_editPatch.applied, SIGNAL( stateChanged( int ) ), SLOT( updatePatchFromEdit() ) );
    connect( m_editPatch.filename, SIGNAL( textChanged( QString ) ), SLOT( updatePatchFromEdit() ) );

    m_editPatch.baseDir->setMode( KFile::Directory );

    connect( m_editPatch.command, SIGNAL( textChanged( QString ) ), this, SLOT( updatePatchFromEdit() ) );
//   connect( m_editPatch.commandToFile, SIGNAL(clicked(bool)), this, SLOT(slotToFile()) );

    connect( m_editPatch.filename->lineEdit(), SIGNAL( returnPressed() ), this, SLOT( updatePatchFromEdit() ) );
    connect( m_editPatch.filename->lineEdit(), SIGNAL( editingFinished() ), this, SLOT( updatePatchFromEdit() ) );
    connect( m_editPatch.filename, SIGNAL( urlSelected( KUrl ) ), this, SLOT( updatePatchFromEdit() ) );
    connect( m_editPatch.command, SIGNAL( textChanged( QString ) ), this, SLOT( updatePatchFromEdit() ) );
//     connect( m_editPatch.commandToFile, SIGNAL(clicked(bool)), m_plugin, SLOT(commandToFile()) );

    connect( m_editPatch.patchSelection, SIGNAL( currentIndexChanged( int ) ), this, SLOT( patchSelectionChanged( int ) ) );

    connect( m_editPatch.updateButton, SIGNAL( clicked( bool ) ), m_plugin, SLOT( forceUpdate() ) );

    connect( m_editPatch.showButton, SIGNAL( clicked( bool ) ), m_plugin, SLOT( updateReview()) );

    connect( m_editPatch.testsButton, SIGNAL( clicked( bool ) ), this, SLOT( runTests() ) );
    
    m_selectAllAction = new QAction(KIcon("edit-select-all"), i18n("Select All"), this );
    connect( m_selectAllAction, SIGNAL(triggered(bool)), SLOT(selectAll()) );
    m_deselectAllAction = new QAction( i18n("Deselect All"), this );
    connect( m_deselectAllAction, SIGNAL(triggered(bool)), SLOT(deselectAll()) );
}

void PatchReviewToolView::customContextMenuRequested(const QPoint& )
{
    KUrl::List urls;
    QModelIndexList selectionIdxs = m_editPatch.filesList->selectionModel()->selectedIndexes();
    foreach(const QModelIndex& idx, selectionIdxs) {
        urls += idx.sibling(idx.row(), 0).data(KDevelop::VcsFileChangesModel::VcsStatusInfoRole).value<VcsStatusInfo>().url();
    }
    
    QPointer<QMenu> menu = new QMenu(m_editPatch.filesList);
    QList<ContextMenuExtension> extensions;
    if(!urls.isEmpty()) {
        KDevelop::FileContext context(urls);
        extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions( &context );
    }

    QList<QAction*> vcsActions;
    foreach( const ContextMenuExtension& ext, extensions )
    {
        vcsActions += ext.actions(ContextMenuExtension::VcsGroup);
    }

    menu->addAction(m_selectAllAction);
    menu->addAction(m_deselectAllAction);
    menu->addActions(vcsActions);
    if ( !menu->isEmpty() ) {
        menu->exec(QCursor::pos());
    }
    
    delete menu;
}

void PatchReviewToolView::nextHunk()
{
    IDocument* current = ICore::self()->documentController()->activeDocument();
    if(current && current->textDocument())
        m_plugin->seekHunk( true, current->textDocument()->url() );
}

void PatchReviewToolView::prevHunk()
{
    IDocument* current = ICore::self()->documentController()->activeDocument();
    if(current && current->textDocument())
        m_plugin->seekHunk( false, current->textDocument()->url() );
}

void PatchReviewToolView::seekFile(bool forwards)
{
    if(!m_plugin->patch())
        return;
    QList<KUrl> checkedUrls = m_fileModel->checkedUrls();
    QList<KUrl> allUrls = m_fileModel->urls();
    IDocument* current = ICore::self()->documentController()->activeDocument();
    if(!current || checkedUrls.empty())
        return;
    kDebug() << "seeking direction" << forwards;
    int currentIndex = allUrls.indexOf(current->url());
    KUrl newUrl;
    if((forwards && current->url() == checkedUrls.back()) ||
            (!forwards && current->url() == checkedUrls[0]))
    {
        newUrl = m_plugin->patch()->file();
        kDebug() << "jumping to patch";
    }
    else if(current->url() == m_plugin->patch()->file() || currentIndex == -1)
    {
        if(forwards)
            newUrl = checkedUrls[0];
        else
            newUrl = checkedUrls.back();
        kDebug() << "jumping from patch";
    }
    else
    {
        QSet<KUrl> checkedUrlsSet( checkedUrls.toSet() );
        for(int offset = 1; offset < allUrls.size(); ++offset)
        {
            int pos;
            if(forwards) {
                pos = (currentIndex + offset) % allUrls.size();
            }else{
                pos = currentIndex - offset;
                if(pos < 0)
                    pos += allUrls.size();
            }
            if(checkedUrlsSet.contains(allUrls[pos]))
            {
                newUrl = allUrls[pos];
                break;
            }
        }
    }

    if(newUrl.isValid())
    {
        activate( newUrl, forwards ? current : 0 );
    }else{
        kDebug() << "found no valid target url";
    }
}

void PatchReviewToolView::activate( const KUrl& url, IDocument* buddy ) const
{
    kDebug() << "activating url" << url;
    // If the document is already open in this area, just re-activate it
    if(KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url)) {
        foreach(Sublime::View* view, ICore::self()->uiController()->activeArea()->views())
        {
            if(view->document() == dynamic_cast<Sublime::Document*>(doc))
            {
                ICore::self()->documentController()->activateDocument(doc);
                return;
            }
        }
    }
    
    // If the document is not open yet, open it in the correct order
    IDocument* newDoc = ICore::self()->documentController()->openDocument(url, KTextEditor::Range(), IDocumentController::DefaultMode, "", buddy);
    if(newDoc && newDoc->textDocument() && newDoc->textDocument()->activeView() && newDoc->textDocument()->activeView()->cursorPosition().line() == 0)
        m_plugin->seekHunk( true, url );
}

void PatchReviewToolView::fileItemChanged( QStandardItem* item )
{
    KUrl url = m_fileModel->statusInfo(item).url();
    if(!m_fileModel->checkedUrls().contains(url))
    {
        // Eventually close the document
        if(KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url)) {
            if(doc->state() == IDocument::Clean)
            {
                foreach(Sublime::View* view, ICore::self()->uiController()->activeArea()->views())
                {
                    if(view->document() == dynamic_cast<Sublime::Document*>(doc))
                    {
                        kDebug() << "closing view of" << url << "because the item was unchecked";
                        ICore::self()->uiController()->activeArea()->closeView(view);
                        return;
                    }
                }
            }
        }
    }
}

void PatchReviewToolView::nextFile()
{
    seekFile(true);
}

void PatchReviewToolView::prevFile()
{
    seekFile(false);
}

void PatchReviewToolView::deselectAll()
{
    m_fileModel->setAllChecked(false);
}

void PatchReviewToolView::selectAll()
{
    m_fileModel->setAllChecked(true);
}

void PatchReviewToolView::finishReview() {
    QList<KUrl> selectedUrls = m_fileModel->checkedUrls();
    kDebug() << "finishing review with" << selectedUrls;
    m_plugin->finishReview( selectedUrls );
}

void PatchReviewToolView::fileDoubleClicked( const QModelIndex& idx ) {
    QModelIndex i = idx.sibling(idx.row(), 0);
    KUrl file = m_fileModel->statusInfo( i ).url();

    activate( file );
}

KUrl PatchReviewPlugin::urlForFileModel( const Diff2::DiffModel* model ) {
    KUrl file = m_patch->baseDir();

    file.addPath( model->destinationPath() );
    file.addPath( model->destinationFile() );

    return file;
}

void PatchReviewToolView::kompareModelChanged() {
    
    QList<KUrl> oldCheckedUrls = m_fileModel->checkedUrls();
    
    m_fileModel->clear();
    
    if ( !m_plugin->modelList() )
        return;

    QMap<KUrl, KDevelop::VcsStatusInfo::State> additionalUrls = m_plugin->patch()->additionalSelectableFiles();

    const Diff2::DiffModelList* models = m_plugin->modelList()->models();
    if( models )
    {
        Diff2::DiffModelList::const_iterator it = models->constBegin();
        for(; it != models->constEnd(); ++it ) {
            Diff2::DifferenceList * diffs = ( *it )->differences();
            int cnt = 0;
            if ( diffs )
                cnt = diffs->count();

            KUrl file = m_plugin->urlForFileModel( *it );
            if( !QFileInfo( file.toLocalFile() ).isReadable() )
                continue;

            VcsStatusInfo status;
            status.setUrl( file );
            status.setState( VcsStatusInfo::ItemModified );

            m_fileModel->updateState( status, cnt );
        }
    }

    for( QMap<KUrl, KDevelop::VcsStatusInfo::State>::const_iterator it = additionalUrls.constBegin(); it != additionalUrls.constEnd(); it++ ) {
        VcsStatusInfo status;
        status.setUrl( it.key() );
        status.setState( it.value() );
        m_fileModel->updateState( status );
    }
    
    if(!m_resetCheckedUrls)
        m_fileModel->setCheckedUrls(oldCheckedUrls);
    else
        m_resetCheckedUrls = false;

    m_editPatch.filesList->resizeColumnToContents( 0 );
    
    // Eventually select the active document
    documentActivated( ICore::self()->documentController()->activeDocument() );
}

void PatchReviewToolView::documentActivated( IDocument* doc ) {
    if( !doc )
        return;

    if ( !m_plugin->modelList() )
        return;

    QStandardItem *fileItem = m_fileModel->fileItemForUrl( doc->url() );
    if ( fileItem ) {
        m_editPatch.filesList->setCurrentIndex( fileItem->index() );
    } else {
        m_editPatch.filesList->setCurrentIndex( QModelIndex() );
    }
}

void PatchReviewToolView::runTests()
{
    IPatchSource::Ptr ipatch = m_plugin->patch();
    if ( !ipatch ) {
        return;
    }

    IProject* project = 0;
    QMap<KUrl, VcsStatusInfo::State> files = ipatch->additionalSelectableFiles();
    QMap<KUrl, VcsStatusInfo::State>::const_iterator it = files.constBegin();

    for (; it != files.constEnd(); ++it) {
        project = ICore::self()->projectController()->findProjectForUrl(it.key());
        if (project) {
           break;
        }
    }

    if (!project) {
        return;
    }

    m_editPatch.testProgressBar->setFormat(i18n("Running tests: %p%"));
    m_editPatch.testProgressBar->setValue(0);
    m_editPatch.testProgressBar->show();

    ProjectTestJob* job = new ProjectTestJob(project, this);
    connect (job, SIGNAL(finished(KJob*)), SLOT(testJobResult(KJob*)));
    connect (job, SIGNAL(percent(KJob*,ulong)), SLOT(testJobPercent(KJob*,ulong)));
    ICore::self()->runController()->registerJob(job);
}

void PatchReviewToolView::testJobPercent(KJob* job, ulong percent)
{
    Q_UNUSED(job);
    m_editPatch.testProgressBar->setValue(percent);
}

void PatchReviewToolView::testJobResult(KJob* job)
{
    ProjectTestJob* testJob = qobject_cast<ProjectTestJob*>(job);
    if (!testJob) {
        return;
    }

    ProjectTestResult result = testJob->testResult();

    QString format;
    if (result.passed > 0 && result.failed == 0 && result.error == 0)
    {
        format = i18np("Test passed", "All %1 tests passed", result.passed);
    }
    else
    {
        format = i18n("Test results: %1 passed, %2 failed, %3 errors", result.passed, result.failed, result.error);
    }
    m_editPatch.testProgressBar->setFormat(format);

    // Needed because some test jobs may raise their own output views
    ICore::self()->uiController()->raiseToolView(this);
}
