/*
 * 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 KCHARTMODELDATACACHE_H
#define KCHARTMODELDATACACHE_H

#include <limits>

#include <QObject>
#include <QModelIndex>
#include <QVector>

#include "kchart_export.h"

QT_BEGIN_NAMESPACE
class QAbstractItemModel;
QT_END_NAMESPACE

namespace KChart
{
    namespace ModelDataCachePrivate
    {
        class ModelSignalMapper
        {
        protected:
            ModelSignalMapper() {}
        public:
            virtual ~ModelSignalMapper() {}
            virtual void resetModel() = 0;
            virtual void columnsInserted( const QModelIndex&, int, int ) = 0;
            virtual void columnsRemoved( const QModelIndex&, int, int ) = 0;
            virtual void dataChanged( const QModelIndex&, const QModelIndex& ) = 0;
            virtual void layoutChanged() = 0;
            virtual void modelReset() = 0;
            virtual void rowsInserted( const QModelIndex&, int, int ) = 0;
            virtual void rowsRemoved( const QModelIndex&, int, int ) = 0;
        };

        // this class maps slots to a non-QObject instantiating ModelSignalMapper
        class KCHART_EXPORT ModelSignalMapperConnector : public QObject
        {
            Q_OBJECT
        public:
            explicit ModelSignalMapperConnector( ModelSignalMapper& mapper );
            ~ModelSignalMapperConnector() override;

            void connectSignals( QAbstractItemModel* model );
            void disconnectSignals( QAbstractItemModel* model );

        protected Q_SLOTS:
            void resetModel();
            void columnsInserted( const QModelIndex&, int, int );
            void columnsRemoved( const QModelIndex&, int, int );
            void dataChanged( const QModelIndex&, const QModelIndex& );
            void layoutChanged();
            void modelReset();
            void rowsInserted( const QModelIndex&, int, int );
            void rowsRemoved( const QModelIndex&, int, int );

        private:
            ModelSignalMapper& m_mapper;
        };

        template< class T>
        T nan()
        {
            return T();
        }

        template<>
        inline qreal nan< qreal >()
        {
            return std::numeric_limits< qreal >::quiet_NaN();
        }
    }

    template< class T, int ROLE >
    class ModelDataCache : public ModelDataCachePrivate::ModelSignalMapper
    {
    public:
        ModelDataCache()
            : m_model( nullptr ),
              m_connector( *this )
        {
        }

        ~ModelDataCache() override
        {
        }

        T data( const QModelIndex& index ) const
        {
            if ( !index.isValid() || index.parent() != m_rootIndex || index.row() >= m_model->rowCount(m_rootIndex) || index.column() >= m_model->columnCount(m_rootIndex) )
                return ModelDataCachePrivate::nan< T >();

            if ( index.row() >= m_data.count() )
            {
                qWarning( "KChart didn't receive signal rowsInserted, resetModel or layoutChanged, "
                          "but an index with a row outside of the known bounds." );

                // apparently, data were added behind our back (w/o signals)
                const_cast< ModelDataCache< T, ROLE >* >( this )->rowsInserted( m_rootIndex,
                                                                                m_data.count(),
                                                                                m_model->rowCount( m_rootIndex ) - 1 );
                Q_ASSERT( index.row() < m_data.count() );
            }

            if ( index.column() >= m_data.first().count() )
            {
                qWarning( "KChart didn't got signal columnsInserted, resetModel or layoutChanged, "
                          "but an index with a column outside of the known bounds." );

                // apparently, data were added behind our back (w/o signals)
                const_cast< ModelDataCache< T, ROLE >* >( this )->columnsInserted( m_rootIndex,
                                                                                   m_data.first().count(),
                                                                                   m_model->columnCount( m_rootIndex ) - 1 );
                Q_ASSERT( index.column() < m_data.first().count() );
            }

            return data( index.row(), index.column() );
        }

        T data( int row, int column ) const
        {
            if ( row < 0 || column < 0 )
                return ModelDataCachePrivate::nan< T >();

            Q_ASSERT( row < m_model->rowCount(m_rootIndex) );
            Q_ASSERT( column < m_model->columnCount(m_rootIndex) );

            Q_ASSERT( row < m_data.count() );
            Q_ASSERT( column < m_data.first().count() );

            if ( isCached( row, column ) )
                return m_data.at( row ).at( column );

            return fetchFromModel( row, column, ROLE );
        }

        void setModel( QAbstractItemModel* model )
        {
            if ( m_model != nullptr )
                m_connector.disconnectSignals( m_model );

            m_model = model;

            if ( m_model != nullptr )
                m_connector.connectSignals( m_model );

            modelReset();
        }

        QAbstractItemModel* model() const
        {
            return m_model;
        }

        void setRootIndex( const QModelIndex& rootIndex )
        {
            Q_ASSERT( rootIndex.model() == m_model || !rootIndex.isValid() );
            m_rootIndex = rootIndex;
            modelReset();
        }

        QModelIndex rootIndex() const
        {
            return m_rootIndex;
        }

    protected:
        bool isCached( int row, int column ) const
        {
            return m_cacheValid.at( row ).at( column );
        }

        T fetchFromModel( int row, int column, int role ) const
        {
            Q_ASSERT( m_model != nullptr );

            const QModelIndex index = m_model->index( row, column, m_rootIndex );
            const QVariant data = index.data( role );
            const T value = data.isNull() ? ModelDataCachePrivate::nan< T >()
                                          : ( data.value< T >() );

            m_data[ row ][ column ] = value;
            m_cacheValid[ row ][ column ] = true;

            return value;
        }

        void columnsInserted( const QModelIndex& parent, int start, int end ) override
        {
            Q_ASSERT( m_model != nullptr );
            Q_ASSERT( parent.model() == m_model || !parent.isValid() );

            if ( parent != m_rootIndex )
                return;

            Q_ASSERT( start <= end );
            Q_ASSERT( start <= m_model->columnCount(m_rootIndex) );

            const int rowCount = m_data.count();
            for ( int row = 0; row < rowCount; ++row )
            {
                m_data[ row ].insert( start, end - start + 1, T() );
                m_cacheValid[ row ].insert( start, end - start + 1, false );
                Q_ASSERT( m_data.at( row ).count() == m_model->columnCount( m_rootIndex ) );
                Q_ASSERT( m_cacheValid.at( row ).count() == m_model->columnCount( m_rootIndex ) );
            }
        }

        void columnsRemoved( const QModelIndex& parent, int start, int end ) override
        {
            Q_ASSERT( m_model != nullptr );
            Q_ASSERT( parent.model() == m_model || !parent.isValid() );

            if ( parent != m_rootIndex )
                return;

            Q_ASSERT( start <= end );

            const int rowCount = m_data.count();
            for ( int row = 0; row < rowCount; ++row )
            {
                m_data[ row ].remove( start, end - start + 1 );
                m_cacheValid[ row ].remove( start, end - start + 1 );
                Q_ASSERT( m_data.at( row ).count() == m_model->columnCount( m_rootIndex ) );
                Q_ASSERT( m_cacheValid.at( row ).count() == m_model->columnCount( m_rootIndex ) );
            }
        }

        void dataChanged( const QModelIndex& topLeft, const QModelIndex& bottomRight ) override
        {
            if ( !m_model )
                return;
            Q_ASSERT( m_model != nullptr );
            Q_ASSERT( topLeft.parent() == bottomRight.parent() );

            if ( !topLeft.isValid() || !bottomRight.isValid() || topLeft.parent() != m_rootIndex )
                return;

            Q_ASSERT( topLeft.model() == m_model && bottomRight.model() == m_model );

            const int minRow = qMax( 0, topLeft.row() );
            const int maxRow = bottomRight.row();
            const int minCol = qMax( 0, topLeft.column() );
            const int maxCol = bottomRight.column();

            Q_ASSERT( minRow <= maxRow );
            Q_ASSERT( minCol <= maxCol );
            Q_ASSERT( maxRow < m_model->rowCount( m_rootIndex ) );
            Q_ASSERT( maxCol < m_model->columnCount( m_rootIndex ) );

            for ( int row = minRow; row <= maxRow; ++row )
            {
                for ( int col = minCol; col <= maxCol; ++col )
                {
                    m_cacheValid[ row ][ col ] = false;
                    Q_ASSERT( !isCached( row, col ) );
                }
            }
        }

        void layoutChanged() override
        {
            modelReset();
        }

        void modelReset() override
        {
            m_data.clear();
            m_cacheValid.clear();

            if ( m_model == nullptr )
                return;

            m_data.fill( QVector< T >( m_model->columnCount( m_rootIndex ) ), m_model->rowCount( m_rootIndex ) );
            m_cacheValid.fill( QVector< bool >( m_model->columnCount( m_rootIndex ), false ), m_model->rowCount( m_rootIndex ) );

            Q_ASSERT( m_data.count() == m_model->rowCount( m_rootIndex ) );
            Q_ASSERT( m_cacheValid.count() == m_model->rowCount( m_rootIndex ) );
        }

        void rowsInserted( const QModelIndex& parent, int start, int end ) override
        {
            Q_ASSERT( m_model != nullptr );
            Q_ASSERT( parent.model() == m_model || !parent.isValid() );

            if ( parent != m_rootIndex || start >= m_model->rowCount(m_rootIndex) )
                return;

            Q_ASSERT( start <= end );
            Q_ASSERT( end - start + 1 <= m_model->rowCount(m_rootIndex) );

            m_data.insert( start, end - start + 1, QVector< T >( m_model->columnCount( m_rootIndex ) ) );
            m_cacheValid.insert( start, end - start + 1, QVector< bool >( m_model->columnCount( m_rootIndex ), false ) );

            Q_ASSERT( m_data.count() == m_model->rowCount( m_rootIndex ) );
            Q_ASSERT( m_cacheValid.count() == m_model->rowCount( m_rootIndex ) );
        }

        void rowsRemoved( const QModelIndex& parent, int start, int end ) override
        {
            Q_ASSERT( m_model != nullptr );
            Q_ASSERT( parent.model() == m_model || !parent.isValid() );

            if ( parent != m_rootIndex || start >= m_data.count() )
                return;

            Q_ASSERT( start <= end );

            m_data.remove( start, end - start + 1 );
            m_cacheValid.remove( start, end - start + 1 );

            Q_ASSERT( m_data.count() == m_model->rowCount( m_rootIndex ) );
            Q_ASSERT( m_cacheValid.count() == m_model->rowCount( m_rootIndex ) );
        }

        void resetModel() override
        {
            // no need to disconnect, this is a response to SIGNAL( destroyed() )
            m_model = nullptr;
            modelReset();
        }

    private:
        QAbstractItemModel* m_model;
        QModelIndex m_rootIndex;
        ModelDataCachePrivate::ModelSignalMapperConnector m_connector;
        mutable QVector< QVector< T > > m_data;
        mutable QVector< QVector< bool > > m_cacheValid;
    };
}

#endif
