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

#include "qcategory3daxis.h"
#include "qquickgraphsscatter_p.h"
#include "qscatter3dseries_p.h"
#include "qvalue3daxis.h"
#include "qgraphs3dlogging_p.h"

QT_BEGIN_NAMESPACE

/*!
 * \class QScatter3DSeries
 * \inmodule QtGraphs
 * \ingroup graphs_3D
 * \brief The QScatter3DSeries class represents a data series in a 3D scatter
 * graph.
 *
 * This class manages the series-specific visual elements, as well as the series
 * data (via a data proxy).
 *
 * Regarding the proxy-series relationship, it is crucial to highlight
 * a couple of key points. In this context, data is stored in series and
 * users can access the dataset through the series. This series is controlled
 * or represented by a proxy object. Thus, the proxy can be used to manage various
 * operations on the data and update the actual dataset. However, it is necessary
 * to create a series associated with this proxy to edit the dataset.
 *
 * If no data proxy is set explicitly for the series, the series creates a
 * default proxy. Setting another proxy will destroy the existing proxy and all
 * data added to the series.
 *
 * QScatter3DSeries supports the following format tags for QAbstract3DSeries::setItemLabelFormat():
 * \table
 *   \row
 *     \li @xTitle    \li Title from x-axis
 *   \row
 *     \li @yTitle    \li Title from y-axis
 *   \row
 *     \li @zTitle    \li Title from z-axis
 *   \row
 *     \li @xLabel    \li Item value formatted using the format of the x-axis.
 *                        For more information, see
 *                        \l{QValue3DAxis::labelFormat}.
 *   \row
 *     \li @yLabel    \li Item value formatted using the format of the y-axis.
 *                        For more information, see
 *                        \l{QValue3DAxis::labelFormat}.
 *   \row
 *     \li @zLabel    \li Item value formatted using the format of the z-axis.
 *                        For more information, see
 *                        \l{QValue3DAxis::labelFormat}.
 *   \row
 *     \li @seriesName \li Name of the series
 * \endtable
 *
 * For example:
 * \snippet doc_src_qtgraphs.cpp labelformat-scatter
 *
 * \sa {Qt Graphs Data Handling with 3D}
 */

/*!
 * \qmltype Scatter3DSeries
 * \inqmlmodule QtGraphs
 * \ingroup graphs_qml_3D
 * \nativetype QScatter3DSeries
 * \inherits Abstract3DSeries
 * \brief Represents a data series in a 3D scatter graph.
 *
 * This type manages the series specific visual elements, as well as the series
 * data (via a data proxy).
 *
 * Scatter3DSeries supports the following format tags for itemLabelFormat:
 * \table
 *   \row
 *     \li @xTitle    \li Title from x-axis
 *   \row
 *     \li @yTitle    \li Title from y-axis
 *   \row
 *     \li @zTitle    \li Title from z-axis
 *   \row
 *     \li @xLabel    \li Item value formatted using the format of the x-axis.
 *                        For more information, see
 *                        \l{QValue3DAxis::labelFormat}{labelFormat}.
 *   \row
 *     \li @yLabel    \li Item value formatted using the format of the y-axis.
 *                        For more information, see
 *                        \l{QValue3DAxis::labelFormat}{labelFormat}.
 *   \row
 *     \li @zLabel    \li Item value formatted using the format of the z-axis.
 *                        For more information, see
 *                        \l{QValue3DAxis::labelFormat}{labelFormat}.
 *   \row
 *     \li @seriesName \li Name of the series
 * \endtable
 *
 * For a more complete description, see QScatter3DSeries.
 *
 * \sa {Qt Graphs Data Handling with 3D}
 */

/*!
 * \qmlproperty ScatterDataProxy Scatter3DSeries::dataProxy
 *
 * Sets the active data proxy. The series assumes ownership of any proxy set to
 * it and deletes any previously set proxy when a new one is added. The proxy
 * cannot be null or set to another series.
 */

/*!
 * \qmlproperty qsizetype Scatter3DSeries::selectedItem
 *
 * The item that is selected at the index in the data array of the series.
 * Only one item can be selected at a time.
 * To clear the selection from this series, the invalidSelectionIndex is set as the
 * index. If this series is added to a graph, the graph can adjust the selection
 * according to user interaction or if it becomes invalid. Selecting an item on
 * another added series will also clear the selection. Removing items from or
 * inserting items into the series before the selected item will adjust the
 * selection so that the same item will stay selected.
 *
 * \sa GraphsItem3D::clearSelection()
 */

/*!
 * \qmlproperty real Scatter3DSeries::itemSize
 *
 * Sets the item size for the series. The size must be between \c 0.0 and
 * \c 1.0. Setting the size to \c 0.0 causes the item size to be automatically
 * scaled based on the total number of items in all the series for the graph.
 * The preset default is \c 0.0.
 */

/*!
 * \qmlproperty qsizetype Scatter3DSeries::invalidSelectionIndex
 * A constant property providing an invalid index for selection. This index is
 * set to the selectedItem property to clear the selection from this series.
 *
 * \sa GraphsItem3D::clearSelection()
 */

/*!
 * \qmlproperty ScatterDataArray Scatter3DSeries::dataArray
 *
 * Holds the reference to the data array.
 *
 * dataArrayChanged signal is emitted when data array is set, unless \a newDataArray
 * is identical to the previous one.
 *
 * \note Before doing anything regarding the data array, a series must be created for
 * the relevant proxy.
 */

/*!
    \qmlsignal Scatter3DSeries::dataProxyChanged(ScatterDataProxy proxy)

    This signal is emitted when dataProxy changes to \a proxy.
*/

/*!
    \qmlsignal Scatter3DSeries::selectedItemChanged(qsizetype index)

    This signal is emitted when selectedItem changes to \a index.
*/

/*!
    \qmlsignal Scatter3DSeries::itemSizeChanged(float size)

    This signal is emitted when itemSize changes to \a size.
*/

/*!
    \qmlsignal Scatter3DSeries::dataArrayChanged(ScatterDataArray array)

    This signal is emitted when dataArray changes to \a array.
*/

/*!
 * Constructs a scatter 3D series with the parent \a parent.
 */
QScatter3DSeries::QScatter3DSeries(QObject *parent)
    : QAbstract3DSeries(*(new QScatter3DSeriesPrivate()), parent)
{
    Q_D(QScatter3DSeries);
    // Default proxy
    d->setDataProxy(new QScatterDataProxy);
}

/*!
 * Constructs a scatter 3D series with the data proxy \a dataProxy and the
 * parent \a parent.
 */
QScatter3DSeries::QScatter3DSeries(QScatterDataProxy *dataProxy, QObject *parent)
    : QAbstract3DSeries(*(new QScatter3DSeriesPrivate()), parent)
{
    Q_D(QScatter3DSeries);
    d->setDataProxy(dataProxy);
}

/*!
 * \internal
 */
QScatter3DSeries::QScatter3DSeries(QScatter3DSeriesPrivate &d, QObject *parent)
    : QAbstract3DSeries(d, parent)
{}

/*!
 * Deletes the scatter 3D series.
 */
QScatter3DSeries::~QScatter3DSeries() {}

/*!
 * \property QScatter3DSeries::dataProxy
 *
 * \brief The active data proxy.
 *
 * Sets the active data proxy for the series to \a proxy. The series assumes
 * ownership of any proxy set to it and deletes any previously set proxy when
 * a new one is added. The \a proxy argument cannot be null or set to another
 * series.
 */
void QScatter3DSeries::setDataProxy(QScatterDataProxy *proxy)
{
    Q_D(QScatter3DSeries);
    d->setDataProxy(proxy);
}

QScatterDataProxy *QScatter3DSeries::dataProxy() const
{
    Q_D(const QScatter3DSeries);
    return static_cast<QScatterDataProxy *>(d->dataProxy());
}

/*!
 * \property QScatter3DSeries::selectedItem
 *
 * \brief The item that is selected in the series.
 *
 * Selects the item at the index \a index in the data array of the series.
 * Only one item can be selected at a time.
 *
 * To clear the selection from this series, invalidSelectionIndex() is set as \a
 * index. If this series is added to a graph, the graph can adjust the selection
 * according to user interaction or if it becomes invalid. Selecting an item on
 * another added series will also clear the selection.
 *
 * Removing items from or inserting items into the series before the selected item
 * will adjust the selection so that the same item will stay selected.
 *
 * \sa Q3DGraphsWidgetItem::clearSelection()
 */
void QScatter3DSeries::setSelectedItem(qsizetype index)
{
    Q_D(QScatter3DSeries);
    // Don't do this in private to avoid loops, as that is used for callback from
    // graph.
    if (d->m_graph)
        static_cast<QQuickGraphsScatter *>(d->m_graph)->setSelectedItem(index, this);
    else
        d->setSelectedItem(index);
}

qsizetype QScatter3DSeries::selectedItem() const
{
    Q_D(const QScatter3DSeries);
    return d->m_selectedItem;
}

/*!
 * \property QScatter3DSeries::itemSize
 *
 * \brief Item size for the series.
 *
 * The size must be between \c 0.0f and \c 1.0f. Setting the size to \c 0.0f
 * causes the item size to be automatically scaled based on the total number of
 * items in all the series for the graph.
 *
 * The preset default is \c 0.0f.
 */
void QScatter3DSeries::setItemSize(float size)
{
    Q_D(QScatter3DSeries);
    if (size < 0.0f || size > 1.0f) {
        qCWarning(lcProperties3D, "%s invalid size. Valid range for itemSize is 0.0f...1.0f",
                  qUtf8Printable(QLatin1String(__FUNCTION__)));
        return;
    } else if (size == d->m_itemSize) {
        qCDebug(lcProperties3D, "%s value is already set to: %.1f",
                qUtf8Printable(QLatin1String(__FUNCTION__)), size);
        return;
    }
    d->setItemSize(size);
    emit itemSizeChanged(size);
}

float QScatter3DSeries::itemSize() const
{
    Q_D(const QScatter3DSeries);
    return d->m_itemSize;
}

/*!
 * \property QScatter3DSeries::dataArray
 *
 * \brief Data array for the series.
 *
 * Holds the reference to the data array.
 *
 * dataArrayChanged signal is emitted when data array is set, unless \a newDataArray
 * is identical to the previous one.
 *
 * \note Before doing anything regarding the data array, a series must be created for
 * the relevant proxy.
 *
 * \sa clearArray()
 */
void QScatter3DSeries::setDataArray(const QScatterDataArray &newDataArray)
{
    Q_D(QScatter3DSeries);
    if (d->m_dataArray.data() == newDataArray.data()) {
        qCDebug(lcProperties3D) << __FUNCTION__
            << "newDataArray is the same than the old one";
        return;
    }
    d->setDataArray(newDataArray);
    emit dataArrayChanged(newDataArray);
}

/*!
 * Clears the data array.
 */
void QScatter3DSeries::clearArray()
{
    Q_D(QScatter3DSeries);
    d->clearArray();
}

const QScatterDataArray &QScatter3DSeries::dataArray() const &
{
    Q_D(const QScatter3DSeries);
    return d->m_dataArray;
}

QScatterDataArray QScatter3DSeries::dataArray() &&
{
    Q_D(QScatter3DSeries);
    return std::move(d->m_dataArray);
}

/*!
 * \property QScatter3DSeries::scaleArray
 *
 * \brief Scale array for the series.
 *
 * Holds the reference to the scale array.
 *
 * scaleArrayChanged signal is emitted when scale array is set, unless \a newScaleArray
 * is identical to the previous one.
 *
 * \note Before doing anything regarding the scale array, a series must be created for
 * the relevant proxy.
 *
 * \note If a scale is not specified for the data, a default value of (1.0,1.0,1.0)
 * will be used.
 *
 * \sa clearScaleArray()
 */
void QScatter3DSeries::setScaleArray(const QList<QVector3D> &newScaleArray)
{
    Q_D(QScatter3DSeries);
    if (d->m_scaleArray.data() == newScaleArray.data()) {
        qCDebug(lcProperties3D) << __FUNCTION__
            << "newScaleArray is the same than the old one";
        return;
    }
    d->setScaleArray(newScaleArray);
    emit scaleArrayChanged(newScaleArray);
}

/*!
 * Clears the scale array.
 */
void QScatter3DSeries::clearScaleArray()
{
    Q_D(QScatter3DSeries);
    d->clearScaleArray();
}

const QList<QVector3D> &QScatter3DSeries::scaleArray() const &
{
    Q_D(const QScatter3DSeries);
    return d->m_scaleArray;
}

QList<QVector3D> QScatter3DSeries::scaleArray() &&
{
    Q_D(QScatter3DSeries);
    return std::move(d->m_scaleArray);
}

/*!
 * Returns an invalid index for selection. This index is set to the selectedItem
 * property to clear the selection from this series.
 *
 * \sa Q3DGraphsWidgetItem::clearSelection()
 */
qsizetype QScatter3DSeries::invalidSelectionIndex()
{
    return QQuickGraphsScatter::invalidSelectionIndex();
}

// QScatter3DSeriesPrivate

QScatter3DSeriesPrivate::QScatter3DSeriesPrivate()
    : QAbstract3DSeriesPrivate(QAbstract3DSeries::SeriesType::Scatter)
    , m_selectedItem(QQuickGraphsScatter::invalidSelectionIndex())
    , m_itemSize(0.0f)
{
    m_itemLabelFormat = QStringLiteral("@xLabel, @yLabel, @zLabel");
    m_mesh = QAbstract3DSeries::Mesh::Sphere;
}

QScatter3DSeriesPrivate::~QScatter3DSeriesPrivate()
{
    clearArray();
}

void QScatter3DSeriesPrivate::setDataProxy(QAbstractDataProxy *proxy)
{
    Q_Q(QScatter3DSeries);
    Q_ASSERT(proxy->type() == QAbstractDataProxy::DataType::Scatter);

    QAbstract3DSeriesPrivate::setDataProxy(proxy);

    emit q->dataProxyChanged(static_cast<QScatterDataProxy *>(proxy));
}

void QScatter3DSeriesPrivate::connectGraphAndProxy(QQuickGraphsItem *newGraph)
{
    Q_Q(QScatter3DSeries);
    QScatterDataProxy *scatterDataProxy = static_cast<QScatterDataProxy *>(m_dataProxy);

    if (m_graph && scatterDataProxy) {
        // Disconnect old graph/old proxy
        QObject::disconnect(scatterDataProxy, 0, m_graph, 0);
        QObject::disconnect(q, 0, m_graph, 0);
    }

    if (newGraph && scatterDataProxy) {
        QQuickGraphsScatter *graph = static_cast<QQuickGraphsScatter *>(newGraph);
        QObject::connect(scatterDataProxy,
                         &QScatterDataProxy::arrayReset,
                         graph,
                         &QQuickGraphsScatter::handleArrayReset);
        QObject::connect(scatterDataProxy,
                         &QScatterDataProxy::itemsAdded,
                         graph,
                         &QQuickGraphsScatter::handleItemsAdded);
        QObject::connect(scatterDataProxy,
                         &QScatterDataProxy::itemsChanged,
                         graph,
                         &QQuickGraphsScatter::handleItemsChanged);
        QObject::connect(scatterDataProxy,
                         &QScatterDataProxy::itemsRemoved,
                         graph,
                         &QQuickGraphsScatter::handleItemsRemoved);
        QObject::connect(scatterDataProxy,
                         &QScatterDataProxy::itemsInserted,
                         graph,
                         &QQuickGraphsScatter::handleItemsInserted);
        QObject::connect(q,
                         &QScatter3DSeries::dataProxyChanged,
                         graph,
                         &QQuickGraphsScatter::handleArrayReset);
    }
}

void QScatter3DSeriesPrivate::createItemLabel()
{
    Q_Q(QScatter3DSeries);
    static const QString xTitleTag(QStringLiteral("@xTitle"));
    static const QString yTitleTag(QStringLiteral("@yTitle"));
    static const QString zTitleTag(QStringLiteral("@zTitle"));
    static const QString xLabelTag(QStringLiteral("@xLabel"));
    static const QString yLabelTag(QStringLiteral("@yLabel"));
    static const QString zLabelTag(QStringLiteral("@zLabel"));
    static const QString seriesNameTag(QStringLiteral("@seriesName"));

    if (m_selectedItem == QScatter3DSeries::invalidSelectionIndex()) {
        m_itemLabel = QString(hiddenLabelTag);
        return;
    }

    QValue3DAxis *axisX = static_cast<QValue3DAxis *>(m_graph->axisX());
    QValue3DAxis *axisY = static_cast<QValue3DAxis *>(m_graph->axisY());
    QValue3DAxis *axisZ = static_cast<QValue3DAxis *>(m_graph->axisZ());
    QVector3D selectedPosition = q->dataProxy()->itemAt(m_selectedItem).position();

    m_itemLabel = m_itemLabelFormat;

    m_itemLabel.replace(xTitleTag, axisX->title());
    m_itemLabel.replace(yTitleTag, axisY->title());
    m_itemLabel.replace(zTitleTag, axisZ->title());

    if (m_itemLabel.contains(xLabelTag)) {
        QString valueLabelText = axisX->formatter()->stringForValue(qreal(selectedPosition.x()),
                                                                    axisX->labelFormat());
        m_itemLabel.replace(xLabelTag, valueLabelText);
    }
    if (m_itemLabel.contains(yLabelTag)) {
        QString valueLabelText = axisY->formatter()->stringForValue(qreal(selectedPosition.y()),
                                                                    axisY->labelFormat());
        m_itemLabel.replace(yLabelTag, valueLabelText);
    }
    if (m_itemLabel.contains(zLabelTag)) {
        QString valueLabelText = axisZ->formatter()->stringForValue(qreal(selectedPosition.z()),
                                                                    axisZ->labelFormat());
        m_itemLabel.replace(zLabelTag, valueLabelText);
    }
    m_itemLabel.replace(seriesNameTag, m_name);
}

void QScatter3DSeriesPrivate::setSelectedItem(qsizetype index)
{
    Q_Q(QScatter3DSeries);
    if (index == m_selectedItem) {
        qCDebug(lcProperties3D, "%s Value is already set to: %" PRIdQSIZETYPE,
                qUtf8Printable(QLatin1String(__FUNCTION__)), index);
        return;
    }
    markItemLabelDirty();
    m_selectedItem = index;
    emit q->selectedItemChanged(m_selectedItem);
}

void QScatter3DSeriesPrivate::setItemSize(float size)
{
    m_itemSize = size;
    if (m_graph)
        m_graph->markSeriesVisualsDirty();
}

void QScatter3DSeriesPrivate::setDataArray(const QScatterDataArray &newDataArray)
{
    m_dataArray = newDataArray;
}

void QScatter3DSeriesPrivate::clearArray()
{
    m_dataArray.clear();
}

void QScatter3DSeriesPrivate::setScaleArray(const QList<QVector3D> &newScaleArray)
{
    m_scaleArray = newScaleArray;
}

void QScatter3DSeriesPrivate::clearScaleArray()
{
    m_scaleArray.clear();
}

QT_END_NAMESPACE
