/*
* This file is part of KDevelop
*
* Copyright 2006 Adam Treat <treat@kde.org>
* Copyright 2006-2008 Hamish Rodda <rodda@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

#include "parsejob.h"

#include <cassert>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <QFile>
#include <QByteArray>
#include <QMutex>
#include <QMutexLocker>
#include <QApplication>

#include <kdebug.h>
#include <klocale.h>
#include <ktexteditor/movinginterface.h>

#include "backgroundparser.h"
#include "duchain/topducontext.h"

#include "duchain/duchainlock.h"
#include "duchain/duchain.h"
#include "duchain/parsingenvironment.h"

#include <interfaces/foregroundlock.h>
#include <interfaces/icore.h>
#include <interfaces/ilanguagecontroller.h>
#include <codegen/coderepresentation.h>
#include <duchain/declaration.h>
#include <duchain/use.h>
#include <interfaces/icodehighlighting.h>
#include <duchain/problem.h>


using namespace KTextEditor;

static QMutex minimumFeaturesMutex;
static QHash<KDevelop::IndexedString, QList<KDevelop::TopDUContext::Features> > staticMinimumFeatures;

namespace KDevelop
{

class ParseJobPrivate
{
public:

    ParseJobPrivate(const IndexedString& url_, ILanguageSupport* languageSupport_) :
          url( url_ )
        , languageSupport( languageSupport_ )
        , abortRequested( 0 )
        , hasReadContents( false )
        , aborted( false )
        , features( TopDUContext::VisibleDeclarationsAndContexts )
        , parsePriority( 0 )
        , sequentialProcessingFlags( ParseJob::IgnoresSequentialProcessing )
    {
    }

    ~ParseJobPrivate()
    {
    }

    ReferencedTopDUContext duContext;

    IndexedString url;
    ILanguageSupport* languageSupport;

    ParseJob::Contents contents;

    QAtomicInt abortRequested;

    bool hasReadContents : 1;
    bool aborted : 1;
    TopDUContext::Features features;
    QList<QWeakPointer<QObject> > notify;
    QWeakPointer<DocumentChangeTracker> tracker;
    RevisionReference revision;
    RevisionReference previousRevision;

    int parsePriority;
    ParseJob::SequentialProcessingFlags sequentialProcessingFlags;
};

ParseJob::ParseJob( const IndexedString& url, KDevelop::ILanguageSupport* languageSupport )
        : ThreadWeaver::JobSequence(),
        d(new ParseJobPrivate(url, languageSupport))
{}

ParseJob::~ParseJob()
{
    typedef QWeakPointer<QObject> QObjectPointer;
    foreach(const QObjectPointer &p, d->notify) {
        if(p) {
            QMetaObject::invokeMethod(p.data(), "updateReady", Qt::QueuedConnection, Q_ARG(KDevelop::IndexedString, d->url), Q_ARG(KDevelop::ReferencedTopDUContext, d->duContext));
        }
    }

    delete d;
}

ILanguageSupport* ParseJob::languageSupport() const
{
    return d->languageSupport;
}

void ParseJob::setParsePriority(int priority)
{
    d->parsePriority = priority;
}

int ParseJob::parsePriority() const
{
    return d->parsePriority;
}

bool ParseJob::requiresSequentialProcessing() const
{
    return d->sequentialProcessingFlags & RequiresSequentialProcessing;
}

bool ParseJob::respectsSequentialProcessing() const
{
    return d->sequentialProcessingFlags & RespectsSequentialProcessing;
}

void ParseJob::setSequentialProcessingFlags(SequentialProcessingFlags flags)
{
    d->sequentialProcessingFlags = flags;
}

IndexedString ParseJob::document() const
{
    return d->url;
}

bool ParseJob::success() const
{
    return !d->aborted;
}

void ParseJob::setMinimumFeatures(TopDUContext::Features features)
{
    d->features = features;
}

bool ParseJob::hasStaticMinimumFeatures()
{
    QMutexLocker lock(&minimumFeaturesMutex);
    return ::staticMinimumFeatures.size();
}

TopDUContext::Features ParseJob::staticMinimumFeatures(const IndexedString& url)
{
    QMutexLocker lock(&minimumFeaturesMutex);
    TopDUContext::Features features = (TopDUContext::Features)0;

    if(::staticMinimumFeatures.contains(url))
        foreach(const TopDUContext::Features &f, ::staticMinimumFeatures[url])
            features = (TopDUContext::Features)(features | f);

    return features;
}

TopDUContext::Features ParseJob::minimumFeatures() const
{
    return (TopDUContext::Features)(d->features | staticMinimumFeatures(d->url));
}

void ParseJob::setDuChain(ReferencedTopDUContext duChain)
{
    d->duContext = duChain;
}

ReferencedTopDUContext ParseJob::duChain() const
{
    return d->duContext;
}

bool ParseJob::abortRequested() const
{
    return d->abortRequested;
}

void ParseJob::requestAbort()
{
    d->abortRequested = 1;
}

void ParseJob::abortJob()
{
    d->aborted = true;
    setFinished(true);
}

void ParseJob::setNotifyWhenReady(const QList< QWeakPointer< QObject > >& notify
) {
    d->notify = notify;
}

void ParseJob::setStaticMinimumFeatures(const IndexedString& url, TopDUContext::Features features) {
    QMutexLocker lock(&minimumFeaturesMutex);
    ::staticMinimumFeatures[url].append(features);
}

void ParseJob::unsetStaticMinimumFeatures(const IndexedString& url, TopDUContext::Features features) {
    QMutexLocker lock(&minimumFeaturesMutex);
    ::staticMinimumFeatures[url].removeOne(features);
    if(::staticMinimumFeatures[url].isEmpty())
      ::staticMinimumFeatures.remove(url);
}

KDevelop::ProblemPointer ParseJob::readContents()
{
    Q_ASSERT(!d->hasReadContents);
    d->hasReadContents = true;

    QString localFile(document().toUrl().toLocalFile());
    QFileInfo fileInfo( localFile );

    QDateTime lastModified = fileInfo.lastModified();

    d->tracker = ICore::self()->languageController()->backgroundParser()->trackerForUrl(document());

    //Try using an artificial code-representation, which overrides everything else
    if(artificialCodeRepresentationExists(document())) {
        CodeRepresentation::Ptr repr = createCodeRepresentation(document());
        d->contents.contents = repr->text().toUtf8();
        kDebug() << "took contents for " << document().str() << " from artificial code-representation";
        return KDevelop::ProblemPointer();
    }

    bool hadTracker = false;
    if(d->tracker)
    {
        ForegroundLock lock;
        if(DocumentChangeTracker* t = d->tracker.data())
        {
            // The file is open in an editor
            d->previousRevision = t->revisionAtLastReset();

            t->reset(); // Reset the tracker to the current revision
            Q_ASSERT(t->revisionAtLastReset());

            d->contents.contents = t->textAtLastReset().toUtf8();
            d->contents.modification = KDevelop::ModificationRevision( lastModified, t->revisionAtLastReset()->revision() );

            d->revision = t->acquireRevision(d->contents.modification.revision);
            hadTracker = true;
        }
    }
    if (!hadTracker) {
        // We have to load the file from disk

        static const int maximumFileSize = 5 * 1024 * 1024; // 5 MB
        if (fileInfo.size() > maximumFileSize) {
            KDevelop::ProblemPointer p(new Problem());
            p->setSource(KDevelop::ProblemData::Disk);
            p->setDescription(i18nc("%1: filename", "Skipped file that is too large: '%1'", localFile ));
            p->setExplanation(i18nc("%1: file size, %2: limit file size",
                                    "The file is %1 and exceeds the limit of %2.",
                                    KGlobal::locale()->formatByteSize(fileInfo.size()),
                                    KGlobal::locale()->formatByteSize(maximumFileSize)));
            p->setFinalLocation(DocumentRange(document(), SimpleRange::invalid()));
            kWarning( 9007 ) << p->description() << p->explanation();
            return p;
        }
        QFile file( localFile );

        if ( !file.open( QIODevice::ReadOnly ) )
        {
            KDevelop::ProblemPointer p(new Problem());
            p->setSource(KDevelop::ProblemData::Disk);
            p->setDescription(i18n( "Could not open file '%1'", localFile ));
            switch (file.error()) {
              case QFile::ReadError:
                  p->setExplanation(i18n("File could not be read from disk."));
                  break;
              case QFile::OpenError:
                  p->setExplanation(i18n("File could not be opened."));
                  break;
              case QFile::PermissionsError:
                  p->setExplanation(i18n("File could not be read from disk due to permissions."));
                  break;
              default:
                  break;
            }
            p->setFinalLocation(DocumentRange(document(), SimpleRange::invalid()));

            kWarning( 9007 ) << "Could not open file" << document().str() << "(path" << localFile << ")" ;

            return p;
        }

        d->contents.contents = file.readAll(); ///@todo Convert from local encoding to utf-8 if they don't match
        d->contents.modification = KDevelop::ModificationRevision(lastModified);

        file.close();
    }

    // To make the parsing more robust, we add some zeroes to the end of the buffer.
    d->contents.contents.push_back((char)0);
    d->contents.contents.push_back((char)0);
    d->contents.contents.push_back((char)0);
    d->contents.contents.push_back((char)0);

    return KDevelop::ProblemPointer();
}

const KDevelop::ParseJob::Contents& ParseJob::contents() const
{
    Q_ASSERT(d->hasReadContents);
    return d->contents;
}

struct MovingRangeTranslator : public DUChainVisitor
{
    MovingRangeTranslator(qint64 _source, qint64 _target, MovingInterface* _moving) : source(_source), target(_target), moving(_moving) {
    }

    virtual void visit(DUContext* context) {
        translateRange(context);
        ///@todo Also map import-positions
        // Translate uses
        uint usesCount = context->usesCount();
        for(uint u = 0; u < usesCount; ++u)
        {
            RangeInRevision r = context->uses()[u].m_range;
            translateRange(r);
            context->changeUseRange(u, r);
        }
    }

    virtual void visit(Declaration* declaration) {
        translateRange(declaration);
    }

    void translateRange(DUChainBase* object)
    {
        RangeInRevision r = object->range();
        translateRange(r);
        object->setRange(r);
    }

    void translateRange(RangeInRevision& r)
    {
        // PHP and python use top contexts that start at (0, 0) end at INT_MAX, so make sure that doesn't overflow
        // or translate the start of the top context away from (0, 0)
        if ( r.start.line != 0 || r.start.column != 0 ) {
            moving->transformCursor(r.start.line, r.start.column, MovingCursor::MoveOnInsert, source, target);
        }
        if ( r.end.line != std::numeric_limits<int>::max() || r.end.column != std::numeric_limits<int>::max() ) {
            moving->transformCursor(r.end.line, r.end.column, MovingCursor::StayOnInsert, source, target);
        }
    }

    KTextEditor::Range range;
    qint64 source;
    qint64 target;
    MovingInterface* moving;
};

void ParseJob::translateDUChainToRevision(TopDUContext* context)
{
    qint64 targetRevision = d->contents.modification.revision;

    if(targetRevision == -1)
    {
        kDebug() << "invalid target revision" << targetRevision;
        return;
    }

    qint64 sourceRevision;

    {
        DUChainReadLocker duChainLock;

        Q_ASSERT(context->parsingEnvironmentFile());

        // Cannot map if there is no source revision
        sourceRevision = context->parsingEnvironmentFile()->modificationRevision().revision;

        if(sourceRevision == -1)
        {
            kDebug() << "invalid source revision" << sourceRevision;
            return;
        }
    }

    if(sourceRevision > targetRevision)
    {
        kDebug() << "for document" << document().str() << ": source revision is higher than target revision:" << sourceRevision << " > " << targetRevision;
        return;
    }

    ForegroundLock lock;
    if(DocumentChangeTracker* t = d->tracker.data())
    {
        if(!d->previousRevision)
        {
            kDebug() << "not translating because there is no valid predecessor-revision";
            return;
        }

        if(sourceRevision != d->previousRevision->revision() || !d->previousRevision->valid())
        {
            kDebug() << "not translating because the document revision does not match the tracker start revision (maybe the document was cleared)";
            return;
        }

        if(!t->holdingRevision(sourceRevision) || !t->holdingRevision(targetRevision))
        {
            kDebug() << "lost one of the translation revisions, not doing the map";
            return;
        }

        // Perform translation
        MovingInterface* moving = t->documentMovingInterface();

        DUChainWriteLocker wLock;

        MovingRangeTranslator translator(sourceRevision, targetRevision, moving);
        context->visit(translator);

        QList< ProblemPointer > problems = context->problems();
        for(QList< ProblemPointer >::iterator problem = problems.begin(); problem != problems.end(); ++problem)
        {
            RangeInRevision r = (*problem)->range();
            translator.translateRange(r);
            (*problem)->setRange(r);
        }

        // Update the modification revision in the meta-data
        ModificationRevision modRev = context->parsingEnvironmentFile()->modificationRevision();
        modRev.revision = targetRevision;
        context->parsingEnvironmentFile()->setModificationRevision(modRev);
    }
}

bool ParseJob::isUpdateRequired(const IndexedString& languageString)
{
    if (abortRequested()) {
        return false;
    }

    if (minimumFeatures() & TopDUContext::ForceUpdate) {
        return true;
    }

    DUChainReadLocker lock;
    if (abortRequested()) {
        return false;
    }
    foreach(const ParsingEnvironmentFilePointer &file, DUChain::self()->allEnvironmentFiles(document())) {
        if (file->language() != languageString) {
            continue;
        }
        if (!file->needsUpdate() && file->featuresSatisfied(minimumFeatures())) {
            kDebug() << "Already up to date" << document().str();
            setDuChain(file->topContext());
            lock.unlock();
            highlightDUChain();
            return false;
        }
        break;
    }
    return !abortRequested();
}

void ParseJob::highlightDUChain()
{
    ENSURE_CHAIN_NOT_LOCKED
    if (!d->languageSupport->codeHighlighting() || !duChain() || abortRequested()) {
        // language doesn't support highlighting
        return;
    }
    if (!d->hasReadContents && !d->tracker) {
        d->tracker = ICore::self()->languageController()->backgroundParser()->trackerForUrl(document());
    }
    if (d->tracker) {
        d->languageSupport->codeHighlighting()->highlightDUChain(duChain());
    }
}

ControlFlowGraph* ParseJob::controlFlowGraph()
{
    return 0;
}

DataAccessRepository* ParseJob::dataAccessInformation()
{
    return 0;
}

bool ParseJob::hasTracker() const
{
    return d->tracker;
}

}

#include "parsejob.moc"
