// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017-2019 Alejandro Sirgo Rica & Contributors

#include "selectionwidget.h"
#include "utils/globalvalues.h"
#include "widgets/capture/capturetoolbutton.h"

#include <QApplication>
#include <QEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPropertyAnimation>
#include <QTimer>
#include <utility>

#define MARGIN (m_THandle.width())

SelectionWidget::SelectionWidget(QColor c, QWidget* parent)
  : QWidget(parent)
  , m_color(std::move(c))
  , m_activeSide(NO_SIDE)
  , m_ignoreMouse(false)
{
    // prevents this widget from consuming CaptureToolButton mouse events
    setAttribute(Qt::WA_TransparentForMouseEvents);
    parent->installEventFilter(this);

    m_animation = new QPropertyAnimation(this, "geometry", this);
    m_animation->setEasingCurve(QEasingCurve::InOutQuad);
    m_animation->setDuration(200);
    connect(m_animation, &QPropertyAnimation::finished, this, [this]() {
        emit geometrySettled();
    });

    int sideVal = GlobalValues::buttonBaseSize() * 0.6;
    int handleSide = sideVal / 2;
    const QRect areaRect(0, 0, sideVal, sideVal);

    const QRect handleRect(0, 0, handleSide, handleSide);
    m_TLHandle = m_TRHandle = m_BLHandle = m_BRHandle = m_LHandle = m_THandle =
      m_RHandle = m_BHandle = handleRect;
    m_TLArea = m_TRArea = m_BLArea = m_BRArea = areaRect;

    m_areaOffset = QPoint(-sideVal / 2, -sideVal / 2);
    m_handleOffset = QPoint(-handleSide / 2, -handleSide / 2);
}

/**
 * @brief Get the side where the mouse cursor is.
 * @param mousePos Mouse cursor position relative to the parent widget.
 */
SelectionWidget::SideType SelectionWidget::getMouseSide(
  const QPoint& mousePos) const
{
    if (!isVisible()) {
        return NO_SIDE;
    }
    QPoint localPos = mapFromParent(mousePos);
    if (m_TLArea.contains(localPos)) {
        return TOPLEFT_SIDE;
    } else if (m_TRArea.contains(localPos)) {
        return TOPRIGHT_SIDE;
    } else if (m_BLArea.contains(localPos)) {
        return BOTTOMLEFT_SIDE;
    } else if (m_BRArea.contains(localPos)) {
        return BOTTOMRIGHT_SIDE;
    } else if (m_LArea.contains(localPos)) {
        return LEFT_SIDE;
    } else if (m_TArea.contains(localPos)) {
        return TOP_SIDE;
    } else if (m_RArea.contains(localPos)) {
        return RIGHT_SIDE;
    } else if (m_BArea.contains(localPos)) {
        return BOTTOM_SIDE;
    } else if (rect().contains(localPos)) {
        return CENTER;
    } else {
        return NO_SIDE;
    }
}

QVector<QRect> SelectionWidget::handlerAreas()
{
    QVector<QRect> areas;
    areas << m_TLHandle << m_TRHandle << m_BLHandle << m_BRHandle << m_LHandle
          << m_THandle << m_RHandle << m_BHandle;
    return areas;
}

// helper function
SelectionWidget::SideType getProperSide(SelectionWidget::SideType side,
                                        const QRect& r)
{
    using SideType = SelectionWidget::SideType;
    int intSide = side;
    if (r.right() < r.left()) {
        intSide ^= SideType::LEFT_SIDE;
        intSide ^= SideType::RIGHT_SIDE;
    }
    if (r.bottom() < r.top()) {
        intSide ^= SideType::TOP_SIDE;
        intSide ^= SideType::BOTTOM_SIDE;
    }

    return (SideType)intSide;
}

void SelectionWidget::setIgnoreMouse(bool ignore)
{
    m_ignoreMouse = ignore;
    updateCursor();
}

/**
 * Set the cursor that will be active when the mouse is inside the selection and
 * the mouse is not clicked.
 */
void SelectionWidget::setIdleCentralCursor(const QCursor& cursor)
{
    m_idleCentralCursor = cursor;
}

void SelectionWidget::setGeometryAnimated(const QRect& r)
{
    if (isVisible()) {
        m_animation->setStartValue(geometry());
        m_animation->setEndValue(r);
        m_animation->start();
    }
}

void SelectionWidget::setGeometry(const QRect& r)
{
    QWidget::setGeometry(r + QMargins(MARGIN, MARGIN, MARGIN, MARGIN));
    updateCursor();
    if (isVisible()) {
        emit geometryChanged();
    }
}

QRect SelectionWidget::geometry() const
{
    return QWidget::geometry() - QMargins(MARGIN, MARGIN, MARGIN, MARGIN);
}

QRect SelectionWidget::fullGeometry() const
{
    return QWidget::geometry();
}

QRect SelectionWidget::rect() const
{
    return QWidget::rect() - QMargins(MARGIN, MARGIN, MARGIN, MARGIN);
}

bool SelectionWidget::eventFilter(QObject* obj, QEvent* event)
{
    if (m_ignoreMouse && dynamic_cast<QMouseEvent*>(event)) {
        m_activeSide = NO_SIDE;
        unsetCursor();
    } else if (event->type() == QEvent::MouseButtonRelease) {
        parentMouseReleaseEvent(static_cast<QMouseEvent*>(event));
    } else if (event->type() == QEvent::MouseButtonPress) {
        parentMousePressEvent(static_cast<QMouseEvent*>(event));
    } else if (event->type() == QEvent::MouseMove) {
        parentMouseMoveEvent(static_cast<QMouseEvent*>(event));
    }
    return false;
}

void SelectionWidget::parentMousePressEvent(QMouseEvent* e)
{
    if (e->button() != Qt::LeftButton) {
        return;
    }

    m_dragStartPos = e->pos();
    m_activeSide = getMouseSide(e->pos());
}

void SelectionWidget::parentMouseReleaseEvent(QMouseEvent* e)
{
    // released outside of the selection area
    if (!getMouseSide(e->pos())) {
        hide();
    }

    m_activeSide = NO_SIDE;
    updateCursor();
    emit geometrySettled();
}

void SelectionWidget::parentMouseMoveEvent(QMouseEvent* e)
{
    updateCursor();

    if (e->buttons() != Qt::LeftButton) {
        return;
    }

    SideType mouseSide = m_activeSide;
    if (!m_activeSide) {
        mouseSide = getMouseSide(e->pos());
    }

    QPoint pos;

    if (!isVisible() || !mouseSide) {
        show();
        m_activeSide = TOPLEFT_SIDE;
        pos = m_dragStartPos;
        setGeometry({ pos, pos });
    } else {
        pos = e->pos();
    }

    auto geom = geometry();
    float aspectRatio = (float)geom.width() / (float)geom.height();
    bool symmetryMod = qApp->keyboardModifiers() & Qt::ShiftModifier;
    bool preserveAspect = qApp->keyboardModifiers() & Qt::ControlModifier;

    QPoint newTopLeft = geom.topLeft(), newBottomRight = geom.bottomRight();
    int oldLeft = newTopLeft.rx(), oldRight = newBottomRight.rx(),
        oldTop = newTopLeft.ry(), oldBottom = newBottomRight.ry();
    int &newLeft = newTopLeft.rx(), &newRight = newBottomRight.rx(),
        &newTop = newTopLeft.ry(), &newBottom = newBottomRight.ry();
    switch (mouseSide) {
        case TOPLEFT_SIDE:
            if (m_activeSide) {
                if (preserveAspect) {
                    if ((float)(oldRight - pos.x()) /
                          (float)(oldBottom - pos.y()) >
                        aspectRatio) {
                        /* width longer than expected width, hence increase
                         * height to compensate for the aspect ratio */
                        newLeft = pos.x();
                        newTop =
                          oldBottom -
                          (int)(((float)(oldRight - pos.x())) / aspectRatio);
                    } else {
                        /* height longer than expected height, hence increase
                         * width to compensate for the aspect ratio */
                        newTop = pos.y();
                        newLeft =
                          oldRight -
                          (int)(((float)(oldBottom - pos.y())) * aspectRatio);
                    }
                } else {
                    newTopLeft = pos;
                }
            }
            break;
        case BOTTOMRIGHT_SIDE:
            if (m_activeSide) {
                if (preserveAspect) {
                    if ((float)(pos.x() - oldLeft) / (float)(pos.y() - oldTop) >
                        aspectRatio) {
                        newRight = pos.x();
                        newBottom =
                          oldTop +
                          (int)(((float)(pos.x() - oldLeft)) / aspectRatio);
                    } else {
                        newBottom = pos.y();
                        newRight = oldLeft + (int)(((float)(pos.y() - oldTop)) *
                                                   aspectRatio);
                    }
                } else {
                    newBottomRight = pos;
                }
            }
            break;
        case TOPRIGHT_SIDE:
            if (m_activeSide) {
                if (preserveAspect) {
                    if ((float)(pos.x() - oldLeft) /
                          (float)(oldBottom - pos.y()) >
                        aspectRatio) {
                        newRight = pos.x();
                        newTop =
                          oldBottom -
                          (int)(((float)(pos.x() - oldLeft)) / aspectRatio);
                    } else {
                        newTop = pos.y();
                        newRight =
                          oldLeft +
                          (int)(((float)(oldBottom - pos.y())) * aspectRatio);
                    }
                } else {
                    newTop = pos.y();
                    newRight = pos.x();
                }
            }
            break;
        case BOTTOMLEFT_SIDE:
            if (m_activeSide) {
                if (preserveAspect) {
                    if ((float)(oldRight - pos.x()) /
                          (float)(pos.y() - oldTop) >
                        aspectRatio) {
                        newLeft = pos.x();
                        newBottom =
                          oldTop +
                          (int)(((float)(oldRight - pos.x())) / aspectRatio);
                    } else {
                        newBottom = pos.y();
                        newLeft = oldRight - (int)(((float)(pos.y() - oldTop)) *
                                                   aspectRatio);
                    }
                } else {
                    newBottom = pos.y();
                    newLeft = pos.x();
                }
            }
            break;
        case LEFT_SIDE:
            if (m_activeSide) {
                newLeft = pos.x();
                if (preserveAspect) {
                    /* By default bottom edge moves when dragging sides, this
                     * behavior feels natural */
                    newBottom = oldTop + (int)(((float)(oldRight - pos.x())) /
                                               aspectRatio);
                }
            }
            break;
        case RIGHT_SIDE:
            if (m_activeSide) {
                newRight = pos.x();
                if (preserveAspect) {
                    newBottom = oldTop + (int)(((float)(pos.x() - oldLeft)) /
                                               aspectRatio);
                }
            }
            break;
        case TOP_SIDE:
            if (m_activeSide) {
                newTop = pos.y();
                if (preserveAspect) {
                    /* By default right edge moves when dragging sides, this
                     * behavior feels natural */
                    newRight =
                      oldLeft +
                      (int)(((float)(oldBottom - pos.y()) * aspectRatio));
                }
            }
            break;
        case BOTTOM_SIDE:
            if (m_activeSide) {
                newBottom = pos.y();
                if (preserveAspect) {
                    newRight = oldLeft +
                               (int)(((float)(pos.y() - oldTop) * aspectRatio));
                }
            }
            break;
        default:
            if (m_activeSide) {
                move(this->pos() + pos - m_dragStartPos);
                m_dragStartPos = pos;
                /* do nothing special in case of preserveAspect */
            }
            return;
    }
    // finalize geometry change
    if (m_activeSide) {
        if (symmetryMod) {
            QPoint deltaTopLeft = newTopLeft - geom.topLeft();
            QPoint deltaBottomRight = newBottomRight - geom.bottomRight();
            newTopLeft = geom.topLeft() + deltaTopLeft - deltaBottomRight;
            newBottomRight =
              geom.bottomRight() + deltaBottomRight - deltaTopLeft;
        }
        geom = { newTopLeft, newBottomRight };
        setGeometry(geom.normalized());
        m_activeSide = getProperSide(m_activeSide, geom);
    }
    m_dragStartPos = e->pos();
}

void SelectionWidget::paintEvent(QPaintEvent*)
{
    QPainter p(this);
    if (!p.isActive()) {
        return;
    }
    p.setPen(m_color);
    p.drawRect(rect() + QMargins(0, 0, -1, -1));
    p.setRenderHint(QPainter::Antialiasing);
    p.setBrush(m_color);
    for (auto rectangle : handlerAreas()) {
        p.drawEllipse(rectangle);
    }
}

void SelectionWidget::resizeEvent(QResizeEvent*)
{
    updateAreas();
    if (isVisible()) {
        emit geometryChanged();
    }
}

void SelectionWidget::moveEvent(QMoveEvent*)
{
    updateAreas();
    if (isVisible()) {
        emit geometryChanged();
    }
}

void SelectionWidget::showEvent(QShowEvent*)
{
    emit visibilityChanged();
}

void SelectionWidget::hideEvent(QHideEvent*)
{
    emit visibilityChanged();
}

void SelectionWidget::updateColor(const QColor& c)
{
    m_color = c;
}

void SelectionWidget::moveLeft()
{
    setGeometryByKeyboard(geometry().adjusted(-1, 0, -1, 0));
}

void SelectionWidget::moveRight()
{
    setGeometryByKeyboard(geometry().adjusted(1, 0, 1, 0));
}

void SelectionWidget::moveUp()
{
    setGeometryByKeyboard(geometry().adjusted(0, -1, 0, -1));
}

void SelectionWidget::moveDown()
{
    setGeometryByKeyboard(geometry().adjusted(0, 1, 0, 1));
}

void SelectionWidget::resizeLeft()
{
    setGeometryByKeyboard(geometry().adjusted(0, 0, -1, 0));
}

void SelectionWidget::resizeRight()
{
    setGeometryByKeyboard(geometry().adjusted(0, 0, 1, 0));
}

void SelectionWidget::resizeUp()
{
    setGeometryByKeyboard(geometry().adjusted(0, 0, 0, -1));
}

void SelectionWidget::resizeDown()
{
    setGeometryByKeyboard(geometry().adjusted(0, 0, 0, 1));
}

void SelectionWidget::symResizeLeft()
{
    setGeometryByKeyboard(geometry().adjusted(1, 0, -1, 0));
}

void SelectionWidget::symResizeRight()
{
    setGeometryByKeyboard(geometry().adjusted(-1, 0, 1, 0));
}

void SelectionWidget::symResizeUp()
{
    setGeometryByKeyboard(geometry().adjusted(0, -1, 0, 1));
}

void SelectionWidget::symResizeDown()
{
    setGeometryByKeyboard(geometry().adjusted(0, 1, 0, -1));
}

void SelectionWidget::updateAreas()
{
    QRect r = rect();
    m_TLArea.moveTo(r.topLeft() + m_areaOffset);
    m_TRArea.moveTo(r.topRight() + m_areaOffset);
    m_BLArea.moveTo(r.bottomLeft() + m_areaOffset);
    m_BRArea.moveTo(r.bottomRight() + m_areaOffset);

    m_LArea = QRect(m_TLArea.bottomLeft(), m_BLArea.topRight());
    m_TArea = QRect(m_TLArea.topRight(), m_TRArea.bottomLeft());
    m_RArea = QRect(m_TRArea.bottomLeft(), m_BRArea.topRight());
    m_BArea = QRect(m_BLArea.topRight(), m_BRArea.bottomLeft());

    m_TLHandle.moveTo(m_TLArea.center() + m_handleOffset);
    m_BLHandle.moveTo(m_BLArea.center() + m_handleOffset);
    m_TRHandle.moveTo(m_TRArea.center() + m_handleOffset);
    m_BRHandle.moveTo(m_BRArea.center() + m_handleOffset);
    m_LHandle.moveTo(m_LArea.center() + m_handleOffset);
    m_THandle.moveTo(m_TArea.center() + m_handleOffset);
    m_RHandle.moveTo(m_RArea.center() + m_handleOffset);
    m_BHandle.moveTo(m_BArea.center() + m_handleOffset);
}

void SelectionWidget::updateCursor()
{
    SideType mouseSide = m_activeSide;
    if (!m_activeSide) {
        mouseSide = getMouseSide(parentWidget()->mapFromGlobal(QCursor::pos()));
    }

    switch (mouseSide) {
        case TOPLEFT_SIDE:
            setCursor(Qt::SizeFDiagCursor);
            break;
        case BOTTOMRIGHT_SIDE:
            setCursor(Qt::SizeFDiagCursor);
            break;
        case TOPRIGHT_SIDE:
            setCursor(Qt::SizeBDiagCursor);
            break;
        case BOTTOMLEFT_SIDE:
            setCursor(Qt::SizeBDiagCursor);
            break;
        case LEFT_SIDE:
            setCursor(Qt::SizeHorCursor);
            break;
        case RIGHT_SIDE:
            setCursor(Qt::SizeHorCursor);
            break;
        case TOP_SIDE:
            setCursor(Qt::SizeVerCursor);
            break;
        case BOTTOM_SIDE:
            setCursor(Qt::SizeVerCursor);
            break;
        default:
            if (m_activeSide) {
                setCursor(Qt::ClosedHandCursor);
            } else {
                setCursor(m_idleCentralCursor);
                return;
            }
            break;
    }
}

void SelectionWidget::setGeometryByKeyboard(const QRect& r)
{
    static QTimer timer;
    QRect rectangle = r.intersected(parentWidget()->rect());
    if (rectangle.width() <= 0) {
        rectangle.setWidth(1);
    }
    if (rectangle.height() <= 0) {
        rectangle.setHeight(1);
    }
    setGeometry(rectangle);
    connect(&timer,
            &QTimer::timeout,
            this,
            &SelectionWidget::geometrySettled,
            Qt::UniqueConnection);
    timer.start(400);
}
