// SPDX-FileCopyrightText: 2025 Prayag Jain <prayagjain2@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "texttool.hpp"

#include <KLocalizedString>
#include <QClipboard>
#include <QGuiApplication>
#include <memory>

#include "canvas/canvas.hpp"
#include "command/commandhistory.hpp"
#include "command/insertitemcommand.hpp"
#include "components/toolbar.hpp"
#include "context/applicationcontext.hpp"
#include "context/coordinatetransformer.hpp"
#include "context/renderingcontext.hpp"
#include "context/selectioncontext.hpp"
#include "context/spatialcontext.hpp"
#include "context/uicontext.hpp"
#include "data-structures/cachegrid.hpp"
#include "data-structures/quadtree.hpp"
#include "event/event.hpp"
#include "item/factory/textfactory.hpp"
#include "keybindings/keybindmanager.hpp"
#include "properties/widgets/propertymanager.hpp"

/*
 * FIXME: This file needs some SERIOUS refactoring which, unfortunately, time is
 * not allowing me to do. Feel free to open pull requests though. I have made
 * sure to implement all necessary functionalities. AI slop will be rejected :D
 * Put your heart and soul into the work you do and see yourself succeed.
 */
using namespace Qt::Literals::StringLiterals;

TextTool::TextTool(ApplicationContext *context)
    : DrawingTool(context)
{
    m_cursor = QCursor(Qt::CrossCursor);
    m_itemFactory = std::make_unique<TextFactory>();

    m_properties = {Property::Type::StrokeColor, Property::Type::FontSize};
}

void TextTool::mousePressed(ApplicationContext *context)
{
    const UIContext *uiContext{context->uiContext()};

    if (uiContext->appEvent()->button() == Qt::LeftButton) {
        auto spatialContext{context->spatialContext()};
        CoordinateTransformer &transformer{spatialContext->coordinateTransformer()};
        auto renderingContext{context->renderingContext()};
        const QuadTree &quadTree{spatialContext->quadtree()};

        const QPointF worldPos{transformer.viewToWorld(uiContext->appEvent()->pos())};
        QList<std::shared_ptr<Item>> intersectingItems{quadTree.queryItems(worldPos, [](const std::shared_ptr<Item> &item, const QPointF &point) {
            return item->formType() == Item::FormType::Text && item->boundingBox().contains(point);
        })};

        if (intersectingItems.empty()) {
            if (m_curItem == nullptr) {
                m_curItem = std::dynamic_pointer_cast<TextItem>(m_itemFactory->create());

                m_curItem->setProperty(Property::Type::StrokeColor, uiContext->propertyManager()->value(Property::Type::StrokeColor));
                m_curItem->setProperty(Property::Type::FontSize, uiContext->propertyManager()->value(Property::Type::FontSize));

                m_curItem->createTextBox(transformer.viewToWorld(uiContext->appEvent()->pos()));

                auto commandHistory{spatialContext->commandHistory()};
                commandHistory->push(std::make_shared<InsertItemCommand>(QList<std::shared_ptr<Item>>{m_curItem}));
            } else {
                cleanup();
                return;
            }
        } else {
            if (m_curItem != nullptr) {
                m_curItem->setMode(TextItem::Mode::Normal);
                renderingContext->cacheGrid().markDirty(transformer.worldToGrid(m_curItem->boundingBox()).toRect());
            }

            m_curItem = std::dynamic_pointer_cast<TextItem>(intersectingItems.back());
            m_curItem->setCaret(worldPos);

            renderingContext->cacheGrid().markDirty(transformer.worldToGrid(m_curItem->boundingBox()).toRect());

            m_isSelecting = true;
            m_mouseMoved = false;

            m_curItem->setSelectionStart(m_curItem->getIndexFromCursor(worldPos));
            m_curItem->setSelectionEnd(TextItem::INVALID);
        }

        context->selectionContext()->reset();
        context->selectionContext()->addToSelection(m_curItem);

        m_curItem->setMode(TextItem::Mode::Edit);
        uiContext->keybindManager()->setEnabled(false);
        context->selectionContext()->setShouldRenderHandles(false);

        renderingContext->markForRender();
        renderingContext->markForUpdate();
    }

    m_isDrawing = true;
}

void TextTool::mouseMoved(ApplicationContext *context)
{
    auto spatialContext{context->spatialContext()};
    CoordinateTransformer &transformer{spatialContext->coordinateTransformer()};
    auto renderingContext{context->renderingContext()};
    const UIContext *uiContext{context->uiContext()};
    const QuadTree &quadTree{spatialContext->quadtree()};
    m_mouseMoved = true;

    const QPointF worldPos{transformer.viewToWorld(uiContext->appEvent()->pos())};
    const QList<std::shared_ptr<Item>> intersectingItems{quadTree.queryItems(worldPos, [](const std::shared_ptr<Item> &item, const QPointF &point) {
        return item->formType() == Item::FormType::Text && item->boundingBox().contains(point);
    })};

    if (!intersectingItems.empty()) {
        renderingContext->canvas()->setCursor(Qt::IBeamCursor);
    } else {
        renderingContext->canvas()->setCursor(Qt::CrossCursor);
    }

    if (m_isSelecting) {
        const qsizetype curIndex{m_curItem->getIndexFromCursor(worldPos)};
        const bool isLeft{curIndex < m_curItem->selectionStart()};

        const qsizetype curStart{m_curItem->selectionStart()};
        const qsizetype curEnd{m_curItem->selectionEnd()};

        if (m_doubleClicked) {
            if (isLeft) {
                m_curItem->setSelectionStart(std::max(curStart, curEnd));
                m_curItem->setSelectionEnd(m_curItem->getPrevBreak(curIndex - 1));
            } else {
                m_curItem->setSelectionStart(std::min(curStart, curEnd));
                m_curItem->setSelectionEnd(m_curItem->getNextBreak(curIndex));
            }
        } else if (m_tripleClicked) {
            auto [start, end] = m_curItem->getLineRangeForPosition(curIndex);
            if (isLeft) {
                m_curItem->setSelectionStart(std::max(curStart, curEnd));
                m_curItem->setSelectionEnd(start);
            } else {
                m_curItem->setSelectionStart(std::min(curStart, curEnd));
                m_curItem->setSelectionEnd(end + 1);
            }
        } else {
            if (curIndex == m_curItem->selectionStart()) {
                m_curItem->setSelectionEnd(TextItem::INVALID);
            } else {
                m_curItem->setSelectionEnd(curIndex);
            }
        }

        renderingContext->cacheGrid().markDirty(transformer.worldToGrid(m_curItem->boundingBox()).toRect());
        renderingContext->markForRender();
        renderingContext->markForUpdate();
    }
}

void TextTool::mouseReleased([[maybe_unused]] ApplicationContext *context)
{
    m_isSelecting = false;
    m_doubleClicked = false;
    m_tripleClicked = false;
}

void TextTool::mouseDoubleClick(ApplicationContext *context)
{
    if (!m_curItem) {
        return;
    }

    m_doubleClicked = true;
    if (!m_mouseMoved) {
        m_isSelecting = true;

        auto spatialContext{context->spatialContext()};
        CoordinateTransformer &transformer{spatialContext->coordinateTransformer()};
        auto renderingContext{context->renderingContext()};
        auto uiContext{context->uiContext()};

        const QPointF worldPos{transformer.viewToWorld(uiContext->appEvent()->pos())};

        const qsizetype curIndex{m_curItem->getIndexFromCursor(worldPos)};

        m_curItem->setSelectionStart(m_curItem->getPrevBreak(curIndex - 1));
        m_curItem->setSelectionEnd(m_curItem->getNextBreak(curIndex));

        renderingContext->cacheGrid().markDirty(transformer.worldToGrid(m_curItem->boundingBox()).toRect());
        renderingContext->markForRender();
        renderingContext->markForUpdate();
    }
}

void TextTool::mouseTripleClick(ApplicationContext *context)
{
    if (!m_curItem) {
        return;
    }

    m_tripleClicked = true;
    if (!m_mouseMoved) {
        m_isSelecting = true;

        auto spatialContext{context->spatialContext()};
        CoordinateTransformer &transformer{spatialContext->coordinateTransformer()};
        auto renderingContext{context->renderingContext()};
        auto uiContext{context->uiContext()};

        const QPointF worldPos{transformer.viewToWorld(uiContext->appEvent()->pos())};

        const qsizetype curIndex{m_curItem->getIndexFromCursor(worldPos)};

        const auto [start, end] = m_curItem->getLineRangeForPosition(curIndex);
        m_curItem->setSelectionStart(start);
        m_curItem->setSelectionEnd(end + 1);

        renderingContext->cacheGrid().markDirty(transformer.worldToGrid(m_curItem->boundingBox()).toRect());
        renderingContext->markForRender();
        renderingContext->markForUpdate();
    }
}

// TODO: Refactor, refactor, refactor!
void TextTool::keyPressed(ApplicationContext *context)
{
    if (!m_curItem) {
        return;
    }

    auto ev{context->uiContext()->appEvent()};

    if (ev->key() == Qt::Key_Escape) {
        m_curItem->setMode(TextItem::Mode::Normal);
        context->uiContext()->keybindManager()->setEnabled(true);
        context->selectionContext()->setShouldRenderHandles(true);
        context->uiContext()->toolBar()->changeTool(Tool::Type::Selection);
        m_curItem = nullptr;

        context->renderingContext()->cacheGrid().markAllDirty();
        context->renderingContext()->markForRender();
        context->renderingContext()->markForUpdate();
    }

    if (m_curItem != nullptr && m_curItem->mode() == TextItem::Mode::Edit) {
        qsizetype caret{m_curItem->caret()};
        const QString &text{m_curItem->text()};
        const qsizetype size{text.size()};

        auto handleDefaultCase = [&]() {
            if (ev->text().isEmpty()) {
                return;
            }

            m_curItem->deleteSelection();
            m_curItem->insertText(ev->text());
        };

        // FIXME: What the fuck
        switch (ev->key()) {
        case Qt::Key_Return:
        case Qt::Key_Enter:
            m_curItem->insertText(QStringLiteral("\n"));
            break;
        case Qt::Key_Left: {
            const qsizetype newIndex{std::max(static_cast<qsizetype>(0), caret - 1)};
            if (ev->modifiers() & Qt::ControlModifier) {
                const qsizetype curPos{m_curItem->caret()};

                if (ev->modifiers() & Qt::ShiftModifier) {
                    const qsizetype pos{m_curItem->selectionEnd()};
                    m_curItem->setSelectionEnd(m_curItem->getPrevBreak(pos == TextItem::INVALID ? curPos - 1 : pos - 1));
                } else {
                    m_curItem->setCaret(m_curItem->getPrevBreak(curPos - 1));
                }
            } else if (ev->modifiers() & Qt::ShiftModifier) {
                const qsizetype selEnd{m_curItem->selectionEnd()};
                m_curItem->setSelectionEnd(selEnd == -1 ? newIndex : selEnd - 1);
            } else {
                m_curItem->setCaret(newIndex);
            }
            break;
        }
        case Qt::Key_Right: {
            const qsizetype newIndex{std::min(size, caret + 1)};
            if (ev->modifiers() & Qt::ControlModifier) {
                const qsizetype curPos{m_curItem->caret()};

                if (ev->modifiers() & Qt::ShiftModifier) {
                    const qsizetype pos{m_curItem->selectionEnd()};
                    m_curItem->setSelectionEnd(m_curItem->getNextBreak(pos == TextItem::INVALID ? curPos : pos));
                } else {
                    m_curItem->setCaret(m_curItem->getNextBreak(curPos));
                }
            } else if (ev->modifiers() & Qt::ShiftModifier) {
                const qsizetype selEnd{m_curItem->selectionEnd()};
                m_curItem->setSelectionEnd(selEnd == -1 ? newIndex : selEnd + 1);
            } else {
                m_curItem->setCaret(newIndex);
            }
            break;
        }
        case Qt::Key_Backspace: {
            if (m_curItem->hasSelection()) {
                m_curItem->deleteSelection();
                break;
            }

            if ((ev->modifiers() & Qt::ControlModifier) && (ev->modifiers() & Qt::ShiftModifier)) {
                qsizetype prevLine{-1};
                for (qsizetype pos{caret - 1}; pos >= 0; pos--) {
                    if (text[pos] == u'\n') {
                        prevLine = pos;
                        break;
                    }
                }

                if (prevLine == caret - 1) {
                    prevLine--;
                }

                m_curItem->deleteSubStr(prevLine + 1, caret - 1);
                m_curItem->setCaret(prevLine + 1);
                break;
            }

            if (ev->modifiers() & Qt::ControlModifier) {
                const qsizetype prevBreak{m_curItem->getPrevBreak(caret - 1)};
                m_curItem->deleteSubStr(prevBreak, caret - 1);
                m_curItem->setCaret(prevBreak);
                break;
            }

            caret--;
            m_curItem->deleteSubStr(caret, caret);
            m_curItem->setCaret(caret);
            break;
        }
        case Qt::Key_Delete: {
            if (m_curItem->hasSelection()) {
                m_curItem->deleteSelection();
                break;
            }

            if ((ev->modifiers() & Qt::ControlModifier) && (ev->modifiers() & Qt::ShiftModifier)) {
                qsizetype nextLine{size};
                for (qsizetype pos{caret}; pos < size; pos++) {
                    if (text[pos] == u'\n') {
                        nextLine = pos;
                        break;
                    }
                }

                m_curItem->deleteSubStr(caret, nextLine - 1);
                break;
            }

            if (ev->modifiers() & Qt::ControlModifier) {
                const qsizetype nextBreak{m_curItem->getNextBreak(caret)};
                m_curItem->deleteSubStr(caret, nextBreak - 1);
                break;
            }

            m_curItem->deleteSubStr(caret, caret);
            break;
        }
        case Qt::Key_Up: {
            const qsizetype prevLineEnd{text.lastIndexOf(u'\n', caret - 1)};

            if (prevLineEnd == -1) {
                break;
            }

            const qsizetype prevLineStart{text.lastIndexOf(u'\n', prevLineEnd - 1) + 1};
            const qsizetype pos{m_curItem->caretPosInLine()};

            const qsizetype length{prevLineEnd - prevLineStart + 1};
            if (length >= pos) {
                m_curItem->setCaret(prevLineStart + pos - 1, false);
            } else {
                m_curItem->setCaret(prevLineEnd, false);
            }
            break;
        }
        case Qt::Key_Down: {
            qsizetype nextLineStart{text.indexOf(u'\n', caret)};

            if (nextLineStart == -1) {
                break;
            }

            qsizetype nextLineEnd{text.indexOf(u'\n', ++nextLineStart)};
            if (nextLineEnd == -1) {
                nextLineEnd = text.size();
            }

            const qsizetype pos{m_curItem->caretPosInLine()};

            const qsizetype length{nextLineEnd - nextLineStart + 1};
            if (length >= pos) {
                m_curItem->setCaret(nextLineStart + pos - 1, false);
            } else {
                m_curItem->setCaret(nextLineEnd, false);
            }
            break;
        }
        case Qt::Key_A: {
            if (ev->modifiers() & Qt::ControlModifier) {
                m_curItem->setSelectionStart(0);
                m_curItem->setSelectionEnd(text.size());
            } else {
                handleDefaultCase();
            }
            break;
        }
        case Qt::Key_C: {
            if (ev->modifiers() & Qt::ControlModifier) {
                if (m_curItem->hasSelection()) {
                    QGuiApplication::clipboard()->setText(m_curItem->selectedText());
                }
            } else {
                handleDefaultCase();
            }
            break;
        }
        case Qt::Key_V: {
            if (ev->modifiers() & Qt::ControlModifier) {
                ev->setText(QGuiApplication::clipboard()->text());
            }

            handleDefaultCase();
            break;
        }
        case Qt::Key_X: {
            if (ev->modifiers() & Qt::ControlModifier) {
                if (m_curItem->hasSelection()) {
                    QGuiApplication::clipboard()->setText(m_curItem->selectedText());
                    m_curItem->deleteSelection();
                }
            } else {
                handleDefaultCase();
            }
            break;
        }
        default: {
            if (ev->modifiers() & Qt::ControlModifier) {
                break;
            }

            handleDefaultCase();
        }
        }

        context->spatialContext()->quadtree().deleteItem(m_curItem);
        context->spatialContext()->quadtree().insertItem(m_curItem);

        context->renderingContext()->cacheGrid().markAllDirty();
        context->renderingContext()->markForRender();
        context->renderingContext()->markForUpdate();
    }
}

void TextTool::keyReleased([[maybe_unused]] ApplicationContext *context)
{
}

void TextTool::cleanup()
{
    if (!m_curItem) {
        return;
    }

    auto spatialContext{m_context->spatialContext()};
    auto renderingContext{m_context->renderingContext()};
    auto uiContext{m_context->uiContext()};
    auto &transformer{spatialContext->coordinateTransformer()};
    auto &quadTree{spatialContext->quadtree()};

    m_curItem->setMode(TextItem::Mode::Normal);
    renderingContext->cacheGrid().markDirty(transformer.worldToGrid(m_curItem->boundingBox()).toRect());

    // enable keybindings again
    uiContext->keybindManager()->setEnabled(true);
    m_context->selectionContext()->setShouldRenderHandles(true);

    if (m_curItem->text().isEmpty()) {
        quadTree.deleteItem(m_curItem);
    }

    m_context->selectionContext()->reset();

    m_curItem = nullptr;
    renderingContext->markForRender();
    renderingContext->markForUpdate();
}

std::shared_ptr<TextItem> TextTool::curItem() const
{
    return m_curItem;
}

Tool::Type TextTool::type() const
{
    return Tool::Type::Text;
}

QString TextTool::icon() const
{
    return u"tool_text"_s;
}
