/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
**
** GNU Lesser General Public License Usage
**
** 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.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**************************************************************************/

#include "manager.h"
#include "highlightdefinition.h"
#include "highlightdefinitionhandler.h"
#include "highlighterexception.h"
#include "definitiondownloader.h"
#include "../highlightersettings.h"
#include "../texteditorconstants.h"
#include "../utils/qtcassert.h"
#include "../utils/QtConcurrentTools"

#include <algorithm>
#include <QtPlugin>
#include <QString>
#include <QLatin1Char>
#include <QLatin1String>
#include <QStringList>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QRegularExpression>
#include <QFuture>
#include <QtConcurrentRun>
#include <QtConcurrentMap>
#include <QUrl>
#include <QSet>
#include <QXmlStreamReader>
#include <QXmlStreamAttributes>
#include <QDesktopServices>
#include <QMessageBox>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QtAlgorithms>

using namespace TextEditor;
using namespace Internal;

Manager::Manager() :
    m_isDownloadingDefinitionsSpec(false),
    m_hasQueuedRegistration(false)
{
    connect(&m_registeringWatcher, &QFutureWatcher<QPair<RegisterData, QList<Core::MimeType> > >::finished, this, &Manager::registerMimeTypesFinished);
    connect(&m_downloadWatcher, &QFutureWatcher<void>::finished, this, &Manager::downloadDefinitionsFinished);
}

Manager::~Manager()
{
    disconnect(&m_registeringWatcher);
    disconnect(&m_downloadWatcher);
    if (m_registeringWatcher.isRunning())
        m_registeringWatcher.cancel();
    if (m_downloadWatcher.isRunning())
        m_downloadWatcher.cancel();
}

Manager *Manager::instance()
{
    /*
        This object will be leaked.  This is due to a known issue
        inside QNetworkAccessManager where the thread that creates 
        the object must be the one to destroy.  For tracking see
        https://jira.arm.com/browse/FORPRDEV-10136
    */
    static Manager* manager = new Manager();
    return manager;
}

QString Manager::definitionIdByName(const QString &name) const
{
    return m_register.m_idByName.value(name);
}

QString Manager::definitionIdByMimeType(const QString &mimeType) const
{
    return m_register.m_idByMimeType.value(mimeType);
}

QString Manager::definitionIdByAnyMimeType(const QStringList &mimeTypes) const
{
    QString definitionId;
    foreach (const QString &mimeType, mimeTypes) {
        definitionId = definitionIdByMimeType(mimeType);
        if (!definitionId.isEmpty())
            break;
    }
    return definitionId;
}

QSharedPointer<HighlightDefinition> Manager::definition(const QString &id)
{
    if (!id.isEmpty() && !m_definitions.contains(id)) {
        QFile definitionFile(id);
        if (!definitionFile.open(QIODevice::ReadOnly | QIODevice::Text))
            return QSharedPointer<HighlightDefinition>();

        QSharedPointer<HighlightDefinition> definition(new HighlightDefinition);
        HighlightDefinitionHandler handler(definition);
        m_isBuildingDefinition.insert(id);
        if (!handler.parse(&definitionFile))
            definition.clear();
        m_isBuildingDefinition.remove(id);
        definitionFile.close();
        m_definitions.insert(id, definition);
    }

    return m_definitions.value(id);
}

QSharedPointer<HighlightDefinitionMetaData> Manager::definitionMetaData(const QString &id) const
{
    return m_register.m_definitionsMetaData.value(id);
}

bool Manager::isBuildingDefinition(const QString &id) const
{
    return m_isBuildingDefinition.contains(id);
}

namespace TextEditor {
namespace Internal {

class ManagerProcessor : public QObject
{
    Q_OBJECT
public:
    ManagerProcessor(const TextEditor::MimeDatabase& db, const HighlighterSettings &settings);
    void process(QFutureInterface<QPair<Manager::RegisterData,
                                        QList<Core::MimeType> > > &future);

    QStringList m_definitionsPaths;
    QSet<QString> m_knownMimeTypes;
    QSet<QString> m_knownSuffixes;
    QHash<QString, Core::MimeType> m_userModified;
    static const int kMaxProgress;

    struct PriorityComp
    {
        bool operator()(const QSharedPointer<HighlightDefinitionMetaData> &a,
                        const QSharedPointer<HighlightDefinitionMetaData> &b) {
            return a->priority() > b->priority();
        }
    };
};

const int ManagerProcessor::kMaxProgress = 200;

ManagerProcessor::ManagerProcessor(const TextEditor::MimeDatabase& db, const HighlighterSettings &settings)
{
    const QList<QString> suffixes = db.suffixes();
    m_knownSuffixes = QSet(suffixes.begin(), suffixes.end());
    m_definitionsPaths.append(settings.definitionFilesPath());
    if (settings.useFallbackLocation())
        m_definitionsPaths.append(settings.fallbackDefinitionFilesPath());

    foreach (const Core::MimeType &userMimeType, db.readUserModifiedMimeTypes())
        m_userModified.insert(userMimeType.type(), userMimeType);
    foreach (const Core::MimeType &mimeType, db.mimeTypes())
        m_knownMimeTypes.insert(mimeType.type());
}

void ManagerProcessor::process(QFutureInterface<QPair<Manager::RegisterData,
                                                      QList<Core::MimeType> > > &future)
{
    future.setProgressRange(0, kMaxProgress);

    // @TODO: Improve MIME database to handle the following limitation.
    // The generic highlighter only register its types after all other plugins
    // have populated Creator's MIME database (so it does not override anything).
    // When the generic highlighter settings change only its internal data is cleaned-up
    // and rebuilt. Creator's MIME database is not touched. So depending on how the
    // user plays around with the generic highlighter file definitions (changing
    // duplicated patterns, for example), some changes might not be reflected.
    // A definitive implementation would require some kind of re-load or update
    // (considering hierarchies, aliases, etc) of the MIME database whenever there
    // is a change in the generic highlighter settings.

    Manager::RegisterData data;
    QList<Core::MimeType> newMimeTypes;

    foreach (const QString &path, m_definitionsPaths) {
        if (path.isEmpty())
            continue;

        QDir definitionsDir(path);
        QStringList filter(QLatin1String("*.xml"));
        definitionsDir.setNameFilters(filter);
        QList<QSharedPointer<HighlightDefinitionMetaData> > allMetaData;
        foreach (const QFileInfo &fileInfo, definitionsDir.entryInfoList()) {
            const QSharedPointer<HighlightDefinitionMetaData> &metaData =
                    Manager::parseMetadata(fileInfo);
            if (!metaData.isNull())
                allMetaData.append(metaData);
        }

        // Consider definitions with higher priority first.
        std::sort(allMetaData.begin(), allMetaData.end(), PriorityComp());

        foreach (const QSharedPointer<HighlightDefinitionMetaData> &metaData, allMetaData) {
            if (future.isCanceled())
                return;
            if (future.progressValue() < kMaxProgress - 1)
                future.setProgressValue(future.progressValue() + 1);

            if (data.m_idByName.contains(metaData->name()))
                // Name already exists... This is a fallback item, do not consider it.
                continue;

            const QString &id = metaData->id();
            data.m_idByName.insert(metaData->name(), id);
            data.m_definitionsMetaData.insert(id, metaData);

            static const QStringList textPlain(QLatin1String("text/plain"));

            // A definition can specify multiple MIME types and file extensions/patterns,
            // but all on a single string. So associate all patterns with all MIME types.
            QList<Core::MimeGlobPattern> globPatterns;
            foreach (const QString &type, metaData->mimeTypes()) {
                if (data.m_idByMimeType.contains(type))
                    continue;

                data.m_idByMimeType.insert(type, id);
                if (!m_knownMimeTypes.contains(type)) {
                    m_knownMimeTypes.insert(type);

                    Core::MimeType mimeType;
                    mimeType.setType(type);
                    mimeType.setSubClassesOf(textPlain);
                    mimeType.setComment(metaData->name());

                    // If there's a user modification for this mime type, we want to use the
                    // modified patterns and rule-based matchers. If not, just consider what
                    // is specified in the definition file.
                    QHash<QString, Core::MimeType>::const_iterator it =
                        m_userModified.find(mimeType.type());
                    if (it == m_userModified.end()) {
                        if (globPatterns.isEmpty()) {
                            foreach (const QString &pattern, metaData->patterns()) {
                                static const QLatin1String mark("*.");
                                if (pattern.startsWith(mark)) {
                                    const QString &suffix = pattern.right(pattern.length() - 2);
                                    if (!m_knownSuffixes.contains(suffix))
                                        m_knownSuffixes.insert(suffix);
                                    else
                                        continue;
                                }
                                QRegularExpression regExp = QRegularExpression(QRegularExpression::wildcardToRegularExpression(pattern), QRegularExpression::NoPatternOption);
                                globPatterns.append(Core::MimeGlobPattern(regExp, metaData->priority()));
                            }
                        }
                        mimeType.setGlobPatterns(globPatterns);
                    } else {
                        mimeType.setGlobPatterns(it.value().globPatterns());
                        mimeType.setMagicRuleMatchers(it.value().magicRuleMatchers());
                    }

                    newMimeTypes.append(mimeType);
                }
            }
        }
    }

    future.reportResult(qMakePair(data, newMimeTypes));
}

} // Internal
} // TextEditor


void Manager::registerMimeTypes()
{
    if (!m_registeringWatcher.isRunning()) {
        clear();

        Q_ASSERT(m_mimeDatabase);

        ManagerProcessor *processor = new ManagerProcessor(*m_mimeDatabase, m_highlighterSettings);
        QFuture<QPair<RegisterData, QList<Core::MimeType> > > future =
            QtConcurrent::run(&ManagerProcessor::process, processor);
        connect(&m_registeringWatcher, &QFutureWatcher<QPair<RegisterData, QList<Core::MimeType> > >::finished, processor, &ManagerProcessor::deleteLater);
        m_registeringWatcher.setFuture(future);

        /*Core::ICore::progressManager()->addTask(future,
                                                            tr("Registering definitions"),
                                                            QLatin1String(Constants::TASK_REGISTER_DEFINITIONS));*/
    } else {
        m_hasQueuedRegistration = true;
        m_registeringWatcher.cancel();
    }
}

void Manager::registerMimeTypesFinished()
{
    if (m_hasQueuedRegistration) {
        m_hasQueuedRegistration = false;
        registerMimeTypes();
    } else if (!m_registeringWatcher.isCanceled()) {
        const QPair<RegisterData, QList<Core::MimeType> > &result = m_registeringWatcher.result();
        m_register = result.first;

        //PlainTextEditorFactory *factory = TextEditorPlugin::instance()->editorFactory();
        //const QSet<QString> &inFactory = factory->mimeTypes().toSet();
        foreach (const Core::MimeType &mimeType, result.second) {
            m_mimeDatabase->addMimeType(mimeType);
            //if (!inFactory.contains(mimeType.type()))
            //    factory->addMimeType(mimeType.type());
        }

        Core::MimeType test = m_mimeDatabase->findByType("text/plain");
        if (test.isNull())
        {
            // The MimeDatabase doesn't work unless there is a top-level entry for test/plain type.
            test.setType("text/plain");
            m_mimeDatabase->addMimeType(test);
        }

        emit mimeTypesRegistered();
    }
}

QSharedPointer<HighlightDefinitionMetaData> Manager::parseMetadata(const QFileInfo &fileInfo)
{
    static const QLatin1Char kSemiColon(';');
    static const QLatin1Char kSpace(' ');
    static const QLatin1Char kDash('-');
    static const QLatin1String kLanguage("language");
    static const QLatin1String kArtificial("text/x-artificial-");

    QFile definitionFile(fileInfo.absoluteFilePath());
    if (!definitionFile.open(QIODevice::ReadOnly | QIODevice::Text))
        return QSharedPointer<HighlightDefinitionMetaData>();

    QSharedPointer<HighlightDefinitionMetaData> metaData(new HighlightDefinitionMetaData);

    QXmlStreamReader reader(&definitionFile);
    while (!reader.atEnd() && !reader.hasError()) {
        if (reader.readNext() == QXmlStreamReader::StartElement && reader.name() == kLanguage) {
            const QXmlStreamAttributes &atts = reader.attributes();

            metaData->setFileName(fileInfo.fileName());
            metaData->setId(fileInfo.absoluteFilePath());
            metaData->setName(atts.value(HighlightDefinitionMetaData::kName).toString());
            metaData->setVersion(atts.value(HighlightDefinitionMetaData::kVersion).toString());
            metaData->setPriority(atts.value(HighlightDefinitionMetaData::kPriority).toString()
                                  .toInt());
            metaData->setPatterns(atts.value(HighlightDefinitionMetaData::kExtensions)
                                  .toString().split(kSemiColon, Qt::SkipEmptyParts));

            QStringList mimeTypes = atts.value(HighlightDefinitionMetaData::kMimeType).
                                    toString().split(kSemiColon, Qt::SkipEmptyParts);
            if (mimeTypes.isEmpty()) {
                // There are definitions which do not specify a MIME type, but specify file
                // patterns. Creating an artificial MIME type is a workaround.
                QString artificialType(kArtificial);
                artificialType.append(metaData->name().trimmed().replace(kSpace, kDash));
                mimeTypes.append(artificialType);
            }
            metaData->setMimeTypes(mimeTypes);

            break;
        }
    }
    reader.clear();
    definitionFile.close();

    return metaData;
}

QList<HighlightDefinitionMetaData> Manager::parseAvailableDefinitionsList(QIODevice *device) const
{
    static const QLatin1Char kSlash('/');
    static const QLatin1String kDefinition("Definition");

    QList<HighlightDefinitionMetaData> metaDataList;
    QXmlStreamReader reader(device);
    while (!reader.atEnd() && !reader.hasError()) {
        if (reader.readNext() == QXmlStreamReader::StartElement &&
            reader.name() == kDefinition) {
            const QXmlStreamAttributes &atts = reader.attributes();

            HighlightDefinitionMetaData metaData;
            metaData.setName(atts.value(HighlightDefinitionMetaData::kName).toString());
            metaData.setVersion(atts.value(HighlightDefinitionMetaData::kVersion).toString());
            QString url(atts.value(HighlightDefinitionMetaData::kUrl).toString());
            metaData.setUrl(QUrl(url));
            const int slash = url.lastIndexOf(kSlash);
            if (slash != -1)
                metaData.setFileName(url.right(url.length() - slash - 1));

            metaDataList.append(metaData);
        }
    }
    reader.clear();

    return metaDataList;
}

void Manager::downloadAvailableDefinitionsMetaData()
{
    QUrl url(QLatin1String("http://www.kate-editor.org/syntax/update-3.2.xml"));
    QNetworkRequest request(url);
    // Currently this takes a couple of seconds on Windows 7: QTBUG-10106.
    QNetworkReply *reply = m_networkManager.get(request);
    connect(reply, &QNetworkReply::finished, this, &Manager::downloadAvailableDefinitionsListFinished);
}

void Manager::downloadAvailableDefinitionsListFinished()
{
    if (QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender())) {
        if (reply->error() == QNetworkReply::NoError)
            emit definitionsMetaDataReady(parseAvailableDefinitionsList(reply));
        else
            emit errorDownloadingDefinitionsMetaData();
        reply->deleteLater();
    }
}

void Manager::downloadDefinitions(const QList<QUrl> &urls, const QString &savePath)
{
    m_downloaders.clear();
    foreach (const QUrl &url, urls)
        m_downloaders.append(new DefinitionDownloader(url, savePath));

    m_isDownloadingDefinitionsSpec = true;
    QFuture<void> future = QtConcurrent::map(m_downloaders, DownloaderStarter());
    m_downloadWatcher.setFuture(future);
    /*Core::ICore::progressManager()->addTask(future,
                                                        tr("Downloading definitions"),
                                                        QLatin1String(Constants::TASK_DOWNLOAD_DEFINITIONS));*/
}

void Manager::downloadDefinitionsFinished()
{
    int errors = 0;
    bool writeError = false;
    foreach (DefinitionDownloader *downloader, m_downloaders) {
        DefinitionDownloader::Status status = downloader->status();
        if (status != DefinitionDownloader::Ok) {
            ++errors;
            if (status == DefinitionDownloader::WriteError && !writeError)
                writeError = true;
        }
        delete downloader;
    }

    if (errors > 0) {
        QString text;
        if (errors == m_downloaders.size())
            text = tr("Error downloading selected definition(s).");
        else
            text = tr("Error downloading one or more definitions.");
        if (writeError)
            text.append(tr("\nPlease check the directory's access rights."));
        QMessageBox::critical(0, tr("Download Error"), text);
    }

    m_isDownloadingDefinitionsSpec = false;
}

bool Manager::isDownloadingDefinitions() const
{
    return m_isDownloadingDefinitionsSpec;
}

void Manager::clear()
{
    m_register.m_idByName.clear();
    m_register.m_idByMimeType.clear();
    m_register.m_definitionsMetaData.clear();
    m_definitions.clear();
}

void Manager::initMimeDatabase(TextEditor::MimeDatabase *db, const HighlighterSettings &hs)
{
    m_mimeDatabase = db;
    m_highlighterSettings = hs;
    registerMimeTypes();
}

#include "manager.moc"
