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

#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses

#include "scatter3dcontroller_p.h"
#include "scatter3drenderer_p.h"
#include "qvalue3daxis_p.h"
#include "qscatterdataproxy_p.h"
#include "qscatter3dseries_p.h"
#include <QtCore/QMutexLocker>

QT_BEGIN_NAMESPACE

static const int insertRemoveRecordReserveSize = 31;

Scatter3DController::Scatter3DController(QRect boundRect, Q3DScene *scene)
    : Abstract3DController(boundRect, scene),
      m_renderer(0),
      m_selectedItem(invalidSelectionIndex()),
      m_selectedItemSeries(0),
      m_recordInsertsAndRemoves(false)
{
    // Setting a null axis creates a new default axis according to orientation and graph type.
    // Note: These cannot be set in Abstract3DController constructor, as they will call virtual
    //       functions implemented by subclasses.
    setAxisX(0);
    setAxisY(0);
    setAxisZ(0);
}

Scatter3DController::~Scatter3DController()
{
}

void Scatter3DController::initializeOpenGL()
{
    QMutexLocker mutexLocker(&m_renderMutex);

    // Initialization is called multiple times when Qt Quick components are used
    if (isInitialized())
        return;

    m_renderer = new Scatter3DRenderer(this);
    setRenderer(m_renderer);

    mutexLocker.unlock();
    synchDataToRenderer();

    emitNeedRender();
}

void Scatter3DController::synchDataToRenderer()
{
    QMutexLocker mutexLocker(&m_renderMutex);

    if (!isInitialized())
        return;

    Abstract3DController::synchDataToRenderer();

    // Notify changes to renderer
    if (m_changeTracker.itemChanged) {
        m_renderer->updateItems(m_changedItems);
        m_changeTracker.itemChanged = false;
        m_changedItems.clear();
    }

    if (m_changeTracker.selectedItemChanged) {
        m_renderer->updateSelectedItem(m_selectedItem, m_selectedItemSeries);
        m_changeTracker.selectedItemChanged = false;
    }
}

void Scatter3DController::addSeries(QAbstract3DSeries *series)
{
    Q_ASSERT(series && series->type() == QAbstract3DSeries::SeriesTypeScatter);

    Abstract3DController::addSeries(series);

    QScatter3DSeries *scatterSeries =  static_cast<QScatter3DSeries *>(series);
    if (scatterSeries->selectedItem() != invalidSelectionIndex())
        setSelectedItem(scatterSeries->selectedItem(), scatterSeries);
}

void Scatter3DController::removeSeries(QAbstract3DSeries *series)
{
    bool wasVisible = (series && series->d_ptr->m_controller == this && series->isVisible());

    Abstract3DController::removeSeries(series);

    if (m_selectedItemSeries == series)
        setSelectedItem(invalidSelectionIndex(), 0);

    if (wasVisible)
        adjustAxisRanges();
}

QList<QScatter3DSeries *> Scatter3DController::scatterSeriesList()
{
    QList<QAbstract3DSeries *> abstractSeriesList = seriesList();
    QList<QScatter3DSeries *> scatterSeriesList;
    for (auto abstractSeries: abstractSeriesList) {
        QScatter3DSeries *scatterSeries = qobject_cast<QScatter3DSeries *>(abstractSeries);
        if (scatterSeries)
            scatterSeriesList.append(scatterSeries);
    }

    return scatterSeriesList;
}

void Scatter3DController::handleArrayReset()
{
    QScatter3DSeries *series;
    if (qobject_cast<QScatterDataProxy *>(sender()))
        series = static_cast<QScatterDataProxy *>(sender())->series();
    else
        series = static_cast<QScatter3DSeries *>(sender());

    if (series->isVisible()) {
        adjustAxisRanges();
        m_isDataDirty = true;
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);
    setSelectedItem(m_selectedItem, m_selectedItemSeries);
    series->d_ptr->markItemLabelDirty();
    emitNeedRender();
}

void Scatter3DController::handleItemsAdded(int startIndex, int count)
{
    Q_UNUSED(startIndex);
    Q_UNUSED(count);
    QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
    if (series->isVisible()) {
        adjustAxisRanges();
        m_isDataDirty = true;
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);
    emitNeedRender();
}

void Scatter3DController::handleItemsChanged(int startIndex, int count)
{
    QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
    int oldChangeCount = m_changedItems.size();
    if (!oldChangeCount)
        m_changedItems.reserve(count);

    for (int i = 0; i < count; i++) {
        bool newItem = true;
        int candidate = startIndex + i;
        for (int j = 0; j < oldChangeCount; j++) {
            const ChangeItem &oldChangeItem = m_changedItems.at(j);
            if (oldChangeItem.index == candidate && series == oldChangeItem.series) {
                newItem = false;
                break;
            }
        }
        if (newItem) {
            ChangeItem newChangeItem = {series, candidate};
            m_changedItems.append(newChangeItem);
            if (series == m_selectedItemSeries && m_selectedItem == candidate)
                series->d_ptr->markItemLabelDirty();
        }
    }

    if (count) {
        m_changeTracker.itemChanged = true;
        if (series->isVisible())
            adjustAxisRanges();
        emitNeedRender();
    }
}

void Scatter3DController::handleItemsRemoved(int startIndex, int count)
{
    Q_UNUSED(startIndex);
    Q_UNUSED(count);
    QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
    if (series == m_selectedItemSeries) {
        // If items removed from selected series before the selection, adjust the selection
        int selectedItem = m_selectedItem;
        if (startIndex <= selectedItem) {
            if ((startIndex + count) > selectedItem)
                selectedItem = -1; // Selected item removed
            else
                selectedItem -= count; // Move selected item down by amount of item removed

            setSelectedItem(selectedItem, m_selectedItemSeries);
        }
    }

    if (series->isVisible()) {
        adjustAxisRanges();
        m_isDataDirty = true;
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);

    if (m_recordInsertsAndRemoves) {
        InsertRemoveRecord record(false, startIndex, count, series);
        m_insertRemoveRecords.append(record);
    }

    emitNeedRender();
}

void Scatter3DController::handleItemsInserted(int startIndex, int count)
{
    Q_UNUSED(startIndex);
    Q_UNUSED(count);
    QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
    if (series == m_selectedItemSeries) {
        // If items inserted to selected series before the selection, adjust the selection
        int selectedItem = m_selectedItem;
        if (startIndex <= selectedItem) {
            selectedItem += count;
            setSelectedItem(selectedItem, m_selectedItemSeries);
        }
    }

    if (series->isVisible()) {
        adjustAxisRanges();
        m_isDataDirty = true;
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);

    if (m_recordInsertsAndRemoves) {
        InsertRemoveRecord record(true, startIndex, count, series);
        m_insertRemoveRecords.append(record);
    }

    emitNeedRender();
}

void Scatter3DController::startRecordingRemovesAndInserts()
{
    m_recordInsertsAndRemoves = false;

    if (m_scene->selectionQueryPosition() != Q3DScene::invalidSelectionPoint()) {
        m_recordInsertsAndRemoves = true;
        if (m_insertRemoveRecords.size()) {
            m_insertRemoveRecords.clear();
            // Reserve some space for remove/insert records to avoid unnecessary reallocations.
            m_insertRemoveRecords.reserve(insertRemoveRecordReserveSize);
        }
    }
}

void Scatter3DController::handleAxisAutoAdjustRangeChangedInOrientation(
        QAbstract3DAxis::AxisOrientation orientation, bool autoAdjust)
{
    Q_UNUSED(orientation);
    Q_UNUSED(autoAdjust);
    adjustAxisRanges();
}

void Scatter3DController::handleAxisRangeChangedBySender(QObject *sender)
{
    Abstract3DController::handleAxisRangeChangedBySender(sender);

    // Update selected index - may be moved offscreen
    setSelectedItem(m_selectedItem, m_selectedItemSeries);
}

void Scatter3DController::handlePendingClick()
{
    int index = m_renderer->clickedIndex();
    QScatter3DSeries *series = static_cast<QScatter3DSeries *>(m_renderer->clickedSeries());

    // Adjust position according to recorded events
    int recordCount = m_insertRemoveRecords.size();
    if (recordCount) {
        for (int i = 0; i < recordCount; i++) {
            const InsertRemoveRecord &record = m_insertRemoveRecords.at(i);
            if (series == record.m_series && record.m_startIndex <= index) {
                if (record.m_isInsert) {
                    index += record.m_count;
                } else {
                    if ((record.m_startIndex + record.m_count) > index) {
                        index = -1; // Selected row removed
                        break;
                    } else {
                        index -= record.m_count; // Move selected item down by amount of items removed
                    }
                }
            }
        }
    }

    setSelectedItem(index, series);

    Abstract3DController::handlePendingClick();

    m_renderer->resetClickedStatus();
}

void Scatter3DController::setSelectionMode(QAbstract3DGraph::SelectionFlags mode)
{
    // We only support single item selection mode and no selection mode
    if (mode != QAbstract3DGraph::SelectionItem && mode != QAbstract3DGraph::SelectionNone) {
        qWarning("Unsupported selection mode - only none and item selection modes are supported.");
        return;
    }

    Abstract3DController::setSelectionMode(mode);
}

void Scatter3DController::setSelectedItem(int index, QScatter3DSeries *series)
{
    const QScatterDataProxy *proxy = 0;

    // Series may already have been removed, so check it before setting the selection.
    if (!m_seriesList.contains(series))
        series = 0;

    if (series)
        proxy = series->dataProxy();

    if (!proxy || index < 0 || index >= proxy->itemCount())
        index = invalidSelectionIndex();

    if (index != m_selectedItem || series != m_selectedItemSeries) {
        bool seriesChanged = (series != m_selectedItemSeries);
        m_selectedItem = index;
        m_selectedItemSeries = series;
        m_changeTracker.selectedItemChanged = true;

        // Clear selection from other series and finally set new selection to the specified series
        for (auto otherSeries: m_seriesList) {
            QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(otherSeries);
            if (scatterSeries != m_selectedItemSeries)
                scatterSeries->dptr()->setSelectedItem(invalidSelectionIndex());
        }
        if (m_selectedItemSeries)
            m_selectedItemSeries->dptr()->setSelectedItem(m_selectedItem);

        if (seriesChanged)
            emit selectedSeriesChanged(m_selectedItemSeries);

        emitNeedRender();
    }
}

void Scatter3DController::clearSelection()
{
    setSelectedItem(invalidSelectionIndex(), 0);
}

void Scatter3DController::adjustAxisRanges()
{
    QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
    QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
    QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
    bool adjustX = (valueAxisX && valueAxisX->isAutoAdjustRange());
    bool adjustY = (valueAxisY && valueAxisY->isAutoAdjustRange());
    bool adjustZ = (valueAxisZ && valueAxisZ->isAutoAdjustRange());

    if (adjustX || adjustY || adjustZ) {
        float minValueX = 0.0f;
        float maxValueX = 0.0f;
        float minValueY = 0.0f;
        float maxValueY = 0.0f;
        float minValueZ = 0.0f;
        float maxValueZ = 0.0f;
        int seriesCount = m_seriesList.size();
        for (int series = 0; series < seriesCount; series++) {
            const QScatter3DSeries *scatterSeries =
                    static_cast<QScatter3DSeries *>(m_seriesList.at(series));
            const QScatterDataProxy *proxy = scatterSeries->dataProxy();
            if (scatterSeries->isVisible() && proxy) {
                QVector3D minLimits;
                QVector3D maxLimits;
                proxy->dptrc()->limitValues(minLimits, maxLimits, valueAxisX, valueAxisY, valueAxisZ);
                if (adjustX) {
                    if (!series) {
                        // First series initializes the values
                        minValueX = minLimits.x();
                        maxValueX = maxLimits.x();
                    } else {
                        minValueX = qMin(minValueX, minLimits.x());
                        maxValueX = qMax(maxValueX, maxLimits.x());
                    }
                }
                if (adjustY) {
                    if (!series) {
                        // First series initializes the values
                        minValueY = minLimits.y();
                        maxValueY = maxLimits.y();
                    } else {
                        minValueY = qMin(minValueY, minLimits.y());
                        maxValueY = qMax(maxValueY, maxLimits.y());
                    }
                }
                if (adjustZ) {
                    if (!series) {
                        // First series initializes the values
                        minValueZ = minLimits.z();
                        maxValueZ = maxLimits.z();
                    } else {
                        minValueZ = qMin(minValueZ, minLimits.z());
                        maxValueZ = qMax(maxValueZ, maxLimits.z());
                    }
                }
            }
        }

        static const float adjustmentRatio = 20.0f;
        static const float defaultAdjustment = 1.0f;

        if (adjustX) {
            // If all points at same coordinate, need to default to some valid range
            float adjustment = 0.0f;
            if (minValueX == maxValueX) {
                if (adjustZ) {
                    // X and Z are linked to have similar unit size, so choose the valid range based on it
                    if (minValueZ == maxValueZ)
                        adjustment = defaultAdjustment;
                    else
                        adjustment = qAbs(maxValueZ - minValueZ) / adjustmentRatio;
                } else {
                    if (valueAxisZ)
                        adjustment = qAbs(valueAxisZ->max() - valueAxisZ->min()) / adjustmentRatio;
                    else
                        adjustment = defaultAdjustment;
                }
            }
            valueAxisX->dptr()->setRange(minValueX - adjustment, maxValueX + adjustment, true);
        }
        if (adjustY) {
            // If all points at same coordinate, need to default to some valid range
            // Y-axis unit is not dependent on other axes, so simply adjust +-1.0f
            float adjustment = 0.0f;
            if (minValueY == maxValueY)
                adjustment = defaultAdjustment;
            valueAxisY->dptr()->setRange(minValueY - adjustment, maxValueY + adjustment, true);
        }
        if (adjustZ) {
            // If all points at same coordinate, need to default to some valid range
            float adjustment = 0.0f;
            if (minValueZ == maxValueZ) {
                if (adjustX) {
                    // X and Z are linked to have similar unit size, so choose the valid range based on it
                    if (minValueX == maxValueX)
                        adjustment = defaultAdjustment;
                    else
                        adjustment = qAbs(maxValueX - minValueX) / adjustmentRatio;
                } else {
                    if (valueAxisX)
                        adjustment = qAbs(valueAxisX->max() - valueAxisX->min()) / adjustmentRatio;
                    else
                        adjustment = defaultAdjustment;
                }
            }
            valueAxisZ->dptr()->setRange(minValueZ - adjustment, maxValueZ + adjustment, true);
        }
    }
}

QT_END_NAMESPACE
