/*
   Copyright 2008 David Nolden <david.nolden.kdevelop@art-master.de>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License version 2 as published by the Free Software Foundation.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include "documentchangeset.h"

#include "coderepresentation.h"
#include <debug.h>

#include <algorithm>

#include <QStringList>
#include <QMimeDatabase>

#include <KLocalizedString>

#include <interfaces/icore.h>
#include <interfaces/ilanguagecontroller.h>
#include <interfaces/idocumentcontroller.h>

#include <language/duchain/duchain.h>
#include <language/duchain/duchainlock.h>
#include <language/duchain/duchainutils.h>
#include <language/duchain/parsingenvironment.h>
#include <language/backgroundparser/backgroundparser.h>
#include <language/editor/modificationrevisionset.h>

#include <interfaces/isourceformattercontroller.h>
#include <interfaces/isourceformatter.h>
#include <interfaces/iproject.h>
#include <interfaces/iprojectcontroller.h>

#include <project/projectmodel.h>

#include <util/path.h>
#include <util/shellutils.h>

namespace KDevelop {

typedef QList<DocumentChangePointer> ChangesList;
typedef QHash<IndexedString, ChangesList> ChangesHash;

class DocumentChangeSetPrivate
{
public:
    DocumentChangeSet::ReplacementPolicy replacePolicy;
    DocumentChangeSet::FormatPolicy formatPolicy;
    DocumentChangeSet::DUChainUpdateHandling updatePolicy;
    DocumentChangeSet::ActivationPolicy activationPolicy;

    ChangesHash changes;
    QHash<IndexedString, IndexedString> documentsRename;

    DocumentChangeSet::ChangeResult addChange(const DocumentChangePointer& change);
    DocumentChangeSet::ChangeResult replaceOldText(CodeRepresentation* repr, const QString& newText,
                                                   const ChangesList& sortedChangesList);
    DocumentChangeSet::ChangeResult generateNewText(const IndexedString& file,
                                                    ChangesList& sortedChanges,
                                                    const CodeRepresentation* repr,
                                                    QString& output);
    DocumentChangeSet::ChangeResult removeDuplicates(const IndexedString& file,
                                                     ChangesList& filteredChanges);
    void formatChanges();
    void updateFiles();
};

// Simple helpers to clear up code clutter
namespace
{
inline bool changeIsValid(const DocumentChange& change, const QStringList& textLines)
{
    return change.m_range.start() <= change.m_range.end() &&
           change.m_range.end().line() < textLines.size() &&
           change.m_range.start().line() >= 0 &&
           change.m_range.start().column() >= 0 &&
           change.m_range.start().column() <= textLines[change.m_range.start().line()].length() &&
           change.m_range.end().column() >= 0 &&
           change.m_range.end().column() <= textLines[change.m_range.end().line()].length();
}

inline bool duplicateChanges(const DocumentChangePointer& previous, const DocumentChangePointer& current)
{
    // Given the option of considering a duplicate two changes in the same range
    // but with different old texts to be ignored
    return previous->m_range == current->m_range &&
           previous->m_newText == current->m_newText &&
           (previous->m_oldText == current->m_oldText ||
           (previous->m_ignoreOldText && current->m_ignoreOldText));
}

inline QString rangeText(const KTextEditor::Range& range, const QStringList& textLines)
{
    QStringList ret;
    ret.reserve(range.end().line() - range.start().line() + 1);
    for(int line = range.start().line(); line <= range.end().line(); ++line) {
        const QString lineText = textLines.at(line);
        int startColumn = 0;
        int endColumn = lineText.length();
        if (line == range.start().line()) {
            startColumn = range.start().column();
        }
        if (line == range.end().line()) {
            endColumn = range.end().column();
        }
        ret << lineText.mid(startColumn, endColumn - startColumn);
    }
    return ret.join(QStringLiteral("\n"));
}

// need to have it as otherwise the arguments can exceed the maximum of 10
static QString printRange(const KTextEditor::Range& r)
{
    return i18nc("text range line:column->line:column", "%1:%2->%3:%4",
                 r.start().line(), r.start().column(),
                 r.end().line(), r.end().column());
}


}

DocumentChangeSet::DocumentChangeSet()
: d(new DocumentChangeSetPrivate)
{
    d->replacePolicy = StopOnFailedChange;
    d->formatPolicy = AutoFormatChanges;
    d->updatePolicy = SimpleUpdate;
    d->activationPolicy = DoNotActivate;
}

DocumentChangeSet::DocumentChangeSet(const DocumentChangeSet& rhs)
: d(new DocumentChangeSetPrivate(*rhs.d))
{
}


DocumentChangeSet& DocumentChangeSet::operator=(const DocumentChangeSet& rhs)
{
    *d = *rhs.d;
    return *this;
}

DocumentChangeSet::~DocumentChangeSet() = default;

DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChange& change)
{
    return d->addChange(DocumentChangePointer(new DocumentChange(change)));
}

DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChangePointer& change)
{
    return d->addChange(change);
}

DocumentChangeSet::ChangeResult DocumentChangeSet::addDocumentRenameChange(const IndexedString& oldFile,
                                                                           const IndexedString& newname)
{
    d->documentsRename.insert(oldFile, newname);
    return DocumentChangeSet::ChangeResult::successfulResult();
}

DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::addChange(const DocumentChangePointer& change)
{
    changes[change->m_document].append(change);
    return DocumentChangeSet::ChangeResult::successfulResult();
}

void DocumentChangeSet::setReplacementPolicy(DocumentChangeSet::ReplacementPolicy policy)
{
    d->replacePolicy = policy;
}

void DocumentChangeSet::setFormatPolicy(DocumentChangeSet::FormatPolicy policy)
{
    d->formatPolicy = policy;
}

void DocumentChangeSet::setUpdateHandling(DocumentChangeSet::DUChainUpdateHandling policy)
{
    d->updatePolicy = policy;
}

void DocumentChangeSet::setActivationPolicy(DocumentChangeSet::ActivationPolicy policy)
{
    d->activationPolicy = policy;
}

DocumentChangeSet::ChangeResult DocumentChangeSet::applyAllChanges()
{
    QUrl oldActiveDoc;
    if (IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) {
        oldActiveDoc = activeDoc->url();
    }

    QList<QUrl> allFiles;
    foreach (IndexedString file, d->documentsRename.keys().toSet() + d->changes.keys().toSet()) {
        allFiles << file.toUrl();
    }

    if (!KDevelop::ensureWritable(allFiles))
    {
        return ChangeResult(QStringLiteral("some affected files are not writable"));
    }

    // rename files
    QHash<IndexedString, IndexedString>::const_iterator it = d->documentsRename.constBegin();
    for(; it != d->documentsRename.constEnd(); ++it) {
        QUrl url = it.key().toUrl();
        IProject* p = ICore::self()->projectController()->findProjectForUrl(url);
        if(p) {
            QList<ProjectFileItem*> files = p->filesForPath(it.key());
            if(!files.isEmpty()) {
                ProjectBaseItem::RenameStatus renamed = files.first()->rename(it.value().str());
                if(renamed == ProjectBaseItem::RenameOk) {
                    const QUrl newUrl = Path(Path(url).parent(), it.value().str()).toUrl();
                    if (url == oldActiveDoc) {
                        oldActiveDoc = newUrl;
                    }
                    IndexedString idxNewDoc(newUrl);

                    // ensure changes operate on new file name
                    ChangesHash::iterator iter = d->changes.find(it.key());
                    if (iter != d->changes.end()) {
                        // copy changes
                        ChangesList value = iter.value();
                        // remove old entry
                        d->changes.erase(iter);
                        // adapt to new url
                        ChangesList::iterator itChange = value.begin();
                        ChangesList::iterator itEnd = value.end();
                        for(; itChange != itEnd; ++itChange) {
                            (*itChange)->m_document = idxNewDoc;
                        }
                        d->changes[idxNewDoc] = value;
                    }
                } else {
                    ///FIXME: share code with project manager for the error code string representation
                    return ChangeResult(i18n("Could not rename '%1' to '%2'", url.toDisplayString(QUrl::PreferLocalFile), it.value().str()));
                }
            } else {
                //TODO: do it outside the project management?
                qCWarning(LANGUAGE) << "tried to rename file not tracked by project - not implemented";
            }
        } else {
            qCWarning(LANGUAGE) << "tried to rename a file outside of a project - not implemented";
        }
    }

    QMap<IndexedString, CodeRepresentation::Ptr> codeRepresentations;
    QMap<IndexedString, QString> newTexts;
    ChangesHash filteredSortedChanges;
    ChangeResult result = ChangeResult::successfulResult();

    QList<IndexedString> files(d->changes.keys());

    foreach(const IndexedString &file, files) {
        CodeRepresentation::Ptr repr = createCodeRepresentation(file);
        if(!repr) {
            return ChangeResult(QStringLiteral("Could not create a Representation for %1").arg(file.str()));
        }

        codeRepresentations[file] = repr;

        QList<DocumentChangePointer>& sortedChangesList(filteredSortedChanges[file]);
        {
            result = d->removeDuplicates(file, sortedChangesList);
            if(!result)
                return result;
        }

        {
            result = d->generateNewText(file, sortedChangesList, repr.data(), newTexts[file]);
            if(!result)
                return result;
        }
    }

    QMap<IndexedString, QString> oldTexts;

    //Apply the changes to the files
    foreach(const IndexedString &file, files) {
        oldTexts[file] = codeRepresentations[file]->text();

        result = d->replaceOldText(codeRepresentations[file].data(), newTexts[file], filteredSortedChanges[file]);
        if(!result && d->replacePolicy == StopOnFailedChange) {
            //Revert all files
            foreach(const IndexedString &revertFile, oldTexts.keys()) {
                codeRepresentations[revertFile]->setText(oldTexts[revertFile]);
            }

            return result;
        }
    }

    d->updateFiles();

    if(d->activationPolicy == Activate) {
        foreach(const IndexedString& file, files) {
            ICore::self()->documentController()->openDocument(file.toUrl());
        }
    }

    // ensure the old document is still activated
    if (oldActiveDoc.isValid()) {
        ICore::self()->documentController()->openDocument(oldActiveDoc);
    }

    return result;
}

DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::replaceOldText(CodeRepresentation* repr,
                                                                         const QString& newText,
                                                                         const ChangesList& sortedChangesList)
{
    DynamicCodeRepresentation* dynamic = dynamic_cast<DynamicCodeRepresentation*>(repr);
    if(dynamic) {
        auto transaction = dynamic->makeEditTransaction();
        //Replay the changes one by one

        for(int pos = sortedChangesList.size()-1; pos >= 0; --pos) {
            const DocumentChange& change(*sortedChangesList[pos]);
            if(!dynamic->replace(change.m_range, change.m_oldText, change.m_newText, change.m_ignoreOldText))
            {
                QString warningString = i18nc("Inconsistent change in <filename> between <range>, found <oldText> (encountered <foundText>) -> <newText>",
                                              "Inconsistent change in %1 between %2, found %3 (encountered \"%4\") -> \"%5\""
                    , change.m_document.str()
                    , printRange(change.m_range)
                    , change.m_oldText
                    , dynamic->rangeText(change.m_range)
                    , change.m_newText);

                if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) {
                    qCWarning(LANGUAGE) << warningString;
                } else if(replacePolicy == DocumentChangeSet::StopOnFailedChange) {
                    return DocumentChangeSet::ChangeResult(warningString);
                }
                //If set to ignore failed changes just continue with the others
            }
        }
        return DocumentChangeSet::ChangeResult::successfulResult();
    }

    //For files on disk
    if (!repr->setText(newText)) {
        QString warningString = i18n("Could not replace text in the document: %1",
                                     sortedChangesList.begin()->data()->m_document.str());
        if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) {
            qCWarning(LANGUAGE) << warningString;
        }

        return DocumentChangeSet::ChangeResult(warningString);
    }

    return DocumentChangeSet::ChangeResult::successfulResult();
}

DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::generateNewText(const IndexedString & file,
                                                                          ChangesList& sortedChanges,
                                                                          const CodeRepresentation * repr,
                                                                          QString & output)
{

    ISourceFormatter* formatter = nullptr;
    if(ICore::self()) {
        formatter = ICore::self()->sourceFormatterController()->formatterForUrl(file.toUrl());
    }

    //Create the actual new modified file
    QStringList textLines = repr->text().split('\n');

    QUrl url = file.toUrl();

    QMimeType mime = QMimeDatabase().mimeTypeForUrl(url);
    QVector<int> removedLines;

    for(int pos = sortedChanges.size()-1; pos >= 0; --pos) {
        DocumentChange& change(*sortedChanges[pos]);
        QString encountered;
        if(changeIsValid(change, textLines)  && //We demand this, although it should be fixed
            ((encountered = rangeText(change.m_range, textLines)) == change.m_oldText || change.m_ignoreOldText))
        {
            ///Problem: This does not work if the other changes significantly alter the context @todo Use the changed context
            QString leftContext = QStringList(textLines.mid(0, change.m_range.start().line()+1)).join(QStringLiteral("\n"));
            leftContext.chop(textLines[change.m_range.start().line()].length() - change.m_range.start().column());

            QString rightContext = QStringList(textLines.mid(change.m_range.end().line())).join(QStringLiteral("\n")).mid(change.m_range.end().column());

            if(formatter && (formatPolicy == DocumentChangeSet::AutoFormatChanges
                                || formatPolicy == DocumentChangeSet::AutoFormatChangesKeepIndentation))
            {
                QString oldNewText = change.m_newText;
                change.m_newText = formatter->formatSource(change.m_newText, url, mime, leftContext, rightContext);

                if(formatPolicy == DocumentChangeSet::AutoFormatChangesKeepIndentation) {
                    // Reproduce the previous indentation
                    QStringList oldLines = oldNewText.split('\n');
                    QStringList newLines = change.m_newText.split('\n');

                    if(oldLines.size() == newLines.size()) {
                        for(int line = 0; line < newLines.size(); ++line) {
                            // Keep the previous indentation
                            QString oldIndentation;
                            for (int a = 0; a < oldLines[line].size(); ++a) {
                                if (oldLines[line][a].isSpace()) {
                                    oldIndentation.append(oldLines[line][a]);
                                } else {
                                    break;
                                }
                            }

                            int newIndentationLength = 0;

                            for(int a = 0; a < newLines[line].size(); ++a) {
                                if(newLines[line][a].isSpace()) {
                                    newIndentationLength = a;
                                } else {
                                    break;
                                }
                            }

                            newLines[line].replace(0, newIndentationLength, oldIndentation);
                        }
                        change.m_newText = newLines.join(QStringLiteral("\n"));
                    } else {
                        qCDebug(LANGUAGE) << "Cannot keep the indentation because the line count has changed" << oldNewText;
                    }
                }
            }

            QString& line = textLines[change.m_range.start().line()];
            if (change.m_range.start().line() == change.m_range.end().line()) {
                // simply replace existing line content
                line.replace(change.m_range.start().column(),
                             change.m_range.end().column()-change.m_range.start().column(),
                             change.m_newText);
            } else {
                // replace first line contents
                line.replace(change.m_range.start().column(), line.length() - change.m_range.start().column(),
                             change.m_newText);
                // null other lines and remember for deletion
                for(int i = change.m_range.start().line() + 1; i <= change.m_range.end().line(); ++i) {
                    textLines[i].clear();
                    removedLines << i;
                }
            }
        }else{
            QString warningString = i18nc("Inconsistent change in <document> at <range>"
                                         " = <oldText> (encountered <encountered>) -> <newText>",
                                         "Inconsistent change in %1 at %2"
                                         " = \"%3\"(encountered \"%4\") -> \"%5\""
                                            , file.str()
                                            , printRange(change.m_range)
                                            , change.m_oldText
                                            , encountered
                                            , change.m_newText);

            if(replacePolicy == DocumentChangeSet::IgnoreFailedChange) {
                //Just don't do the replacement
            } else if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) {
                qCWarning(LANGUAGE) << warningString;
            } else {
                return DocumentChangeSet::ChangeResult(warningString, sortedChanges[pos]);
            }
        }
    }

    if (!removedLines.isEmpty()) {
        int offset = 0;
        std::sort(removedLines.begin(), removedLines.end());
        foreach(int l, removedLines) {
            textLines.removeAt(l - offset);
            ++offset;
        }
    }
    output = textLines.join(QStringLiteral("\n"));
    return DocumentChangeSet::ChangeResult::successfulResult();
}

//Removes all duplicate changes for a single file, and then returns (via filteredChanges) the filtered duplicates
DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::removeDuplicates(const IndexedString& file,
                                                                           ChangesList& filteredChanges)
{
    typedef QMultiMap<KTextEditor::Cursor, DocumentChangePointer> ChangesMap;
    ChangesMap sortedChanges;

    foreach(const DocumentChangePointer &change, changes[file]) {
        sortedChanges.insert(change->m_range.end(), change);
    }

    //Remove duplicates
    ChangesMap::iterator previous = sortedChanges.begin();
    for(ChangesMap::iterator it = ++sortedChanges.begin(); it != sortedChanges.end(); ) {
        if(( *previous ) && ( *previous )->m_range.end() > (*it)->m_range.start()) {
            //intersection
            if(duplicateChanges(( *previous ), *it)) {
                //duplicate, remove one
                it = sortedChanges.erase(it);
                continue;
            }

            //When two changes contain each other, and the container change is set to ignore old text, then it should be safe to
            //just ignore the contained change, and apply the bigger change
            else if((*it)->m_range.contains(( *previous )->m_range) && (*it)->m_ignoreOldText  ) {
                qCDebug(LANGUAGE) << "Removing change: " << ( *previous )->m_oldText << "->" << ( *previous )->m_newText
                         << ", because it is contained by change: " << (*it)->m_oldText << "->" << (*it)->m_newText;
                sortedChanges.erase(previous);
            }
            //This case is for when both have the same end, either of them could be the containing range
            else if((*previous)->m_range.contains((*it)->m_range) && (*previous)->m_ignoreOldText  ) {
                qCDebug(LANGUAGE) << "Removing change: " << (*it)->m_oldText << "->" << (*it)->m_newText
                         << ", because it is contained by change: " << ( *previous )->m_oldText
                         << "->" << ( *previous )->m_newText;
                it = sortedChanges.erase(it);
                continue;
            } else {
                return DocumentChangeSet::ChangeResult(
                       i18nc("Inconsistent change-request at <document>:"
                             "intersecting changes: "
                             "<previous-oldText> -> <previous-newText> @<range> & "
                             "<new-oldText> -> <new-newText> @<range>",
                               "Inconsistent change-request at %1; "
                               "intersecting changes: "
                               "\"%2\"->\"%3\"@%4 & \"%5\"->\"%6\"@%7 "
                        , file.str()
                        , ( *previous )->m_oldText
                        , ( *previous )->m_newText
                        , printRange(( *previous )->m_range)
                        , (*it)->m_oldText
                        , (*it)->m_newText
                        , printRange((*it)->m_range)));
            }

        }
        previous = it;
        ++it;
    }

    filteredChanges = sortedChanges.values();
    return DocumentChangeSet::ChangeResult::successfulResult();
}

void DocumentChangeSetPrivate::updateFiles()
{
    ModificationRevisionSet::clearCache();
    foreach(const IndexedString& file, changes.keys()) {
        ModificationRevision::clearModificationCache(file);
    }

    if(updatePolicy != DocumentChangeSet::NoUpdate && ICore::self())
    {
        // The active document should be updated first, so that the user sees the results instantly
        if(IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) {
            ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(activeDoc->url()));
        }

        // If there are currently open documents that now need an update, update them too
        foreach(const IndexedString& doc, ICore::self()->languageController()->backgroundParser()->managedDocuments()) {
            DUChainReadLocker lock(DUChain::lock());
            TopDUContext* top = DUChainUtils::standardContextForUrl(doc.toUrl(), true);
            if((top && top->parsingEnvironmentFile() && top->parsingEnvironmentFile()->needsUpdate()) || !top) {
                lock.unlock();
                ICore::self()->languageController()->backgroundParser()->addDocument(doc);
            }
        }

        // Eventually update _all_ affected files
        foreach(const IndexedString &file, changes.keys()) {
            if(!file.toUrl().isValid()) {
                qCWarning(LANGUAGE) << "Trying to apply changes to an invalid document";
                continue;
            }

            ICore::self()->languageController()->backgroundParser()->addDocument(file);
        }
    }
}

}
