// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include <QtGraphs/private/customrenderer_p.h>
#include <QtGraphs/qcustomseries.h>
#if QT_CONFIG(graphs_2d_high_performance_backend)
#include <QtGraphs/qcustomseriescanvasrenderer.h>
#endif
#include <private/charthelpers_p.h>
#include <private/qcustomseries_p.h>
#include <private/qcustomseriesdata_p.h>
#include <private/qgraphsview_p.h>

QT_BEGIN_NAMESPACE

/*!
    \class QCustomSeries
    \inmodule QtGraphs
    \brief The QCustomSeries class allows presenting customized graph types.
    \since 6.11

    Use QCustomSeries to create customized graphs. Think of it as a scatter graph
    that lets you access custom data for each element.

    QCustomSeries defines a delegate that is used to render every item added to
    the graph. Each item includes user-defined data stored in a \l QVariantMap.
    Index of the item in the graph is also automatically added to the data map.
    The series passes this map to each element created based on the delegate.
    The delegate determines how to use the data.

    To map data to the render coordinates defined by the \c QGraphsView axes,
    use the \l mapX and \l mapY functions.

    \note Currently, individual elements in a custom series do not share
    information with one another. For this reason, you can't implement a custom
    line series.
*/

/*!
    \qmltype CustomSeries
    \nativetype QCustomSeries
    \inqmlmodule QtGraphs
    \brief The CustomSeries type allows presenting customized graph types.
    \since 6.11

    Use CustomSeries to create customized graphs. Think of it as a scatter graph
    that lets you access custom data for each element.

    CustomSeries defines a delegate that is used to render every item added to
    the graph. Each item includes user-defined data stored in a \l QVariantMap.
    Index of the item in the graph is also automatically added to the data map.
    In the QML context the variant map is defined by a JavaScript object.
    The series passes this map to each element created based on the delegate.
    The delegate determines how to use the data.

    To map data to the render coordinates defined by the \l GraphsView axes,
    use the \l mapX and \l mapY functions.

    \note Currently, individual elements in a custom series do not share
    information with one another. For this reason, you can't implement a custom
    line series.

    \image graphs2d-custom.png {Bar graph with two gradient-filled bars}

    The following code implements the depicted custom graph. In this case the
    custom data contains two values: upper and lower. Those are then defined
    in the delegate to affect the height and the y coordinate of the element.

    \qml
    import QtQuick
    import QtGraphs

    Window {
        width: 640
        height: 480
        visible: true

        GraphsView {
            anchors.fill: parent

            axisX: BarCategoryAxis {
                categories: ["ABC", "DEF"]
            }

            axisY: ValueAxis {
                max: 8
                tickInterval: 1
            }

            CustomSeries {
                id: custom

                delegate: Rectangle {
                    property var data
                    gradient: Gradient {
                        GradientStop { position: 0.0; color: "lightsteelblue" }
                        GradientStop { position: 1.0; color: "blue" }
                    }
                    border.width: 1
                    x: custom.mapX(data.index + 0.5) - width * 0.5
                    y: custom.mapY((data.upper + data.lower) * 0.5) - height * 0.5
                    width: Math.abs(custom.mapX(1) - custom.mapX(0)) * 0.5
                    height: Math.abs(custom.mapY(data.upper) - custom.mapY(data.lower))
                }

                CustomSeriesData {
                    data: ({
                        upper: 5,
                        lower: 1
                    })
                }

                CustomSeriesData {
                    data: ({
                        upper: 6,
                        lower: 4
                    })
                }
            }
        }
    }
    \endqml
*/

QCustomSeries::QCustomSeries(QObject *parent)
    : QAbstractSeries(*(new QCustomSeriesPrivate()), parent)
{}

QCustomSeries::~QCustomSeries()
{
#if QT_CONFIG(graphs_2d_high_performance_backend)
    Q_D(QCustomSeries);
    delete d->m_customSeriesPainter;
#endif
}

QCustomSeries::QCustomSeries(QCustomSeriesPrivate &dd, QObject *parent)
    : QAbstractSeries(dd, parent)
{}

QAbstractSeries::SeriesType QCustomSeries::type() const
{
    return QAbstractSeries::SeriesType::Custom;
}

QList<QCustomSeriesData *> QCustomSeries::dataItems() const
{
    Q_D(const QCustomSeries);
    return d->m_dataItems;
}

/*!
    \property QCustomSeries::delegate

    The delegate is used to insert elements to QML corresponding to provided
    custom data. Data is defined in the CustomSeries itself and given to
    the delegated elements through the "data" property.
*/
/*!
    \qmlproperty Component CustomSeries::delegate
    The delegate is used to insert elements to QML corresponding to provided
    custom data. Data is defined in the the CustomSeries itself and given to
    the delegated elements through the "data" property.

    \code
        delegate: Rectangle {
                property var data
                Rectangle {
                    color: "tan"
                    border.color: "black"
                    border.width: 4
                    y: series.mapY(data.index + 1) - height / 2 + 1
                    x: series.mapX(data.median) - width / 2
                    height: 32
                    width: Math.abs(series.mapX(data.upperQuartile) - series.mapX(data.lowerQuartile))
                }
            }
        }
    \endcode
*/
QQmlComponent *QCustomSeries::delegate() const
{
    Q_D(const QCustomSeries);
    return d->m_delegate;
}

void QCustomSeries::setDelegate(QQmlComponent *newDelegate)
{
    Q_D(QCustomSeries);
    if (d->m_delegate == newDelegate) {
        qCDebug(lcProperties2D) << "QCustomSeries::setDelegate. Point delegate is already set to:"
                                << newDelegate;
        return;
    }

    d->m_delegate = newDelegate;
    emit delegateChanged();
    emit update();
}
#if QT_CONFIG(graphs_2d_high_performance_backend) || defined(Q_QDOC)
/*!
    Sets \a painter as the canvas painter renderer used to draw this series
    when the high-performance canvas painter backend is in use.

    QCustomSeries takes ownership of \a painter. It is adopted by the
    CanvasPainter-backend renderer the next time this series is
    synchronized, after which it is destroyed only by the renderer, on the
    render thread -- never by QCustomSeries directly. If \a painter is
    replaced or cleared before that first synchronization, or if this series
    is destroyed before it, QCustomSeries deletes it itself.
*/
void QCustomSeries::setCustomSeriesPainter(QCustomSeriesCanvasRenderer *painter)
{
    Q_D(QCustomSeries);
    if (d->m_customSeriesPainter == painter)
        return;

    delete d->m_customSeriesPainter;
    d->m_customSeriesPainter = painter;
    update();
}
#endif
/*!
    \qmlmethod real CustomSeries::mapX(real x)
    Returns \a x axis-space coordinate converted into render-space.
*/
/*!
    Returns \a x axis-space coordinate converted into render-space.
*/
qreal QCustomSeries::mapX(qreal x)
{
    Q_D(const QCustomSeries);
    if (d->m_graph) {
        auto renderer = d->m_graph->customRenderer();
        if (renderer) {
            auto metaObj = renderer->metaObject();
            auto propIndex = metaObj->indexOfProperty("plotArea");
            if (propIndex >= 0) {
                auto prop = metaObj->property(propIndex);
                qmlEngine(this)->captureProperty(renderer, prop);
            }
        }
        return d->m_graph->mapX(this, x);
    }
    return x;
}

/*!
    \qmlmethod real CustomSeries::mapY(real y)
    Returns \a y axis-space coordinate converted into render-space.
*/
/*!
    Returns \a y axis-space coordinate converted into render-space.
*/
qreal QCustomSeries::mapY(qreal y)
{
    Q_D(const QCustomSeries);
    if (d->m_graph) {
        auto renderer = d->m_graph->customRenderer();
        if (renderer) {
            auto metaObj = renderer->metaObject();
            auto propIndex = metaObj->indexOfProperty("plotArea");
            if (propIndex >= 0) {
                auto prop = metaObj->property(propIndex);
                qmlEngine(this)->captureProperty(renderer, prop);
            }
        }
        return d->m_graph->mapY(this, y);
    }
    return y;
}

/*!
    \qmlmethod void CustomSeries::append()
    Appends an empty data item to the series.
*/
/*!
    Appends an empty data item to the series.
*/
void QCustomSeries::append()
{
    append(QVariantMap());
}

/*!
    \qmlmethod void CustomSeries::append(var data)
    Appends a data item defined by \a data to the series.
*/
/*!
    Appends a data item defined by \a data to the series.
*/
void QCustomSeries::append(const QVariantMap &data)
{
    Q_D(QCustomSeries);
    QCustomSeriesData *dataItem = new QCustomSeriesData(this);
    dataItem->setData(data);
    d->m_dataItems.append(dataItem);
}

/*!
    \qmlmethod void CustomSeries::insert(int index)
    Appends an empty data item to the series at position \a index.
*/
/*!
    Appends an empty data item to the series at position \a index.
*/
void QCustomSeries::insert(qsizetype index)
{
    insert(index, QVariantMap());
}

/*!
    \qmlmethod void CustomSeries::insert(int index, var data)
    Appends a data item defined by \a data to the series at position \a index.
*/
/*!
    Appends a data item defined by \a data to the series at position \a index.
*/
void QCustomSeries::insert(qsizetype index, const QVariantMap &data)
{
    Q_D(QCustomSeries);
    if (d->m_dataItems.size() <= index)
        return;

    QCustomSeriesData *dataItem = new QCustomSeriesData(this);
    dataItem->setData(data);
    d->m_dataItems.insert(index, dataItem);
}

/*!
    \qmlmethod void CustomSeries::remove(int index)
    Remove a data item at position \a index.
*/
/*!
    Remove a data item at position \a index.
*/
void QCustomSeries::remove(qsizetype index)
{
    Q_D(QCustomSeries);
    if (d->m_dataItems.size() <= index)
        return;

    delete d->m_dataItems[index];

    d->m_dataItems.remove(index);
}

/*!
    \qmlmethod void CustomSeries::clear()
    Clear all data items.
*/
/*!
    Clear all data items.
*/
void QCustomSeries::clear()
{
    Q_D(QCustomSeries);

    qDeleteAll(d->m_dataItems);

    d->m_dataItems.clear();
}

void QCustomSeries::componentComplete()
{
    Q_D(QCustomSeries);

    for (auto *child : children()) {
        if (auto dataItem = qobject_cast<QCustomSeriesData *>(child)) {
            if (!d->m_dataItems.contains(dataItem)) {
                d->m_dataItems.append(dataItem);
                qCDebug(lcSeries2D, "append dataItem to customSeries");
            }
        }
    }

    qCDebug(lcEvents2D, "QCustomSeries::componentComplete");

    QAbstractSeries::componentComplete();
}

/*!
    Called by the graph renderer to give access to the \a item
    representing the data, and the \a index corresponding to the
    data item.

    Override to implement custom property values for QQuickItem
    instance(s).
*/
void QCustomSeries::updateDelegate(QQuickItem *item, qsizetype index)
{
    auto &&data = dataItems()[index]->data();
    data[QStringLiteral("index")] = index;
    item->setProperty("data", data);
}

bool QCustomSeries::event(QEvent *event)
{
    return QObject::event(event);
}

QCustomSeriesPrivate::QCustomSeriesPrivate()
    : QAbstractSeriesPrivate(QAbstractSeries::SeriesType::Custom)
{}

QT_END_NAMESPACE

#include "moc_qcustomseries.cpp"
