/****************************************************************************
** Copyright (c) 2016, Fougue SAS <https://www.fougue.pro>
** SPDX-License-Identifier: BSD-2-Clause
****************************************************************************/

#include "widget_occ_view_controller.h"
#include "widget_occ_view.h"
#include "qtgui_utils.h"
#include "theme.h"

#include <QtCore/QElapsedTimer>
#include <QtGui/QCursor>
#include <QtGui/QPainter>
#include <QtGui/QPixmap>
#include <QtGui/QMouseEvent>
#include <QtGui/QWheelEvent>
#include <QtWidgets/QRubberBand>
#include <QtWidgets/QStyleFactory>
#include <algorithm>

namespace Mayo {

namespace {

static const QCursor& rotateCursor()
{
    static QCursor cursor;
    if (!cursor.pixmap()) {
        constexpr int cursorHotX = 6;
        constexpr int cursorHotY = 8;
        const QPixmap pixmap(":/images/graphics/rotate_cursor.png");
        cursor = QCursor(pixmap, cursorHotX, cursorHotY);
    }

    return cursor;
}

#if OCC_VERSION_HEX >= 0x070600
using RubberBandWidget_ParentType = QWidget;
#else
using RubberBandWidget_ParentType = QRubberBand;
#endif

class RubberBandWidget : public RubberBandWidget_ParentType {
public:
    explicit RubberBandWidget(QWidget* parent)
#if OCC_VERSION_HEX >= 0x070600
        : RubberBandWidget_ParentType(parent)
    {}
#else
        : RubberBandWidget_ParentType(QRubberBand::Rectangle, parent)
    {
        // QWidget::setStyle() is important, set to windows style will just draw
        // rectangle frame, otherwise will draw a solid rectangle.
        this->setStyle(QStyleFactory::create("windows"));
    }
#endif

protected:
#if OCC_VERSION_HEX >= 0x070600
    void paintEvent(QPaintEvent*) override
    {
        QPainter painter(this);

        const QColor lineColor = mayoTheme()->color(Theme::Color::RubberBandView3d_Line);
        QColor fillColor = mayoTheme()->color(Theme::Color::RubberBandView3d_Fill);
        fillColor.setAlpha(60);
        QPen pen = painter.pen();
        pen.setColor(lineColor);
        pen.setWidth(2);
        pen.setCapStyle(Qt::FlatCap);
        pen.setJoinStyle(Qt::MiterJoin);

        painter.setPen(pen);
        painter.setBrush(fillColor);
        painter.drawRect(this->rect().adjusted(1, 1, -1, -1));
    }
#endif
};

} // namespace

WidgetOccViewController::WidgetOccViewController(IWidgetOccView* occView)
    : QObject(occView->widget()),
      V3dViewController(occView->v3dView()),
      m_occView(occView),
      m_navigStyle(View3dNavigationStyle::Catia),
      m_actionMatcher(createActionMatcher(m_navigStyle, m_inputSequence))
{
    m_occView->widget()->installEventFilter(this);
    m_inputSequence.setPrePushCallback([=](Input in) { m_actionMatcher->onInputPrePush(in); });
    m_inputSequence.setPreReleaseCallback([=](Input in) { m_actionMatcher->onInputPreRelease(in); });
    m_inputSequence.setClearCallback([=] { m_actionMatcher->onInputCleared(); });
}

WidgetOccViewController::~WidgetOccViewController()
{
}

bool WidgetOccViewController::eventFilter(QObject* watched, QEvent* event)
{
    if (watched != m_occView->widget())
        return false;

    if (event->type() == QEvent::Enter) {
        m_inputSequence.clear();
        m_occView->widget()->grabKeyboard();
        return false;
    }
    else if (event->type() == QEvent::Leave) {
        m_occView->widget()->releaseKeyboard();
        return false;
    }

    this->handleEvent(event);
    return false;
}

void WidgetOccViewController::setNavigationStyle(View3dNavigationStyle style)
{
    m_navigStyle = style;
    m_inputSequence.clear();
    m_actionMatcher = createActionMatcher(style, m_inputSequence);
}

void WidgetOccViewController::redrawView()
{
    //V3dViewController::redrawView();
    m_occView->redraw();
}

void WidgetOccViewController::startDynamicAction(V3dViewController::DynamicAction action)
{
    if (action == DynamicAction::Rotation)
        this->setViewCursor(rotateCursor());
    else if (action == DynamicAction::Panning)
        this->setViewCursor(Qt::SizeAllCursor);
    else if (action == DynamicAction::Zoom)
        this->setViewCursor(Qt::SizeVerCursor);
    else if (action == DynamicAction::WindowZoom)
        this->setViewCursor(Qt::SizeBDiagCursor);

    V3dViewController::startDynamicAction(action);
}

void WidgetOccViewController::stopDynamicAction()
{
    this->setViewCursor(Qt::ArrowCursor);
    V3dViewController::stopDynamicAction();
}

void WidgetOccViewController::setViewCursor(const QCursor &cursor)
{
    if (m_occView->widget())
        m_occView->widget()->setCursor(cursor);
}

struct WidgetOccViewController::RubberBand : public V3dViewController::IRubberBand {
    explicit RubberBand(QWidget* parent)
        : m_rubberBand(parent)
    {
    }

    void updateGeometry(int x, int y, int width, int height) override {
        m_rubberBand.setGeometry(x, y, width, height);
    }

    void setVisible(bool on) override {
        m_rubberBand.setVisible(on);
    }

private:
    RubberBandWidget m_rubberBand;
};

std::unique_ptr<V3dViewController::IRubberBand> WidgetOccViewController::createRubberBand()
{
    return std::make_unique<RubberBand>(m_occView->widget());
}

void WidgetOccViewController::handleEvent(const QEvent* event)
{
    switch (event->type()) {
    case QEvent::KeyPress:
        this->handleKeyPress(static_cast<const QKeyEvent*>(event));
        break;
    case QEvent::KeyRelease:
        this->handleKeyRelease(static_cast<const QKeyEvent*>(event));
        break;
    case QEvent::MouseButtonPress:
        this->handleMouseButtonPress(static_cast<const QMouseEvent*>(event));
        break;
    case QEvent::MouseMove:
        this->handleMouseMove(static_cast<const QMouseEvent*>(event));
        break;
    case QEvent::MouseButtonRelease:
        this->handleMouseButtonRelease(static_cast<const QMouseEvent*>(event));
        break;
    case QEvent::Wheel:
        this->handleMouseWheel(static_cast<const QWheelEvent*>(event));
        break;
    default:
        break;
    } // end switch
}

void WidgetOccViewController::handleKeyPress(const QKeyEvent* event)
{
    if (event->isAutoRepeat())
        return;

    m_inputSequence.push(event->key());
    if (m_inputSequence.equal({ Qt::Key_Space }))
        this->startInstantZoom(toPosition(m_occView->widget()->mapFromGlobal(QCursor::pos())));

    if (m_inputSequence.equal({ Qt::Key_Shift }) && !this->hasCurrentDynamicAction())
        this->signalMultiSelectionToggled.send(true);
}

void WidgetOccViewController::handleKeyRelease(const QKeyEvent* event)
{
    if (event->isAutoRepeat())
        return;

    m_inputSequence.release(event->key());
    if (!m_inputSequence.equal({}))
        return;

    if (m_inputSequence.lastInput() == Qt::Key_Space && this->currentDynamicAction() == DynamicAction::InstantZoom)
        this->stopInstantZoom();

    if (m_inputSequence.lastInput() == Qt::Key_Shift && !this->hasCurrentDynamicAction())
        this->signalMultiSelectionToggled.send(false);
}

void WidgetOccViewController::handleMouseButtonPress(const QMouseEvent* event)
{
    m_inputSequence.push(event->button());
    const QPoint currPos = m_occView->widget()->mapFromGlobal(QtGuiUtils::globalPosition(event));
    m_prevPos = toPosition(currPos);
}

void WidgetOccViewController::handleMouseMove(const QMouseEvent* event)
{
    const Position currPos = toPosition(m_occView->widget()->mapFromGlobal(QtGuiUtils::globalPosition(event)));
    const Position prevPos = m_prevPos;
    m_prevPos = currPos;
    if (m_actionMatcher->matchRotation())
        this->rotation(currPos);
    else if (m_actionMatcher->matchPan())
        this->pan(prevPos, currPos);
    else if (m_actionMatcher->matchZoom())
        this->zoom(prevPos, currPos);
    else if (m_actionMatcher->matchWindowZoom())
        this->windowZoomRubberBand(currPos);
    else
        this->signalMouseMoved.send(currPos.x, currPos.y);
}

void WidgetOccViewController::handleMouseButtonRelease(const QMouseEvent* event)
{
    auto fnOccMouseBtn = [](Qt::MouseButton btn) -> Aspect_VKeyMouse {
        switch (btn) {
        case Qt::NoButton: return Aspect_VKeyMouse_NONE;
        case Qt::LeftButton: return Aspect_VKeyMouse_LeftButton;
        case Qt::RightButton: return Aspect_VKeyMouse_RightButton;
        case Qt::MiddleButton: return Aspect_VKeyMouse_MiddleButton;
        default: return Aspect_VKeyMouse_UNKNOWN;
        }
    };

    m_inputSequence.release(event->button());
    const bool hadDynamicAction = this->hasCurrentDynamicAction();
    if (this->isWindowZoomingStarted())
        this->windowZoom(toPosition(m_occView->widget()->mapFromGlobal(QtGuiUtils::globalPosition(event))));

    this->stopDynamicAction();
    if (!hadDynamicAction)
        this->signalMouseButtonClicked.send(fnOccMouseBtn(event->button()));
}

void WidgetOccViewController::handleMouseWheel(const QWheelEvent* event)
{
    const QPoint delta = event->angleDelta();
    if (delta.y() > 0 || (delta.y() == 0 && delta.x() > 0))
        this->zoomIn();
    else
        this->zoomOut();
}

class WidgetOccViewController::Mayo_ActionMatcher : public ActionMatcher {
public:
    // Inherit ActionMatcher constructor
    using ActionMatcher::ActionMatcher;

    bool matchRotation() const override {
        return this->inputs.equal({ Qt::LeftButton });
    }

    bool matchPan() const override {
        return this->inputs.equal({ Qt::RightButton });
    }

    bool matchZoom() const override {
        return this->inputs.equal({ Qt::LeftButton, Qt::RightButton })
                || this->inputs.equal({ Qt::RightButton, Qt::LeftButton });
    }

    bool matchWindowZoom() const override {
        return this->inputs.equal({ Qt::Key_Control, Qt::LeftButton });
    }
};

class WidgetOccViewController::Catia_ActionMatcher : public ActionMatcher {
public:
    explicit Catia_ActionMatcher(const InputSequence& seq) : ActionMatcher(seq) {
        m_timer.start();
    }

    bool matchRotation() const override {
        return this->inputs.equal({ Qt::MiddleButton, Qt::LeftButton })
                || this->inputs.equal({ Qt::MiddleButton, Qt::RightButton });
    }

    bool matchPan() const override {
        return this->inputs.equal({ Qt::MiddleButton }) && !this->matchZoom();
    }

    bool matchZoom() const override {
        return this->inputs.equal({ Qt::MiddleButton })
                && m_beforeLastOp == InputSequence::Operation::Push
                && (m_beforeLastInput == Qt::LeftButton || m_beforeLastInput == Qt::RightButton)
                && this->inputs.lastOperation() == InputSequence::Operation::Release
                && this->inputs.lastInput() == m_beforeLastInput
                && (m_lastTimestamp_ms - m_beforeLastTimestamp_ms) < 750
                ;
    }

    bool matchWindowZoom() const override {
        return this->inputs.equal({ Qt::Key_Control, Qt::MiddleButton });
    }

    void onInputPrePush(Input /*in*/) override {
        this->recordBeforeLastOperation();
    }

    void onInputPreRelease(Input /*in*/) override {
        this->recordBeforeLastOperation();
    }

    void onInputCleared() override {
        m_beforeLastOp = InputSequence::Operation::None;
        m_beforeLastInput = -1;
        m_beforeLastTimestamp_ms = 0;
        m_lastTimestamp_ms = 0;
        m_timer.restart();
    }

private:
    void recordBeforeLastOperation() {
        m_beforeLastOp = this->inputs.lastOperation();
        m_beforeLastInput = this->inputs.lastInput();
        m_beforeLastTimestamp_ms = m_lastTimestamp_ms;
        m_lastTimestamp_ms = m_timer.elapsed();
    }

    InputSequence::Operation m_beforeLastOp = InputSequence::Operation::None;
    Input m_beforeLastInput = -1;
    int64_t m_beforeLastTimestamp_ms = 0;
    int64_t m_lastTimestamp_ms = 0;
    QElapsedTimer m_timer;
};

class WidgetOccViewController::SolidWorks_ActionMatcher : public ActionMatcher {
public:
    // Inherit ActionMatcher constructor
    using ActionMatcher::ActionMatcher;

    bool matchRotation() const override {
        return this->inputs.equal({ Qt::MiddleButton });
    }

    bool matchPan() const override {
        return this->inputs.equal({ Qt::Key_Control, Qt::MiddleButton });
    }

    bool matchZoom() const override {
        return this->inputs.equal({ Qt::Key_Shift, Qt::MiddleButton });
    }

    bool matchWindowZoom() const override {
        return this->inputs.equal({ Qt::Key_Control, Qt::LeftButton });
    }
};

class WidgetOccViewController::Unigraphics_ActionMatcher : public ActionMatcher {
public:
    // Inherit ActionMatcher constructor
    using ActionMatcher::ActionMatcher;

    bool matchRotation() const override {
        return this->inputs.equal({ Qt::MiddleButton });
    }

    bool matchPan() const override {
        return this->inputs.equal({ Qt::MiddleButton, Qt::RightButton });
    }

    bool matchZoom() const override {
        return this->inputs.equal({ Qt::MiddleButton, Qt::LeftButton });;
    }

    bool matchWindowZoom() const override {
        return this->inputs.equal({ Qt::Key_Control, Qt::LeftButton });
    }
};

class WidgetOccViewController::ProEngineer_ActionMatcher : public ActionMatcher {
public:
    // Inherit ActionMatcher constructor
    using ActionMatcher::ActionMatcher;

    bool matchRotation() const override {
        return this->inputs.equal({ Qt::MiddleButton });
    }

    bool matchPan() const override {
        return this->inputs.equal({ Qt::Key_Shift, Qt::MiddleButton });
    }

    bool matchZoom() const override {
        return this->inputs.equal({ Qt::Key_Control, Qt::MiddleButton });
    }

    bool matchWindowZoom() const override {
        return this->inputs.equal({ Qt::Key_Control, Qt::LeftButton });
    }
};

std::unique_ptr<WidgetOccViewController::ActionMatcher>
WidgetOccViewController::createActionMatcher(View3dNavigationStyle style, const InputSequence& seq)
{
    switch(style) {
    case View3dNavigationStyle::Mayo: return std::make_unique<Mayo_ActionMatcher>(seq);
    case View3dNavigationStyle::Catia: return std::make_unique<Catia_ActionMatcher>(seq);
    case View3dNavigationStyle::SolidWorks: return std::make_unique<SolidWorks_ActionMatcher>(seq);
    case View3dNavigationStyle::Unigraphics: return std::make_unique<Unigraphics_ActionMatcher>(seq);
    case View3dNavigationStyle::ProEngineer: return std::make_unique<ProEngineer_ActionMatcher>(seq);
    }
    return {};
}

void WidgetOccViewController::InputSequence::push(Input in)
{
    auto itFound = std::find(m_inputs.cbegin(), m_inputs.cend(), in);
    if (itFound != m_inputs.cend())
        m_inputs.erase(itFound);

    if (m_fnPrePushCallback)
        m_fnPrePushCallback(in);

    m_inputs.push_back(in);
    m_lastOperation = Operation::Push;
    m_lastInput = in;
}

void WidgetOccViewController::InputSequence::release(Input in)
{
    auto itRemoved = std::remove(m_inputs.begin(), m_inputs.end(), in);
    if (itRemoved != m_inputs.end()) {
        if (m_fnPreReleaseCallback)
            m_fnPreReleaseCallback(in);

        m_inputs.erase(itRemoved, m_inputs.end());
        m_lastOperation = Operation::Release;
        m_lastInput = in;
    }
}

void WidgetOccViewController::InputSequence::clear()
{
    m_inputs.clear();
    m_lastOperation = Operation::None;
    m_lastInput = -1;
    if (m_fnClearCallback)
        m_fnClearCallback();
}

bool WidgetOccViewController::InputSequence::equal(std::initializer_list<Input> other) const
{
    return std::equal(m_inputs.cbegin(), m_inputs.cend(), other.begin(), other.end());
}

} // namespace Mayo
