/*
    SPDX-FileCopyrightText: 2008-2009 Friedrich W. H. Kossebau <kossebau@kde.org>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include "mousenavigator.hpp"

// lib
#include <abstractbytearrayview.hpp>
#include <abstractbytearrayview_p.hpp>
#include <bytearraytableranges.hpp>
#include <bytearraytablecursor.hpp>
#include <bytearraytablelayout.hpp>
// Okteta core
#include <Okteta/TextByteArrayAnalyzer>
// Qt
#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <QMouseEvent>
#include <QDrag>
// Std
#include <chrono>

using namespace std::chrono_literals;

namespace Okteta {

static constexpr auto DefaultScrollTimerPeriod = 100ms;

MouseNavigator::MouseNavigator(AbstractByteArrayViewPrivate* view, AbstractMouseController* parent)
    : AbstractMouseController(view, parent)
    , mLMBPressed(false)
    , mInLMBDoubleClick(false)
    , mInLMBTripleClick(false)
    , mDragStartPossible(false)
{
    connect(&mScrollTimer,      &QTimer::timeout, this, &MouseNavigator::autoScrollTimerDone);
    connect(&mDragStartTimer,   &QTimer::timeout, this, &MouseNavigator::startDrag);
    mDragStartTimer.setSingleShot(true);
    m_tripleClickTimer.setSingleShot(true);

    // TODO: mView->q_func() not yet working here. needs explicit call to init(), find a nicer solution
}

void MouseNavigator::init()
{
    connect(mView->q_func(), &AbstractByteArrayView::readOnlyChanged, this, &MouseNavigator::updateCursorShape);
    updateCursorShape(mView->isEffectiveReadOnly());
}

MouseNavigator::~MouseNavigator() = default;

bool MouseNavigator::handleMousePressEvent(QMouseEvent* mouseEvent)
{
    bool eventUsed = false;

    if (mouseEvent->button() == Qt::LeftButton) {
        mLMBPressed = true;

        // select whole line?
        if (m_tripleClickTimer.isActive()
            && (mouseEvent->globalPosition().toPoint() - mDoubleClickPoint).manhattanLength() < QApplication::startDragDistance()) {
            m_tripleClickTimer.stop();
            mInLMBTripleClick = true;

            std::ignore = mView->selectLine(m_doubleClickIndex);
        } else {
            ByteArrayTableRanges* const tableRanges = mView->tableRanges();
            mView->pauseCursor();
            mView->finishByteEditor();

            // TODO: pos() is now, not at the moment of the event, use globalPos() for that,.says dox
            const QPoint mousePoint = mView->viewportToColumns(mouseEvent->pos());

            // start of a drag perhaps?
            if (tableRanges->hasSelection() && tableRanges->selectionIncludes(mView->indexByPoint(mousePoint))) {
                mDragStartPossible = true;
                mDragStartTimer.start(QApplication::startDragTime());
                mDragStartPoint = mousePoint;
            } else {
                mView->placeCursor(mousePoint);
                mView->ensureCursorVisible();

                const Address realIndex = mView->tableCursor()->realIndex();
                if (tableRanges->selectionStarted()) {
                    if (mouseEvent->modifiers() & Qt::SHIFT) {
                        tableRanges->setSelectionEnd(realIndex);
                    } else {
                        tableRanges->removeSelection();
                        tableRanges->setSelectionStart(realIndex);
                    }
                } else {   // start of a new selection possible
                    tableRanges->setSelectionStart(realIndex);

                    if (!mView->isEffectiveReadOnly() && (mouseEvent->modifiers() & Qt::SHIFT)) { // TODO: why only for readwrite?
                        tableRanges->setSelectionEnd(realIndex);
                    }
                }

                tableRanges->removeFurtherSelections();
            }

            if (tableRanges->isModified()) {
                mView->updateChanged();
                updateCursorShape(mView->isEffectiveReadOnly());
            }

            mView->unpauseCursor();
            mView->emitSelectionUpdates();
        }

        eventUsed = true;
    }

    return eventUsed ? true : AbstractMouseController::handleMousePressEvent(mouseEvent);
}

bool MouseNavigator::handleMouseMoveEvent(QMouseEvent* mouseEvent)
{
    bool eventUsed = false;

    if (mouseEvent->buttons() == Qt::LeftButton) {
        const QPoint movePoint = mView->viewportToColumns(mouseEvent->pos());

        if (mLMBPressed) {
            if (mDragStartPossible) {
                mDragStartTimer.stop();
                // moved enough for a drag?
                if ((movePoint - mDragStartPoint).manhattanLength() > QApplication::startDragDistance()) {
                    startDrag();
                }
                if (!mView->isEffectiveReadOnly()) {
                    mView->setMouseCursor(Qt::IBeamCursor);
                }
            } else {
                // selecting
                handleMouseMove(movePoint);
            }
        } else if (!mView->isEffectiveReadOnly()) {
            const ByteArrayTableRanges* const tableRanges = mView->tableRanges();

            // visual feedback for possible dragging
            const bool InSelection =
                tableRanges->hasSelection() && tableRanges->selectionIncludes(mView->indexByPoint(movePoint));
            mView->setMouseCursor(InSelection ? Qt::ArrowCursor : Qt::IBeamCursor);
        }
        eventUsed = true;
    }

    return eventUsed ? true : AbstractMouseController::handleMouseMoveEvent(mouseEvent);
}

bool MouseNavigator::handleMouseReleaseEvent(QMouseEvent* mouseEvent)
{
    bool eventUsed = false;

    if (mouseEvent->button() == Qt::LeftButton) {
        ByteArrayTableRanges* const tableRanges = mView->tableRanges();

//         const QPoint releasePoint = mView->viewportToColumns( mouseEvent->pos() );

        // this is not the release of a double-click so we need to process it?
        if (!mInLMBDoubleClick) {
//             const int line = mView->lineAt( releasePoint.y() );
//             const int pos = mActiveColumn->linePositionOfX( releasePoint.x() ); // TODO: can we be sure here about the active column?
//             const Address index = tableLayout->indexAtCCoord( Coord(pos,line) ); // TODO: can this be another index than the one of the cursor???
//             Q_EMIT mView->clicked( index ); // TODO: who needs this?
        }

        if (mLMBPressed) {
            mLMBPressed = false;

            if (mScrollTimer.isActive()) {
                mScrollTimer.stop();
            }

            // was only click inside selection, nothing dragged?
            if (mDragStartPossible) {
                mView->selectAll(false);
                mDragStartTimer.stop();
                mDragStartPossible = false;

                mView->placeCursor(mDragStartPoint);
                mView->ensureCursorVisible();

                mView->unpauseCursor();
            }
            // was end of selection operation?
            else if (tableRanges->hasSelection()) {
                if (QApplication::clipboard()->supportsSelection()) {
                    mView->copyToClipboard(QClipboard::Selection);
                }
            }
        }

        mView->emitCursorPositionChanged();

        mInLMBDoubleClick = false;
        mInLMBTripleClick = false;

        if (tableRanges->selectionJustStarted()) {
            tableRanges->removeSelection();
        }

        mView->emitSelectionUpdates();
        eventUsed = true;
    }

    return eventUsed ? true : AbstractMouseController::handleMouseReleaseEvent(mouseEvent);
}

bool MouseNavigator::handleMouseDoubleClickEvent(QMouseEvent* mouseEvent)
{
    bool eventUsed = false;

    if (mouseEvent->button() == Qt::LeftButton) {
        const ByteArrayTableCursor* const tableCursor = mView->tableCursor();

        const Address index = tableCursor->validIndex();

        m_doubleClickIndex = index;

        std::ignore = mView->selectGroup(index);

        // as we already have a double-click maybe it is a triple-click
        m_tripleClickTimer.start(QApplication::doubleClickInterval());
        mDoubleClickPoint = mouseEvent->globalPosition().toPoint();

        // mValueEditor->goInsideByte(); TODO: make this possible again in some way?

        mInLMBDoubleClick = true; //
        mLMBPressed = true;

        mView->emitDoubleClicked(index);
        eventUsed = true;
    }

    return eventUsed ? true : AbstractMouseController::handleMouseDoubleClickEvent(mouseEvent);
}

void MouseNavigator::autoScrollTimerDone()
{
    if (mLMBPressed) {
        handleMouseMove(mView->viewportToColumns(mView->mapViewportFromGlobal(QCursor::pos())));
    }
}

void MouseNavigator::updateCursorShape(bool isReadOnly)
{
    mView->setMouseCursor(isReadOnly ? Qt::ArrowCursor : Qt::IBeamCursor);
}

void MouseNavigator::handleMouseMove(QPoint point)   // handles the move of the mouse with pressed buttons
{
    ByteArrayTableRanges* const tableRanges = mView->tableRanges();

    const int yOffset = mView->yOffset();
    const int behindLastYOffset = yOffset + mView->visibleHeight();
    // scrolltimer but inside of viewport?
    if (mScrollTimer.isActive()) {
        if (yOffset <= point.y() && point.y() < behindLastYOffset) {
            mScrollTimer.stop();
        }
    }
    // no scrolltimer and outside of viewport?
    else {
        if (point.y() < yOffset || behindLastYOffset <= point.y()) {
            mScrollTimer.start(DefaultScrollTimerPeriod);
        }
    }
    mView->pauseCursor();

    mView->placeCursor(point);
    mView->ensureCursorVisible();

    // do group-wise selection?
    if ((mInLMBDoubleClick || mInLMBTripleClick) && tableRanges->hasFirstGroupSelection()) {
        ByteArrayTableCursor* const tableCursor = mView->tableCursor();
        Address newIndex = tableCursor->realIndex();
        const AddressRange firstGroupSelection = tableRanges->firstGroupSelection();
        // are we before the selection?
        if (firstGroupSelection.startsBehind(newIndex)) {
            tableRanges->ensureGroupSelectionForward(false);
            if (mInLMBTripleClick) {
                newIndex = mView->tableLayout()->indexAtLineStart(newIndex);
            } else {
                if (mView->activeCoding() == AbstractByteArrayView::CharCodingId) {
                    const TextByteArrayAnalyzer textAnalyzer(mView->byteArrayModel(), mView->charCodec());
                    newIndex = textAnalyzer.indexOfLeftWordSelect(newIndex);
                    // prevent picking word border behind the anchor section
                    if (firstGroupSelection.startsBefore(newIndex)) {
                        newIndex = firstGroupSelection.start();
                    }
                } else {
                    const int noOfGroupedBytes = mView->q_func()->noOfGroupedBytes();
                    newIndex = mView->tableLayout()->indexAtGroupStart(newIndex, noOfGroupedBytes);
                }
            }
        }
        // or behind?
        else if (firstGroupSelection.endsBefore(newIndex - 1)) {
            tableRanges->ensureGroupSelectionForward(true);
            const Address indexBefore = newIndex - 1;
            if (mInLMBTripleClick) {
                newIndex = mView->tableLayout()->indexAtLineEnd(indexBefore) +  1;
            } else {
                if (mView->activeCoding() == AbstractByteArrayView::CharCodingId) {
                    const TextByteArrayAnalyzer textAnalyzer(mView->byteArrayModel(), mView->charCodec());
                    newIndex = textAnalyzer.indexOfRightWordSelect(indexBefore);
                    // prevent picking word border before the anchor section
                    if (firstGroupSelection.endsBehind(newIndex - 1)) {
                        newIndex = firstGroupSelection.nextBehindEnd();
                    }
                } else {
                    const int noOfGroupedBytes = mView->q_func()->noOfGroupedBytes();
                    newIndex = mView->tableLayout()->indexAtGroupEnd(indexBefore, noOfGroupedBytes) + 1;
                }
            }
        }
        // or inside?
        else {
            tableRanges->ensureGroupSelectionForward(true);
            newIndex = firstGroupSelection.nextBehindEnd();
        }

        tableCursor->gotoCIndex(newIndex);
    }

    if (tableRanges->selectionStarted()) {
        tableRanges->setSelectionEnd(mView->cursorPosition());
    }

    mView->updateChanged();
    mView->unpauseCursor();
    mView->emitSelectionUpdates();
}

void MouseNavigator::startDrag()
{
    // reset states
    mLMBPressed = false;
    mInLMBDoubleClick = false;
    mInLMBTripleClick = false;
    mDragStartPossible = false;

    // create data
    auto dragData = mView->selectedBytesAsMimeData();
    if (!dragData) {
        return;
    }

    // QDrag instance owned by Qt, gets deleted internally by deleteLater after exec() call
    auto* const drag = new QDrag(mView->q_func());
    drag->setMimeData(dragData.release());

    Qt::DropActions request = (mView->isEffectiveReadOnly() || mView->isOverwriteMode()) ? Qt::CopyAction : Qt::CopyAction | Qt::MoveAction;
    Qt::DropAction dropAction = drag->exec(request);

    if (dropAction == Qt::MoveAction) {
        auto* const targetByteArrayView = qobject_cast<AbstractByteArrayView*>(drag->target());
        // Not inside this widget itself?
        if (!targetByteArrayView
            || targetByteArrayView->byteArrayModel() != mView->byteArrayModel()) {
            mView->removeSelectedBytes();
        }
    }
}

}

#include "moc_mousenavigator.cpp"
