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

#include "qabstract3daxis_p.h"
#include "qsurface3dseries_p.h"
#include "qsurfacedataproxy_p.h"
#include "qgraphs3dlogging_p.h"

QT_BEGIN_NAMESPACE

/*!
 * \class QSurfaceDataProxy
 * \inmodule QtGraphs
 * \ingroup graphs_3D
 * \brief The QSurfaceDataProxy class is the data proxy for a 3D surface graph.
 *
 * A surface data proxy handles surface related data in rows. For this it
 * provides two auxiliary type aliases: QtGraphs::QSurfaceDataArray and
 * QtGraphs::QSurfaceDataRow. \c QSurfaceDataArray is a QList that
 * controls the rows. \c QSurfaceDataRow is a QList that contains
 * QSurfaceDataItem objects. For more information about how to feed the data to
 * the proxy, see the sample code in the Q3DSurfaceWidgetItem documentation. Since data is
 * stored in series, it is necessary to create a series associated with the
 * proxy before using these functions for the dataset.
 *
 * All rows must have the same number of items.
 *
 * QSurfaceDataProxy takes ownership of all \c QSurfaceDataRow objects passed to
 * it, whether directly or in a \c QSurfaceDataArray container.
 * To use surface data row pointers to directly modify data after adding the
 * array to the proxy, the appropriate signal must be emitted to update the
 * graph.
 *
 * To make a sensible surface, the x-value of each successive item in all rows
 * must be either ascending or descending throughout the row. Similarly, the
 * z-value of each successive item in all columns must be either ascending or
 * descending throughout the column.
 *
 * \note Currently only surfaces with straight rows and columns are fully
 * supported. Any row with items that do not have the exact same z-value or any
 * column with items that do not have the exact same x-value may get clipped
 * incorrectly if the whole surface does not completely fit within the visible
 * x-axis or z-axis ranges.
 *
 * \note Surfaces with less than two rows or columns are not considered valid
 * surfaces and will not be rendered.
 *
 * \note On some environments, surfaces with a lot of visible vertices may not
 * render, because they exceed the per-draw vertex count supported by the
 * graphics driver. This is mostly an issue on 32-bit and OpenGL ES2 platforms.
 *
 * \sa {Qt Graphs Data Handling with 3D}
 */

/*!
 * \typealias QSurfaceDataRow
 * \relates QSurfaceDataProxy
 *
 * A list of \l {QSurfaceDataItem} objects.
 */

/*!
 * \typealias QSurfaceDataArray
 * \relates QSurfaceDataProxy
 *
 * A list of pointers to \l {QSurfaceDataRow} objects.
 */

/*!
 * \qmltype SurfaceDataProxy
 * \inqmlmodule QtGraphs
 * \ingroup graphs_qml_3D
 * \nativetype QSurfaceDataProxy
 * \inherits AbstractDataProxy
 * \brief The data proxy for a 3D surface graph.
 *
 * This type handles surface data items. The data is arranged into rows and
 * columns, and all rows must have the same number of columns.
 *
 * This type is uncreatable, but contains properties that are exposed via
 * subtypes.
 *
 * For a more complete description, see QSurfaceDataProxy.
 *
 * \sa ItemModelSurfaceDataProxy, {Qt Graphs Data Handling with 3D}
 */

/*!
 * \qmlproperty int SurfaceDataProxy::rowCount
 * The number of rows in the data array.
 */

/*!
 * \qmlproperty int SurfaceDataProxy::columnCount
 * The number of columns in the data array.
 */

/*!
 * \qmlproperty Surface3DSeries SurfaceDataProxy::series
 *
 * The series this proxy is attached to.
 */

/*!
    \qmlsignal SurfaceDataProxy::rowCountChanged(int count)

    This signal is emitted when rowCount changes to \a count.
*/
/*!
    \qmlsignal SurfaceDataProxy::columnCountChanged(int count)

    This signal is emitted when columnCount changes to \a count.
*/
/*!
    \qmlsignal SurfaceDataProxy::seriesChanged(Surface3DSeries series)

    This signal is emitted when \l series changes to \a series.
*/

/*!
 * Constructs QSurfaceDataProxy with the given \a parent.
 */
QSurfaceDataProxy::QSurfaceDataProxy(QObject *parent)
    : QAbstractDataProxy(*(new QSurfaceDataProxyPrivate()), parent)
{}

/*!
 * \internal
 */
QSurfaceDataProxy::QSurfaceDataProxy(QSurfaceDataProxyPrivate &d, QObject *parent)
    : QAbstractDataProxy(d, parent)
{}

/*!
 * Deletes the surface data proxy.
 */
QSurfaceDataProxy::~QSurfaceDataProxy() {}

/*!
 * \property QSurfaceDataProxy::series
 *
 *  \brief The series this proxy is attached to.
 */
QSurface3DSeries *QSurfaceDataProxy::series() const
{
    Q_D(const QSurfaceDataProxy);
    if (!d->series())
        qCWarning(lcProperties3D, "%s series needs to be created to access data members",
                  qUtf8Printable(QLatin1String(__FUNCTION__)));
    return static_cast<QSurface3DSeries *>(d->series());
}

/*!
 * Clears the existing array and triggers the arrayReset() signal.
 */
void QSurfaceDataProxy::resetArray()
{
    Q_D(QSurfaceDataProxy);
    d->resetArray(QSurfaceDataArray());

    emit arrayReset();
    emit rowCountChanged(rowCount());
    emit columnCountChanged(columnCount());
}

/*!
 * Sets the array from \a newArray. If the new array is equal to the
 * existing one, this function simply triggers the arrayReset() signal.
*/
void QSurfaceDataProxy::resetArray(QSurfaceDataArray newArray)
{
    Q_D(QSurfaceDataProxy);
    if (!series())
        return;

    if (series()->dataArray().data() != newArray.data())
        d->resetArray(std::move(newArray));

    emit arrayReset();
    emit rowCountChanged(rowCount());
    emit columnCountChanged(columnCount());
}

/*!
 * Changes an existing row by replacing the row at the position \a rowIndex
 * with the new row specified by \a row. The new row can be the same as the
 * existing row already stored at the \a rowIndex. The new row must have
 * the same number of columns as the row it is replacing.
 */
void QSurfaceDataProxy::setRow(qsizetype rowIndex, QSurfaceDataRow row)
{
    Q_D(QSurfaceDataProxy);
    d->setRow(rowIndex, std::move(row));
    emit rowsChanged(rowIndex, 1);
}

/*!
 * Changes existing rows by replacing the rows starting at the position
 * \a rowIndex with the new rows specifies by \a rows.
 * The rows in the \a rows array can be the same as the existing rows already
 * stored at the \a rowIndex. The new rows must have the same number of columns
 * as the rows they are replacing.
 */
void QSurfaceDataProxy::setRows(qsizetype rowIndex, QSurfaceDataArray rows)
{
    Q_D(QSurfaceDataProxy);
    d->setRows(rowIndex, std::move(rows));
    emit rowsChanged(rowIndex, rows.size());
}

/*!
 * Changes a single item at the position specified by \a rowIndex and
 * \a columnIndex to the item \a item.
 */
void QSurfaceDataProxy::setItem(qsizetype rowIndex, qsizetype columnIndex, QSurfaceDataItem item)
{
    Q_D(QSurfaceDataProxy);
    d->setItem(rowIndex, columnIndex, std::move(item));
    emit itemChanged(rowIndex, columnIndex);
}

/*!
 * Changes a single item at the position \a position to the item \a item.
 * The x-value of \a position indicates the row and the y-value indicates the
 * column.
 */
void QSurfaceDataProxy::setItem(QPoint position, QSurfaceDataItem item)
{
    setItem(position.x(), position.y(), item);
}

/*!
 * Adds the new row \a row to the end of an array. The new row must have
 * the same number of columns as the rows in the initial array.
 *
 * Returns the index of the added row.
 */
qsizetype QSurfaceDataProxy::addRow(QSurfaceDataRow row)
{
    Q_D(QSurfaceDataProxy);
    qsizetype addIndex = d->addRow(std::move(row));
    emit rowsAdded(addIndex, 1);
    emit rowCountChanged(rowCount());
    return addIndex;
}

/*!
 * Adds new \a rows to the end of an array. The new rows must have the same
 * number of columns as the rows in the initial array.
 *
 * Returns the index of the first added row.
 */
qsizetype QSurfaceDataProxy::addRows(QSurfaceDataArray rows)
{
    Q_D(QSurfaceDataProxy);
    qsizetype addIndex = d->addRows(std::move(rows));
    emit rowsAdded(addIndex, rows.size());
    emit rowCountChanged(rowCount());
    return addIndex;
}

/*!
 * Inserts the new row \a row into \a rowIndex.
 * If \a rowIndex is equal to the array size, the rows are added to the end of
 * the array. The new row must have the same number of columns as the rows in
 * the initial array.
 */
void QSurfaceDataProxy::insertRow(qsizetype rowIndex, QSurfaceDataRow row)
{
    Q_D(QSurfaceDataProxy);
    d->insertRow(rowIndex, std::move(row));
    emit rowsInserted(rowIndex, 1);
    emit rowCountChanged(rowCount());
}

/*!
 * Inserts new \a rows into \a rowIndex.
 * If \a rowIndex is equal to the array size, the rows are added to the end of
 * the array. The new \a rows must have the same number of columns as the rows
 * in the initial array.
 */
void QSurfaceDataProxy::insertRows(qsizetype rowIndex, QSurfaceDataArray rows)
{
    Q_D(QSurfaceDataProxy);
    d->insertRows(rowIndex, std::move(rows));
    emit rowsInserted(rowIndex, rows.size());
    emit rowCountChanged(rowCount());
}

/*!
 * Removes the number of rows specified by \a removeCount starting at the
 * position \a rowIndex. Attempting to remove rows past the end of the
 * array does nothing.
 */
void QSurfaceDataProxy::removeRows(qsizetype rowIndex, qsizetype removeCount)
{
    Q_D(QSurfaceDataProxy);
    if (rowIndex < rowCount() && removeCount >= 1) {
        d->removeRows(rowIndex, removeCount);
        emit rowsRemoved(rowIndex, removeCount);
        emit rowCountChanged(rowCount());
    }
}

/*!
 * Returns the pointer to the item at the position specified by \a rowIndex and
 * \a columnIndex. It is guaranteed to be valid only
 * until the next call that modifies data.
 */
const QSurfaceDataItem &QSurfaceDataProxy::itemAt(qsizetype rowIndex, qsizetype columnIndex) const
{
    const QSurfaceDataArray &dataArray = series()->dataArray();
    Q_ASSERT(rowIndex >= 0 && rowIndex < dataArray.size());
    const QSurfaceDataRow &dataRow = dataArray[rowIndex];
    Q_ASSERT(columnIndex >= 0 && columnIndex < dataRow.size());
    return dataRow.at(columnIndex);
}

/*!
 * Returns the pointer to the item at the position \a position. The x-value of
 * \a position indicates the row and the y-value indicates the column. The item
 * is guaranteed to be valid only until the next call that modifies data.
 */
const QSurfaceDataItem &QSurfaceDataProxy::itemAt(QPoint position) const
{
    return itemAt(position.x(), position.y());
}

/*!
 * \property QSurfaceDataProxy::rowCount
 *
 * \brief The number of rows in the data array.
 */
qsizetype QSurfaceDataProxy::rowCount() const
{
    if (series())
        return series()->dataArray().size();
    else
        return 0;
}

/*!
 * \property QSurfaceDataProxy::columnCount
 *
 * \brief The number of columns in the data array.
 */
qsizetype QSurfaceDataProxy::columnCount() const
{
    if (series() && series()->dataArray().size() > 0)
        return series()->dataArray().at(0).size();
    else
        return 0;
}

/*!
 * \fn void QSurfaceDataProxy::arrayReset()
 *
 * This signal is emitted when the data array is reset.
 * If the contents of the whole array are changed without calling resetArray(),
 * this signal needs to be emitted to update the graph.
 */

/*!
 * \fn void QSurfaceDataProxy::rowsAdded(qsizetype startIndex, qsizetype count)
 *
 * This signal is emitted when the number of rows specified by \a count is
 * added, starting at the position \a startIndex.
 * If rows are added to the array without calling addRow() or addRows(),
 * this signal needs to be emitted to update the graph.
 */

/*!
 * \fn void QSurfaceDataProxy::rowsChanged(qsizetype startIndex, qsizetype count)
 *
 * This signal is emitted when the number of rows specified by \a count is
 * changed, starting at the position \a startIndex.
 * If rows are changed in the array without calling setRow() or setRows(),
 * this signal needs to be emitted to update the graph.
 */

/*!
 * \fn void QSurfaceDataProxy::rowsRemoved(qsizetype startIndex, qsizetype count)
 *
 * This signal is emitted when the number of rows specified by \a count is
 * removed, starting at the position \a startIndex.
 *
 * The index is the current array size if the rows were removed from the end of
 * the array. If rows are removed from the array without calling removeRows(),
 * this signal needs to be emitted to update the graph.
 */

/*!
 * \fn void QSurfaceDataProxy::rowsInserted(qsizetype startIndex, qsizetype count)
 *
 * This signal is emitted when the number of rows specified by \a count is
 * inserted at the position \a startIndex.
 *
 * If rows are inserted into the array without calling insertRow() or
 * insertRows(), this signal needs to be emitted to update the graph.
 */

/*!
 * \fn void QSurfaceDataProxy::itemChanged(qsizetype rowIndex, qsizetype columnIndex)
 *
 * This signal is emitted when the item at the position specified by \a rowIndex
 * and \a columnIndex changes.
 * If the item is changed in the array without calling setItem(),
 * this signal needs to be emitted to update the graph.
 */

//  QSurfaceDataProxyPrivate

QSurfaceDataProxyPrivate::QSurfaceDataProxyPrivate()
    : QAbstractDataProxyPrivate(QAbstractDataProxy::DataType::Surface)
{}

QSurfaceDataProxyPrivate::~QSurfaceDataProxyPrivate() {}

void QSurfaceDataProxyPrivate::resetArray(QSurfaceDataArray &&newArray)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    if (newArray.data() != surfaceSeries->dataArray().data()) {
        surfaceSeries->clearArray();
        surfaceSeries->setDataArray(newArray);
    }
}

void QSurfaceDataProxyPrivate::setRow(qsizetype rowIndex, QSurfaceDataRow &&row)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    Q_ASSERT(rowIndex >= 0 && rowIndex < surfaceSeries->dataArray().size());
    Q_ASSERT(surfaceSeries->dataArray().at(rowIndex).size() == row.size());

    if (row.data() != surfaceSeries->dataArray().at(rowIndex).data()) {
        surfaceSeries->clearRow(rowIndex);
        QSurfaceDataArray array = surfaceSeries->dataArray();
        array[rowIndex] = row;
        surfaceSeries->setDataArray(array);
    }
}

void QSurfaceDataProxyPrivate::setRows(qsizetype rowIndex, QSurfaceDataArray &&rows)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    QSurfaceDataArray array = surfaceSeries->dataArray();
    Q_ASSERT(rowIndex >= 0 && (rowIndex + rows.size()) <= array.size());

    for (int i = 0; i < rows.size(); i++) {
        Q_ASSERT(surfaceSeries->dataArray().at(rowIndex).size() == rows.at(i).size());
        if (rows.at(i).data() != array.at(rowIndex).data()) {
            surfaceSeries->clearRow(rowIndex);
            array[rowIndex] = rows.at(i);
        }
        rowIndex++;
    }
    surfaceSeries->setDataArray(array);
}

void QSurfaceDataProxyPrivate::setItem(qsizetype rowIndex, qsizetype columnIndex, QSurfaceDataItem &&item)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    Q_ASSERT(rowIndex >= 0 && rowIndex < surfaceSeries->dataArray().size());
    QSurfaceDataArray array = surfaceSeries->dataArray();
    QSurfaceDataRow &row = array[rowIndex];
    Q_ASSERT(columnIndex < row.size());
    row[columnIndex] = item;
    surfaceSeries->setDataArray(array);
}

qsizetype QSurfaceDataProxyPrivate::addRow(QSurfaceDataRow &&row)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    Q_ASSERT(surfaceSeries->dataArray().isEmpty()
             || surfaceSeries->dataArray().at(0).size() == row.size());
    qsizetype currentSize = surfaceSeries->dataArray().size();
    QSurfaceDataArray array = surfaceSeries->dataArray();
    array.append(row);
    surfaceSeries->setDataArray(array);
    return currentSize;
}

qsizetype QSurfaceDataProxyPrivate::addRows(QSurfaceDataArray &&rows)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    qsizetype currentSize = surfaceSeries->dataArray().size();
    QSurfaceDataArray array = surfaceSeries->dataArray();
    for (int i = 0; i < rows.size(); i++) {
        Q_ASSERT(surfaceSeries->dataArray().isEmpty()
                 || surfaceSeries->dataArray().at(0).size() == rows.at(i).size());
        array.append(rows.at(i));
    }
    surfaceSeries->setDataArray(array);
    return currentSize;
}

void QSurfaceDataProxyPrivate::insertRow(qsizetype rowIndex, QSurfaceDataRow &&row)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    Q_ASSERT(rowIndex >= 0 && rowIndex <= surfaceSeries->dataArray().size());
    Q_ASSERT(surfaceSeries->dataArray().isEmpty()
             || surfaceSeries->dataArray().at(0).size() == row.size());
    QSurfaceDataArray array = surfaceSeries->dataArray();
    array.insert(rowIndex, row);
    surfaceSeries->setDataArray(array);
}

void QSurfaceDataProxyPrivate::insertRows(qsizetype rowIndex, QSurfaceDataArray &&rows)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    Q_ASSERT(rowIndex >= 0 && rowIndex <= surfaceSeries->dataArray().size());
    QSurfaceDataArray array = surfaceSeries->dataArray();

    for (int i = 0; i < rows.size(); i++) {
        Q_ASSERT(surfaceSeries->dataArray().isEmpty()
                 || surfaceSeries->dataArray().at(0).size() == rows.at(i).size());
        array.insert(rowIndex++, rows.at(i));
    }
    surfaceSeries->setDataArray(array);
}

void QSurfaceDataProxyPrivate::removeRows(qsizetype rowIndex, qsizetype removeCount)
{
    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    Q_ASSERT(rowIndex >= 0);
    qsizetype maxRemoveCount = surfaceSeries->dataArray().size() - rowIndex;
    removeCount = qMin(removeCount, maxRemoveCount);
    QSurfaceDataArray array = surfaceSeries->dataArray();
    for (int i = 0; i < removeCount; i++) {
        surfaceSeries->clearRow(rowIndex);
        array.removeAt(rowIndex);
    }
    surfaceSeries->setDataArray(array);
}

void QSurfaceDataProxyPrivate::limitValues(QVector3D &minValues,
                                           QVector3D &maxValues,
                                           QAbstract3DAxis *axisX,
                                           QAbstract3DAxis *axisY,
                                           QAbstract3DAxis *axisZ) const
{
    float min = 0.0f;
    float max = 0.0f;

    auto *surfaceSeries = static_cast<QSurface3DSeries *>(series());
    qsizetype rows = surfaceSeries->dataArray().size();
    qsizetype columns = 0;
    if (rows)
        columns = surfaceSeries->dataArray().at(0).size();

    if (rows && columns) {
        min = surfaceSeries->dataArray().at(0).at(0).y();
        max = surfaceSeries->dataArray().at(0).at(0).y();
    }

    for (int i = 0; i < rows; i++) {
        const QSurfaceDataRow &row = surfaceSeries->dataArray().at(i);
        if (!row.isEmpty()) {
            for (int j = 0; j < columns; j++) {
                float itemValue = surfaceSeries->dataArray().at(i).at(j).y();
                if (qIsNaN(itemValue) || qIsInf(itemValue))
                    continue;
                if ((min > itemValue || (qIsNaN(min) || qIsInf(min)))
                    && isValidValue(itemValue, axisY)) {
                    min = itemValue;
                }
                if (max < itemValue || (qIsNaN(max) || qIsInf(max)))
                    max = itemValue;
            }
        }
    }

    minValues.setY(min);
    maxValues.setY(max);

    if (columns) {
        // Have some defaults
        float xLow = surfaceSeries->dataArray().at(0).at(0).x();
        float xHigh = surfaceSeries->dataArray().at(0).last().x();
        float zLow = surfaceSeries->dataArray().at(0).at(0).z();
        float zHigh = surfaceSeries->dataArray().last().at(0).z();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; j++) {
                float zItemValue = surfaceSeries->dataArray().at(i).at(j).z();
                if (qIsNaN(zItemValue) || qIsInf(zItemValue))
                    continue;
                else if (isValidValue(zItemValue, axisZ))
                    zLow = qMin(zLow, zItemValue);
            }
            if (!qIsNaN(zLow) && !qIsInf(zLow))
                break;
        }
        for (qsizetype i = rows - 1; i >= 0; i--) {
            for (qsizetype j = 0; j < columns; j++) {
                float zItemValue = surfaceSeries->dataArray().at(i).at(j).z();
                if (qIsNaN(zItemValue) || qIsInf(zItemValue))
                    continue;
                else if (isValidValue(zItemValue, axisZ)) {
                    if (!qIsNaN(zHigh) && !qIsInf(zHigh))
                        zHigh = qMax(zHigh, zItemValue);
                    else
                        zHigh = zItemValue;
                }
            }
            if (!qIsNaN(zHigh) && !qIsInf(zHigh))
                break;
        }
        for (qsizetype j = 0; j < columns; j++) {
            for (qsizetype i = 0; i < rows; i++) {
                float xItemValue = surfaceSeries->dataArray().at(i).at(j).x();
                if (qIsNaN(xItemValue) || qIsInf(xItemValue))
                    continue;
                else if (isValidValue(xItemValue, axisX))
                    xLow = qMin(xLow, xItemValue);
            }
            if (!qIsNaN(xLow) && !qIsInf(xLow))
                break;
        }
        for (qsizetype j = columns - 1; j >= 0; j--) {
            for (qsizetype i = 0; i < rows; i++) {
                float xItemValue = surfaceSeries->dataArray().at(i).at(j).x();
                if (qIsNaN(xItemValue) || qIsInf(xItemValue)) {
                    continue;
                } else if (isValidValue(xItemValue, axisX)) {
                    if (!qIsNaN(xHigh) && !qIsInf(xHigh))
                        xHigh = qMax(xHigh, xItemValue);
                    else
                        xHigh = xItemValue;
                }
            }
            if (!qIsNaN(xHigh) && !qIsInf(xHigh))
                break;
        }
        minValues.setX(xLow);
        minValues.setZ(zLow);
        maxValues.setX(xHigh);
        maxValues.setZ(zHigh);
    } else {
        minValues.setX(axisX->d_func()->allowZero() ? 0.0f : 1.0f);
        minValues.setZ(axisZ->d_func()->allowZero() ? 0.0f : 1.0f);
        maxValues.setX(axisX->d_func()->allowZero() ? 0.0f : 1.0f);
        maxValues.setZ(axisZ->d_func()->allowZero() ? 0.0f : 1.0f);
    }
}

bool QSurfaceDataProxyPrivate::isValidValue(float value, QAbstract3DAxis *axis) const
{
    return (value > 0.0f || (value == 0.0f && axis->d_func()->allowZero())
            || (value < 0.0f && axis->d_func()->allowNegatives()));
}

void QSurfaceDataProxyPrivate::setSeries(QAbstract3DSeries *series)
{
    Q_Q(QSurfaceDataProxy);
    QAbstractDataProxyPrivate::setSeries(series);
    QSurface3DSeries *surfaceSeries = static_cast<QSurface3DSeries *>(series);
    emit q->seriesChanged(surfaceSeries);
}

QT_END_NAMESPACE
