// edbee - Copyright (c) 2012-2025 by Rick Blommers and contributors
// SPDX-License-Identifier: MIT

#include "tabcommand.h"

#include "edbee/texteditorcontroller.h"
#include "edbee/models/changes/mergablechangegroup.h"
#include "edbee/models/texteditorconfig.h"
#include "edbee/models/textdocument.h"
#include "edbee/views/textselection.h"

#include "edbee/debug.h"

namespace edbee {


/// Constructs the tabcommand
/// @param direction the direction of the tab (Forward or Backward)
/// @param insertTab should we use a tab character?
TabCommand::TabCommand(TabCommand::Direction direction, bool insertTab)
    : dir_(direction)
    , insertTab_(insertTab)
{
}


/// indents or outdents in the given controller
/// @parma controller the controller
void TabCommand::indent(TextEditorController* controller)
{
    TextDocument* doc  = controller->textDocument();
    TextSelection* sel = controller->textSelection();
    int tabSize = doc->config()->indentSize();
    bool useTab = false; //doc->config()->useTabChar();
    QString tab = useTab ? "\t" : QStringLiteral(" ").repeated(tabSize);

    controller->beginUndoGroup(new MergableChangeGroup(controller));

    sel->beginChanges();

    ptrdiff_t lastLine = -1;
    for (size_t i = 0, cnt = sel->rangeCount(); i < cnt; ++i) {
        TextRange& range = sel->range(i);

        // calculate the start and endline
        size_t startLine = doc->lineFromOffset(range.min());
        size_t endLine   = doc->lineFromOffset(range.max());

        // when the last line is blank, we do NOT want to indent it (fixes #61)
        if (startLine != endLine && doc->columnFromOffsetAndLine(range.max(), endLine) == 0) {
            --endLine;
        }

        // make sure we at least indent one line
        if (static_cast<ptrdiff_t>(startLine) <= lastLine) startLine = static_cast<size_t>(lastLine + 1);

        // now indent all lines
        for (size_t line = startLine; line <= endLine; ++line) {
            size_t offset = doc->offsetFromLine(line);

            // insert-tab
            if (dir_ == Forward) {
                doc->replace(offset, 0, tab);
                sel->changeSpatial(offset, 0, static_cast<size_t>(tab.length()), true);

            // remove tab
            } else {
                size_t offsetNextLine = doc->offsetFromLine(line + 1) - 1;
                size_t endOffset = offset;
                for (size_t j = 0, cnt = static_cast<size_t>(tabSize); j < cnt; ++j ) {
                    size_t off = offset + j;
                    if (off >= doc->length()) break;

                    QChar chr = doc->charAt(off);
                    if (chr != ' ' && chr != '\t' ) break;
                    if (off >= offsetNextLine) break;
                    ++endOffset;
                    if (chr == '\t') break;
                }

                if (offset < endOffset) {
                    doc->replace(offset, endOffset - offset, "");
                    sel->changeSpatial(offset, endOffset - offset, 0, true);
                }
            }
        }
        lastLine = static_cast<ptrdiff_t>(endLine);
    }
    sel->endChanges();

    controller->endUndoGroup(CoalesceId_Indent + dir_, true);
}


/// TODO: Refactor :)
void TabCommand::execute(TextEditorController* controller)
{
    if (!insertTab_) {
        indent(controller);
        return;
    }

    // retrieve the settings
    TextDocument* doc  = controller->textDocument();
    TextSelection* sel = controller->textSelection();
    int tabSize = doc->config()->indentSize();
    bool useTab = doc->config()->useTabChar();
    QString ws  = doc->config()->whitespaces();

    // check if the changes are multi-line
    bool multiLine = false;
    bool atStartOfLine = true;
    for (size_t i=0, cnt = sel->rangeCount(); i < cnt && !multiLine; ++i) {
        TextRange& range = sel->range(i);
        multiLine = multiLine || doc->lineFromOffset(range.anchor()) != doc->lineFromOffset(range.caret());

        // no selection? we need to check if we are at the start of the line
        if (!range.hasSelection() && atStartOfLine) {
            size_t pos = range.caret();
            size_t startPos = doc->offsetFromLine( doc->lineFromOffset(pos));
            --pos;  // look hat the character before the pos

            // Q_ASSERT(startPos <= pos);
            if (pos >= startPos) {
                while (pos>startPos && atStartOfLine) {
                    QChar chr = doc->charAt(pos);
                    if (!ws.contains(chr)) {
                        atStartOfLine = false;
                    }
                    --pos;
                }
            }

        } else {
            atStartOfLine = false;
        }
    }

    // a Line-moving operation
    if (multiLine || (atStartOfLine && dir_ == Backward)) {
        indent(controller);

    // a character inserting operation
    } else {
        if (dir_ == Forward) {
            if (useTab) {
                controller->replaceSelection("\t", CoalesceId_Indent + SubCoalesceId_Indent_InsertTab);
            } else {

                // we need to 'calculate' the number of spaces to add (per caret)
                QStringList texts;
                for (size_t i = 0, cnt = sel->rangeCount(); i < cnt; ++i) {
                    TextRange& range = sel->range(i);
                    size_t offset = range.min();
                    size_t col = doc->columnFromOffsetAndLine(offset);

                    // calculate the number of spaces depending on the column
                    int spaces = tabSize - static_cast<int>(col) % tabSize;
                    if (!spaces) spaces = tabSize;

                    // append the text
                    texts.push_back(QStringLiteral(" ").repeated(spaces));
                }
                controller->replaceSelection(texts, CoalesceId_Indent + SubCoalesceId_Indent_InsertSpaces );
            }
        }
    }
}


/// Returs the name of this command
QString TabCommand::toString()
{
    if( dir_ == Backward ) return "TabCommand(back)";
    return "TabCommand(fwd)";
}

} // edbee
