/*
 * 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
 */

#include "KChartCartesianDiagramDataCompressor_p.h"

#include <QtDebug>
#include <QAbstractItemModel>

#include "KChartAbstractCartesianDiagram.h"
#include "KChartMath_p.h"


using namespace KChart;
using namespace std;

CartesianDiagramDataCompressor::CartesianDiagramDataCompressor( QObject* parent )
    : QObject( parent )
    , m_mode( Precise )
    , m_xResolution( 0 )
    , m_yResolution( 0 )
    , m_sampleStep( 0 )
    , m_datasetDimension( 1 )
{
    calculateSampleStepWidth();
    m_data.resize( 0 );
}

static bool contains( const CartesianDiagramDataCompressor::AggregatedDataValueAttributes& aggregated,
                      const DataValueAttributes& attributes )
{
    CartesianDiagramDataCompressor::AggregatedDataValueAttributes::const_iterator it = aggregated.constBegin();
    for ( ; it != aggregated.constEnd(); ++it ) {
        if ( it.value() == attributes ) {
            return true;
        }
    }
    return false;
}

CartesianDiagramDataCompressor::AggregatedDataValueAttributes CartesianDiagramDataCompressor::aggregatedAttrs(
        const AbstractDiagram* diagram,
        const QModelIndex & index,
        const CachePosition& position ) const
{
    // return cached attrs, if any
    DataValueAttributesCache::const_iterator i = m_dataValueAttributesCache.constFind( position );
    if ( i != m_dataValueAttributesCache.constEnd() ) {
        return i.value();
    }

    // aggregate attributes from all indices in the same CachePosition as index
    CartesianDiagramDataCompressor::AggregatedDataValueAttributes aggregated;
    const auto neighborIndexes = mapToModel( position );
    for ( const QModelIndex& neighborIndex : neighborIndexes ) {
        DataValueAttributes attrs = diagram->dataValueAttributes( neighborIndex );
        // only store visible and unique attributes
        if ( !attrs.isVisible() ) {
            continue;
        }
        if ( !contains( aggregated, attrs ) ) {
            aggregated[ neighborIndex ] = attrs;
        }
    }
    // if none of the attributes had the visible flag set, we just take the one set for the index
    // to avoid returning an empty list (### why not return an empty list?)
    if ( aggregated.isEmpty() ) {
        aggregated[index] = diagram->dataValueAttributes( index );
    }

    m_dataValueAttributesCache[position] = aggregated;
    return aggregated;
}

bool CartesianDiagramDataCompressor::prepareDataChange( const QModelIndex& parent, bool isRows,
                                                        int* start, int* end )
{
    if ( parent != m_rootIndex ) {
        return false;
    }
    Q_ASSERT( *start <= *end );

    CachePosition startPos = isRows ? mapToCache( *start, 0  ) : mapToCache( 0, *start );
    CachePosition endPos = isRows ? mapToCache( *end, 0  ) : mapToCache( 0, *end );

    static const CachePosition nullPosition;
    if ( startPos == nullPosition ) {
        rebuildCache();
        startPos = isRows ? mapToCache( *start, 0  ) : mapToCache( 0, *start );
        endPos = isRows ? mapToCache( *end, 0  ) : mapToCache( 0, *end );
        // The start position still isn't valid,
        // means that no resolution was set yet or we're about to add the first rows
        if ( startPos == nullPosition ) {
            return false;
        }
    }

    *start = isRows ? startPos.row : startPos.column;
    *end = isRows ? endPos.row : endPos.column;
    return true;
}

void CartesianDiagramDataCompressor::slotRowsAboutToBeInserted( const QModelIndex& parent, int start, int end )
{
    if ( !prepareDataChange( parent, true, &start, &end ) ) {
        return;
    }
    for ( int i = 0; i < m_data.size(); ++i )
    {
        Q_ASSERT( start >= 0 && start <= m_data[ i ].size() );
        m_data[ i ].insert( start, end - start + 1, DataPoint() );
    }
}

void CartesianDiagramDataCompressor::slotRowsInserted( const QModelIndex& parent, int start, int end )
{
    if ( !prepareDataChange( parent, true, &start, &end ) ) {
        return;
    }
    for ( int i = 0; i < m_data.size(); ++i )
    {
        for ( int j = start; j < m_data[i].size(); ++j ) {
            retrieveModelData( CachePosition( j, i ) );
        }
    }
}

void CartesianDiagramDataCompressor::slotColumnsAboutToBeInserted( const QModelIndex& parent, int start, int end )
{
    if ( !prepareDataChange( parent, false, &start, &end ) ) {
        return;
    }
    const int rowCount = qMin( m_model ? m_model->rowCount( m_rootIndex ) : 0, m_xResolution );
    Q_ASSERT( start >= 0 && start <= m_data.size() );
    m_data.insert( start, end - start + 1, QVector< DataPoint >( rowCount ) );
}

void CartesianDiagramDataCompressor::slotColumnsInserted( const QModelIndex& parent, int start, int end )
{
    if ( !prepareDataChange( parent, false, &start, &end ) ) {
        return;
    }
    for ( int i = start; i < m_data.size(); ++i )
    {
        for (int j = 0; j < m_data[i].size(); ++j ) {
            retrieveModelData( CachePosition( j, i ) );
        }
    }
}

void CartesianDiagramDataCompressor::slotRowsAboutToBeRemoved( const QModelIndex& parent, int start, int end )
{
    if ( !prepareDataChange( parent, true, &start, &end ) ) {
        return;
    }
    for ( int i = 0; i < m_data.size(); ++i ) {
        m_data[ i ].remove( start, end - start + 1 );
    }
}

void CartesianDiagramDataCompressor::slotRowsRemoved( const QModelIndex& parent, int start, int end )
{
    if ( parent != m_rootIndex )
        return;
    Q_ASSERT( start <= end );
    Q_UNUSED( end )

    CachePosition startPos = mapToCache( start, 0 );
    static const CachePosition nullPosition;
    if ( startPos == nullPosition ) {
        // Since we should already have rebuilt the cache, it won't help to rebuild it again.
        // Do not Q_ASSERT() though, since the resolution might simply not be set or we might now have 0 rows
        return;
    }

    for ( int i = 0; i < m_data.size(); ++i ) {
        for (int j = startPos.row; j < m_data[i].size(); ++j ) {
            retrieveModelData( CachePosition( j, i ) );
        }
    }
}

void CartesianDiagramDataCompressor::slotColumnsAboutToBeRemoved( const QModelIndex& parent, int start, int end )
{
    if ( !prepareDataChange( parent, false, &start, &end ) ) {
        return;
    }
    m_data.remove( start, end - start + 1 );
}

void CartesianDiagramDataCompressor::slotColumnsRemoved( const QModelIndex& parent, int start, int end )
{
    if ( parent != m_rootIndex )
        return;
    Q_ASSERT( start <= end );
    Q_UNUSED( end );

    const CachePosition startPos = mapToCache( 0, start );

    static const CachePosition nullPosition;
    if ( startPos == nullPosition ) {
        // Since we should already have rebuilt the cache, it won't help to rebuild it again.
        // Do not Q_ASSERT() though, since the resolution might simply not be set or we might now have 0 columns
        return;
    }

    for ( int i = startPos.column; i < m_data.size(); ++i ) {
        for ( int j = 0; j < m_data[i].size(); ++j ) {
            retrieveModelData( CachePosition( j, i ) );
        }
    }
}

void CartesianDiagramDataCompressor::slotModelHeaderDataChanged( Qt::Orientation orientation, int first, int last )
{
    if ( orientation != Qt::Vertical )
        return;

    if ( m_model->rowCount( m_rootIndex ) > 0 ) {
        const QModelIndex firstRow = m_model->index( 0, first, m_rootIndex ); // checked
        const QModelIndex lastRow = m_model->index( m_model->rowCount( m_rootIndex ) - 1, last, m_rootIndex ); // checked

        slotModelDataChanged( firstRow, lastRow );
    }
}

void CartesianDiagramDataCompressor::slotModelDataChanged(
    const QModelIndex& topLeftIndex,
    const QModelIndex& bottomRightIndex )
{
    if ( topLeftIndex.parent() != m_rootIndex )
        return;
    Q_ASSERT( topLeftIndex.parent() == bottomRightIndex.parent() );
    Q_ASSERT( topLeftIndex.row() <= bottomRightIndex.row() );
    Q_ASSERT( topLeftIndex.column() <= bottomRightIndex.column() );
    CachePosition topleft = mapToCache( topLeftIndex );
    CachePosition bottomright = mapToCache( bottomRightIndex );
    for ( int row = topleft.row; row <= bottomright.row; ++row )
        for ( int column = topleft.column; column <= bottomright.column; ++column )
            invalidate( CachePosition( row, column ) );
}

void CartesianDiagramDataCompressor::slotModelLayoutChanged()
{
    rebuildCache();
    calculateSampleStepWidth();
}

void CartesianDiagramDataCompressor::slotDiagramLayoutChanged( AbstractDiagram* diagramBase )
{
    AbstractCartesianDiagram* diagram = qobject_cast< AbstractCartesianDiagram* >( diagramBase );
    Q_ASSERT( diagram );
    if ( diagram->datasetDimension() != m_datasetDimension ) {
        setDatasetDimension( diagram->datasetDimension() );
    }
}

int CartesianDiagramDataCompressor::modelDataColumns() const
{
    Q_ASSERT( m_datasetDimension != 0 );
    // only operational if there is a model and a resolution
    if ( m_model ) {
        const int effectiveDimension = m_datasetDimension == 2 ? 2 : 1;
        const int columns = m_model->columnCount( m_rootIndex ) / effectiveDimension;
        Q_ASSERT( columns == m_data.size() );
        return columns;
    } else {
        return 0;
    }
}

int CartesianDiagramDataCompressor::modelDataRows() const
{
    // only operational if there is a model, columns, and a resolution
    if ( m_model && m_model->columnCount( m_rootIndex ) > 0 && m_xResolution > 0 ) {
        return m_data.isEmpty() ? 0 : m_data.first().size();
    } else {
        return 0;
    }
}

void CartesianDiagramDataCompressor::setModel( QAbstractItemModel* model )
{
    if ( model == m_model ) {
        return;
    }

    if ( m_model != nullptr ) {
        disconnect( m_model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
                 this, SLOT(slotModelHeaderDataChanged(Qt::Orientation,int,int)) );
        disconnect( m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
                 this, SLOT(slotModelDataChanged(QModelIndex,QModelIndex)) );
        disconnect( m_model, SIGNAL(layoutChanged()),
                 this, SLOT(slotModelLayoutChanged()) );
        disconnect( m_model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
                 this, SLOT(slotRowsAboutToBeInserted(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(rowsInserted(QModelIndex,int,int)),
                 this, SLOT(slotRowsInserted(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
                 this, SLOT(slotRowsAboutToBeRemoved(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
                 this, SLOT(slotRowsRemoved(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)),
                 this, SLOT(slotColumnsAboutToBeInserted(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(columnsInserted(QModelIndex,int,int)),
                 this, SLOT(slotColumnsInserted(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(columnsRemoved(QModelIndex,int,int)),
                 this, SLOT(slotColumnsRemoved(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)),
                 this, SLOT(slotColumnsAboutToBeRemoved(QModelIndex,int,int)) );
        disconnect( m_model, SIGNAL(modelReset()),
                    this, SLOT(rebuildCache()) );
        m_model = nullptr;
    }

    m_modelCache.setModel( model );

    if ( model != nullptr ) {
        m_model = model;
        connect( m_model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
                 SLOT(slotModelHeaderDataChanged(Qt::Orientation,int,int)) );
        connect( m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
                 SLOT(slotModelDataChanged(QModelIndex,QModelIndex)) );
        connect( m_model, SIGNAL(layoutChanged()),
                 SLOT(slotModelLayoutChanged()) );
        connect( m_model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
                 SLOT(slotRowsAboutToBeInserted(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(rowsInserted(QModelIndex,int,int)),
                 SLOT(slotRowsInserted(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
                 SLOT(slotRowsAboutToBeRemoved(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
                 SLOT(slotRowsRemoved(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)),
                 SLOT(slotColumnsAboutToBeInserted(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(columnsInserted(QModelIndex,int,int)),
                 SLOT(slotColumnsInserted(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(columnsRemoved(QModelIndex,int,int)),
                 SLOT(slotColumnsRemoved(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)),
                 SLOT(slotColumnsAboutToBeRemoved(QModelIndex,int,int)) );
        connect( m_model, SIGNAL(modelReset()), SLOT(rebuildCache()) );
    }
    rebuildCache();
    calculateSampleStepWidth();
}

void CartesianDiagramDataCompressor::setRootIndex( const QModelIndex& root )
{
    if ( m_rootIndex != root ) {
        Q_ASSERT( root.model() == m_model || !root.isValid() );
        m_rootIndex = root;
        m_modelCache.setRootIndex( root );
        rebuildCache();
        calculateSampleStepWidth();
    }
}

void CartesianDiagramDataCompressor::recalcResolution()
{
    setResolution( m_xResolution, m_yResolution );
}

void CartesianDiagramDataCompressor::setResolution( int x, int y )
{
    if ( setResolutionInternal( x, y ) ) {
        rebuildCache();
        calculateSampleStepWidth();
    }
}

bool CartesianDiagramDataCompressor::setResolutionInternal( int x, int y )
{
    const int oldXRes = m_xResolution;
    const int oldYRes = m_yResolution;

    if ( m_datasetDimension != 1 ) {
        // just ignore the X resolution in that case
        m_xResolution = m_model ? m_model->rowCount( m_rootIndex ) : 0;
    } else {
        m_xResolution = qMax( 0, x );
    }
    m_yResolution = qMax( 0, y );

    return m_xResolution != oldXRes || m_yResolution != oldYRes;
}

void CartesianDiagramDataCompressor::clearCache()
{
    for ( int column = 0; column < m_data.size(); ++column )
        m_data[column].fill( DataPoint() );
}

void CartesianDiagramDataCompressor::rebuildCache()
{
    Q_ASSERT( m_datasetDimension != 0 );

    m_data.clear();
    setResolutionInternal( m_xResolution, m_yResolution );
    const int columnDivisor = m_datasetDimension == 2 ? 2 : 1;
    const int columnCount = m_model ? m_model->columnCount( m_rootIndex ) / columnDivisor : 0;
    const int rowCount = qMin( m_model ? m_model->rowCount( m_rootIndex ) : 0, m_xResolution );
    m_data.resize( columnCount );
    for ( int i = 0; i < columnCount; ++i ) {
        m_data[i].resize( rowCount );
    }
    // also empty the attrs cache
    m_dataValueAttributesCache.clear();
}

const CartesianDiagramDataCompressor::DataPoint& CartesianDiagramDataCompressor::data( const CachePosition& position ) const
{
    static DataPoint nullDataPoint;
    if ( ! mapsToModelIndex( position ) ) {
        return nullDataPoint;
    }
    if ( ! isCached( position ) ) {
        retrieveModelData( position );
    }
    return m_data.at( position.column ).at( position.row );
}

QPair< QPointF, QPointF > CartesianDiagramDataCompressor::dataBoundaries() const
{
    const int colCount = modelDataColumns();
    qreal xMin = std::numeric_limits< qreal >::quiet_NaN();
    qreal xMax = std::numeric_limits< qreal >::quiet_NaN();
    qreal yMin = std::numeric_limits< qreal >::quiet_NaN();
    qreal yMax = std::numeric_limits< qreal >::quiet_NaN();

    for ( int column = 0; column < colCount; ++column )
    {
        const DataPointVector& data = m_data.at( column );
        int row = 0;
        for ( DataPointVector::const_iterator it = data.begin(); it != data.end(); ++it, ++row )
        {
            const DataPoint& p = *it;
            if ( !p.index.isValid() )
                retrieveModelData( CachePosition( row, column ) );

            if ( ISNAN( p.key ) || ISNAN( p.value ) ) {
                continue;
            }

            if ( ISNAN( xMin ) ) {
                xMin = p.key;
                xMax = p.key;
                yMin = p.value;
                yMax = p.value;
            } else {
                xMin = qMin( xMin, p.key );
                xMax = qMax( xMax, p.key );
                yMin = qMin( yMin, p.value );
                yMax = qMax( yMax, p.value );
            }
        }
    }

    const QPointF bottomLeft( xMin, yMin );
    const QPointF topRight( xMax, yMax );
    return qMakePair( bottomLeft, topRight );
}

void CartesianDiagramDataCompressor::retrieveModelData( const CachePosition& position ) const
{
    Q_ASSERT( mapsToModelIndex( position ) );
    DataPoint result;
    result.hidden = true;

    switch ( m_mode ) {
    case Precise:
    {
        const QModelIndexList indexes = mapToModel( position );

        if ( m_datasetDimension == 2 ) {
            Q_ASSERT( indexes.count() == 2 );
            const QModelIndex& xIndex = indexes.at( 0 );
            result.index = xIndex;
            result.key = m_modelCache.data( xIndex );
            result.value = m_modelCache.data( indexes.at( 1 ) );
        } else {
            if ( indexes.isEmpty() ) {
                break;
            }
            result.value = std::numeric_limits< qreal >::quiet_NaN();
            result.key = 0.0;
            for ( const QModelIndex& index : indexes ) {
                const qreal value = m_modelCache.data( index );
                if ( !ISNAN( value ) ) {
                    result.value = ISNAN( result.value ) ? value : result.value + value;
                }
                result.key += index.row();
            }
            result.index = indexes.at( 0 );
            result.key /= indexes.size();
            result.value /= indexes.size();
        }

        for ( const QModelIndex& index : indexes ) {
            // the DataPoint point is visible if any of the underlying, aggregated points is visible
            if ( m_model->data( index, DataHiddenRole ).value<bool>() == false ) {
                result.hidden = false;
            }
        }
        break;
    }
    case SamplingSeven:
        break;
    }

    m_data[ position.column ][ position.row ] = result;
    Q_ASSERT( isCached( position ) );
}

CartesianDiagramDataCompressor::CachePosition CartesianDiagramDataCompressor::mapToCache(
        const QModelIndex& index ) const
{
    Q_ASSERT( m_datasetDimension != 0 );

    static const CachePosition nullPosition;
    if ( !index.isValid() ) {
        return nullPosition;
    }
    return mapToCache( index.row(), index.column() );
}

CartesianDiagramDataCompressor::CachePosition CartesianDiagramDataCompressor::mapToCache(
        int row, int column ) const
{
    Q_ASSERT( m_datasetDimension != 0 );

    if ( m_data.size() == 0 || m_data.at( 0 ).size() == 0 ) {
        return mapToCache( QModelIndex() );
    }
    // assumption: indexes per column == 1
    if ( indexesPerPixel() == 0 ) {
        return mapToCache( QModelIndex() );
    }
    return CachePosition( int( row / indexesPerPixel() ), column / m_datasetDimension );
}

QModelIndexList CartesianDiagramDataCompressor::mapToModel( const CachePosition& position ) const
{
    QModelIndexList indexes;
    if ( !mapsToModelIndex( position ) ) {
        return indexes;
    }

    Q_ASSERT( position.column < modelDataColumns() );
    if ( m_datasetDimension == 2 ) {
        indexes << m_model->index( position.row, position.column * 2, m_rootIndex ); // checked
        indexes << m_model->index( position.row, position.column * 2 + 1, m_rootIndex ); // checked
    } else {
        // here, indexes per column is usually but not always 1 (e.g. stock diagrams can have three
        // or four dimensions: High-Low-Close or Open-High-Low-Close)
        const qreal ipp = indexesPerPixel();
        const int baseRow = floor( position.row * ipp );
        // the following line needs to work for the last row(s), too...
        const int endRow = floor( ( position.row + 1 ) * ipp );
        for ( int row = baseRow; row < endRow; ++row ) {
            Q_ASSERT( row < m_model->rowCount( m_rootIndex ) );
            const QModelIndex index = m_model->index( row, position.column, m_rootIndex );
            if ( index.isValid() ) {
                indexes << index;
            }
        }
    }
    return indexes;
}

qreal CartesianDiagramDataCompressor::indexesPerPixel() const
{
    if ( !m_model || m_data.size() == 0 || m_data.at( 0 ).size() == 0 )  {
        return 0;
    }
    return qreal( m_model->rowCount( m_rootIndex ) ) / qreal( m_data.at( 0 ).size() );
}

bool CartesianDiagramDataCompressor::mapsToModelIndex( const CachePosition& position ) const
{
    return m_model && m_data.size() > 0 && m_data.at( 0 ).size() > 0 &&
           position.column >= 0 && position.column < m_data.size() &&
           position.row >=0 && position.row < m_data.at( 0 ).size();
}

void CartesianDiagramDataCompressor::invalidate( const CachePosition& position )
{
    if ( mapsToModelIndex( position ) ) {
        m_data[ position.column ][ position.row ] = DataPoint();
        // Also invalidate the data value attributes at "position".
        // Otherwise the user overwrites the attributes without us noticing
        // it because we keep reading what's in the cache.
        m_dataValueAttributesCache.remove( position );
    }
}

bool CartesianDiagramDataCompressor::isCached( const CachePosition& position ) const
{
    Q_ASSERT( mapsToModelIndex( position ) );
    const DataPoint& p = m_data.at( position.column ).at( position.row );
    return p.index.isValid();
}

void CartesianDiagramDataCompressor::calculateSampleStepWidth()
{
    if ( m_mode == Precise ) {
        m_sampleStep = 1;
        return;
    }

    static unsigned int SomePrimes[] = {
        2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
        53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101,
        151, 211, 313, 401, 503, 607, 701, 811, 911, 1009,
        10037, 12911, 16001, 20011, 50021,
        100003, 137867, 199999, 500009, 707753, 1000003, 0
    }; // ... after that, having a model at all becomes impractical

    // we want at least 17 samples per data point, using a prime step width
    const qreal WantedSamples = 17;
    if ( WantedSamples > indexesPerPixel() ) {
        m_sampleStep = 1;
    } else {
        int i;
        for ( i = 0; SomePrimes[i] != 0; ++i ) {
            if ( WantedSamples * SomePrimes[i+1] > indexesPerPixel() ) {
                break;
            }
        }
        m_sampleStep = SomePrimes[i];
        if ( SomePrimes[i] == 0 ) {
            m_sampleStep = SomePrimes[i-1];
        } else {
            m_sampleStep = SomePrimes[i];
        }
    }
}

void CartesianDiagramDataCompressor::setDatasetDimension( int dimension )
{
    if ( dimension != m_datasetDimension ) {
        m_datasetDimension = dimension;
        rebuildCache();
        calculateSampleStepWidth();
    }
}
