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

#include <private/scatterchartitem_p.h>
#include <QtCharts/QScatterSeries>
#include <private/qscatterseries_p.h>
#include <private/chartpresenter_p.h>
#include <private/abstractdomain_p.h>
#include <QtCharts/QChart>
#include <QtGui/QPainter>
#include <QtWidgets/QGraphicsScene>
#include <QtCore/QDebug>
#include <QtWidgets/QGraphicsSceneMouseEvent>

QT_BEGIN_NAMESPACE

namespace {
constexpr short STAR_SPIKES = 5;
}

ScatterChartItem::ScatterChartItem(QScatterSeries *series, QGraphicsItem *item)
    : XYChart(series,item),
      m_series(series),
      m_items(this),
      m_visible(true),
      m_markerShape(QScatterSeries::MarkerShapeRectangle),
      m_pointsVisible(true),
      m_pointLabelsVisible(false),
      m_markerSize(series->markerSize()),
      m_pointLabelsFormat(series->pointLabelsFormat()),
      m_pointLabelsFont(series->pointLabelsFont()),
      m_pointLabelsColor(series->pointLabelsColor()),
      m_pointLabelsClipping(true),
      m_lastHoveredPoint(QPointF(qQNaN(), qQNaN())),
      m_mousePressed(false)
{
    connect(series->d_func(), &QXYSeriesPrivate::seriesUpdated,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::lightMarkerChanged, this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::selectedLightMarkerChanged, this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::markerSizeChanged, this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::visibleChanged, this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::opacityChanged, this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::pointLabelsFormatChanged,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::pointLabelsVisibilityChanged,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::pointLabelsFontChanged,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::pointLabelsColorChanged,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::pointLabelsClippingChanged,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::selectedColorChanged,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QXYSeries::selectedPointsChanged,
            this, &ScatterChartItem::handleSeriesUpdated);
    connect(series, &QScatterSeries::pointsConfigurationChanged,
            this, &ScatterChartItem::handleSeriesUpdated);

    setZValue(ChartPresenter::ScatterSeriesZValue);
    setFlags(QGraphicsItem::ItemClipsChildrenToShape | QGraphicsItem::ItemIsSelectable);

    handleSeriesUpdated();

    m_items.setHandlesChildEvents(false);
}

QRectF ScatterChartItem::boundingRect() const
{
    return m_rect;
}

void ScatterChartItem::createPoints(int count)
{
    for (int i = 0; i < count; ++i) {

        QGraphicsItem *item = 0;

        switch (m_markerShape) {
        case QScatterSeries::MarkerShapeCircle: {
            item = new ChartMarker<QGraphicsEllipseItem>(0, 0, m_markerSize, m_markerSize, this);
            break;
        }
        case QScatterSeries::MarkerShapeRectangle: {
            item = new ChartMarker<QGraphicsRectItem>(0, 0, m_markerSize, m_markerSize, this);
            break;
        }
        case QScatterSeries::MarkerShapeRotatedRectangle: {
            item = new RotatedRectangleMarker(0, 0, m_markerSize, m_markerSize, this);
            break;
        }
        case QScatterSeries::MarkerShapeTriangle: {
            item = new TriangleMarker(0, 0, m_markerSize, m_markerSize, this);
            break;
        }
        case QScatterSeries::MarkerShapeStar: {
            item = new StarMarker(0, 0, m_markerSize, m_markerSize, this);
            break;
        }
        case QScatterSeries::MarkerShapePentagon: {
            item = new PentagonMarker(0, 0, m_markerSize, m_markerSize, this);
            break;
        }
        default:
            qWarning() << "Unsupported marker type";
            break;
        }

        m_items.addToGroup(item);
    }
}

void ScatterChartItem::deletePoints(int count)
{
    QList<QGraphicsItem *> items = m_items.childItems();

    for (int i = 0; i < count; ++i) {
        QGraphicsItem *item = items.takeLast();
        m_markerMap.remove(item);
        delete(item);
    }
}

void ScatterChartItem::resizeMarker(QGraphicsItem *marker, const int size)
{
    switch (m_markerShape) {
    case QScatterSeries::MarkerShapeCircle: {
        QGraphicsEllipseItem *item = static_cast<QGraphicsEllipseItem *>(marker);
        item->setRect(item->x(), item->y(), size, size);
        break;
    }
    case QScatterSeries::MarkerShapeRectangle: {
        QGraphicsRectItem *item = static_cast<QGraphicsRectItem *>(marker);
        item->setRect(item->x(), item->y(), size, size);
        break;
    }
    case QScatterSeries::MarkerShapeRotatedRectangle: {
        QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
        item->setPolygon(RotatedRectangleMarker::polygon(item->x(), item->y(), size, size));
        break;
    }
    case QScatterSeries::MarkerShapeTriangle: {
        QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
        item->setPolygon(TriangleMarker::polygon(item->x(), item->y(), size, size));
        break;
    }
    case QScatterSeries::MarkerShapeStar: {
        QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
        item->setPolygon(StarMarker::polygon(item->x(), item->y(), size, size));
        break;
    }
    case QScatterSeries::MarkerShapePentagon: {
        QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
        item->setPolygon(PentagonMarker::polygon(item->x(), item->y(), size, size));
        break;
    }
    default:
        qWarning() << "Unsupported marker type";
        break;
    }
}

void ScatterChartItem::markerSelected(QGraphicsItem *marker)
{
    emit XYChart::clicked(m_markerMap[marker]);
}

void ScatterChartItem::markerHovered(QGraphicsItem *marker, bool state)
{
    emit XYChart::hovered(m_markerMap[marker], state);
}

void ScatterChartItem::markerPressed(QGraphicsItem *marker)
{
    emit XYChart::pressed(m_markerMap[marker]);
}

void ScatterChartItem::markerReleased(QGraphicsItem *marker)
{
    emit XYChart::released(m_markerMap[marker]);
}

void ScatterChartItem::markerDoubleClicked(QGraphicsItem *marker)
{
    emit XYChart::doubleClicked(m_markerMap[marker]);
}

void ScatterChartItem::updateGeometry()
{
    if (m_series->useOpenGL()) {
        if (m_items.childItems().count())
            deletePoints(m_items.childItems().count());
        if (!m_rect.isEmpty()) {
            prepareGeometryChange();
            // Changed signal seems to trigger even with empty region
            m_rect = QRectF();
        }
        update();
        return;
    }

    const QList<QPointF> &points = geometryPoints();

    if (points.size() == 0) {
        deletePoints(m_items.childItems().count());
        return;
    }

    int diff = m_items.childItems().size() - points.size();

    if (diff > 0)
        deletePoints(diff);
    else if (diff < 0)
        createPoints(-diff);

    if (diff != 0)
        handleSeriesUpdated();

    QList<QGraphicsItem *> items = m_items.childItems();

    QRectF clipRect(QPointF(0,0),domain()->size());

    // Only zoom in if the clipRect fits inside int limits. QWidget::update() uses
    // a region that has to be compatible with QRect.
    if (clipRect.height() <= INT_MAX
            && clipRect.width() <= INT_MAX) {
        const QList<bool> offGridStatus = offGridStatusVector();
        const int seriesLastIndex = m_series->count() - 1;

        for (int i = 0; i < points.size(); i++) {
            QAbstractGraphicsShapeItem *item =
                    static_cast<QAbstractGraphicsShapeItem *>(items.at(i));
            const QPointF &point = points.at(i);

            if (m_pointsConfiguration.contains(i) && m_pointsConfigurationDirty) {
                const auto &conf = m_pointsConfiguration[i];
                if (conf.contains(QXYSeries::PointConfiguration::Size))
                    resizeMarker(
                            item,
                            m_pointsConfiguration[i][QXYSeries::PointConfiguration::Size].toReal());
            }

            const QRectF &rect = item->boundingRect();
            // During remove animation series may have different number of points,
            // so ensure we don't go over the index. Animation handling itself ensures that
            // if there is actually no points in the series, then it won't generate a fake point,
            // so we can be assured there is always at least one point in m_series here.
            // Note that marker map values can be technically incorrect during the animation,
            // if it was caused by an insert, but this shouldn't be a problem as the points are
            // fake anyway. After remove animation stops, geometry is updated to correct one.
            m_markerMap[item] = m_series->at(qMin(seriesLastIndex, i));
            QPointF position;
            position.setX(point.x() - rect.width() / 2);
            position.setY(point.y() - rect.height() / 2);
            item->setPos(position);

            if (!m_visible || offGridStatus.at(i)) {
                item->setVisible(false);
            } else {
                bool drawPoint = m_pointsVisible;
                if (m_pointsConfiguration.contains(i)) {
                    const auto &conf = m_pointsConfiguration[i];

                    if (conf.contains(QXYSeries::PointConfiguration::Visibility)) {
                        drawPoint
                                = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility]
                                .toBool();
                    }

                    if (drawPoint && conf.contains(QXYSeries::PointConfiguration::Color)) {
                        item->setBrush(
                                m_pointsConfiguration[i][QXYSeries::PointConfiguration::Color]
                                        .value<QColor>());
                    }
                }

                if (m_series->isPointSelected(i))
                    drawPoint = m_series->selectedLightMarker().isNull();

                item->setVisible(drawPoint);
            }
        }

        prepareGeometryChange();
        m_rect = clipRect;
    }
}

void ScatterChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    QPointF matchedP = matchForLightMarker(event->pos());
    if (!qIsNaN(matchedP.x())) {
        emit XYChart::pressed(matchedP);
        m_lastMousePos = event->pos();
        m_mousePressed = true;
    } else {
        event->ignore();
    }

    QGraphicsItem::mousePressEvent(event);
}

void ScatterChartItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
{
    QPointF matchedP = matchForLightMarker(event->pos());
    if (!qIsNaN(matchedP.x())) {
        if (matchedP != m_lastHoveredPoint) {
            if (!qIsNaN(m_lastHoveredPoint.x()))
                emit XYChart::hovered(m_lastHoveredPoint, false);

            m_lastHoveredPoint = matchedP;
            emit XYChart::hovered(matchedP, true);
        }
    } else if (!qIsNaN(m_lastHoveredPoint.x())) {
        emit XYChart::hovered(m_lastHoveredPoint, false);
        m_lastHoveredPoint = QPointF(qQNaN(), qQNaN());
    }

    QGraphicsItem::hoverMoveEvent(event);
}

void ScatterChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    QPointF result;
    QPointF matchedP = matchForLightMarker(m_lastMousePos);
    if (!qIsNaN(matchedP.x()) && m_mousePressed) {
        result = matchedP;
        emit XYChart::released(result);
        emit XYChart::clicked(result);
    }

    m_mousePressed = false;
    QGraphicsItem::mouseReleaseEvent(event);
}

void ScatterChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
    QPointF matchedP = matchForLightMarker(event->pos());
    if (!qIsNaN(matchedP.x()))
        emit XYChart::doubleClicked(matchedP);
    else
        emit XYChart::doubleClicked(domain()->calculateDomainPoint(m_lastMousePos));

    QGraphicsItem::mouseDoubleClickEvent(event);
}

void ScatterChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option);
    Q_UNUSED(widget);

    if (m_series->useOpenGL())
        return;

    QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());

    painter->save();
    painter->setClipRect(clipRect);

    // Draw markers if a marker or marker for selected points only has been
    // set (set to QImage() to disable)
    if (!m_series->lightMarker().isNull() || !m_series->selectedLightMarker().isNull()) {
        const QImage &marker = m_series->lightMarker();
        const QImage &selectedMarker = m_series->selectedLightMarker();
        qreal markerHalfSize = m_markerSize / 2.0;

        for (int i = 0; i < m_points.size(); ++i) {
            // Documentation of light markers says that points visibility and
            // light markers are independent features. Therefore m_pointsVisible
            // is not used here as light markers are drawn if lightMarker is not null.
            // However points visibility configuration can be still used here.
            bool drawPoint = !m_series->lightMarker().isNull();
            if (m_pointsConfiguration.contains(i)) {
                const auto &conf = m_pointsConfiguration[i];

                if (conf.contains(QXYSeries::PointConfiguration::Visibility)) {
                    drawPoint = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility]
                                        .toBool();
                }
            }

            bool drawSelectedPoint = false;
            if (m_series->isPointSelected(i)) {
                drawPoint = true;
                drawSelectedPoint = !selectedMarker.isNull();
            }
            if (drawPoint) {
                const QRectF rect(m_points[i].x() - markerHalfSize,
                                  m_points[i].y() - markerHalfSize,
                                  m_markerSize, m_markerSize);
                painter->drawImage(rect, drawSelectedPoint ? selectedMarker : marker);
            }
        }
    }

    if (m_series->bestFitLineVisible())
        m_series->d_func()->drawBestFitLine(painter, clipRect);

    m_series->d_func()->drawPointLabels(painter, m_points, m_series->markerSize() / 2 + m_series->pen().width());

    painter->restore();
}

void ScatterChartItem::setPen(const QPen &pen)
{
    QPen penToUse(pen);
    if (!m_series->lightMarker().isNull())
        penToUse.setColor(Qt::transparent);

    const auto &items = m_items.childItems();
    for (auto item : items)
        static_cast<QAbstractGraphicsShapeItem*>(item)->setPen(penToUse);
}

void ScatterChartItem::setBrush(const QBrush &brush)
{
    const auto &items = m_items.childItems();
    for (auto item : items) {
        if (m_series->lightMarker().isNull()) {
            if (m_markerMap.contains(item)) {
                auto index = m_series->points().indexOf(m_markerMap[item]);
                if (m_selectedPoints.contains(index) && m_selectedColor.isValid()) {
                    static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(m_selectedColor);
                } else {
                    bool useBrush = true;
                    if (m_pointsConfiguration.contains(index)) {
                        const auto &conf = m_pointsConfiguration[index];
                        if (conf.contains(QXYSeries::PointConfiguration::Color))
                            useBrush = false;
                    }

                    if (useBrush)
                        static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(brush);
                }
            } else {
                static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(brush);
            }
        } else {
            QBrush brushToUse;
            brushToUse.setColor(Qt::transparent);
            static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(brushToUse);
        }
    }
}

void ScatterChartItem::handleSeriesUpdated()
{
    if (m_series->useOpenGL()) {
        if ((m_series->isVisible() != m_visible)) {
            m_visible = m_series->isVisible();
            refreshGlChart();
        }
        return;
    }

    int count = m_items.childItems().count();
    if (count == 0)
        return;

    m_pointsConfigurationDirty = m_series->pointsConfiguration() != m_pointsConfiguration;

    bool recreate = m_visible != m_series->isVisible()
                    || m_pointsVisible != m_series->pointsVisible()
                    || m_markerSize != m_series->markerSize()
                    || m_markerShape != m_series->markerShape()
                    || m_selectedColor != m_series->selectedColor()
                    || m_selectedPoints != m_series->selectedPoints()
                    || m_pointsConfigurationDirty;
    m_visible = m_series->isVisible();
    m_markerSize = m_series->markerSize();
    m_markerShape = m_series->markerShape();
    setVisible(m_visible);
    setOpacity(m_series->opacity());
    m_pointsVisible = m_series->pointsVisible();
    m_pointLabelsFormat = m_series->pointLabelsFormat();
    m_pointLabelsVisible = m_series->pointLabelsVisible();
    m_pointLabelsFont = m_series->pointLabelsFont();
    m_pointLabelsColor = m_series->pointLabelsColor();
    m_selectedColor = m_series->selectedColor();
    m_selectedPoints = m_series->selectedPoints();
    m_pointsConfiguration = m_series->pointsConfiguration();
    bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping();
    m_pointLabelsClipping = m_series->pointLabelsClipping();

    if (recreate) {
        deletePoints(count);
        createPoints(count);

        // Updating geometry is now safe, because it won't call handleSeriesUpdated unless it
        // creates/deletes points
        updateGeometry();
    }

    // Only accept hover events when light/selection markers are in use so we don't unnecessarily
    // eat the events in the regular case
    setAcceptHoverEvents(!(m_series->lightMarker().isNull()
                           && (m_series->selectedLightMarker().isNull()
                               || m_series->selectedPoints().isEmpty())));

    setPen(m_series->pen());
    setBrush(m_series->brush());
    // Update whole chart in case label clipping changed as labels can be outside series area
    if (labelClippingChanged)
        m_series->chart()->update();
    else
        update();
}

template<class T>
ChartMarker<T>::ChartMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
    : T(x, y, w, h, parent)
    , m_parent(parent)
{
    T::setAcceptHoverEvents(true);
    T::setFlag(QGraphicsItem::ItemIsSelectable);
}

template<class T>
ChartMarker<T>::ChartMarker(ScatterChartItem *parent)
    : T(parent)
    , m_parent(parent)
{
    T::setAcceptHoverEvents(true);
    T::setFlag(QGraphicsItem::ItemIsSelectable);
}

template<class T>
void ChartMarker<T>::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    T::mousePressEvent(event);
    m_parent->markerPressed(this);
    m_parent->setMousePressed();
}

template<class T>
void ChartMarker<T>::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
    T::hoverEnterEvent(event);
    m_parent->markerHovered(this, true);
}

template<class T>
void ChartMarker<T>::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
    T::hoverLeaveEvent(event);
    m_parent->markerHovered(this, false);
}

template<class T>
void ChartMarker<T>::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    T::mouseReleaseEvent(event);
    m_parent->markerReleased(this);
    if (m_parent->mousePressed())
        m_parent->markerSelected(this);
    m_parent->setMousePressed(false);
}

template<class T>
void ChartMarker<T>::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
    T::mouseDoubleClickEvent(event);
    m_parent->markerDoubleClicked(this);
}

RotatedRectangleMarker::RotatedRectangleMarker(qreal x, qreal y, qreal w, qreal h,
                                               ScatterChartItem *parent)
    : ChartMarker<QGraphicsPolygonItem>(parent)
{
    setPolygon(RotatedRectangleMarker::polygon(x, y, w, h));
}

QPolygonF RotatedRectangleMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
    QPolygonF rotatedRectPoly;
    rotatedRectPoly << QPointF(x, y + h / 2.0);
    rotatedRectPoly << QPointF(x + w / 2.0, y + h);
    rotatedRectPoly << QPointF(x + w, y + h / 2.0);
    rotatedRectPoly << QPointF(x + w / 2.0, y);

    return rotatedRectPoly;
}

TriangleMarker::TriangleMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
    : ChartMarker<QGraphicsPolygonItem>(parent)
{
    setPolygon(TriangleMarker::polygon(x, y, w, h));
}

QPolygonF TriangleMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
    QPolygonF trianglePoly;
    trianglePoly << QPointF(x, y + h);
    trianglePoly << QPointF(x + w, y + h);
    trianglePoly << QPointF(x + w / 2.0, y);

    return trianglePoly;
}

StarMarker::StarMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
    : ChartMarker<QGraphicsPolygonItem>(parent)
{
    setPolygon(StarMarker::polygon(x, y, w, h));
}

QPolygonF StarMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
    QPolygonF starPoly;

    constexpr qreal step = M_PI / STAR_SPIKES;
    const qreal radius = w / 2.0;
    const qreal innerRadius = radius * 0.5;
    const QPointF &center = QPointF(x + w / 2.0, y + h / 2.0);
    qreal rot = M_PI / 2 * 3;

    for (int i = 0; i < STAR_SPIKES; ++i) {
        starPoly << QPointF(center.x() + std::cos(rot) * radius,
                            center.y() + std::sin(rot) * radius);
        rot += step;

        starPoly << QPointF(center.x() + std::cos(rot) * innerRadius,
                            center.y() + std::sin(rot) * innerRadius);
        rot += step;
    }

    return starPoly;
}

PentagonMarker::PentagonMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
    : ChartMarker<QGraphicsPolygonItem>(parent)
{
    setPolygon(PentagonMarker::polygon(x, y, w, h));
}

QPolygonF PentagonMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
    QPolygonF pentagonPoly;

    constexpr qreal step = 2 * M_PI / 5;
    const qreal radius = w / 2.0;
    const QPointF &center = QPointF(x + w / 2.0, y + h / 2.0);
    qreal rot = M_PI / 2 * 3;

    for (int i = 0; i < 5; ++i) {
        pentagonPoly << QPointF(center.x() + std::cos(rot) * radius,
                                center.y() + std::sin(rot) * radius);
        rot += step;
    }

    return pentagonPoly;
}

QT_END_NAMESPACE

#include "moc_scatterchartitem_p.cpp"
