/*  This file is part of the KDE libraries and the Kate part.
 *
 *  Copyright (C) 2004,2010 Joseph Wenninger <jowenn@kde.org>
 *  Copyright (C) 2009 Milian Wolff <mail@milianw.de>
 *
 *  This library 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 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 <QQueue>

#include <ktexteditor/movingcursor.h>
#include <ktexteditor/movingrange.h>

#include "katetemplatehandler.h"
#include "katedocument.h"
#include "kateview.h"
#include "kateconfig.h"
#include "katerenderer.h"
#include "kateundomanager.h"
#include "kateregexpsearch.h"
#include "kateglobal.h"
#include "script/katetemplatescript.h"
#include "script/katescriptmanager.h"
#include "katepartdebug.h"

using namespace KTextEditor;

#define ifDebug(x)

static bool cmp_moving_ranges(const KTextEditor::MovingRange *r1, const KTextEditor::MovingRange *r2)
{
    return r1->start() < r2->start();
}

/// just like Range::contains() but returns true when the cursor is at the end of the range
static bool customContains(MovingRange *range, const Cursor &cursor)
{
    return range->start() <= cursor && range->end() >= cursor;
}

static bool customContains(const KTextEditor::Range &range, const Cursor &cursor)
{
    return range.start() <= cursor && range.end() >= cursor;
}

/* ####################################### */

KateTemplateHandler::KateTemplateHandler(KTextEditor::ViewPrivate *view,
        const Cursor &position,
        const QString &templateString,
        const QMap<QString, QString> &initialValues,
        KateUndoManager *undoManager,
        KateTemplateScript *templateScript)
    : QObject(view)
    , m_view(view)
    , m_undoManager(undoManager)
    , m_wholeTemplateRange(0)
    , m_finalCursorPosition(0)
    , m_lastCaretPosition(position)
    , m_isMirroring(false)
    , m_editWithUndo(false)
    , m_jumping(false)
    , m_templateScript(templateScript)
{
    /**
     * we always need a view
     */
    Q_ASSERT(m_view);

    ifDebug(qCDebug(LOG_PART) << templateString << initialValues;)

    QMap<QString, QString> initial_Values(initialValues);

    if (initial_Values.contains(QLatin1String("selection"))) {
        if (initial_Values[QLatin1String("selection")].isEmpty()) {
            initial_Values[QLatin1String("selection")] = m_view->selectionText();
        }
    }

    if (m_view->selection()) {
        m_lastCaretPosition = m_view->selectionRange().start();
        m_view->removeSelectionText();
    }

    ifDebug(qCDebug(LOG_PART) << initial_Values;)

    connect(doc(), &KTextEditor::Document::aboutToReload,
            this, &KateTemplateHandler::cleanupAndExit);

    connect(doc(), &KTextEditor::DocumentPrivate::textInserted,
            this, &KateTemplateHandler::slotTemplateInserted);

    ///TODO: maybe use Kate::CutCopyPasteEdit or similar?

    // we want one undo action >= START
    doc()->setUndoMergeAllEdits(true);
    doc()->editStart();

    /**
     * bail out if we can't insert text
     */
    if (!doc()->insertText(m_lastCaretPosition, templateString)) {
        doc()->editEnd();
        doc()->setUndoMergeAllEdits(false);
        cleanupAndExit();
    }

    /**
     * now there must be a range
     */
    Q_ASSERT(m_wholeTemplateRange);

    // end edit here, we need correct syntax highlighting in align!
    doc()->editEnd();

    // indent the inserted template properly, this makes it possible
    // to share snippets e.g. via GHNS without caring about
    // what indent-style to use.
    doc()->align(m_view, *m_wholeTemplateRange);

    ///TODO: maybe support delayed actions, i.e.:
    /// - create doc
    /// - insert template
    /// - create view => ranges are added
    /// for now simply "just insert" the code when we have no active view

    doc()->editStart();
    handleTemplateString(initial_Values);
    m_undoManager->undoSafePoint();
    doc()->editEnd();
    doc()->setUndoMergeAllEdits(false);

    if (!initialValues.isEmpty()) {
        // only do complex stuff when required
        if (!m_templateRanges.isEmpty()) {
            foreach (View *view, doc()->views()) {
                setupEventHandler(view);
            }

            connect(doc(), &KTextEditor::Document::viewCreated,
                    this, &KateTemplateHandler::slotViewCreated);
            connect(doc(), &KTextEditor::DocumentPrivate::textInserted,
                    this, &KateTemplateHandler::slotTextChanged);
            connect(doc(), &KTextEditor::DocumentPrivate::textRemoved,
                    this, &KateTemplateHandler::slotTextChanged);

            setEditWithUndo(undoManager->isActive());

            connect(undoManager, SIGNAL(isActiveChanged(bool)),
                    this, SLOT(setEditWithUndo(bool)));

        } else {
            // when no interesting ranges got added, we can terminate directly
            jumpToFinalCursorPosition();
            cleanupAndExit();
        }

    } else {
        cleanupAndExit();
    }
}

KateTemplateHandler::~KateTemplateHandler()
{
}

void KateTemplateHandler::slotTemplateInserted(Document *document, const Range &range)
{
    Q_ASSERT(document == doc());
    Q_UNUSED(document);
    ifDebug(qCDebug(LOG_PART) << "template range inserted" << range;)

    m_wholeTemplateRange = doc()->newMovingRange(range, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);

    disconnect(doc(), &KTextEditor::DocumentPrivate::textInserted,
               this, &KateTemplateHandler::slotTemplateInserted);
}

void KateTemplateHandler::cleanupAndExit()
{
    ifDebug(qCDebug(LOG_PART) << "cleaning up and exiting";)
    disconnect(doc(), &KTextEditor::Document::viewCreated,
               this, &KateTemplateHandler::slotViewCreated);
    disconnect(doc(), &KTextEditor::DocumentPrivate::textInserted,
               this, &KateTemplateHandler::slotTextChanged);
    disconnect(doc(), &KTextEditor::DocumentPrivate::textRemoved,
               this, &KateTemplateHandler::slotTextChanged);

    if (!m_templateRanges.isEmpty()) {
        foreach (MovingRange *range, m_templateRanges) {
            // delete all children
            foreach (MovingRange *child, m_templateRangesChildren[range]) {
                delete child;
            }

            delete range;
        }

        m_templateRanges.clear();
        m_templateRangesChildren.clear();
        m_templateRangesChildToParent.clear();
    }

    // no children if no ranges around
    Q_ASSERT(m_templateRangesChildren.isEmpty());
    Q_ASSERT(m_templateRangesChildToParent.isEmpty());

    if (!m_spacersMovingRanges.isEmpty()) {
        foreach (MovingRange *range, m_spacersMovingRanges) {
            doc()->removeText(*range);
            delete range;
        }

        m_spacersMovingRanges.clear();
    }

    delete m_wholeTemplateRange;
    delete m_finalCursorPosition;
    delete this;
}

void KateTemplateHandler::jumpToFinalCursorPosition()
{
    if (!m_wholeTemplateRange || customContains(m_wholeTemplateRange->toRange(), m_view->cursorPosition())) {
        m_view->setSelection(Range::invalid());
        m_view->setCursorPosition(*m_finalCursorPosition);
    }
}

KTextEditor::DocumentPrivate *KateTemplateHandler::doc()
{
    return m_view->doc();
}

void KateTemplateHandler::setEditWithUndo(const bool &enabled)
{
    m_editWithUndo = enabled;
}

void KateTemplateHandler::slotViewCreated(Document *document, View *view)
{
    Q_ASSERT(document == doc());
    Q_UNUSED(document)
    setupEventHandler(view);
}

void KateTemplateHandler::setupEventHandler(View *view)
{
    view->focusProxy()->installEventFilter(this);
}

bool KateTemplateHandler::eventFilter(QObject *object, QEvent *event)
{
    // prevent indenting by eating the keypress event for TAB
    if (event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) {
            if (!m_view->isCompletionActive()) {
                return true;
            }
        }
    }

    // actually offer shortcuts for navigation

    if (event->type() == QEvent::ShortcutOverride) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Return && keyEvent->modifiers() & Qt::AltModifier) {
            // terminate
            jumpToFinalCursorPosition();
            cleanupAndExit();
            keyEvent->accept();
            return true;

        } else if (keyEvent->key() == Qt::Key_Escape) {
            if (!m_view->selection()) {
                // terminate
                jumpToFinalCursorPosition();
                cleanupAndExit();
                keyEvent->accept();
                return true;
            }

        } else if (keyEvent->key() == Qt::Key_Tab && !m_view->isCompletionActive()) {
            if (keyEvent->modifiers() & Qt::Key_Shift) {
                jumpToPreviousRange();

            } else {
                jumpToNextRange();
            }

            keyEvent->accept();
            return true;

        } else if (keyEvent->key() == Qt::Key_Backtab && !m_view->isCompletionActive()) {
            jumpToPreviousRange();
            keyEvent->accept();
            return true;
        }
    }

    return QObject::eventFilter(object, event);
}

void KateTemplateHandler::jumpToPreviousRange()
{
    const Cursor &cursor = m_view->cursorPosition();

    if (cursor == *m_finalCursorPosition) {
        // wrap and jump to last range
        setCurrentRange(m_masterRanges.last());
        return;
    }

    MovingRange *previousRange = 0;

    foreach (MovingRange *range, m_masterRanges) {
        if (range->start() >= cursor) {
            continue;
        }

        if (!previousRange || range->start() > previousRange->start()) {
            previousRange = range;

            if (m_templateRangesChildToParent.value(previousRange)) {
                previousRange = m_templateRangesChildToParent.value(previousRange);
            }
        }
    }

    if (previousRange) {
        setCurrentRange(previousRange);

    } else {
        // wrap and jump to final cursor
        jumpToFinalCursorPosition();
    }
}

void KateTemplateHandler::jumpToNextRange()
{
    const Cursor &cursor = m_view->cursorPosition();

    if (cursor == *m_finalCursorPosition) {
        // wrap and jump to first range
        setCurrentRange(m_masterRanges.first());
        return;
    }

    MovingRange *nextRange = 0;

    foreach (MovingRange *range, m_masterRanges) {
        if (range->start() <= cursor) {
            continue;
        }

        if (!nextRange || range->start() < nextRange->start()) {
            nextRange = range;
        }
    }

    if (nextRange) {
        if (m_templateRangesChildToParent.value(nextRange)) {
            nextRange = m_templateRangesChildToParent.value(nextRange);
        }

        setCurrentRange(nextRange);

    } else {
        // wrap and jump to final cursor
        jumpToFinalCursorPosition();
    }
}

void KateTemplateHandler::setCurrentRange(MovingRange *range)
{
    if (!m_templateRangesChildren[range].isEmpty()) {
        ifDebug(qCDebug(LOG_PART) << "looking for mirroring range";)
        // jump to first mirrored range
        bool found = false;
        foreach (MovingRange *childRange, m_templateRangesChildren[range]) {
            ifDebug(qCDebug(LOG_PART) << "checking range equality";)

            if (m_masterRanges.contains(childRange)) {
                ifDebug(qCDebug(LOG_PART) << "found master range";)
                range = childRange;
                found = true;
                break;
            }
        }

        if (!found) {
            range = m_templateRangesChildren[range].first();
        }
    }

    m_jumping = true;

    if (m_uneditedRanges.contains(range)) {
        m_view->setSelection(*range);
    }

    m_view->setCursorPosition(range->start());

    m_jumping = false;

    m_lastCaretPosition = range->start();
}

/**
 * Returns an attribute with \p color as background with @p alpha alpha value.
 */
Attribute::Ptr getAttribute(QColor color, int alpha = 230)
{
    Attribute::Ptr attribute(new Attribute());
    color.setAlpha(alpha);
    attribute->setBackground(QBrush(color));
    return attribute;
}

void KateTemplateHandler::handleTemplateString(const QMap< QString, QString > &initialValues)
{
    QString templateString = doc()->text(*m_wholeTemplateRange);

    int line = m_wholeTemplateRange->start().line();
    int column = m_wholeTemplateRange->start().column();

    // not equal -1 when we found a start position
    int startPos = -1;

    bool lastWasBrace = false;

    // each found variable gets its range(s) added to the list.
    // the key is the varname, e.g. the same as in initialValues
    // to be able to iterate over them in a FIFO matter, also store
    // the keys in a queue.
    QQueue<QString> keyQueue;
    QMultiMap<QString, Range> ranges;
    QMap<Range, MirrorBehaviour> mirrorBehaviourBuildHelper;

    QList<Range> spacers;
    // valid, if we find an occurrence of ${cursor}
    Cursor finalCursorPosition = Cursor::invalid();

    // parse string for ${VAR} or %{VAR}
    // VAR must not contain $ or %
    // VAR must not contain newlines
    // VAR must be set as key in initialValues
    // expression must not be escaped

    for (int i = 0; i < templateString.size(); ++i) {
        ifDebug(qCDebug(LOG_PART) << "checking character:" << templateString[i];)

        if (templateString[i] == QLatin1Char('\n')) {
            lastWasBrace = false;
            ++line;
            column = 0;

            if (startPos != -1) {
                // don't allow variables to span multiple lines
                startPos = -1;
            }

        } else if ((templateString[i] == QLatin1Char('%') || templateString[i] == QLatin1Char('$'))
                   && i + 1 < templateString.size() && templateString[i + 1] == QLatin1Char('{')) {

            // check whether this var is escaped
            int escapeChars = 0;

            while (i - escapeChars > 0 && templateString[i - escapeChars - 1] == QLatin1Char('\\')) {
                ++escapeChars;
            }

            if (escapeChars > 0) {
                ifDebug(qCDebug(LOG_PART) << "found" << escapeChars << "escape chars at " << templateString.mid(i - escapeChars - 10, escapeChars + 10);)
                // remove half of the escape chars (i.e. \\ => \) and make sure the
                // odd rest is removed as well (i.e. the one that escapes this var)
                int toRemove = (escapeChars + 1) / 2;
                ifDebug(qCDebug(LOG_PART) << "will remove" << toRemove << "of those escape chars";)
                templateString.remove(i - escapeChars, toRemove);
                i -= toRemove;
                column -= toRemove;
            }

            if (escapeChars % 2 == 0) {
                // don't check for startPos == -1 here, overwrite blindly since nested variables are not supported
                if (lastWasBrace) {
                    templateString.insert(i, QLatin1String(" "));
                    spacers.append(Range(line, column, line, column + 1));
                    ++i;
                    ++column;
                }

                startPos = i;
            }

            lastWasBrace = false;

            // skip '{'
            ++i;
            column += 2;

        } else if ((startPos != -1) && (templateString[i] == QLatin1Char(':'))) {      // skip init value, handled by KTE
            i++;
            column++;
            int backslash_count = 0;

            for (; i < templateString.size(); i++, column++) {
                if (templateString[i] == QLatin1Char('\n')) {
                    ++line;
                    column = 0;

                    if (startPos != -1) {
                        // don't allow variables to span multiple lines
                        startPos = -1;
                    }

                    break;
                }

                if (templateString[i] == QLatin1Char('}')) {
                    if ((backslash_count % 2) == 0) {
                        i--;
                        //column--;
                        break;

                    } else {
                        backslash_count = 0;
                    }

                } else if (templateString[i] == QLatin1Char('\\')) {
                    backslash_count++;

                } else { // any character teminates a backslash sequence
                    backslash_count = 0;
                }
            }

        } else if ((startPos != -1) && (templateString[i] == QLatin1Char('/'))) {      // skip regexp
            i++;
            column++;
            int backslash_count = 0;
            int slashcount = 1;

            for (; i < templateString.size(); i++, column++) {
                if (templateString[i] == QLatin1Char('\n')) {
                    ++line;
                    column = 0;

                    if (startPos != -1) {
                        // don't allow variables to span multiple lines
                        startPos = -1;
                    }

                    break;
                }

                if (templateString[i] == QLatin1Char('/')) {
                    if ((backslash_count % 2) == 0) {
                        slashcount++;
                    }

                    backslash_count = 0;

                } else if (templateString[i] == QLatin1Char('\\')) {
                    backslash_count++;

                } else { // any character teminates a backslash sequence
                    backslash_count = 0;
                }

                if (slashcount == 3) {
                    column++;
                    break;
                }
            }

        } else if (templateString[i] == QLatin1Char('}') && startPos != -1) {
            lastWasBrace = true;
            bool force_first = false;
            // get key, i.e. contents between ${..}
            QString key = templateString.mid(startPos + 2, i - (startPos + 2));
            int keyLength = key.length();
            QString searchReplace;
            ifDebug(qCDebug(LOG_PART) << "key found:" << key;)
            bool check_slash = false;
            bool check_colon = false;
            bool check_backtick = false;
            int pos_slash = key.indexOf(QLatin1String("/"));
            int pos_colon = key.indexOf(QLatin1String(":"));
            int pos_backtick = key.indexOf(QLatin1String("`"));

            if ((pos_slash == -1) && (pos_colon == -1)) {
                // do nothing
            } else if ((pos_slash != -1) && (pos_colon == -1)) {
                check_slash = true;
            } else if ((pos_slash == -1) && (pos_colon != -1)) {
                check_colon = true;
            } else {
                if (pos_colon < pos_slash) {
                    check_colon = true;
                } else {
                    check_slash = true;
                }
            }

            if (!check_slash && !check_colon && pos_backtick >= 0) {
                check_backtick = true;
            }

            QString functionName;

            if (check_slash) {
                searchReplace = key.mid(pos_slash + 1);
                key = key.left(pos_slash);
                ifDebug(qCDebug(LOG_PART) << "search_replace" << searchReplace;)
            } else if (check_colon) {
                key = key.left(pos_colon);
                ifDebug(qCDebug(LOG_PART) << "real key found:" << key;)
            } else if (check_backtick) {
                functionName = key.mid(pos_backtick + 1);
                functionName = functionName.left(functionName.indexOf(QLatin1String("`")));
                key = key.left(pos_backtick);
                ifDebug(qCDebug(LOG_PART) << "real key found:" << key << "function:" << functionName;)
            }

            if (key.contains(QLatin1String("@"))) {
                key = key.left(key.indexOf(QLatin1String("@")));
                force_first = true;
            }

            ifDebug(qCDebug(LOG_PART) << "real key found:" << key;)

            if (!initialValues.contains(key)) {
                qCWarning(LOG_PART) << "unknown variable key:" << key;
            } else if (key == QLatin1String("cursor")) {
                finalCursorPosition = Cursor(line, column - keyLength - 2);
                // don't insert anything, just remove the placeholder
                templateString.remove(startPos, i - startPos + 1);
                // correct iterator pos, 3 == $ + { + }
                i -= 3 + keyLength;
                column -= 2 + keyLength;
                startPos = -1;
            } else {
                MirrorBehaviour behaviour;

                if (!searchReplace.isEmpty()) {
                    QString search;
                    bool searchValid = false;
                    QString replace;
                    bool replaceValid = false;
                    QString flags;
                    //search part;

                    while (!searchReplace.isEmpty()) {
                        ifDebug(qCDebug(LOG_PART) << "searchReplace=" << searchReplace;)
                        int regescapes = 0;
                        int pos = searchReplace.indexOf(QLatin1String("/"));

                        for (int epos = pos - 1; epos >= 0 && searchReplace.at(epos) == QLatin1Char('\\'); epos--) {
                            regescapes++;
                        }

                        ifDebug(qCDebug(LOG_PART) << "regescapes=" << regescapes;)
                        if ((regescapes % 2) == 1) {
                            search += searchReplace.left(pos + 1);
                            searchReplace = searchReplace.mid(pos + 1);
                            ifDebug(qCDebug(LOG_PART) << "intermediate search string is=" << search;)
                        } else {
                            search += searchReplace.left(pos);
                            searchReplace = searchReplace.mid(pos + 1);
                            searchValid = true;
                            ifDebug(qCDebug(LOG_PART) << "final search string is=" << search;)
                            ifDebug(qCDebug(LOG_PART) << "remaining characters in searchReplace" << searchReplace;)
                            break;
                        }
                    }

                    //replace part

                    if (searchValid) {
                        int last_slash = searchReplace.lastIndexOf(QLatin1String("/"));

                        if (last_slash != -1) {
                            replace = searchReplace.left(last_slash);
                            replaceValid = true;
                            flags = searchReplace.mid(last_slash + 1);
                        }
                    }

                    if (searchValid && replaceValid) {
                        behaviour = MirrorBehaviour(search, replace, flags);
                    }

                } else if (!functionName.isEmpty()) {
                    behaviour = MirrorBehaviour(m_templateScript, functionName, this);
                }

                const QString initialVal = behaviour.getMirrorString(initialValues[key]);

                // whether the variable starts with % or $
                QChar c = templateString[startPos];

                // replace variable with initial value
                templateString.replace(startPos, i - startPos + 1, initialVal);

                // correct iterator pos, 3 == % + { + }
                i -= 3 + keyLength - initialVal.length();

                // correct column to point at end of range, taking replacement width diff into account
                // 2 == % + {
                column -= 2 + keyLength - initialVal.length();

                // always add ${...} to the editable ranges
                // only add %{...} to the editable ranges when its value equals the key
                ifDebug(qCDebug(LOG_PART) << "char is:" << c << "initial value is:" << initialValues[key] << " after fixup is:" << initialVal;)
                if (c == QLatin1Char('$') || key == initialValues[key]) {
                    if (!keyQueue.contains(key)) {
                        keyQueue.append(key);
                    }

                    // support for multiline initial val, e.g. selection
                    int endColumn = column - initialVal.length();
                    int endLine = line;
                    for (int j = 0; j < initialVal.length(); ++j) {
                        if (initialVal.at(j) == QLatin1Char('\n')) {
                            endColumn = 0;
                            ++endLine;
                        } else {
                            ++endColumn;
                        }
                    }
                    Range tmp = Range(line, column - initialVal.length(), endLine, endColumn);
                    ifDebug(qCDebug(LOG_PART) << "range is:" << tmp;)

                    if (force_first) {
                        QList<Range> range_list = ranges.values(key);
                        range_list.append(tmp);
                        ranges.remove(key);

                        while (!range_list.isEmpty()) {
                            ranges.insert(key, range_list.takeLast());
                        }
                    } else {
                        ranges.insert(key, tmp);
                    }

                    mirrorBehaviourBuildHelper.insert(tmp, behaviour);
                }
            }

            startPos = -1;

            ifDebug(qCDebug(LOG_PART) << "i=" << i << " template size=" << templateString.size();)
        } else {
            ++column;
            lastWasBrace = false;
        }
    }

    if (lastWasBrace && (!finalCursorPosition.isValid())) {
        templateString += QLatin1String(" ");
        spacers.append(Range(line, column, line, column + 1));
        ++column;
    }

    doc()->replaceText(*m_wholeTemplateRange, templateString);

    Q_ASSERT(!m_wholeTemplateRange->toRange().isEmpty());

    ///HACK: make sure the view cache is up2date so we can safely set the cursor position
    ///      else one might hit the Q_ASSERT(false) in KateViewInternal::endPos()
    ///      the cause is that the view get's ::update()'d, but that does not trigger a
    ///      paint event right away.
    m_view->updateView(true);

    if (finalCursorPosition.isValid()) {
        m_finalCursorPosition = doc()->newMovingCursor(finalCursorPosition);
    } else {
        m_finalCursorPosition = doc()->newMovingCursor(Cursor(line, column));
    }

    if (ranges.isEmpty()) {
        return;
    }

    KateRendererConfig *config = m_view->renderer()->config();

    // TODO: finetune the attribute alpha values, text ranges behave a bit different there...

    Attribute::Ptr editableAttribute = getAttribute(config->templateEditablePlaceholderColor(), 255);
    editableAttribute->setDynamicAttribute(
        Attribute::ActivateCaretIn, getAttribute(config->templateFocusedEditablePlaceholderColor(), 255)
    );

    Attribute::Ptr mirroredAttribute = getAttribute(config->templateNotEditablePlaceholderColor(), 255);

    m_wholeTemplateRange->setAttribute(getAttribute(config->templateBackgroundColor(), 200));

    // create moving ranges ranges for each found variable
    // if the variable exists more than once, create "mirrored" ranges
    foreach (const QString &key, keyQueue) {
        const QList<Range> &values = ranges.values(key);
        // used as parent for mirrored variables,
        // and as only item for not-mirrored variables
        MovingRange *parent = doc()->newMovingRange(values.last(), MovingRange::ExpandLeft | MovingRange::ExpandRight);

        if (values.size() > 1) {
            // add all "real" ranges as children
            for (int i = 0; i < values.size(); ++i)  {
                MovingRange *range = doc()->newMovingRange(values[i], MovingRange::ExpandLeft | MovingRange::ExpandRight);

                // remember child - parent mapping
                m_templateRangesChildren[parent].push_back(range);
                m_templateRangesChildToParent[range] = parent;

                // the last item will be our real first range (see multimap docs)
                if (i == values.size() - 1) {
                    range->setAttribute(editableAttribute);
                    m_uneditedRanges.append(range);
                    m_masterRanges.append(range);
                } else {
                    range->setAttribute(mirroredAttribute);
                    m_mirrorBehaviour.insert(range, mirrorBehaviourBuildHelper[values[i]]);
                }
            }
        } else {
            // just a single range
            parent->setAttribute(editableAttribute);
            m_uneditedRanges.append(parent);
            m_masterRanges.append(parent);
        }

        m_templateRanges.append(parent);
    }

    // create spacers, only once create the attribute
    Attribute::Ptr attribute(new Attribute());
    attribute->setFont(QFont(QLatin1String("fixed"), 1));
    attribute->setFontStrikeOut(true);
    attribute->setFontOverline(true);
    attribute->setFontUnderline(true);
    foreach (const Range &spacer, spacers) {
        MovingRange *r = doc()->newMovingRange(spacer);
        r->setAttribute(attribute);
        m_spacersMovingRanges.append(r);
    }

    qSort(m_masterRanges.begin(), m_masterRanges.end(), cmp_moving_ranges);

    setCurrentRange(m_templateRanges.first());
    mirrorBehaviourBuildHelper.clear();

    //OPTIMIZE ME, PERHAPS ONLY DO THE MIRRORING ACTION FOR THE MASTER RANGE IN THE CODE ABOVE
    //THIS WOULD REDUCE MIRRORING ACTIONS

    m_initialRemodify = true;
    foreach (MovingRange *smr, m_masterRanges) {
        slotTextChanged(doc(), Range(*smr));
    }

    m_initialRemodify = false;

}

void KateTemplateHandler::slotTextChanged(Document *document, const Range &range)
{
    Q_ASSERT(document == doc());

    ifDebug(qCDebug(LOG_PART) << "text changed" << document << range;)

    if (m_isMirroring) {
        ifDebug(qCDebug(LOG_PART) << "mirroring - ignoring edit";)
        return;
    }

    if (!m_initialRemodify) {
        if ((!m_editWithUndo && doc()->isEditRunning()) || range.isEmpty()) {
            ifDebug(qCDebug(LOG_PART) << "slotTextChanged returning prematurely";)
            return;
        }
    }

    if (m_wholeTemplateRange->toRange().isEmpty()) {
        ifDebug(qCDebug(LOG_PART) << "template range got deleted, exiting";)
        cleanupAndExit();
        return;
    }

    if (range.end() == *m_finalCursorPosition) {
        ifDebug(qCDebug(LOG_PART) << "editing at final cursor position, exiting.";)
        cleanupAndExit();
        return;
    }

    if (!m_wholeTemplateRange->toRange().contains(range.start())) {
        // outside our template or one of our own changes
        ifDebug(qCDebug(LOG_PART) << range << "not contained in" << m_wholeTemplateRange->toRange();)
        cleanupAndExit();
        return;
    }

    ifDebug(qCDebug(LOG_PART) << "see if we have to mirror the edit";)

    // Check if @p range is mirrored.
    // If we have two adjacent mirrored ranges, think ${first}${second},
    // the second will be mirrored, as that's what the user will see "activated",
    // since Kate uses contains() that does a comparison "< end()".
    // We use "<= end()" though so we can handle contents that were added at the end of a range.
    // TODO: make it possible to select either, the left or right rannge (LOWPRIO)

    // The found child range to act as base for mirroring.
    MovingRange *baseRange = 0;
    // The left-adjacent range that gets some special treatment (if it exists).
    MovingRange *leftAdjacentRange = 0;

    foreach (MovingRange *parent, m_templateRanges) {
        if (customContains(parent, range.start())) {
            if (m_templateRangesChildren[parent].isEmpty()) {
                // simple, not-mirrored range got changed
                if (!m_initialRemodify) {
                    m_uneditedRanges.removeOne(parent);
                }
            } else {
                // handle mirrored ranges
                if (baseRange) {
                    // look for adjacent range (we find the right-handed one here)
                    ifDebug(qCDebug(LOG_PART) << "looking for adjacent mirror to" << *baseRange;)
                    foreach (MovingRange *child, m_templateRangesChildren[parent]) {
                        if (child->start() == range.start() && child->end() >= range.end()) {
                            ifDebug(qCDebug(LOG_PART) << "found adjacent mirror - using as base" << *child;)
                            leftAdjacentRange = baseRange;
                            baseRange = child;
                            break;
                        }
                    }

                    // adjacent mirror handled, we can finish
                    break;
                } else {
                    // find mirrored range that got edited
                    foreach (MovingRange *child, m_templateRangesChildren[parent]) {
                        if (customContains(child, range.start())) {
                            baseRange = child;
                            break;
                        }
                    }

                    if (baseRange && baseRange->end() != range.end()) {
                        // finish, don't look for adjacent mirror as we are not at the end of this range
                        break;
                    } else if (baseRange && (baseRange->isEmpty() || range == *baseRange)) {
                        // always add to empty ranges first. else ${foo}${bar} will make foo unusable when
                        // it gets selected and an edit takes place.
                        break;
                    } // else don't finish, look for baseRange or adjacent mirror first
                }
            }

        } else if (baseRange) {
            // no adjacent mirrored range found, we can finish
            break;
        }
    }

    if (baseRange) {
        m_uneditedRanges.removeOne(baseRange);

        if (leftAdjacentRange) {
            // revert that something got added to the adjacent range
            ifDebug(qCDebug(LOG_PART) << "removing edited range" << range << "from left adjacent range" << *leftAdjacentRange;)
            leftAdjacentRange->setRange(Range(leftAdjacentRange->start(), range.start()));
            ifDebug(qCDebug(LOG_PART) << "new range:" << *leftAdjacentRange;)
        }

        syncMirroredRanges(baseRange);

        if (range.start() == baseRange->start()) {
            // TODO: update the attribute, since kate doesn't do that automatically
            // TODO: fix in kate itself
            // TODO: attribute->changed == undefined reference...
        }
    } else {
        ifDebug(qCDebug(LOG_PART) << "no range found to mirror this edit";)
    }
}

void KateTemplateHandler::syncMirroredRanges(MovingRange *range)
{
    Q_ASSERT(m_templateRanges.contains(m_templateRangesChildToParent[range]));

    m_isMirroring = true;
    doc()->editStart();

    const QString &newText = doc()->text(*range);
    ifDebug(qCDebug(LOG_PART) << "mirroring" << newText << "from" << *range;)

    foreach (MovingRange *sibling, m_templateRangesChildren[m_templateRangesChildToParent[range]]) {
        if (sibling != range) {
            doc()->replaceText(*sibling, m_mirrorBehaviour[sibling].getMirrorString(newText));
        }
    }

    /// TODO: now undo only undos the last char edit, and does not
    ///       merge those edits as usual
    doc()->editEnd();
    m_isMirroring = false;
}

KTextEditor::ViewPrivate *KateTemplateHandler::view()
{
    return m_view;
}

//BEGIN MIRROR BEHAVIOUR
KateTemplateHandler::MirrorBehaviour::MirrorBehaviour()
    : m_behaviour(Clone)
{
}

KateTemplateHandler::MirrorBehaviour::MirrorBehaviour(const QString &regexp,
        const QString &replacement,
        const QString &flags)
    : m_behaviour(Regexp)
    , m_search(regexp)
    , m_replace(replacement)
{
    m_global = flags.contains(QLatin1String("g"));
    m_expr = QRegExp(regexp, flags.contains(QLatin1String("i")) ? Qt::CaseInsensitive : Qt::CaseSensitive, QRegExp::RegExp2);
}

KateTemplateHandler::MirrorBehaviour::MirrorBehaviour(KateTemplateScript *templateScript,
        const QString &functionName,
        KateTemplateHandler *handler)
    : m_behaviour(Scripted)
    , m_templateScript(templateScript)
    , m_functionName(functionName)
    , m_handler(handler)
{
}

KateTemplateHandler::MirrorBehaviour::~MirrorBehaviour()
{
}

QString KateTemplateHandler::MirrorBehaviour::getMirrorString(const QString &source)
{
    QString ahead;
    QString output;
    QString finalOutput;
    int pos;
    int matchCounter = 0;

    switch (m_behaviour) {

    case Clone:
        return source;
        break;

    case Regexp: {
        ifDebug(qCDebug(LOG_PART) << "regexp " << m_search << " replacement " << m_replace;)

        if (m_global) {
            ahead = source;

            while (ahead.length() > 0) {
                if ((pos = m_expr.indexIn(ahead)) == -1) {
                    return finalOutput + ahead;
                }

                QStringList results = m_expr.capturedTexts();
                output = KateRegExpSearch::buildReplacement(m_replace, results, ++matchCounter);

                finalOutput = finalOutput + ahead.left(pos) + output;
                ahead = ahead.mid(pos + m_expr.matchedLength());
            }

            return finalOutput;
        } else {
            if ((pos = m_expr.indexIn(source)) == -1) {
                return source;
            }

            QStringList results = m_expr.capturedTexts();
            output = KateRegExpSearch::buildReplacement(m_replace, results, 1);
            return source.left(pos) + output + source.mid(pos + m_expr.matchedLength());
        }

        break;
    }

    case Scripted: {
        KateTemplateScript *script = KTextEditor::EditorPrivate::self()->scriptManager()->templateScript(m_templateScript);

        if (script) {
            QString result = script->invoke(m_handler->view(), m_functionName, source);

            if (!result.isNull()) {
                return result;
            }
        }

        return source;
    }

    default:
        return QString();
    }

    return QString();
}

//END MIRROR BEHAVOUR
