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

#include "qsurface3dseries_p.h"
#include "surfaceitemmodelhandler_p.h"

#include <qtgraphs_tracepoints_p.h>

QT_BEGIN_NAMESPACE

Q_TRACE_PREFIX(qtgraphs,
                  "QT_BEGIN_NAMESPACE" \
                  "class QQuickGraphsScatter;" \
                  "QT_END_NAMESPACE"
              )

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceItemModelHandlerResolveModel_entry);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceItemModelHandlerResolveModel_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceItemModelHandlerHandleDataChanged_entry, bool useModelCategories);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceItemModelHandlerHandleDataChanged_exit);

SurfaceItemModelHandler::SurfaceItemModelHandler(QItemModelSurfaceDataProxy *proxy, QObject *parent)
    : AbstractItemModelHandler(parent)
    , m_proxy(proxy)
    , m_proxyArray(0)
    , m_xPosRole(noRoleIndex)
    , m_yPosRole(noRoleIndex)
    , m_zPosRole(noRoleIndex)
    , m_haveXPosPattern(false)
    , m_haveYPosPattern(false)
    , m_haveZPosPattern(false)
{}

SurfaceItemModelHandler::~SurfaceItemModelHandler() {}

void SurfaceItemModelHandler::handleDataChanged(const QModelIndex &topLeft,
                                                const QModelIndex &bottomRight,
                                                const QList<int> &roles)
{
    // Do nothing if full reset already pending
    if (!m_fullReset) {
        Q_TRACE(QGraphs3DSurfaceItemModelHandlerHandleDataChanged_entry, m_proxy->useModelCategories());
        if (!m_proxy->useModelCategories()) {
            // If the data model doesn't directly map rows and columns, we cannot
            // optimize
            AbstractItemModelHandler::handleDataChanged(topLeft, bottomRight, roles);
        } else {
            int startRow = qMin(topLeft.row(), bottomRight.row());
            int endRow = qMax(topLeft.row(), bottomRight.row());
            int startCol = qMin(topLeft.column(), bottomRight.column());
            int endCol = qMax(topLeft.column(), bottomRight.column());

            for (int i = startRow; i <= endRow; i++) {
                for (int j = startCol; j <= endCol; j++) {
                    QModelIndex index = m_itemModel->index(i, j);
                    QSurfaceDataItem item;
                    QVariant xValueVar = index.data(m_xPosRole);
                    QVariant yValueVar = index.data(m_yPosRole);
                    QVariant zValueVar = index.data(m_zPosRole);
                    const QSurfaceDataItem &oldItem = m_proxy->itemAt(i, j);
                    float xPos;
                    float yPos;
                    float zPos;
                    if (m_xPosRole != noRoleIndex) {
                        if (m_haveXPosPattern) {
                            xPos = xValueVar.toString()
                                       .replace(m_xPosPattern, m_xPosReplace)
                                       .toFloat();
                        } else {
                            xPos = xValueVar.toFloat();
                        }
                    } else {
                        xPos = oldItem.x();
                    }

                    if (m_haveYPosPattern)
                        yPos = yValueVar.toString().replace(m_yPosPattern, m_yPosReplace).toFloat();
                    else
                        yPos = yValueVar.toFloat();

                    if (m_zPosRole != noRoleIndex) {
                        if (m_haveZPosPattern) {
                            zPos = zValueVar.toString()
                                       .replace(m_zPosPattern, m_zPosReplace)
                                       .toFloat();
                        } else {
                            zPos = zValueVar.toFloat();
                        }
                    } else {
                        zPos = oldItem.z();
                    }
                    item.setPosition(QVector3D(xPos, yPos, zPos));
                    m_proxy->setItem(i, j, item);
                }
            }
        }
        Q_TRACE(QGraphs3DSurfaceItemModelHandlerHandleDataChanged_exit);
     }
}

// Resolve entire item model into QSurfaceDataArray.
void SurfaceItemModelHandler::resolveModel()
{
    if (m_itemModel.isNull()) {
        m_proxy->resetArray();
        m_proxyArray.clear();
        return;
    }

    if (!m_proxy->useModelCategories()
        && (m_proxy->rowRole().isEmpty() || m_proxy->columnRole().isEmpty())) {
        m_proxy->resetArray();
        m_proxyArray.clear();
        return;
    }

    Q_TRACE(QGraphs3DSurfaceItemModelHandlerResolveModel_entry);
    // Position patterns can be reused on single item changes, so store them to
    // member variables.
    QRegularExpression rowPattern(m_proxy->rowRolePattern());
    QRegularExpression colPattern(m_proxy->columnRolePattern());
    m_xPosPattern = m_proxy->xPosRolePattern();
    m_yPosPattern = m_proxy->yPosRolePattern();
    m_zPosPattern = m_proxy->zPosRolePattern();
    QString rowReplace = m_proxy->rowRoleReplace();
    QString colReplace = m_proxy->columnRoleReplace();
    m_xPosReplace = m_proxy->xPosRoleReplace();
    m_yPosReplace = m_proxy->yPosRoleReplace();
    m_zPosReplace = m_proxy->zPosRoleReplace();
    bool haveRowPattern = !rowPattern.namedCaptureGroups().isEmpty() && rowPattern.isValid();
    bool haveColPattern = !colPattern.namedCaptureGroups().isEmpty() && colPattern.isValid();
    m_haveXPosPattern = !m_xPosPattern.namedCaptureGroups().isEmpty() && m_xPosPattern.isValid();
    m_haveYPosPattern = !m_yPosPattern.namedCaptureGroups().isEmpty() && m_yPosPattern.isValid();
    m_haveZPosPattern = !m_zPosPattern.namedCaptureGroups().isEmpty() && m_zPosPattern.isValid();

    QHash<int, QByteArray> roleHash = m_itemModel->roleNames();

    // Default to display role if no mapping
    m_xPosRole = roleHash.key(m_proxy->xPosRole().toLatin1(), noRoleIndex);
    m_yPosRole = roleHash.key(m_proxy->yPosRole().toLatin1(), Qt::DisplayRole);
    m_zPosRole = roleHash.key(m_proxy->zPosRole().toLatin1(), noRoleIndex);
    int rowCount = m_itemModel->rowCount();
    int columnCount = m_itemModel->columnCount();

    if (m_proxy->useModelCategories()) {
        // If dimensions have changed, recreate the array
        if (m_proxyArray.data() != m_proxy->series()->dataArray().data()
            || columnCount != m_proxy->columnCount() || rowCount != m_proxyArray.size()) {
            m_proxyArray.clear();
            m_proxyArray.reserve(rowCount);
            for (int i = 0; i < rowCount; i++)
                m_proxyArray.append(QSurfaceDataRow(columnCount));
        }
        for (int i = 0; i < rowCount; i++) {
            QSurfaceDataRow &newProxyRow = m_proxyArray[i];
            for (int j = 0; j < columnCount; j++) {
                QModelIndex index = m_itemModel->index(i, j);
                QVariant xValueVar = index.data(m_xPosRole);
                QVariant yValueVar = index.data(m_yPosRole);
                QVariant zValueVar = index.data(m_zPosRole);
                float xPos;
                float yPos;
                float zPos;
                if (m_xPosRole != noRoleIndex) {
                    if (m_haveXPosPattern)
                        xPos = xValueVar.toString().replace(m_xPosPattern, m_xPosReplace).toFloat();
                    else
                        xPos = xValueVar.toFloat();
                } else {
                    QString header = m_itemModel->headerData(j, Qt::Horizontal).toString();
                    bool ok = false;
                    float headerValue = header.toFloat(&ok);
                    if (ok)
                        xPos = headerValue;
                    else
                        xPos = float(j);
                }

                if (m_haveYPosPattern)
                    yPos = yValueVar.toString().replace(m_yPosPattern, m_yPosReplace).toFloat();
                else
                    yPos = yValueVar.toFloat();

                if (m_zPosRole != noRoleIndex) {
                    if (m_haveZPosPattern)
                        zPos = zValueVar.toString().replace(m_zPosPattern, m_zPosReplace).toFloat();
                    else
                        zPos = zValueVar.toFloat();
                } else {
                    QString header = m_itemModel->headerData(i, Qt::Vertical).toString();
                    bool ok = false;
                    float headerValue = header.toFloat(&ok);
                    if (ok)
                        zPos = headerValue;
                    else
                        zPos = float(i);
                }

                newProxyRow[j].setPosition(QVector3D(xPos, yPos, zPos));
            }
        }
    } else {
        int rowRole = roleHash.key(m_proxy->rowRole().toLatin1());
        int columnRole = roleHash.key(m_proxy->columnRole().toLatin1());
        if (m_xPosRole == noRoleIndex)
            m_xPosRole = columnRole;
        if (m_zPosRole == noRoleIndex)
            m_zPosRole = rowRole;

        bool generateRows = m_proxy->autoRowCategories();
        bool generateColumns = m_proxy->autoColumnCategories();

        QStringList rowList;
        QStringList columnList;
        // For detecting duplicates in categories generation, using QHashes should
        // be faster than simple QStringList::contains() check.
        QHash<QString, bool> rowListHash;
        QHash<QString, bool> columnListHash;

        bool cumulative = m_proxy->multiMatchBehavior()
                              == QItemModelSurfaceDataProxy::MultiMatchBehavior::Average
                          || m_proxy->multiMatchBehavior()
                                 == QItemModelSurfaceDataProxy::MultiMatchBehavior::CumulativeY;
        bool average = m_proxy->multiMatchBehavior()
                       == QItemModelSurfaceDataProxy::MultiMatchBehavior::Average;
        bool takeFirst = m_proxy->multiMatchBehavior()
                         == QItemModelSurfaceDataProxy::MultiMatchBehavior::First;
        QHash<QString, QHash<QString, int>> *matchCountMap = 0;
        if (cumulative)
            matchCountMap = new QHash<QString, QHash<QString, int>>;

        // Sort values into rows and columns
        typedef QHash<QString, QVector3D> ColumnValueMap;
        QHash<QString, ColumnValueMap> itemValueMap;
        for (int i = 0; i < rowCount; i++) {
            for (int j = 0; j < columnCount; j++) {
                QModelIndex index = m_itemModel->index(i, j);
                QString rowRoleStr = index.data(rowRole).toString();
                if (haveRowPattern)
                    rowRoleStr.replace(rowPattern, rowReplace);
                QString columnRoleStr = index.data(columnRole).toString();
                if (haveColPattern)
                    columnRoleStr.replace(colPattern, colReplace);
                QVariant xValueVar = index.data(m_xPosRole);
                QVariant yValueVar = index.data(m_yPosRole);
                QVariant zValueVar = index.data(m_zPosRole);
                float xPos;
                float yPos;
                float zPos;
                if (m_haveXPosPattern)
                    xPos = xValueVar.toString().replace(m_xPosPattern, m_xPosReplace).toFloat();
                else
                    xPos = xValueVar.toFloat();
                if (m_haveYPosPattern)
                    yPos = yValueVar.toString().replace(m_yPosPattern, m_yPosReplace).toFloat();
                else
                    yPos = yValueVar.toFloat();
                if (m_haveZPosPattern)
                    zPos = zValueVar.toString().replace(m_zPosPattern, m_zPosReplace).toFloat();
                else
                    zPos = zValueVar.toFloat();

                QVector3D itemPos(xPos, yPos, zPos);

                if (cumulative)
                    (*matchCountMap)[rowRoleStr][columnRoleStr]++;

                if (cumulative) {
                    itemValueMap[rowRoleStr][columnRoleStr] += itemPos;
                } else {
                    if (takeFirst && itemValueMap.contains(rowRoleStr)) {
                        if (itemValueMap.value(rowRoleStr).contains(columnRoleStr))
                            continue; // We already have a value for this row/column combo
                    }
                    itemValueMap[rowRoleStr][columnRoleStr] = itemPos;
                }

                if (generateRows && !rowListHash.value(rowRoleStr, false)) {
                    rowListHash.insert(rowRoleStr, true);
                    rowList << rowRoleStr;
                }
                if (generateColumns && !columnListHash.value(columnRoleStr, false)) {
                    columnListHash.insert(columnRoleStr, true);
                    columnList << columnRoleStr;
                }
            }
        }

        if (generateRows)
            m_proxy->d_func()->m_rowCategories = rowList;
        else
            rowList = m_proxy->rowCategories();

        if (generateColumns)
            m_proxy->d_func()->m_columnCategories = columnList;
        else
            columnList = m_proxy->columnCategories();

        // If dimensions have changed, recreate the array
        if (m_proxyArray.data() != m_proxy->series()->dataArray().data()
            || columnList.size() != m_proxy->columnCount()
            || rowList.size() != m_proxyArray.size()) {
            m_proxyArray.clear();
            m_proxyArray.reserve(rowList.size());
            for (int i = 0; i < rowList.size(); i++)
                m_proxyArray.append(QSurfaceDataRow(columnList.size()));
        }
        // Create data array from itemValueMap
        for (int i = 0; i < rowList.size(); i++) {
            QString rowKey = rowList.at(i);
            QSurfaceDataRow &newProxyRow = m_proxyArray[i];
            for (int j = 0; j < columnList.size(); j++) {
                QVector3D &itemPos = itemValueMap[rowKey][columnList.at(j)];
                if (cumulative) {
                    float divisor = float((*matchCountMap)[rowKey][columnList.at(j)]);
                    if (divisor) {
                        if (average) {
                            itemPos /= divisor;
                        } else { // cumulativeY
                            itemPos.setX(itemPos.x() / divisor);
                            itemPos.setZ(itemPos.z() / divisor);
                        }
                    }
                }
                newProxyRow[j].setPosition(itemPos);
            }
        }

        delete matchCountMap;
    }
    Q_TRACE(QGraphs3DSurfaceItemModelHandlerResolveModel_exit);
    m_proxy->resetArray(m_proxyArray);
}

QT_END_NAMESPACE
