/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** GNU Lesser General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/

#include "vcsbaseplugin.h"
#include "vcsbasesubmiteditor.h"
#include "vcsplugin.h"
#include "vcsbaseoutputwindow.h"
#include "corelistener.h"

#include <coreplugin/icore.h>
#include <coreplugin/ifile.h>
#include <coreplugin/iversioncontrol.h>
#include <coreplugin/filemanager.h>
#include <coreplugin/vcsmanager.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/project.h>
#include <utils/qtcassert.h>

#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QSharedData>

#include <QtGui/QAction>
#include <QtGui/QMessageBox>
#include <QtGui/QFileDialog>
#include <QtGui/QMainWindow>

enum { debug = 0 };

namespace VCSBase {

namespace Internal {

// Internal state created by the state listener and
// VCSBasePluginState.

struct State {
    void clearFile();
    void clearProject();
    inline void clear();

    bool equals(const State &rhs) const;

    inline bool hasFile() const     { return !currentFile.isEmpty(); }
    inline bool hasProject() const  { return !currentProjectPath.isEmpty(); }
    inline bool isEmpty() const     { return !hasFile() && !hasProject(); }

    QString currentFile;
    QString currentFileName;
    QString currentFileDirectory;
    QString currentFileTopLevel;

    QString currentProjectPath;
    QString currentProjectName;
    QString currentProjectTopLevel;
};

void State::clearFile()
{
    currentFile.clear();
    currentFileName.clear();
    currentFileDirectory.clear();
    currentFileTopLevel.clear();
}

void State::clearProject()
{
    currentProjectPath.clear();
    currentProjectName.clear();
    currentProjectTopLevel.clear();
}

void State::clear()
{
    clearFile();
    clearProject();
}

bool State::equals(const State &rhs) const
{
    return currentFile == rhs.currentFile
            && currentFileName == rhs.currentFileName
            && currentFileTopLevel == rhs.currentFileTopLevel
            && currentProjectPath == rhs.currentProjectPath
            && currentProjectName == rhs.currentProjectName
            && currentProjectTopLevel == rhs.currentProjectTopLevel;
}

QDebug operator<<(QDebug in, const State &state)
{
    QDebug nospace = in.nospace();
    nospace << "State: ";
    if (state.isEmpty()) {
        nospace << "<empty>";
    } else {
        if (state.hasFile()) {
            nospace << "File=" << state.currentFile
                    << ',' << state.currentFileTopLevel;
        } else {
            nospace << "<no file>";
        }
        nospace << '\n';
        if (state.hasProject()) {
            nospace << "       Project=" << state.currentProjectName
            << ',' << state.currentProjectPath
            << ',' << state.currentProjectTopLevel;

        } else {
            nospace << "<no project>";
        }
        nospace << '\n';
    }
    return in;
}

// StateListener: Connects to the relevant signals, tries to find version
// controls and emits signals to the plugin instances.

class StateListener : public QObject {
    Q_OBJECT
public:
    explicit StateListener(QObject *parent);

signals:
    void stateChanged(const VCSBase::Internal::State &s, Core::IVersionControl *vc);

private slots:
    void slotStateChanged();
};

StateListener::StateListener(QObject *parent) :
        QObject(parent)
{
    Core::ICore *core = Core::ICore::instance();
    connect(core, SIGNAL(contextChanged(Core::IContext *)),
            this, SLOT(slotStateChanged()));
    connect(core->fileManager(), SIGNAL(currentFileChanged(QString)),
            this, SLOT(slotStateChanged()));

    if (ProjectExplorer::ProjectExplorerPlugin *pe = ProjectExplorer::ProjectExplorerPlugin::instance())
        connect(pe, SIGNAL(currentProjectChanged(ProjectExplorer::Project*)),
                this, SLOT(slotStateChanged()));
}

void StateListener::slotStateChanged()
{
    const ProjectExplorer::ProjectExplorerPlugin *pe = ProjectExplorer::ProjectExplorerPlugin::instance();
    const Core::ICore *core = Core::ICore::instance();
    Core::VCSManager *vcsManager = core->vcsManager();

    // Get the current file. Are we on a temporary submit editor indicated by
    // temporary path prefix or does the file contains a hash, indicating a project
    // folder?
    State state;
    state.currentFile = core->fileManager()->currentFile();
    if (!state.currentFile.isEmpty()) {
        if (state.currentFile.contains(QLatin1Char('#')) || state.currentFile.startsWith(QDir::tempPath()))
            state.currentFile.clear();
    }
    // Get the file and its control. Do not use the file unless we find one
    Core::IVersionControl *fileControl = 0;
    if (!state.currentFile.isEmpty()) {
        const QFileInfo fi(state.currentFile);
        state.currentFileDirectory = fi.absolutePath();
        state.currentFileName = fi.fileName();
        fileControl = vcsManager->findVersionControlForDirectory(state.currentFileDirectory,
                                                                 &state.currentFileTopLevel);
        if (!fileControl)
            state.clearFile();
    }
    // Check for project, find the control
    Core::IVersionControl *projectControl = 0;
    if (const ProjectExplorer::Project *currentProject = pe->currentProject()) {
        state.currentProjectPath = QFileInfo(currentProject->file()->fileName()).absolutePath();
        state.currentProjectName = currentProject->displayName();
        projectControl = vcsManager->findVersionControlForDirectory(state.currentProjectPath,
                                                                    &state.currentProjectTopLevel);
        if (projectControl) {
            // If we have both, let the file's one take preference
            if (fileControl && projectControl != fileControl) {
                state.clearProject();
            }
        } else {
            state.clearProject(); // No control found
        }
    }
    // Assemble state and emit signal.
    Core::IVersionControl *vc = state.currentFile.isEmpty() ? projectControl : fileControl;
    if (debug)
        qDebug() << state << (vc ? vc->displayName() : QString(QLatin1String("No version control")));
    emit stateChanged(state, vc);
}

} // namespace Internal

class VCSBasePluginStateData : public QSharedData {
public:
    Internal::State m_state;
};

VCSBasePluginState::VCSBasePluginState() : data(new VCSBasePluginStateData)
{
}

VCSBasePluginState::VCSBasePluginState(const VCSBasePluginState &rhs) : data(rhs.data)
{
}

VCSBasePluginState &VCSBasePluginState::operator=(const VCSBasePluginState &rhs)
{
    if (this != &rhs)
        data.operator=(rhs.data);
    return *this;
}

VCSBasePluginState::~VCSBasePluginState()
{
}

QString VCSBasePluginState::currentFile() const
{
    return data->m_state.currentFile;
}

QString VCSBasePluginState::currentFileName() const
{
    return data->m_state.currentFileName;
}

QString VCSBasePluginState::currentFileTopLevel() const
{
    return data->m_state.currentFileTopLevel;
}

QString VCSBasePluginState::currentFileDirectory() const
{
    return data->m_state.currentFileDirectory;
}

QString VCSBasePluginState::relativeCurrentFile() const
{
    QTC_ASSERT(hasFile(), return QString())
    return QDir(data->m_state.currentFileTopLevel).relativeFilePath(data->m_state.currentFile);
}

QString VCSBasePluginState::currentProjectPath() const
{
    return data->m_state.currentProjectPath;
}

QString VCSBasePluginState::currentProjectName() const
{
    return data->m_state.currentProjectName;
}

QString VCSBasePluginState::currentProjectTopLevel() const
{
    return data->m_state.currentProjectTopLevel;
}

QStringList VCSBasePluginState::relativeCurrentProject() const
{
    QStringList rc;
    QTC_ASSERT(hasProject(), return rc)
    if (data->m_state.currentProjectTopLevel != data->m_state.currentProjectPath)
        rc.append(QDir(data->m_state.currentProjectTopLevel).relativeFilePath(data->m_state.currentProjectPath));
    return rc;
}

bool VCSBasePluginState::hasTopLevel() const
{
    return data->m_state.hasFile() || data->m_state.hasProject();
}

QString VCSBasePluginState::topLevel() const
{
    return hasFile() ? data->m_state.currentFileTopLevel : data->m_state.currentProjectTopLevel;
}

bool VCSBasePluginState::equals(const Internal::State &rhs) const
{
    return data->m_state.equals(rhs);
}

bool VCSBasePluginState::equals(const VCSBasePluginState &rhs) const
{
    return equals(rhs.data->m_state);
}

void VCSBasePluginState::clear()
{
    data->m_state.clear();
}

void VCSBasePluginState::setState(const Internal::State &s)
{
    data->m_state = s;
}

bool VCSBasePluginState::isEmpty() const
{
    return data->m_state.isEmpty();
}

bool VCSBasePluginState::hasFile() const
{
    return data->m_state.hasFile();
}

bool VCSBasePluginState::hasProject() const
{
    return data->m_state.hasProject();
}

VCSBASE_EXPORT QDebug operator<<(QDebug in, const VCSBasePluginState &state)
{
    in << state.data->m_state;
    return in;
}

//  VCSBasePlugin
struct VCSBasePluginPrivate {
    explicit VCSBasePluginPrivate(const QString &submitEditorId);

    inline bool supportsRepositoryCreation() const;

    const QString m_submitEditorId;
    Core::IVersionControl *m_versionControl;
    VCSBasePluginState m_state;
    int m_actionState;
    QAction *m_testSnapshotAction;
    QAction *m_testListSnapshotsAction;
    QAction *m_testRestoreSnapshotAction;
    QAction *m_testRemoveSnapshotAction;
    QString m_testLastSnapshot;

    static Internal::StateListener *m_listener;
};

VCSBasePluginPrivate::VCSBasePluginPrivate(const QString &submitEditorId) :
    m_submitEditorId(submitEditorId),
    m_versionControl(0),
    m_actionState(-1),
    m_testSnapshotAction(0),
    m_testListSnapshotsAction(0),
    m_testRestoreSnapshotAction(0),
    m_testRemoveSnapshotAction(0)
{
}

bool VCSBasePluginPrivate::supportsRepositoryCreation() const
{
    return m_versionControl && m_versionControl->supportsOperation(Core::IVersionControl::CreateRepositoryOperation);
}

Internal::StateListener *VCSBasePluginPrivate::m_listener = 0;

VCSBasePlugin::VCSBasePlugin(const QString &submitEditorId) :
    d(new VCSBasePluginPrivate(submitEditorId))
{
}

VCSBasePlugin::~VCSBasePlugin()
{
    delete d;
}

void VCSBasePlugin::initialize(Core::IVersionControl *vc)
{
    d->m_versionControl = vc;
    addAutoReleasedObject(vc);

    Internal::VCSPlugin *plugin = Internal::VCSPlugin::instance();
    connect(plugin->coreListener(), SIGNAL(submitEditorAboutToClose(VCSBaseSubmitEditor*,bool*)),
            this, SLOT(slotSubmitEditorAboutToClose(VCSBaseSubmitEditor*,bool*)));
    // First time: create new listener
    if (!VCSBasePluginPrivate::m_listener)
        VCSBasePluginPrivate::m_listener = new Internal::StateListener(plugin);
    connect(VCSBasePluginPrivate::m_listener,
            SIGNAL(stateChanged(VCSBase::Internal::State, Core::IVersionControl*)),
            this,
            SLOT(slotStateChanged(VCSBase::Internal::State,Core::IVersionControl*)));
}

void VCSBasePlugin::slotSubmitEditorAboutToClose(VCSBaseSubmitEditor *submitEditor, bool *result)
{
    if (debug)
        qDebug() << this << d->m_submitEditorId << "Closing submit editor" << submitEditor << submitEditor->id();
    if (submitEditor->id() == d->m_submitEditorId)
        *result = submitEditorAboutToClose(submitEditor);
}

Core::IVersionControl *VCSBasePlugin::versionControl() const
{
    return d->m_versionControl;
}

void VCSBasePlugin::slotStateChanged(const VCSBase::Internal::State &newInternalState, Core::IVersionControl *vc)
{
    if (vc == d->m_versionControl) {
        // We are directly affected: Change state
        if (!d->m_state.equals(newInternalState)) {
            d->m_state.setState(newInternalState);
            updateActions(VCSEnabled);
        }
    } else {
        // Some other VCS plugin or state changed: Reset us to empty state.
        const ActionState newActionState = vc ? OtherVCSEnabled : NoVCSEnabled;
        if (d->m_actionState != newActionState || !d->m_state.isEmpty()) {
            d->m_actionState = newActionState;
            const VCSBasePluginState emptyState;
            d->m_state = emptyState;
            updateActions(newActionState);
        }
    }
}

const VCSBasePluginState &VCSBasePlugin::currentState() const
{
    return d->m_state;
}

bool VCSBasePlugin::enableMenuAction(ActionState as, QAction *menuAction) const
{
    if (debug)
        qDebug() << "enableMenuAction" << menuAction->text() << as;
    switch (as) {
    case VCSBase::VCSBasePlugin::NoVCSEnabled: {
        const bool supportsCreation = d->supportsRepositoryCreation();
        menuAction->setVisible(true);
        menuAction->setEnabled(supportsCreation);
        return supportsCreation;
    }
    case VCSBase::VCSBasePlugin::OtherVCSEnabled:
        menuAction->setVisible(false);
        return false;
    case VCSBase::VCSBasePlugin::VCSEnabled:
        menuAction->setVisible(true);
        menuAction->setEnabled(true);
        break;
    }
    return true;
}

void VCSBasePlugin::promptToDeleteCurrentFile()
{
    const VCSBasePluginState state = currentState();
    QTC_ASSERT(state.hasFile(), return)
    const bool rc = Core::ICore::instance()->vcsManager()->promptToDelete(versionControl(), state.currentFile());
    if (!rc)
        QMessageBox::warning(0, tr("Version Control"), tr("The file '%1' could not be deleted.").arg(state.currentFile()), QMessageBox::Ok);
}

static inline bool ask(QWidget *parent, const QString &title, const QString &question, bool defaultValue = true)

{
    const QMessageBox::StandardButton defaultButton = defaultValue ? QMessageBox::Yes : QMessageBox::No;
    return QMessageBox::question(parent, title, question, QMessageBox::Yes|QMessageBox::No, defaultButton) == QMessageBox::Yes;
}

void VCSBasePlugin::createRepository()
{
    QTC_ASSERT(d->m_versionControl->supportsOperation(Core::IVersionControl::CreateRepositoryOperation), return);
    // Find current starting directory
    QString directory;
    if (const ProjectExplorer::Project *currentProject = ProjectExplorer::ProjectExplorerPlugin::instance()->currentProject())
        directory = QFileInfo(currentProject->file()->fileName()).absolutePath();
    // Prompt for a directory that is not under version control yet
    QMainWindow *mw = Core::ICore::instance()->mainWindow();
    do {
        directory = QFileDialog::getExistingDirectory(mw, tr("Choose repository directory"), directory);
        if (directory.isEmpty())
            return;
        const Core::IVersionControl *managingControl = Core::ICore::instance()->vcsManager()->findVersionControlForDirectory(directory);
        if (managingControl == 0)
            break;
        const QString question = tr("The directory '%1' is already managed by a version control system (%2)."
                                    " Would you like to specify another directory?").arg(directory, managingControl->displayName());

        if (!ask(mw, tr("Repository already under version control"), question))
            return;
    } while (true);
    // Create
    const bool rc = d->m_versionControl->vcsCreateRepository(directory);
    if (rc) {
        QMessageBox::information(mw, tr("Repository created"),
                                 tr("A version control repository has been created in %1.").arg(directory));
    } else {
        QMessageBox::warning(mw, tr("Repository creation failed"),
                                 tr("A version control repository could not be created in %1.").arg(directory));
    }
}

// For internal tests: Create actions driving IVersionControl's snapshot interface.
QList<QAction*> VCSBasePlugin::createSnapShotTestActions()
{
    if (!d->m_testSnapshotAction) {
        d->m_testSnapshotAction = new QAction(QLatin1String("Take snapshot"), this);
        connect(d->m_testSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestSnapshot()));
        d->m_testListSnapshotsAction = new QAction(QLatin1String("List snapshots"), this);
        connect(d->m_testListSnapshotsAction, SIGNAL(triggered()), this, SLOT(slotTestListSnapshots()));
        d->m_testRestoreSnapshotAction = new QAction(QLatin1String("Restore snapshot"), this);
        connect(d->m_testRestoreSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestRestoreSnapshot()));
        d->m_testRemoveSnapshotAction = new QAction(QLatin1String("Remove snapshot"), this);
        connect(d->m_testRemoveSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestRemoveSnapshot()));
    }
    QList<QAction*> rc;
    rc << d->m_testSnapshotAction << d->m_testListSnapshotsAction << d->m_testRestoreSnapshotAction
       << d->m_testRemoveSnapshotAction;
    return rc;
}

void VCSBasePlugin::slotTestSnapshot()
{
    QTC_ASSERT(currentState().hasTopLevel(), return)
    d->m_testLastSnapshot = versionControl()->vcsCreateSnapshot(currentState().topLevel());
    qDebug() << "Snapshot " << d->m_testLastSnapshot;
    VCSBaseOutputWindow::instance()->append(QLatin1String("Snapshot: ") + d->m_testLastSnapshot);
    if (!d->m_testLastSnapshot.isEmpty())
        d->m_testRestoreSnapshotAction->setText(QLatin1String("Restore snapshot ") + d->m_testLastSnapshot);
}

void VCSBasePlugin::slotTestListSnapshots()
{
    QTC_ASSERT(currentState().hasTopLevel(), return)
    const QStringList snapshots = versionControl()->vcsSnapshots(currentState().topLevel());
    qDebug() << "Snapshots " << snapshots;
    VCSBaseOutputWindow::instance()->append(QLatin1String("Snapshots: ") + snapshots.join(QLatin1String(", ")));
}

void VCSBasePlugin::slotTestRestoreSnapshot()
{
    QTC_ASSERT(currentState().hasTopLevel() && !d->m_testLastSnapshot.isEmpty(), return)
    const bool ok = versionControl()->vcsRestoreSnapshot(currentState().topLevel(), d->m_testLastSnapshot);
    const QString msg = d->m_testLastSnapshot+ (ok ? QLatin1String(" restored") : QLatin1String(" failed"));
    qDebug() << msg;
    VCSBaseOutputWindow::instance()->append(msg);
}

void VCSBasePlugin::slotTestRemoveSnapshot()
{
    QTC_ASSERT(currentState().hasTopLevel() && !d->m_testLastSnapshot.isEmpty(), return)
    const bool ok = versionControl()->vcsRemoveSnapshot(currentState().topLevel(), d->m_testLastSnapshot);
    const QString msg = d->m_testLastSnapshot+ (ok ? QLatin1String(" removed") : QLatin1String(" failed"));
    qDebug() << msg;
    VCSBaseOutputWindow::instance()->append(msg);
    d->m_testLastSnapshot.clear();
}

} // namespace VCSBase

#include "vcsbaseplugin.moc"