/*
 * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved.
 *
 * This file is part of the KD Chart library.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#ifndef KCHARTCARTESIANDIAGRAMDATACOMPRESSOR_H
#define KCHARTCARTESIANDIAGRAMDATACOMPRESSOR_H

//
//  W A R N I N G
//  -------------
//
// This file is not part of the KD Chart API.  It exists purely as an
// implementation detail.  This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//

#include <limits>

#include <QModelIndex>
#include <QObject>
#include <QPair>
#include <QPointer>
#include <QVector>

#include "KChartDataValueAttributes.h"
#include "KChartModelDataCache_p.h"

#include "kchart_export.h"

class CartesianDiagramDataCompressorTests;
QT_BEGIN_NAMESPACE
class QAbstractItemModel;
QT_END_NAMESPACE

namespace KChart
{

class AbstractDiagram;

// - transparently compress table model data if the diagram widget
// size does not allow to display all data points in an acceptable way
// - the class acts much like a proxy model, but is not
// implemented as one, to avoid performance penalties by QVariant
// conversions
// - a wanted side effect is that the compressor will deliver
// more precise values for more precise media, like paper
// (a) this is absolutely strictly seriously private API of KChart
// (b) if possible, this class is going to be templatized for
// different diagram types

// KCHART_EXPORT is needed as long there's a test using
// this class directly
class KCHART_EXPORT CartesianDiagramDataCompressor : public QObject
{
    Q_OBJECT
    friend class ::CartesianDiagramDataCompressorTests;

public:
    class DataPoint
    {
    public:
        DataPoint()
            : key(std::numeric_limits<qreal>::quiet_NaN())
            , value(std::numeric_limits<qreal>::quiet_NaN())
            , hidden(false)
        {
        }
        qreal key;
        qreal value;
        bool hidden;
        QModelIndex index;
    };
    typedef QVector<DataPoint> DataPointVector;
    class CachePosition
    {
    public:
        CachePosition()
            : row(-1)
            , column(-1)
        {
        }
        CachePosition(int row, int column)
            : row(row)
            , column(column)
        {
        }
        int row;
        int column;

        bool operator==(const CachePosition &rhs) const
        {
            return row == rhs.row && column == rhs.column;
        }
        bool operator<(const CachePosition &rhs) const
        {
            // This function is used to topologically sort all cache positions.

            // Think of them as entries in a matrix or table:
            // An entry comes before another entry if it is either above the other
            // entry, or in the same row and to the left of the other entry.
            return row < rhs.row || (row == rhs.row && column < rhs.column);
        }
    };

    typedef QMap<QModelIndex, DataValueAttributes> AggregatedDataValueAttributes;
    typedef QMap<CartesianDiagramDataCompressor::CachePosition, AggregatedDataValueAttributes> DataValueAttributesCache;

    enum ApproximationMode {
        // do not approximate, interpolate by averaging all
        // datapoints for a pixel
        Precise,
        // approximate by averaging out over prime number distances
        SamplingSeven
    };

    explicit CartesianDiagramDataCompressor(QObject *parent = nullptr);

    // input: model, chart resolution, approximation mode
    void setModel(QAbstractItemModel *);
    void setRootIndex(const QModelIndex &root);
    void setResolution(int x, int y);
    void recalcResolution();
    void setApproximationMode(ApproximationMode mode);
    void setDatasetDimension(int dimension);

    // output: resulting model resolution, data points
    // FIXME (Mirko) rather stupid naming, Mirko!
    int modelDataColumns() const;
    int modelDataRows() const;
    const DataPoint &data(const CachePosition &) const;

    QPair<QPointF, QPointF> dataBoundaries() const;

    AggregatedDataValueAttributes aggregatedAttrs(const AbstractDiagram *diagram, const QModelIndex &index, const CachePosition &position) const;

public Q_SLOTS:
    // FIXME resolution changes and root index changes should all
    // be catchable with this method:
    void slotDiagramLayoutChanged(KChart::AbstractDiagram *);

private Q_SLOTS:
    void slotRowsAboutToBeInserted(const QModelIndex &, int, int);
    void slotRowsInserted(const QModelIndex &, int, int);
    void slotRowsAboutToBeRemoved(const QModelIndex &, int, int);
    void slotRowsRemoved(const QModelIndex &, int, int);

    void slotColumnsAboutToBeInserted(const QModelIndex &, int, int);
    void slotColumnsInserted(const QModelIndex &, int, int);
    void slotColumnsAboutToBeRemoved(const QModelIndex &, int, int);
    void slotColumnsRemoved(const QModelIndex &, int, int);

    void slotModelHeaderDataChanged(Qt::Orientation, int, int);
    void slotModelDataChanged(const QModelIndex &, const QModelIndex &);
    void slotModelLayoutChanged();

    // geometry has changed
    void rebuildCache();
    // reset all cached values, without changing the cache geometry
    void clearCache();

private:
    // private version of setResolution() that does *not* call rebuildCache()
    bool setResolutionInternal(int x, int y);
    // forget cached data at the position
    void invalidate(const CachePosition &);
    // check if position is inside the dataset's index range
    bool mapsToModelIndex(const CachePosition &) const;

    CachePosition mapToCache(const QModelIndex &) const;
    CachePosition mapToCache(int row, int column) const;
    // Note: returns only valid model indices
    QModelIndexList mapToModel(const CachePosition &) const;
    qreal indexesPerPixel() const;

    // common logic for slot{Rows,Columns}[AboutToBe]{Inserted,Removed}
    bool prepareDataChange(const QModelIndex &parent,
                           bool isRows, /* columns otherwise */
                           int *start,
                           int *end);

    // retrieve data from the model, put it into the cache
    void retrieveModelData(const CachePosition &) const;
    // check if a data point is in the cache:
    bool isCached(const CachePosition &) const;
    // set sample step width according to settings:
    void calculateSampleStepWidth();

    QPointer<QAbstractItemModel> m_model;
    QModelIndex m_rootIndex;

    ApproximationMode m_mode;
    int m_xResolution;
    int m_yResolution;
    unsigned int m_sampleStep;

    mutable QVector<DataPointVector> m_data; // one per dataset
    ModelDataCache<qreal, Qt::DisplayRole> m_modelCache;
    mutable DataValueAttributesCache m_dataValueAttributesCache;
    int m_datasetDimension;
};
}

#endif
