/*
    SPDX-FileCopyrightText: 2013 Christoph Cullmann <cullmann@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "katetexthistory.h"
#include "katetextbuffer.h"

namespace Kate
{
TextHistory::TextHistory(TextBuffer &buffer)
    : m_buffer(buffer)
    , m_lastSavedRevision(-1)
    , m_firstHistoryEntryRevision(0)
{
    // clear will be triggered in buffer constructor
}

TextHistory::~TextHistory() = default;

qint64 TextHistory::revision() const
{
    // just output last revisions of buffer
    return m_buffer.revision();
}

void TextHistory::clear()
{
    // reset last saved revision
    m_lastSavedRevision = -1;

    // remove all history entries and add no-change dummy for first revision
    m_historyEntries.clear();
    m_historyEntries.push_back(Entry());

    // first entry will again belong to the current revision
    m_firstHistoryEntryRevision = revision();
}

void TextHistory::setLastSavedRevision()
{
    // current revision was successful saved
    m_lastSavedRevision = revision();
}

void TextHistory::wrapLine(const KTextEditor::Cursor position)
{
    // create and add new entry
    Entry entry;
    entry.type = Entry::WrapLine;
    entry.line = position.line();
    entry.column = position.column();
    addEntry(entry);
}

void TextHistory::unwrapLine(int line, int oldLineLength)
{
    // create and add new entry
    Entry entry;
    entry.type = Entry::UnwrapLine;
    entry.line = line;
    entry.column = 0;
    entry.oldLineLength = oldLineLength;
    addEntry(entry);
}

void TextHistory::insertText(const KTextEditor::Cursor position, int length, int oldLineLength)
{
    // create and add new entry
    Entry entry;
    entry.type = Entry::InsertText;
    entry.line = position.line();
    entry.column = position.column();
    entry.length = length;
    entry.oldLineLength = oldLineLength;
    addEntry(entry);
}

void TextHistory::removeText(KTextEditor::Range range, int oldLineLength)
{
    // create and add new entry
    Entry entry;
    entry.type = Entry::RemoveText;
    entry.line = range.start().line();
    entry.column = range.start().column();
    entry.length = range.end().column() - range.start().column();
    entry.oldLineLength = oldLineLength;
    addEntry(entry);
}

void TextHistory::addEntry(const Entry &entry)
{
    // history should never be empty
    Q_ASSERT(!m_historyEntries.empty());

    // simple efficient check: if we only have one entry, and the entry is not referenced
    // just replace it with the new one and adjust the revision
    if ((m_historyEntries.size() == 1) && !m_historyEntries.front().referenceCounter) {
        // remember new revision for first element, it is the revision we get after this change
        m_firstHistoryEntryRevision = revision() + 1;

        // remember edit
        m_historyEntries.front() = entry;

        // be done...
        return;
    }

    // ok, we have more than one entry or the entry is referenced, just add up new entries
    m_historyEntries.push_back(entry);
}

void TextHistory::lockRevision(qint64 revision)
{
    // some invariants must hold
    Q_ASSERT(!m_historyEntries.empty());
    Q_ASSERT(revision >= m_firstHistoryEntryRevision);
    Q_ASSERT(revision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size())));

    // increment revision reference counter
    Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision];
    ++entry.referenceCounter;
}

void TextHistory::unlockRevision(qint64 revision)
{
    // some invariants must hold
    Q_ASSERT(!m_historyEntries.empty());
    Q_ASSERT(revision >= m_firstHistoryEntryRevision);
    Q_ASSERT(revision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size())));

    // decrement revision reference counter
    Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision];
    Q_ASSERT(entry.referenceCounter);
    --entry.referenceCounter;

    // clean up no longer used revisions...
    if (!entry.referenceCounter) {
        // search for now unused stuff
        qint64 unreferencedEdits = 0;
        for (qint64 i = 0; i + 1 < qint64(m_historyEntries.size()); ++i) {
            if (m_historyEntries[i].referenceCounter) {
                break;
            }

            // remember deleted count
            ++unreferencedEdits;
        }

        // remove unreferred from the list now
        if (unreferencedEdits > 0) {
            // remove stuff from history
            m_historyEntries.erase(m_historyEntries.begin(), m_historyEntries.begin() + unreferencedEdits);

            // patch first entry revision
            m_firstHistoryEntryRevision += unreferencedEdits;
        }
    }
}

void TextHistory::Entry::transformCursor(int &cursorLine, int &cursorColumn, bool moveOnInsert) const
{
    // simple stuff, sort out generic things

    // no change, if this change is in line behind cursor
    if (line > cursorLine) {
        return;
    }

    // handle all history types
    switch (type) {
    // Wrap a line
    case WrapLine:
        // we wrap this line
        if (cursorLine == line) {
            // skip cursors with too small column
            if (cursorColumn <= column) {
                if (cursorColumn < column || !moveOnInsert) {
                    return;
                }
            }

            // adjust column
            cursorColumn = cursorColumn - column;
        }

        // always increment cursor line
        cursorLine += 1;
        return;

    // Unwrap a line
    case UnwrapLine:
        // we unwrap this line, adjust column
        if (cursorLine == line) {
            cursorColumn += oldLineLength;
        }

        // decrease cursor line
        cursorLine -= 1;
        return;

    // Insert text
    case InsertText:
        // only interesting, if same line
        if (cursorLine != line) {
            return;
        }

        // skip cursors with too small column
        if (cursorColumn <= column) {
            if (cursorColumn < column || !moveOnInsert) {
                return;
            }
        }

        // patch column of cursor
        if (cursorColumn <= oldLineLength) {
            cursorColumn += length;
        }

        // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode
        else if (cursorColumn < oldLineLength + length) {
            cursorColumn = oldLineLength + length;
        }

        return;

    // Remove text
    case RemoveText:
        // only interesting, if same line
        if (cursorLine != line) {
            return;
        }

        // skip cursors with too small column
        if (cursorColumn <= column) {
            return;
        }

        // patch column of cursor
        if (cursorColumn <= column + length) {
            cursorColumn = column;
        } else {
            cursorColumn -= length;
        }

        return;

    // nothing
    default:
        return;
    }
}

void TextHistory::Entry::reverseTransformCursor(int &cursorLine, int &cursorColumn, bool moveOnInsert) const
{
    // handle all history types
    switch (type) {
    // Wrap a line
    case WrapLine:
        // ignore this line
        if (cursorLine <= line) {
            return;
        }

        // next line is unwrapped
        if (cursorLine == line + 1) {
            // adjust column
            cursorColumn = cursorColumn + column;
        }

        // always decrement cursor line
        cursorLine -= 1;
        return;

    // Unwrap a line
    case UnwrapLine:
        // ignore lines before unwrapped one
        if (cursorLine < line - 1) {
            return;
        }

        // we unwrap this line, try to adjust cursor column if needed
        if (cursorLine == line - 1) {
            // skip cursors with to small columns
            if (cursorColumn <= oldLineLength) {
                if (cursorColumn < oldLineLength || !moveOnInsert) {
                    return;
                }
            }

            cursorColumn -= oldLineLength;
        }

        // increase cursor line
        cursorLine += 1;
        return;

    // Insert text
    case InsertText:
        // only interesting, if same line
        if (cursorLine != line) {
            return;
        }

        // skip cursors with too small column
        if (cursorColumn <= column) {
            return;
        }

        // patch column of cursor
        if (cursorColumn - length < column) {
            cursorColumn = column;
        } else {
            cursorColumn -= length;
        }

        return;

    // Remove text
    case RemoveText:
        // only interesting, if same line
        if (cursorLine != line) {
            return;
        }

        // skip cursors with too small column
        if (cursorColumn <= column) {
            if (cursorColumn < column || !moveOnInsert) {
                return;
            }
        }

        // patch column of cursor
        if (cursorColumn <= oldLineLength) {
            cursorColumn += length;
        }

        // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode
        else if (cursorColumn < oldLineLength + length) {
            cursorColumn = oldLineLength + length;
        }
        return;

    // nothing
    default:
        return;
    }
}

void TextHistory::transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision)
{
    // -1 special meaning for from/toRevision
    if (fromRevision == -1) {
        fromRevision = revision();
    }

    if (toRevision == -1) {
        toRevision = revision();
    }

    // shortcut, same revision
    if (fromRevision == toRevision) {
        return;
    }

    // some invariants must hold
    Q_ASSERT(!m_historyEntries.empty());
    Q_ASSERT(fromRevision != toRevision);
    Q_ASSERT(fromRevision >= m_firstHistoryEntryRevision);
    Q_ASSERT(fromRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size())));
    Q_ASSERT(toRevision >= m_firstHistoryEntryRevision);
    Q_ASSERT(toRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size())));

    // transform cursor
    bool moveOnInsert = insertBehavior == KTextEditor::MovingCursor::MoveOnInsert;

    // forward or reverse transform?
    if (toRevision > fromRevision) {
        for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) {
            const Entry &entry = m_historyEntries.at(rev);
            entry.transformCursor(line, column, moveOnInsert);
        }
    } else {
        for (int rev = fromRevision - m_firstHistoryEntryRevision; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) {
            const Entry &entry = m_historyEntries.at(rev);
            entry.reverseTransformCursor(line, column, moveOnInsert);
        }
    }
}

void TextHistory::transformRange(KTextEditor::Range &range,
                                 KTextEditor::MovingRange::InsertBehaviors insertBehaviors,
                                 KTextEditor::MovingRange::EmptyBehavior emptyBehavior,
                                 qint64 fromRevision,
                                 qint64 toRevision)
{
    // invalidate on empty?
    bool invalidateIfEmpty = emptyBehavior == KTextEditor::MovingRange::InvalidateIfEmpty;
    if (invalidateIfEmpty && range.end() <= range.start()) {
        range = KTextEditor::Range::invalid();
        return;
    }

    // -1 special meaning for from/toRevision
    if (fromRevision == -1) {
        fromRevision = revision();
    }

    if (toRevision == -1) {
        toRevision = revision();
    }

    // shortcut, same revision
    if (fromRevision == toRevision) {
        return;
    }

    // some invariants must hold
    Q_ASSERT(!m_historyEntries.empty());
    Q_ASSERT(fromRevision != toRevision);
    Q_ASSERT(fromRevision >= m_firstHistoryEntryRevision);
    Q_ASSERT(fromRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size())));
    Q_ASSERT(toRevision >= m_firstHistoryEntryRevision);
    Q_ASSERT(toRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size())));

    // transform cursors

    // first: copy cursors, without range association
    int startLine = range.start().line();
    int startColumn = range.start().column();
    int endLine = range.end().line();
    int endColumn = range.end().column();

    bool moveOnInsertStart = !(insertBehaviors & KTextEditor::MovingRange::ExpandLeft);
    bool moveOnInsertEnd = (insertBehaviors & KTextEditor::MovingRange::ExpandRight);

    // forward or reverse transform?
    if (toRevision > fromRevision) {
        for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) {
            const Entry &entry = m_historyEntries.at(rev);

            entry.transformCursor(startLine, startColumn, moveOnInsertStart);

            entry.transformCursor(endLine, endColumn, moveOnInsertEnd);

            // got empty?
            if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) {
                if (invalidateIfEmpty) {
                    range = KTextEditor::Range::invalid();
                    return;
                } else {
                    // else normalize them
                    endLine = startLine;
                    endColumn = startColumn;
                }
            }
        }
    } else {
        for (int rev = fromRevision - m_firstHistoryEntryRevision; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) {
            const Entry &entry = m_historyEntries.at(rev);

            entry.reverseTransformCursor(startLine, startColumn, moveOnInsertStart);

            entry.reverseTransformCursor(endLine, endColumn, moveOnInsertEnd);

            // got empty?
            if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) {
                if (invalidateIfEmpty) {
                    range = KTextEditor::Range::invalid();
                    return;
                } else {
                    // else normalize them
                    endLine = startLine;
                    endColumn = startColumn;
                }
            }
        }
    }

    // now, copy cursors back
    range.setRange(KTextEditor::Cursor(startLine, startColumn), KTextEditor::Cursor(endLine, endColumn));
}

}
