/*
 * 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 "KChartPlotterDiagramCompressor.h"

#include "KChartMath_p.h"
#include "KChartPlotterDiagramCompressor_p.h"

#include <QPointF>

using namespace KChart;

qreal calculateSlope(const PlotterDiagramCompressor::DataPoint &lhs, const PlotterDiagramCompressor::DataPoint &rhs)
{
    return (rhs.value - lhs.value) / (rhs.key - lhs.key);
}

PlotterDiagramCompressor::Iterator::Iterator(int dataSet, PlotterDiagramCompressor *parent)
    : m_parent(parent)
    , m_index(0)
    , m_dataset(dataSet)
    , m_bufferIndex(0)
    , m_rebuffer(true)
{
    if (m_parent) {
        if (parent->rowCount() > m_dataset && parent->rowCount() > 0) {
            m_buffer.append(parent->data(CachePosition(m_index, m_dataset)));
        }
    } else {
        m_dataset = -1;
        m_index = -1;
    }
}

PlotterDiagramCompressor::Iterator::Iterator(int dataSet, PlotterDiagramCompressor *parent, QVector<DataPoint> buffer)
    : m_parent(parent)
    , m_buffer(buffer)
    , m_index(0)
    , m_dataset(dataSet)
    , m_bufferIndex(0)
    , m_rebuffer(false)
    , m_timeOfCreation(QDateTime::currentDateTime())
{
    if (!m_parent) {
        m_dataset = -1;
        m_index = -1;
    } else {
        // buffer needs to be filled
        if (parent->datasetCount() > m_dataset && parent->rowCount() > 0 && m_buffer.isEmpty()) {
            m_buffer.append(parent->data(CachePosition(m_index, m_dataset)));
            m_rebuffer = true;
        }
    }
}

PlotterDiagramCompressor::Iterator::~Iterator()
{
    if (m_parent) {
        if (m_parent.data()->d->m_timeOfLastInvalidation < m_timeOfCreation)
            m_parent.data()->d->m_bufferlist[m_dataset] = m_buffer;
    }
}

bool PlotterDiagramCompressor::Iterator::isValid() const
{
    if (m_parent == nullptr)
        return false;
    return m_dataset >= 0 && m_index >= 0 && m_parent.data()->rowCount() > m_index;
}

// PlotterDiagramCompressor::Iterator& PlotterDiagramCompressor::Iterator::operator++()
//{
//     ++m_index;

//    ++m_bufferIndex;
//    // the version that checks dataBoundaries is separated here, this is to avoid the runtime cost
//    // of checking every time the boundaries if that's not necessary
//    if ( m_parent.data()->d->forcedBoundaries( Qt::Vertical ) || m_parent.data()->d->forcedBoundaries( Qt::Vertical ) )
//    {
//        if ( m_bufferIndex >= m_buffer.count()  && m_rebuffer )
//        {
//            if ( m_index < m_parent.data()->rowCount() )
//            {
//                PlotterDiagramCompressor::DataPoint dp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                if ( m_parent.data()->d->inBoundaries( Qt::Vertical, dp ) && m_parent.data()->d->inBoundaries( Qt::Horizontal, dp ) )
//                {
//                    m_buffer.append( dp );
//                }
//                else
//                {
//                    if ( m_index + 1 < m_parent.data()->rowCount() )
//                    {
//                        PlotterDiagramCompressor::DataPoint dp1 = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                        if ( m_parent.data()->d->inBoundaries( Qt::Vertical, dp1 ) && m_parent.data()->d->inBoundaries( Qt::Horizontal, dp1 ) )
//                        {
//                            m_buffer.append( dp );
//                        }
//                    }
//                }
//            }
//        }
//        else
//        {
//            if ( m_bufferIndex == m_buffer.count() )
//                m_index = - 1;
//            return *this;
//        }
//        PlotterDiagramCompressor::DataPoint dp;
//        if ( isValid() )
//            dp = m_parent.data()->data( CachePosition( m_index - 1, m_dataset ) );
//        if ( m_parent )
//        {
//            if ( m_index >= m_parent.data()->rowCount() )
//                m_index = -1;
//            else
//            {
//                const qreal mergeRadius = m_parent.data()->d->m_mergeRadius;
//                PlotterDiagramCompressor::DataPoint newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                while ( dp.distance( newdp ) <= mergeRadius
//                        || !( m_parent.data()->d->inBoundaries( Qt::Vertical, dp ) || m_parent.data()->d->inBoundaries( Qt::Horizontal, dp ) ) )
//                {
//                    ++m_index;
//                    if ( m_index >= m_parent.data()->rowCount() )
//                    {
//                        m_index = - 1;
//                        break;
//                    }
//                    newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                }
//            }
//        }
//    }
//    else
//    {
//        // we have a new point in the buffer
//        if ( m_bufferIndex >= m_buffer.count()  && m_rebuffer )
//        {
//            if ( m_index < m_parent.data()->rowCount() )
//                m_buffer.append( m_parent.data()->data( CachePosition( m_index, m_dataset ) ) );
//        }
//        else
//        {
//            if ( m_bufferIndex == m_buffer.count() )
//                m_index = - 1;
//            return *this;
//        }
//        PlotterDiagramCompressor::DataPoint dp;
//        if ( isValid() )
//            dp = m_parent.data()->data( CachePosition( m_index - 1, m_dataset ) );
//        // make sure we switch to the next point which would be in the buffer
//        if ( m_parent )
//        {
//            PlotterDiagramCompressor *parent = m_parent.data();
//            if ( m_index >= parent->rowCount() )
//                m_index = -1;
//            else
//            {
//                switch ( parent->d->m_mode )
//                {
//                case( PlotterDiagramCompressor::DISTANCE ):
//                    {
//                        const qreal mergeRadius = m_parent.data()->d->m_mergeRadius;
//                        PlotterDiagramCompressor::DataPoint newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                        while ( dp.distance( newdp ) <= mergeRadius )
//                        {
//                            ++m_index;
//                            if ( m_index >= m_parent.data()->rowCount() )
//                            {
//                                m_index = - 1;
//                                break;
//                            }
//                            newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                        }
//                    }
//                    break;
//                case( PlotterDiagramCompressor::BOTH ):
//                    {
//                        const qreal mergeRadius = m_parent.data()->d->m_mergeRadius;
//                        PlotterDiagramCompressor::DataPoint newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                        while ( dp.distance( newdp ) <= mergeRadius )
//                        {
//                            ++m_index;
//                            if ( m_index >= m_parent.data()->rowCount() )
//                            {
//                                m_index = - 1;
//                                break;
//                            }
//                            newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                        }
//                    }
//                    break;
//                case ( PlotterDiagramCompressor::SLOPE ):
//                    {
//                        const qreal mergedist = parent->d->m_maxSlopeRadius;
//                        qreal oldSlope = 0;
//                        qreal newSlope = 0;

//                        PlotterDiagramCompressor::DataPoint newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                        PlotterDiagramCompressor::DataPoint olddp = PlotterDiagramCompressor::DataPoint();
//                        if ( m_bufferIndex > 1 )
//                        {
//                            oldSlope = calculateSlope( m_buffer[ m_bufferIndex - 2 ], m_buffer[ m_bufferIndex - 1 ] );
//                            newSlope = calculateSlope( m_buffer[ m_bufferIndex - 1 ], newdp );
//                        }
//                        bool first = true;
//                        while ( qAbs( newSlope - oldSlope ) < mergedist )
//                        {
//                            ++m_index;
//                            if ( m_index >= m_parent.data()->rowCount() )
//                            {
//                                m_index = - 1;
//                                break;
//                            }
//                            if ( first )
//                            {
//                                oldSlope = newSlope;
//                                first = false;
//                            }
//                            olddp = newdp;
//                            newdp = m_parent.data()->data( CachePosition( m_index, m_dataset ) );
//                            newSlope = calculateSlope( olddp, newdp );
//                        }
//                    }
//                    break;
//                default:
//                    Q_ASSERT( false );
//                }
//            }
//        }
//    }
//    return *this;
//}

void PlotterDiagramCompressor::Iterator::handleSlopeForward(const DataPoint &dp)
{
    PlotterDiagramCompressor *parent = m_parent.data();
    const qreal mergedist = parent->d->m_maxSlopeRadius;
    qreal oldSlope = 0;
    qreal newSlope = 0;

    PlotterDiagramCompressor::DataPoint newdp = dp;
    PlotterDiagramCompressor::DataPoint olddp = PlotterDiagramCompressor::DataPoint();
    if (m_bufferIndex > 1) {
        // oldSlope = calculateSlope( m_buffer[ m_bufferIndex - 2 ], m_buffer[ m_bufferIndex - 1 ] );
        // newSlope = calculateSlope( m_buffer[ m_bufferIndex - 1 ], newdp );
        oldSlope = calculateSlope(parent->data(CachePosition(m_index - 2, m_dataset)), parent->data(CachePosition(m_index - 1, m_dataset)));
        newSlope = calculateSlope(parent->data(CachePosition(m_index - 1, m_dataset)), newdp);
        qreal accumulatedDist = qAbs(newSlope - oldSlope);
        qreal olddist = accumulatedDist;
        qreal newdist;
        int counter = 0;
        while (accumulatedDist < mergedist) {
            ++m_index;
            if (m_index >= m_parent.data()->rowCount()) {
                m_index = -1;
                if (m_buffer.last() != parent->data(CachePosition(parent->rowCount() - 1, m_dataset)))
                    m_index = parent->rowCount();
                break;
            }
            oldSlope = newSlope;
            olddp = newdp;
            newdp = parent->data(CachePosition(m_index, m_dataset));
            newSlope = calculateSlope(olddp, newdp);
            newdist = qAbs(newSlope - oldSlope);
            if (olddist == newdist) {
                ++counter;
            } else {
                if (counter > 10)
                    break;
            }
            accumulatedDist += newdist;
            olddist = newdist;
        }
        m_buffer.append(newdp);
    } else
        m_buffer.append(dp);
}

PlotterDiagramCompressor::Iterator &PlotterDiagramCompressor::Iterator::operator++()
{
    PlotterDiagramCompressor *parent = m_parent.data();
    Q_ASSERT(parent);
    const int count = parent->rowCount();
    // increment the indexes
    ++m_index;
    ++m_bufferIndex;
    // if the index reached the end of the datamodel, make this iterator an enditerator
    // and make sure the buffer was not already built. If that's the case it's not necessary
    // to rebuild it and it would be hard to extend it as we had to know where m_index was
    if (m_index >= count || (!m_rebuffer && m_bufferIndex == m_buffer.count())) {
        if (m_bufferIndex == m_buffer.count()) {
            if (m_buffer.last() != parent->data(CachePosition(parent->rowCount() - 1, m_dataset)))
                m_index = parent->rowCount();
            else
                m_index = -1;
            ++m_bufferIndex;
        } else
            m_index = -1;
    }
    // if we reached the end of the buffer continue filling the buffer
    if (m_bufferIndex == m_buffer.count() && m_index >= 0 && m_rebuffer) {
        PlotterDiagramCompressor::DataPoint dp = parent->data(CachePosition(m_index, m_dataset));
        if (parent->d->inBoundaries(Qt::Vertical, dp) && parent->d->inBoundaries(Qt::Horizontal, dp)) {
            if (parent->d->m_mode == PlotterDiagramCompressor::SLOPE)
                handleSlopeForward(dp);
        } else {
            m_index = -1;
        }
    }
    return *this;
}

PlotterDiagramCompressor::Iterator PlotterDiagramCompressor::Iterator::operator++(int)
{
    Iterator result = *this;
    ++result;
    return result;
}

PlotterDiagramCompressor::Iterator &PlotterDiagramCompressor::Iterator::operator+=(int value)
{
    for (int index = m_index; index + value != m_index; ++(*this)) { };
    return *this;
}

PlotterDiagramCompressor::Iterator &PlotterDiagramCompressor::Iterator::operator--()
{
    --m_index;
    --m_bufferIndex;
    return *this;
}

PlotterDiagramCompressor::Iterator PlotterDiagramCompressor::Iterator::operator--(int)
{
    Iterator result = *this;
    --result;
    return result;
}

PlotterDiagramCompressor::Iterator &PlotterDiagramCompressor::Iterator::operator-=(int value)
{
    m_index -= value;
    return *this;
}

PlotterDiagramCompressor::DataPoint PlotterDiagramCompressor::Iterator::operator*()
{
    if (!m_parent)
        return PlotterDiagramCompressor::DataPoint();
    Q_ASSERT(m_parent);
    if (m_index == m_parent.data()->rowCount())
        return m_parent.data()->data(CachePosition(m_parent.data()->rowCount() - 1, m_dataset));
    return m_buffer[m_bufferIndex];
}

bool PlotterDiagramCompressor::Iterator::operator==(const PlotterDiagramCompressor::Iterator &other) const
{
    return m_parent.data() == other.m_parent.data() && m_index == other.m_index && m_dataset == other.m_dataset;
}

bool PlotterDiagramCompressor::Iterator::operator!=(const PlotterDiagramCompressor::Iterator &other) const
{
    return !(*this == other);
}

void PlotterDiagramCompressor::Iterator::invalidate()
{
    m_dataset = -1;
}

PlotterDiagramCompressor::Private::Private(PlotterDiagramCompressor *parent)
    : m_parent(parent)
    , m_model(nullptr)
    , m_mergeRadius(0.1)
    , m_maxSlopeRadius(0.1)
    , m_boundary(qMakePair(QPointF(std::numeric_limits<qreal>::quiet_NaN(), std::numeric_limits<qreal>::quiet_NaN()),
                           QPointF(std::numeric_limits<qreal>::quiet_NaN(), std::numeric_limits<qreal>::quiet_NaN())))
    , m_forcedXBoundaries(qMakePair(std::numeric_limits<qreal>::quiet_NaN(), std::numeric_limits<qreal>::quiet_NaN()))
    , m_forcedYBoundaries(qMakePair(std::numeric_limits<qreal>::quiet_NaN(), std::numeric_limits<qreal>::quiet_NaN()))
    , m_mode(PlotterDiagramCompressor::SLOPE)
{
}

void PlotterDiagramCompressor::Private::setModelToZero()
{
    m_model = nullptr;
}

inline bool inBoundary(const QPair<qreal, qreal> &bounds, qreal value)
{
    return bounds.first <= value && value <= bounds.second;
}

bool PlotterDiagramCompressor::Private::inBoundaries(Qt::Orientation orient, const PlotterDiagramCompressor::DataPoint &dp) const
{
    if (orient == Qt::Vertical && forcedBoundaries(Qt::Vertical)) {
        return inBoundary(m_forcedYBoundaries, dp.value);
    } else if (forcedBoundaries(Qt::Horizontal)) {
        return inBoundary(m_forcedXBoundaries, dp.key);
    }
    return true;
}

#if 0
// TODO this is not threadsafe do never try to invoke the painting in a different thread than this
// method
void PlotterDiagramCompressor::Private::rowsInserted( const QModelIndex& /*parent*/, int start, int end )
{

   if ( m_bufferlist.count() > 0 && !m_bufferlist[ 0 ].isEmpty() && start < m_bufferlist[ 0 ].count() )
   {
       calculateDataBoundaries();
       clearBuffer();
       return;
   }
   // we are handling appends only here, a prepend might be added, insert is expensive if not needed
   qreal minX = std::numeric_limits< qreal >::max();
   qreal minY = std::numeric_limits< qreal >::max();
   qreal maxX = std::numeric_limits< qreal >::min();
   qreal maxY = std::numeric_limits< qreal >::min();
   for ( int dataset = 0; dataset < m_bufferlist.size(); ++dataset )
   {
       PlotterDiagramCompressor::DataPoint predecessor = m_bufferlist[ dataset ].isEmpty() ? DataPoint() : m_bufferlist[ dataset ].last();

       qreal oldSlope = 0;
       qreal newSlope = 0;
       PlotterDiagramCompressor::DataPoint newdp = m_parent->data( CachePosition( start, dataset ) );
       PlotterDiagramCompressor::DataPoint olddp = PlotterDiagramCompressor::DataPoint();
       const int datacount = m_bufferlist[ dataset ].count();
       if ( m_mode != PlotterDiagramCompressor::DISTANCE && m_bufferlist[ dataset ].count() > 1 )
       {
           oldSlope = calculateSlope( m_bufferlist[ dataset ][ datacount - 2 ], m_bufferlist[ dataset ][ datacount - 1 ] );
           newSlope = calculateSlope( m_bufferlist[ dataset ][ datacount - 1 ], newdp );
       }
       bool first = true;
       for ( int row = start; row <= end; ++row )
       {
           PlotterDiagramCompressor::DataPoint curdp = m_parent->data( CachePosition( row, dataset ) );
           const bool checkcur = inBoundaries( Qt::Vertical, curdp ) && inBoundaries( Qt::Horizontal, curdp );
           const bool checkpred = inBoundaries( Qt::Vertical, predecessor ) && inBoundaries( Qt::Horizontal, predecessor );
           const bool check = checkcur || checkpred;
           switch ( m_mode )
           {
           case( PlotterDiagramCompressor::BOTH ):
               {
                   if ( predecessor.distance( curdp ) > m_mergeRadius && check )
                   {
                       if ( start > m_bufferlist[ dataset ].count() && !m_bufferlist[ dataset ].isEmpty() )
                       {
                           m_bufferlist[ dataset ].append( curdp );
                       }
                       else if ( !m_bufferlist[ dataset ].isEmpty() )
                       {
                           m_bufferlist[ dataset ].insert( row, curdp );
                       }
                       predecessor = curdp;
                       minX = qMin( curdp.key, m_boundary.first.x() );
                       minY = qMin( curdp.value, m_boundary.first.y() );
                       maxX = qMax( curdp.key, m_boundary.second.x() );
                       maxY = qMax( curdp.value, m_boundary.second.y() );
                   }
               }
               break;
           case ( PlotterDiagramCompressor::DISTANCE ):
               {
                   if ( predecessor.distance( curdp ) > m_mergeRadius && check )
                   {
                       if ( start > m_bufferlist[ dataset ].count() && !m_bufferlist[ dataset ].isEmpty() )
                       {
                           m_bufferlist[ dataset ].append( curdp );
                       }
                       else if ( !m_bufferlist[ dataset ].isEmpty() )
                       {
                           m_bufferlist[ dataset ].insert( row, curdp );
                       }
                       predecessor = curdp;
                       minX = qMin( curdp.key, m_boundary.first.x() );
                       minY = qMin( curdp.value, m_boundary.first.y() );
                       maxX = qMax( curdp.key, m_boundary.second.x() );
                       maxY = qMax( curdp.value, m_boundary.second.y() );
                   }
               }
               break;
           case( PlotterDiagramCompressor::SLOPE ):
               {
                   if ( check && qAbs( newSlope - oldSlope ) >= m_maxSlopeRadius )
                   {
                       if ( start > m_bufferlist[ dataset ].count() && !m_bufferlist[ dataset ].isEmpty() )
                       {
                           m_bufferlist[ dataset ].append( curdp );
                           oldSlope = newSlope;
                       }
                       else if ( !m_bufferlist[ dataset ].isEmpty() )
                       {
                           m_bufferlist[ dataset ].insert( row, curdp );
                           oldSlope = newSlope;
                       }

                       predecessor = curdp;
                       minX = qMin( curdp.key, m_boundary.first.x() );
                       minY = qMin( curdp.value, m_boundary.first.y() );
                       maxX = qMax( curdp.key, m_boundary.second.x() );
                       maxY = qMax( curdp.value, m_boundary.second.y() );

                       if ( first )
                       {
                           oldSlope = newSlope;
                           first = false;
                       }
                       olddp = newdp;
                       newdp = m_parent->data( CachePosition( row, dataset ) );
                       newSlope = calculateSlope( olddp, newdp );
                   }
               }
               break;
           }
       }
   }
   setBoundaries( qMakePair( QPointF( minX, minY ), QPointF( maxX, maxY ) ) );
   Q_EMIT m_parent->rowCountChanged();
}
#endif
#include <QDebug>
// TODO this is not threadsafe do never try to invoke the painting in a different thread than this
// method
void PlotterDiagramCompressor::Private::rowsInserted(const QModelIndex & /*parent*/, int start, int end)
{
    // Q_ASSERT( std::numeric_limits<qreal>::quiet_NaN() < 5 || std::numeric_limits<qreal>::quiet_NaN() > 5 );
    // Q_ASSERT( 5 == qMin( std::numeric_limits<qreal>::quiet_NaN(),  5.0 ) );
    // Q_ASSERT( 5 == qMax( 5.0, std::numeric_limits<qreal>::quiet_NaN() ) );
    if (m_bufferlist.count() > 0 && !m_bufferlist[0].isEmpty() && start < m_bufferlist[0].count()) {
        calculateDataBoundaries();
        clearBuffer();
        return;
    }

    // we are handling appends only here, a prepend might be added, insert is expensive if not needed
    qreal minX = m_boundary.first.x();
    qreal minY = m_boundary.first.y();
    qreal maxX = m_boundary.second.x();
    qreal maxY = m_boundary.second.y();
    for (int dataset = 0; dataset < m_bufferlist.size(); ++dataset) {
        if (m_mode == PlotterDiagramCompressor::SLOPE) {
            PlotterDiagramCompressor::DataPoint predecessor = m_bufferlist[dataset].isEmpty() ? DataPoint() : m_bufferlist[dataset].last();
            qreal oldSlope = 0;
            qreal newSlope = 0;
            int counter = 0;

            PlotterDiagramCompressor::DataPoint newdp = m_parent->data(CachePosition(start, dataset));
            PlotterDiagramCompressor::DataPoint olddp = PlotterDiagramCompressor::DataPoint();
            if (start > 1) {
                oldSlope = calculateSlope(m_parent->data(CachePosition(start - 2, dataset)), m_parent->data(CachePosition(start - 1, dataset)));
                olddp = m_parent->data(CachePosition(start - 1, dataset));
            } else {
                m_bufferlist[dataset].append(newdp);
                minX = qMin(minX, newdp.key);
                minY = qMin(minY, newdp.value);
                maxX = qMax(newdp.key, maxX);
                maxY = qMax(newdp.value, maxY);
                continue;
            }

            qreal olddist = 0;
            qreal newdist = 0;
            for (int row = start; row <= end; ++row) {
                PlotterDiagramCompressor::DataPoint curdp = m_parent->data(CachePosition(row, dataset));
                newdp = curdp;
                newSlope = calculateSlope(olddp, newdp);
                olddist = newdist;
                newdist = qAbs(newSlope - oldSlope);
                m_accumulatedDistances[dataset] += newdist;
                const bool checkcur = inBoundaries(Qt::Vertical, curdp) && inBoundaries(Qt::Horizontal, curdp);
                const bool checkpred = inBoundaries(Qt::Vertical, predecessor) && inBoundaries(Qt::Horizontal, predecessor);
                const bool check = checkcur || checkpred;

                if (m_accumulatedDistances[dataset] >= m_maxSlopeRadius && check) {
                    if (start > m_bufferlist[dataset].count() && !m_bufferlist[dataset].isEmpty()) {
                        m_bufferlist[dataset].append(curdp);
                    } else if (!m_bufferlist[dataset].isEmpty()) {
                        m_bufferlist[dataset].insert(row, curdp);
                    }
                    predecessor = curdp;
                    m_accumulatedDistances[dataset] = 0;
                }
                minX = qMin(minX, curdp.key);
                minY = qMin(minY, curdp.value);
                maxX = qMax(curdp.key, maxX);
                maxY = qMax(curdp.value, maxY);

                oldSlope = newSlope;
                olddp = newdp;
                if (olddist == newdist) {
                    ++counter;
                } else {
                    if (counter > 10) {
                        m_bufferlist[dataset].append(curdp);
                        predecessor = curdp;
                        m_accumulatedDistances[dataset] = 0;
                    }
                }
            }
            setBoundaries(qMakePair(QPointF(minX, minY), QPointF(maxX, maxY)));
        } else {
            PlotterDiagramCompressor::DataPoint predecessor = m_bufferlist[dataset].isEmpty() ? DataPoint() : m_bufferlist[dataset].last();

            for (int row = start; row <= end; ++row) {
                PlotterDiagramCompressor::DataPoint curdp = m_parent->data(CachePosition(row, dataset));
                const bool checkcur = inBoundaries(Qt::Vertical, curdp) && inBoundaries(Qt::Horizontal, curdp);
                const bool checkpred = inBoundaries(Qt::Vertical, predecessor) && inBoundaries(Qt::Horizontal, predecessor);
                const bool check = checkcur || checkpred;
                if (predecessor.distance(curdp) > m_mergeRadius && check) {
                    if (start > m_bufferlist[dataset].count() && !m_bufferlist[dataset].isEmpty()) {
                        m_bufferlist[dataset].append(curdp);
                    } else if (!m_bufferlist[dataset].isEmpty()) {
                        m_bufferlist[dataset].insert(row, curdp);
                    }
                    predecessor = curdp;
                    qreal minX = qMin(curdp.key, m_boundary.first.x());
                    qreal minY = qMin(curdp.value, m_boundary.first.y());
                    qreal maxX = qMax(curdp.key, m_boundary.second.x());
                    qreal maxY = qMax(curdp.value, m_boundary.second.y());
                    setBoundaries(qMakePair(QPointF(minX, minY), QPointF(maxX, maxY)));
                }
            }
        }
    }
    Q_EMIT m_parent->rowCountChanged();
}

void PlotterDiagramCompressor::setCompressionModel(CompressionMode value)
{
    Q_ASSERT(d);
    if (d->m_mode != value) {
        d->m_mode = value;
        d->clearBuffer();
        Q_EMIT rowCountChanged();
    }
}

void PlotterDiagramCompressor::Private::setBoundaries(const Boundaries &bound)
{
    if (bound != m_boundary) {
        m_boundary = bound;
        Q_EMIT m_parent->boundariesChanged();
    }
}

void PlotterDiagramCompressor::Private::calculateDataBoundaries()
{
    if (!forcedBoundaries(Qt::Vertical) || !forcedBoundaries(Qt::Horizontal)) {
        qreal minX = std::numeric_limits<qreal>::quiet_NaN();
        qreal minY = std::numeric_limits<qreal>::quiet_NaN();
        qreal maxX = std::numeric_limits<qreal>::quiet_NaN();
        qreal maxY = std::numeric_limits<qreal>::quiet_NaN();
        for (int dataset = 0; dataset < m_parent->datasetCount(); ++dataset) {
            for (int row = 0; row < m_parent->rowCount(); ++row) {
                PlotterDiagramCompressor::DataPoint dp = m_parent->data(CachePosition(row, dataset));
                minX = qMin(minX, dp.key);
                minY = qMin(minY, dp.value);
                maxX = qMax(dp.key, maxX);
                maxY = qMax(dp.value, maxY);
                Q_ASSERT(!ISNAN(minX));
                Q_ASSERT(!ISNAN(minY));
                Q_ASSERT(!ISNAN(maxX));
                Q_ASSERT(!ISNAN(maxY));
            }
        }
        if (forcedBoundaries(Qt::Vertical)) {
            minY = m_forcedYBoundaries.first;
            maxY = m_forcedYBoundaries.second;
        }
        if (forcedBoundaries(Qt::Horizontal)) {
            minX = m_forcedXBoundaries.first;
            maxX = m_forcedXBoundaries.second;
        }
        setBoundaries(qMakePair(QPointF(minX, minY), QPointF(maxX, maxY)));
    }
}

QModelIndexList PlotterDiagramCompressor::Private::mapToModel(const CachePosition &pos)
{
    QModelIndexList indexes;
    QModelIndex index;
    index = m_model->index(pos.first, pos.second * 2, QModelIndex());
    Q_ASSERT(index.isValid());
    indexes << index;
    index = m_model->index(pos.first, pos.second * 2 + 1, QModelIndex());
    Q_ASSERT(index.isValid());
    indexes << index;
    return indexes;
}

bool PlotterDiagramCompressor::Private::forcedBoundaries(Qt::Orientation orient) const
{
    if (orient == Qt::Vertical)
        return !ISNAN(m_forcedYBoundaries.first) && !ISNAN(m_forcedYBoundaries.second);
    else
        return !ISNAN(m_forcedXBoundaries.first) && !ISNAN(m_forcedXBoundaries.second);
}

void PlotterDiagramCompressor::Private::clearBuffer()
{
    // TODO all iterator have to be invalid after this operation
    // TODO make sure there are no regressions, the timeOfLastInvalidation should stop iterators from
    //  corrupting the cache
    m_bufferlist.clear();
    m_bufferlist.resize(m_parent->datasetCount());
    m_accumulatedDistances.clear();
    m_accumulatedDistances.resize(m_parent->datasetCount());
    m_timeOfLastInvalidation = QDateTime::currentDateTime();
}

PlotterDiagramCompressor::PlotterDiagramCompressor(QObject *parent)
    : QObject(parent)
    , d(new Private(this))
{
}

PlotterDiagramCompressor::~PlotterDiagramCompressor()
{
    delete d;
    d = nullptr;
}

void PlotterDiagramCompressor::setForcedDataBoundaries(const QPair<qreal, qreal> &bounds, Qt::Orientation direction)
{
    if (direction == Qt::Vertical) {
        d->m_forcedYBoundaries = bounds;
    } else {
        d->m_forcedXBoundaries = bounds;
    }
    d->clearBuffer();
    Q_EMIT boundariesChanged();
}

QAbstractItemModel *PlotterDiagramCompressor::model() const
{
    Q_ASSERT(d);
    return d->m_model;
}

void PlotterDiagramCompressor::setModel(QAbstractItemModel *model)
{
    Q_ASSERT(d);
    if (d->m_model) {
        d->m_model->disconnect(this);
        d->m_model->disconnect(d);
    }
    d->m_model = model;
    if (d->m_model) {
        d->m_bufferlist.resize(datasetCount());
        d->m_accumulatedDistances.resize(datasetCount());
        d->calculateDataBoundaries();
        connect(d->m_model, SIGNAL(rowsInserted(QModelIndex, int, int)), d, SLOT(rowsInserted(QModelIndex, int, int)));
        connect(d->m_model, SIGNAL(modelReset()), d, SLOT(clearBuffer()));
        connect(d->m_model, SIGNAL(destroyed(QObject *)), d, SLOT(setModelToZero()));
    }
}

PlotterDiagramCompressor::DataPoint PlotterDiagramCompressor::data(const CachePosition &pos) const
{
    DataPoint point;
    QModelIndexList indexes = d->mapToModel(pos);
    Q_ASSERT(indexes.count() == 2);
    QVariant yValue = d->m_model->data(indexes.last());
    QVariant xValue = d->m_model->data(indexes.first());
    Q_ASSERT(xValue.isValid());
    Q_ASSERT(yValue.isValid());
    bool ok = false;
    point.key = xValue.toReal(&ok);
    Q_ASSERT(ok);
    ok = false;
    point.value = yValue.toReal(&ok);
    Q_ASSERT(ok);
    point.index = indexes.first();
    return point;
}

void PlotterDiagramCompressor::setMergeRadius(qreal radius)
{
    if (d->m_mergeRadius != radius) {
        d->m_mergeRadius = radius;
        if (d->m_mode != PlotterDiagramCompressor::SLOPE)
            Q_EMIT rowCountChanged();
    }
}

void PlotterDiagramCompressor::setMaxSlopeChange(qreal value)
{
    if (d->m_maxSlopeRadius != value) {
        d->m_maxSlopeRadius = value;
        Q_EMIT boundariesChanged();
    }
}

qreal PlotterDiagramCompressor::maxSlopeChange() const
{
    return d->m_maxSlopeRadius;
}

void PlotterDiagramCompressor::setMergeRadiusPercentage(qreal radius)
{
    Boundaries bounds = dataBoundaries();
    const qreal width = radius * (bounds.second.x() - bounds.first.x());
    const qreal height = radius * (bounds.second.y() - bounds.first.y());
    const qreal realRadius = std::sqrt(width * height);
    setMergeRadius(realRadius);
}

int PlotterDiagramCompressor::rowCount() const
{
    return d->m_model ? d->m_model->rowCount() : 0;
}

void PlotterDiagramCompressor::cleanCache()
{
    d->clearBuffer();
}

int PlotterDiagramCompressor::datasetCount() const
{
    if (d->m_model && d->m_model->columnCount() == 0)
        return 0;
    return d->m_model ? (d->m_model->columnCount() + 1) / 2 : 0;
}

QPair<QPointF, QPointF> PlotterDiagramCompressor::dataBoundaries() const
{
    Boundaries bounds = d->m_boundary;
    if (d->forcedBoundaries(Qt::Vertical)) {
        bounds.first.setY(d->m_forcedYBoundaries.first);
        bounds.second.setY(d->m_forcedYBoundaries.second);
    }
    if (d->forcedBoundaries(Qt::Horizontal)) {
        bounds.first.setX(d->m_forcedXBoundaries.first);
        bounds.second.setX(d->m_forcedXBoundaries.second);
    }
    return bounds;
}

PlotterDiagramCompressor::Iterator PlotterDiagramCompressor::begin(int dataSet)
{
    Q_ASSERT(dataSet >= 0 && dataSet < d->m_bufferlist.count());
    return Iterator(dataSet, this, d->m_bufferlist[dataSet]);
}

PlotterDiagramCompressor::Iterator PlotterDiagramCompressor::end(int dataSet)
{
    Iterator it(dataSet, this);
    it.m_index = -1;
    return it;
}
