// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "splineeditor.h"
#include "segmentproperties.h"

#include <QPainter>
#include <QPainterPath>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QDebug>
#include <QApplication>
#include <QVector>
#include <QPainterPath>

const int canvasWidth = 640;
const int canvasHeight = 320;

const int canvasMargin = 160;

SplineEditor::SplineEditor(QWidget *parent) :
    QWidget(parent), m_pointListWidget(nullptr), m_block(false)
{
    setFixedSize(canvasWidth + canvasMargin * 2, canvasHeight + canvasMargin * 2);

    m_controlPoints.append(QPointF(0.4, 0.075));
    m_controlPoints.append(QPointF(0.45,0.24));
    m_controlPoints.append(QPointF(0.5,0.5));

    m_controlPoints.append(QPointF(0.55,0.76));
    m_controlPoints.append(QPointF(0.7,0.9));
    m_controlPoints.append(QPointF(1.0, 1.0));

    m_numberOfSegments = 2;

    m_activeControlPoint = -1;

    m_mouseDrag = false;

    m_pointContextMenu = new QMenu(this);
    m_deleteAction = new QAction(tr("Delete point"), m_pointContextMenu);
    m_smoothAction = new QAction(tr("Smooth point"), m_pointContextMenu);
    m_cornerAction = new QAction(tr("Corner point"), m_pointContextMenu);

    m_smoothAction->setCheckable(true);

    m_pointContextMenu->addAction(m_deleteAction);
    m_pointContextMenu->addAction(m_smoothAction);
    m_pointContextMenu->addAction(m_cornerAction);

    m_curveContextMenu = new QMenu(this);

    m_addPoint = new QAction(tr("Add point"), m_pointContextMenu);

    m_curveContextMenu->addAction(m_addPoint);

    initPresets();

    invalidateSmoothList();
}

static inline QPointF mapToCanvas(const QPointF &point)
{
    return QPointF(point.x() * canvasWidth + canvasMargin,
                   canvasHeight - point.y() * canvasHeight + canvasMargin);
}

static inline QPointF mapFromCanvas(const QPointF &point)
{
    return QPointF((point.x() - canvasMargin) / canvasWidth ,
                   1 - (point.y() - canvasMargin) / canvasHeight);
}

static inline void paintControlPoint(const QPointF &controlPoint, QPainter *painter, bool edit,
                                     bool realPoint, bool active, bool smooth)
{
    int pointSize = 4;

    if (active)
        painter->setBrush(QColor(140, 140, 240, 255));
    else
        painter->setBrush(QColor(120, 120, 220, 255));

    if (realPoint) {
        pointSize = 6;
        painter->setBrush(QColor(80, 80, 210, 150));
    }

    painter->setPen(QColor(50, 50, 50, 140));

    if (!edit)
        painter->setBrush(QColor(160, 80, 80, 250));

    if (smooth) {
        painter->drawEllipse(QRectF(mapToCanvas(controlPoint).x() - pointSize + 0.5,
                                    mapToCanvas(controlPoint).y() - pointSize + 0.5,
                                    pointSize * 2, pointSize * 2));
    } else {
        painter->drawRect(QRectF(mapToCanvas(controlPoint).x() - pointSize + 0.5,
                                 mapToCanvas(controlPoint).y() - pointSize + 0.5,
                                 pointSize * 2, pointSize * 2));
    }
}

static inline bool indexIsRealPoint(int i)
{
    return  !((i + 1) % 3);
}

static inline int pointForControlPoint(int i)
{
    if ((i % 3) == 0)
        return i - 1;

    if ((i % 3) == 1)
        return i + 1;

    return i;
}

void drawCleanLine(QPainter *painter, const QPoint p1, QPoint p2)
{
    painter->drawLine(p1 + QPointF(0.5 , 0.5), p2 + QPointF(0.5, 0.5));
}

void SplineEditor::paintEvent(QPaintEvent *)
{
    QPainter painter(this);

    QPen pen(Qt::black);
    pen.setWidth(1);
    painter.fillRect(0,0,width() - 1, height() - 1, QBrush(Qt::white));
    painter.drawRect(0,0,width() - 1, height() - 1);

    painter.setRenderHint(QPainter::Antialiasing);

    pen = QPen(Qt::gray);
    pen.setWidth(1);
    pen.setStyle(Qt::DashLine);
    painter.setPen(pen);
    drawCleanLine(&painter,mapToCanvas(QPoint(0, 0)).toPoint(), mapToCanvas(QPoint(1, 0)).toPoint());
    drawCleanLine(&painter,mapToCanvas(QPoint(0, 1)).toPoint(), mapToCanvas(QPoint(1, 1)).toPoint());

    for (int i = 0; i < m_numberOfSegments; i++) {
        QPainterPath path;
        QPointF p0;

        if (i == 0)
            p0 = mapToCanvas(QPointF(0.0, 0.0));
        else
            p0 = mapToCanvas(m_controlPoints.at(i * 3 - 1));

        path.moveTo(p0);

        QPointF p1 = mapToCanvas(m_controlPoints.at(i * 3));
        QPointF p2 = mapToCanvas(m_controlPoints.at(i * 3 + 1));
        QPointF p3 = mapToCanvas(m_controlPoints.at(i * 3 + 2));
        path.cubicTo(p1, p2, p3);
        painter.strokePath(path, QPen(QBrush(Qt::black), 2));

        QPen pen(Qt::black);
        pen.setWidth(1);
        pen.setStyle(Qt::DashLine);
        painter.setPen(pen);
        painter.drawLine(p0, p1);
        painter.drawLine(p3, p2);
    }

    paintControlPoint(QPointF(0.0, 0.0), &painter, false, true, false, false);
    paintControlPoint(QPointF(1.0, 1.0), &painter, false, true, false, false);

    for (int i = 0; i < m_controlPoints.size() - 1; ++i)
        paintControlPoint(m_controlPoints.at(i),
                          &painter,
                          true,
                          indexIsRealPoint(i),
                          i == m_activeControlPoint,
                          isControlPointSmooth(i));
}

void SplineEditor::mousePressEvent(QMouseEvent *e)
{
    if (e->button() == Qt::LeftButton) {
        m_activeControlPoint = findControlPoint(e->position().toPoint());

        if (m_activeControlPoint != -1) {
            mouseMoveEvent(e);
        }
        m_mousePress = e->position().toPoint();
        e->accept();
    }
}

void SplineEditor::mouseReleaseEvent(QMouseEvent *e)
{
    if (e->button() == Qt::LeftButton) {
        m_activeControlPoint = -1;

        m_mouseDrag = false;
        e->accept();
    }
}

#if QT_CONFIG(contextmenu)
void SplineEditor::contextMenuEvent(QContextMenuEvent *e)
{
    int index = findControlPoint(e->pos());

    if (index > 0 && indexIsRealPoint(index)) {
        m_smoothAction->setChecked(isControlPointSmooth(index));
        QAction* action = m_pointContextMenu->exec(e->globalPos());
        if (action == m_deleteAction)
            deletePoint(index);
        else if (action == m_smoothAction)
            smoothPoint(index);
        else if (action == m_cornerAction)
            cornerPoint(index);
    } else {
        QAction* action = m_curveContextMenu->exec(e->globalPos());
        if (action == m_addPoint)
            addPoint(e->pos());
    }
}
#endif // contextmenu

void SplineEditor::invalidate()
{
    QEasingCurve easingCurve(QEasingCurve::BezierSpline);

    for (int i = 0; i < m_numberOfSegments; ++i) {
        easingCurve.addCubicBezierSegment(m_controlPoints.at(i * 3),
                                          m_controlPoints.at(i * 3 + 1),
                                          m_controlPoints.at(i * 3 + 2));
    }
    setEasingCurve(easingCurve);
    invalidateSegmentProperties();
}

void SplineEditor::invalidateSmoothList()
{
    m_smoothList.clear();

    for (int i = 0; i < (m_numberOfSegments - 1); ++i)
        m_smoothList.append(isSmooth(i * 3 + 2));

}

void SplineEditor::invalidateSegmentProperties()
{
    for (int i = 0; i < m_numberOfSegments; ++i) {
        SegmentProperties *segmentProperties = m_segmentProperties.at(i);
        bool smooth = false;
        if (i < (m_numberOfSegments - 1)) {
            smooth = m_smoothList.at(i);
        }
        segmentProperties->setSegment(i, m_controlPoints.mid(i * 3, 3), smooth, i == (m_numberOfSegments - 1));
    }
}

QHash<QString, QEasingCurve> SplineEditor::presets() const
{
    return m_presets;
}

QString SplineEditor::generateCode()
{
    QString s = QLatin1String("[");
    for (const QPointF &point : std::as_const(m_controlPoints)) {
        s += QString::number(point.x(), 'g', 2) + QLatin1Char(',')
             + QString::number(point.y(), 'g', 3) + QLatin1Char(',');
    }
    s.chop(1); //removing last ","
    s += QLatin1Char(']');

    return s;
}

QStringList SplineEditor::presetNames() const
{
    return m_presets.keys();
}

QWidget *SplineEditor::pointListWidget()
{
    if (!m_pointListWidget) {
        setupPointListWidget();
    }

    return m_pointListWidget;
}

int SplineEditor::findControlPoint(const QPoint &point)
{
    int pointIndex = -1;
    qreal distance = -1;
    for (int i = 0; i<m_controlPoints.size() - 1; ++i) {
        qreal d = QLineF(point, mapToCanvas(m_controlPoints.at(i))).length();
        if ((distance < 0 && d < 10) || d < distance) {
            distance = d;
            pointIndex = i;
        }
    }
    return pointIndex;
}

static inline bool veryFuzzyCompare(qreal r1, qreal r2)
{
    if (qFuzzyCompare(r1, 2))
        return true;

    int r1i = qRound(r1 * 20);
    int r2i = qRound(r2 * 20);

    if (qFuzzyCompare(qreal(r1i) / 20, qreal(r2i) / 20))
        return true;

    return false;
}

bool SplineEditor::isSmooth(int i) const
{
    if (i == 0)
        return false;

    QPointF p = m_controlPoints.at(i);
    QPointF p_before = m_controlPoints.at(i - 1);
    QPointF p_after = m_controlPoints.at(i + 1);

    QPointF v1 = p_after - p;
    v1 = v1 / v1.manhattanLength(); //normalize

    QPointF v2 = p - p_before;
    v2 = v2 / v2.manhattanLength(); //normalize

    return veryFuzzyCompare(v1.x(), v2.x()) && veryFuzzyCompare(v1.y(), v2.y());
}

void SplineEditor::smoothPoint(int index)
{
    if (m_smoothAction->isChecked()) {

        QPointF before = QPointF(0,0);
        if (index > 3)
            before = m_controlPoints.at(index - 3);

        QPointF after = QPointF(1.0, 1.0);
        if ((index + 3) < m_controlPoints.size())
            after = m_controlPoints.at(index + 3);

        QPointF tangent = (after - before) / 6;

        QPointF thisPoint =  m_controlPoints.at(index);

        if (index > 0)
            m_controlPoints[index - 1] = thisPoint - tangent;

        if (index + 1  < m_controlPoints.size())
            m_controlPoints[index + 1] = thisPoint + tangent;

        m_smoothList[index / 3] = true;
    } else {
        m_smoothList[index / 3] = false;
    }
    invalidate();
    update();
}

void SplineEditor::cornerPoint(int index)
{
    QPointF before = QPointF(0,0);
    if (index > 3)
        before = m_controlPoints.at(index - 3);

    QPointF after = QPointF(1.0, 1.0);
    if ((index + 3) < m_controlPoints.size())
        after = m_controlPoints.at(index + 3);

    QPointF thisPoint =  m_controlPoints.at(index);

    if (index > 0)
        m_controlPoints[index - 1] = (before - thisPoint) / 3 + thisPoint;

    if (index + 1  < m_controlPoints.size())
        m_controlPoints[index + 1] = (after - thisPoint) / 3 + thisPoint;

    m_smoothList[(index) / 3] = false;
    invalidate();
}

void SplineEditor::deletePoint(int index)
{
    m_controlPoints.remove(index - 1, 3);
    m_numberOfSegments--;

    invalidateSmoothList();
    setupPointListWidget();
    invalidate();
}

void SplineEditor::addPoint(const QPointF point)
{
    QPointF newPos = mapFromCanvas(point);
    int splitIndex = 0;
    for (int i=0; i < m_controlPoints.size() - 1; ++i) {
        if (indexIsRealPoint(i) && m_controlPoints.at(i).x() > newPos.x()) {
            break;
        } else if (indexIsRealPoint(i))
            splitIndex = i;
    }
    QPointF before = QPointF(0,0);
    if (splitIndex > 0)
        before = m_controlPoints.at(splitIndex);

    QPointF after = QPointF(1.0, 1.0);
    if ((splitIndex + 3) < m_controlPoints.size())
        after = m_controlPoints.at(splitIndex + 3);

    if (splitIndex > 0) {
        m_controlPoints.insert(splitIndex + 2, (newPos + after) / 2);
        m_controlPoints.insert(splitIndex + 2, newPos);
        m_controlPoints.insert(splitIndex + 2, (newPos + before) / 2);
    } else {
        m_controlPoints.insert(splitIndex + 1, (newPos + after) / 2);
        m_controlPoints.insert(splitIndex + 1, newPos);
        m_controlPoints.insert(splitIndex + 1, (newPos + before) / 2);
    }
    m_numberOfSegments++;

    invalidateSmoothList();
    setupPointListWidget();
    invalidate();
}

void SplineEditor::initPresets()
{
    const QPointF endPoint(1.0, 1.0);
    {
        QEasingCurve easingCurve(QEasingCurve::BezierSpline);
        easingCurve.addCubicBezierSegment(QPointF(0.4, 0.075), QPointF(0.45, 0.24), QPointF(0.5, 0.5));
        easingCurve.addCubicBezierSegment(QPointF(0.55, 0.76), QPointF(0.7, 0.9), endPoint);
        m_presets.insert(tr("Standard Easing"), easingCurve);
    }

    {
        QEasingCurve easingCurve(QEasingCurve::BezierSpline);
        easingCurve.addCubicBezierSegment(QPointF(0.43, 0.0025), QPointF(0.65, 1), endPoint);
        m_presets.insert(tr("Simple"), easingCurve);
    }

    {
        QEasingCurve easingCurve(QEasingCurve::BezierSpline);
        easingCurve.addCubicBezierSegment(QPointF(0.43, 0.0025), QPointF(0.38, 0.51), QPointF(0.57, 0.99));
        easingCurve.addCubicBezierSegment(QPointF(0.8, 0.69), QPointF(0.65, 1), endPoint);
        m_presets.insert(tr("Simple Bounce"), easingCurve);
    }

    {
        QEasingCurve easingCurve(QEasingCurve::BezierSpline);
        easingCurve.addCubicBezierSegment(QPointF(0.4, 0.075), QPointF(0.64, -0.0025), QPointF(0.74, 0.23));
        easingCurve.addCubicBezierSegment(QPointF(0.84, 0.46), QPointF(0.91, 0.77), endPoint);
        m_presets.insert(tr("Slow in fast out"), easingCurve);
    }

    {
        QEasingCurve easingCurve(QEasingCurve::BezierSpline);
        easingCurve.addCubicBezierSegment(QPointF(0.43, 0.0025), QPointF(0.47, 0.51), QPointF(0.59, 0.94));
        easingCurve.addCubicBezierSegment(QPointF(0.84, 0.95), QPointF( 0.99, 0.94), endPoint);
        m_presets.insert(tr("Snapping"), easingCurve);
    }

    {
        QEasingCurve easingCurve(QEasingCurve::BezierSpline);
        easingCurve.addCubicBezierSegment(QPointF( 0.38, 0.35),QPointF(0.38, 0.7), QPointF(0.45, 0.99));
        easingCurve.addCubicBezierSegment(QPointF(0.48, 0.66), QPointF(0.62, 0.62), QPointF(0.66, 0.99));
        easingCurve.addCubicBezierSegment(QPointF(0.69, 0.76), QPointF(0.77, 0.76), QPointF(0.79, 0.99));
        easingCurve.addCubicBezierSegment(QPointF(0.83, 0.91), QPointF(0.87, 0.92), QPointF(0.91, 0.99));
        easingCurve.addCubicBezierSegment(QPointF(0.95, 0.95), QPointF(0.97, 0.94), endPoint);
        m_presets.insert(tr("Complex Bounce"), easingCurve);
    }

    {
        QEasingCurve easingCurve4(QEasingCurve::BezierSpline);
        easingCurve4.addCubicBezierSegment(QPointF(0.12, -0.12),QPointF(0.23, -0.19), QPointF( 0.35, -0.09));
        easingCurve4.addCubicBezierSegment(QPointF(0.47, 0.005), QPointF(0.52, 1), QPointF(0.62, 1.1));
        easingCurve4.addCubicBezierSegment(QPointF(0.73, 1.2), QPointF(0.91,1 ), endPoint);
        m_presets.insert(tr("Overshoot"), easingCurve4);
    }
}

void SplineEditor::setupPointListWidget()
{
    if (!m_pointListWidget)
        m_pointListWidget = new QScrollArea(this);

    if (m_pointListWidget->widget())
        delete m_pointListWidget->widget();

    m_pointListWidget->setFrameStyle(QFrame::NoFrame);
    m_pointListWidget->setWidgetResizable(true);
    m_pointListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    m_pointListWidget->setWidget(new QWidget(m_pointListWidget));
    QVBoxLayout *layout = new QVBoxLayout(m_pointListWidget->widget());
    layout->setContentsMargins(QMargins());
    layout->setSpacing(2);
    m_pointListWidget->widget()->setLayout(layout);

    m_segmentProperties.clear();

    { //implicit 0,0
        QWidget *widget = new QWidget(m_pointListWidget->widget());
        Ui_Pane pane;
        pane.setupUi(widget);
        pane.p1_x->setValue(0);
        pane.p1_y->setValue(0);
        layout->addWidget(widget);
        pane.label->setText("p0");
        widget->setEnabled(false);
    }

    for (int i = 0; i < m_numberOfSegments; ++i) {
        SegmentProperties *segmentProperties = new SegmentProperties(m_pointListWidget->widget());
        layout->addWidget(segmentProperties);
        bool smooth = false;
        if (i < (m_numberOfSegments - 1)) {
            smooth = m_smoothList.at(i);
        }
        segmentProperties->setSegment(i, m_controlPoints.mid(i * 3, 3), smooth, i == (m_numberOfSegments - 1));
        segmentProperties->setSplineEditor(this);
        m_segmentProperties << segmentProperties;
    }
    layout->addSpacerItem(new QSpacerItem(10, 10, QSizePolicy::Expanding, QSizePolicy::Expanding));

    m_pointListWidget->viewport()->show();
    m_pointListWidget->viewport()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
    m_pointListWidget->show();
}

bool SplineEditor::isControlPointSmooth(int i) const
{
    if (i == 0)
        return false;

    if (i == m_controlPoints.size() - 1)
        return false;

    if (m_numberOfSegments == 1)
        return false;

    int index = pointForControlPoint(i);

    if (index == 0)
        return false;

    if (index == m_controlPoints.size() - 1)
        return false;

    return m_smoothList.at(index / 3);
}

QPointF limitToCanvas(const QPointF point)
{
    qreal left = -qreal( canvasMargin) / qreal(canvasWidth);
    qreal width = 1.0 - 2.0 * left;

    qreal top = -qreal( canvasMargin) / qreal(canvasHeight);
    qreal height = 1.0 - 2.0 * top;

    QPointF p = point;
    QRectF r(left, top, width, height);

    if (p.x() > r.right()) {
        p.setX(r.right());
    }
    if (p.x() < r.left()) {
        p.setX(r.left());
    }
    if (p.y() < r.top()) {
        p.setY(r.top());
    }
    if (p.y() > r.bottom()) {
        p.setY(r.bottom());
    }
    return p;
}

void SplineEditor::mouseMoveEvent(QMouseEvent *e)
{
    // If we've moved more then 25 pixels, assume user is dragging
    if (!m_mouseDrag && QPoint(m_mousePress - e->position().toPoint()).manhattanLength() > qApp->startDragDistance())
        m_mouseDrag = true;

    QPointF p = mapFromCanvas(e->position().toPoint());

    if (m_mouseDrag && m_activeControlPoint >= 0 && m_activeControlPoint < m_controlPoints.size()) {
        p = limitToCanvas(p);
        if (indexIsRealPoint(m_activeControlPoint)) {
            //move also the tangents
            QPointF targetPoint = p;
            QPointF distance = targetPoint - m_controlPoints.at(m_activeControlPoint);
            m_controlPoints[m_activeControlPoint] = targetPoint;
            m_controlPoints[m_activeControlPoint - 1] += distance;
            m_controlPoints[m_activeControlPoint + 1] += distance;
        } else {
            if (!isControlPointSmooth(m_activeControlPoint)) {
                m_controlPoints[m_activeControlPoint] = p;
            } else {
                QPointF targetPoint = p;
                QPointF distance = targetPoint - m_controlPoints.at(m_activeControlPoint);
                m_controlPoints[m_activeControlPoint] = p;

                if ((m_activeControlPoint > 1) && (m_activeControlPoint % 3) == 0) { //right control point
                    m_controlPoints[m_activeControlPoint - 2] -= distance;
                } else if ((m_activeControlPoint < (m_controlPoints.size() - 2)) //left control point
                           && (m_activeControlPoint % 3) == 1) {
                    m_controlPoints[m_activeControlPoint + 2] -= distance;
                }
            }
        }
        invalidate();
    }
}

void SplineEditor::setEasingCurve(const QEasingCurve &easingCurve)
{
    if (m_easingCurve == easingCurve)
        return;
    m_block = true;
    m_easingCurve = easingCurve;
    m_controlPoints = m_easingCurve.toCubicSpline();
    m_numberOfSegments = m_controlPoints.size() / 3;
    update();
    emit easingCurveChanged();

    const QString code = generateCode();
    emit easingCurveCodeChanged(code);

    m_block = false;
}

void SplineEditor::setPreset(const QString &name)
{
    setEasingCurve(m_presets.value(name));
    invalidateSmoothList();
    setupPointListWidget();
}

void SplineEditor::setEasingCurve(const QString &code)
{
    if (m_block)
        return;
    if (code.startsWith(QLatin1Char('[')) && code.endsWith(QLatin1Char(']'))) {
        const auto cleanCode = QStringView(code).mid(1, code.size() - 2);
        const auto stringList = cleanCode.split(QLatin1Char(','), Qt::SkipEmptyParts);
        if (stringList.size() >= 6 && (stringList.size() % 6 == 0)) {
            QVector<qreal> realList;
            realList.reserve(stringList.size());
            for (const QStringView &string : stringList) {
                bool ok;
                realList.append(string.toDouble(&ok));
                if (!ok)
                    return;
            }
            QVector<QPointF> points;
            const int count = realList.size() / 2;
            points.reserve(count);
            for (int i = 0; i < count; ++i)
                points.append(QPointF(realList.at(i * 2), realList.at(i * 2 + 1)));
            if (points.constLast() == QPointF(1.0, 1.0)) {
                QEasingCurve easingCurve(QEasingCurve::BezierSpline);

                for (int i = 0; i < points.size() / 3; ++i) {
                    easingCurve.addCubicBezierSegment(points.at(i * 3),
                                                      points.at(i * 3 + 1),
                                                      points.at(i * 3 + 2));
                }
                setEasingCurve(easingCurve);
                invalidateSmoothList();
                setupPointListWidget();
            }
        }
    }
}

#include "moc_splineeditor.cpp"
