/*
 * 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 "KChartLeveyJenningsDiagram.h"
#include "KChartLeveyJenningsDiagram_p.h"

#include "KChartChart.h"
#include "KChartTextAttributes.h"
#include "KChartAbstractGrid.h"
#include "KChartPainterSaver_p.h"

#include <QDateTime>
#include <QFontMetrics>
#include <QPainter>
#include <QSvgRenderer>
#include <QVector>

using namespace KChart;
using namespace std;

LeveyJenningsDiagram::Private::Private()
{
}

LeveyJenningsDiagram::Private::~Private() {}


#define d d_func()


LeveyJenningsDiagram::LeveyJenningsDiagram( QWidget* parent, LeveyJenningsCoordinatePlane* plane )
    : LineDiagram( new Private(), parent, plane )
{
    init();
}

void LeveyJenningsDiagram::init()
{
    d->lotChangedPosition = Qt::AlignTop;
    d->fluidicsPackChangedPosition = Qt::AlignBottom;
    d->sensorChangedPosition = Qt::AlignBottom;

    d->scanLinePen = QPen( Qt::blue );
    setPen( d->scanLinePen );

    d->expectedMeanValue = 0.0;
    d->expectedStandardDeviation = 0.0;

    d->diagram = this;

    d->icons[ LotChanged ] = QString::fromLatin1( ":/KDE/kchart/LeveyJennings/karo_black.svg" );
    d->icons[ SensorChanged ] = QString::fromLatin1( ":/KDE/kchart/LeveyJennings/karo_red.svg" );
    d->icons[ FluidicsPackChanged ] = QString::fromLatin1( ":/KDE/kchart/LeveyJennings/karo_blue.svg" );
    d->icons[ OkDataPoint ] = QString::fromLatin1( ":/KDE/kchart/LeveyJennings/circle_blue.svg" );
    d->icons[ NotOkDataPoint ] = QString::fromLatin1( ":/KDE/kchart/LeveyJennings/circle_blue_red.svg" );

    setSelectionMode( QAbstractItemView::SingleSelection );
}

LeveyJenningsDiagram::~LeveyJenningsDiagram()
{
}

LineDiagram * LeveyJenningsDiagram::clone() const
{
    LeveyJenningsDiagram* newDiagram = new LeveyJenningsDiagram( new Private( *d ) );
    return newDiagram;
}

bool LeveyJenningsDiagram::compare( const LeveyJenningsDiagram* other ) const
{
    if ( other == this ) return true;
    if ( ! other ) {
        return false;
    }
    /*
    qDebug() <<"\n             LineDiagram::compare():";
            // compare own properties
    qDebug() << (type() == other->type());
    */
    return  // compare the base class
            ( static_cast<const LineDiagram*>(this)->compare( other ) );
}

void LeveyJenningsDiagram::setLotChangedSymbolPosition( Qt::Alignment pos )
{
    if ( d->lotChangedPosition == pos )
        return;

    d->lotChangedPosition = pos;
    update();
}

Qt::Alignment LeveyJenningsDiagram::lotChangedSymbolPosition() const
{
    return d->lotChangedPosition;
}

void LeveyJenningsDiagram::setFluidicsPackChangedSymbolPosition( Qt::Alignment pos )
{
    if ( d->fluidicsPackChangedPosition == pos )
        return;

    d->fluidicsPackChangedPosition = pos;
    update();
}

Qt::Alignment LeveyJenningsDiagram::fluidicsPackChangedSymbolPosition() const
{
    return d->fluidicsPackChangedPosition;
}

void LeveyJenningsDiagram::setSensorChangedSymbolPosition( Qt::Alignment pos )
{
    if ( d->sensorChangedPosition == pos )
        return;

    d->sensorChangedPosition = pos;
    update();
}

Qt::Alignment LeveyJenningsDiagram::sensorChangedSymbolPosition() const
{
    return d->sensorChangedPosition;
}

void LeveyJenningsDiagram::setFluidicsPackChanges( const QVector< QDateTime >& changes )
{
    if ( d->fluidicsPackChanges == changes )
        return;

    d->fluidicsPackChanges = changes;
    update();
}

QVector< QDateTime > LeveyJenningsDiagram::fluidicsPackChanges() const
{
    return d->fluidicsPackChanges;
}

void LeveyJenningsDiagram::setSensorChanges( const QVector< QDateTime >& changes )
{
    if ( d->sensorChanges == changes )
        return;

    d->sensorChanges = changes;
    update();
}

void LeveyJenningsDiagram::setScanLinePen( const QPen& pen )
{
    if ( d->scanLinePen == pen )
        return;

    d->scanLinePen = pen;
    update();
}

QPen LeveyJenningsDiagram::scanLinePen() const
{
    return d->scanLinePen;
}

QString LeveyJenningsDiagram::symbol( Symbol symbol ) const
{
    return d->icons[ symbol ];
}

void LeveyJenningsDiagram::setSymbol( Symbol symbol, const QString& filename )
{
    if ( d->icons[ symbol ] == filename )
        return;

    delete d->iconRenderer[ symbol ];
    d->iconRenderer[ symbol ] = nullptr;

    d->icons[ symbol ] = filename;

    update();
}

QVector< QDateTime > LeveyJenningsDiagram::sensorChanges() const
{
    return d->sensorChanges;
}

void LeveyJenningsDiagram::setExpectedMeanValue( float meanValue )
{
    if ( d->expectedMeanValue == meanValue )
        return;

    d->expectedMeanValue = meanValue;
    d->setYAxisRange();
    update();
}

float LeveyJenningsDiagram::expectedMeanValue() const
{
    return d->expectedMeanValue;
}

void LeveyJenningsDiagram::setExpectedStandardDeviation( float sd )
{
    if ( d->expectedStandardDeviation == sd )
        return;

    d->expectedStandardDeviation = sd;
    d->setYAxisRange();
    update();
}

float LeveyJenningsDiagram::expectedStandardDeviation() const
{
    return d->expectedStandardDeviation;
}

float LeveyJenningsDiagram::calculatedMeanValue() const
{
    return d->calculatedMeanValue;
}

float LeveyJenningsDiagram::calculatedStandardDeviation() const
{
    return d->calculatedStandardDeviation;
}

void LeveyJenningsDiagram::setModel( QAbstractItemModel* model )
{
    if ( this->model() != nullptr )
    {
        disconnect( this->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
                                   this, SLOT(calculateMeanAndStandardDeviation()) );
        disconnect( this->model(), SIGNAL(rowsInserted(QModelIndex,int,int)),
                                   this, SLOT(calculateMeanAndStandardDeviation()) );
        disconnect( this->model(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
                                   this, SLOT(calculateMeanAndStandardDeviation()) );
        disconnect( this->model(), SIGNAL(columnsInserted(QModelIndex,int,int)),
                                   this, SLOT(calculateMeanAndStandardDeviation()) );
        disconnect( this->model(), SIGNAL(columnsRemoved(QModelIndex,int,int)),
                                   this, SLOT(calculateMeanAndStandardDeviation()) );
        disconnect( this->model(), SIGNAL(modelReset()),
                                   this, SLOT(calculateMeanAndStandardDeviation()) );
        disconnect( this->model(), SIGNAL(layoutChanged()),
                                   this, SLOT(calculateMeanAndStandardDeviation()) );
    }
    LineDiagram::setModel( model );
    if ( this->model() != nullptr )
    {
        connect( this->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
                                this, SLOT(calculateMeanAndStandardDeviation()) );
        connect( this->model(), SIGNAL(rowsInserted(QModelIndex,int,int)),
                                this, SLOT(calculateMeanAndStandardDeviation()) );
        connect( this->model(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
                                this, SLOT(calculateMeanAndStandardDeviation()) );
        connect( this->model(), SIGNAL(columnsInserted(QModelIndex,int,int)),
                                this, SLOT(calculateMeanAndStandardDeviation()) );
        connect( this->model(), SIGNAL(columnsRemoved(QModelIndex,int,int)),
                                this, SLOT(calculateMeanAndStandardDeviation()) );
        connect( this->model(), SIGNAL(modelReset()),
                                this, SLOT(calculateMeanAndStandardDeviation()) );
        connect( this->model(), SIGNAL(layoutChanged()),
                                this, SLOT(calculateMeanAndStandardDeviation()) );

        calculateMeanAndStandardDeviation();
    }
}

// TODO: This is the 'easy' solution
// evaluate whether this is enough or we need some better one or even boost here
void LeveyJenningsDiagram::calculateMeanAndStandardDeviation() const
{
    QVector< qreal > values;
    // first fetch all values
    const QAbstractItemModel& m = *model();
    const int rowCount = m.rowCount( rootIndex() );

    for ( int row = 0; row < rowCount; ++row )
    {
        const QVariant var = m.data( m.index( row, 1, rootIndex() ) );
        if ( !var.isValid() )
            continue;
        const qreal value = var.toReal();
        if ( ISNAN( value ) )
            continue;
        values << value;
    }

    qreal sum = 0.0;
    qreal sumSquares = 0.0;
    for ( qreal value : qAsConst(values) )
    {
        sum += value;
        sumSquares += value * value;
    }

    const int N = values.count();

    d->calculatedMeanValue = sum / N;
    d->calculatedStandardDeviation = sqrt( ( static_cast< qreal >( N ) * sumSquares - sum * sum ) / ( N * ( N - 1 ) ) );
}

// calculates the largest QDate not greater than \a dt.
static QDate floorDay( const QDateTime& dt )
{
    return dt.date();
}

// calculates the smallest QDate not less than \a dt.
static QDate ceilDay( const QDateTime& dt )
{
    QDate result = dt.date();

    if ( QDateTime( result, QTime() ) < dt )
        result = result.addDays( 1 );

    return result;
}

// calculates the largest QDateTime like xx:00 not greater than \a dt.
static QDateTime floorHour( const QDateTime& dt )
{
    return QDateTime( dt.date(), QTime( dt.time().hour(), 0 ) );
}

// calculates the smallest QDateTime like xx:00 not less than \a dt.
static QDateTime ceilHour( const QDateTime& dt )
{
    QDateTime result( dt.date(), QTime( dt.time().hour(), 0 ) );

    if ( result < dt )
        result = result.addSecs( 3600 );

    return result;
}

const QPair<QPointF, QPointF> LeveyJenningsDiagram::calculateDataBoundaries() const
{
    const qreal yMin = d->expectedMeanValue - 4 * d->expectedStandardDeviation;
    const qreal yMax = d->expectedMeanValue + 4 * d->expectedStandardDeviation;

    d->setYAxisRange();

    // rounded down/up to the prev/next midnight (at least that's the default)
    const QPair< QDateTime, QDateTime > range = timeRange();
    const unsigned int minTime = range.first.toSecsSinceEpoch();
    const unsigned int maxTime = range.second.toSecsSinceEpoch();

    const qreal xMin = minTime / static_cast< qreal >( 24 * 60 * 60 );
    const qreal xMax = maxTime / static_cast< qreal >( 24 * 60 * 60 ) - xMin;

    const QPointF bottomLeft( QPointF( 0, yMin ) );
    const QPointF topRight( QPointF( xMax, yMax ) );

    return QPair< QPointF, QPointF >( bottomLeft, topRight );
}

QPair< QDateTime, QDateTime > LeveyJenningsDiagram::timeRange() const
{
    if ( d->timeRange != QPair< QDateTime, QDateTime >() )
        return d->timeRange;

    const QAbstractItemModel& m = *model();
    const int rowCount = m.rowCount( rootIndex() );

    const QDateTime begin = m.data( m.index( 0, 3, rootIndex() ) ).toDateTime();
    const QDateTime end = m.data( m.index( rowCount - 1, 3, rootIndex() ) ).toDateTime();

    if ( begin.secsTo( end ) > 86400 )
    {
        // if begin to end is more than 24h
        // round down/up to the prev/next midnight
        const QDate min = floorDay( begin );
        const QDate max = ceilDay( end );
        return QPair< QDateTime, QDateTime >( QDateTime( min, QTime() ), QDateTime( max, QTime() ) );
    }
    else if ( begin.secsTo( end ) > 3600 )
    {
        // more than 1h: rond down up to the prex/next hour
        // if begin to end is more than 24h
        const QDateTime min = floorHour( begin );
        const QDateTime max = ceilHour( end );
        return QPair< QDateTime, QDateTime >( min, max );
    }
    return QPair< QDateTime, QDateTime >( begin, end );
}

void LeveyJenningsDiagram::setTimeRange( const QPair< QDateTime, QDateTime >& timeRange )
{
    if ( d->timeRange == timeRange )
        return;

    d->timeRange = timeRange;
    update();
}

void LeveyJenningsDiagram::drawChanges( PaintContext* ctx )
{
    const unsigned int minTime = timeRange().first.toSecsSinceEpoch();

    for ( const QDateTime& dt : qAsConst(d->fluidicsPackChanges) )
    {
        const qreal xValue = ( dt.toSecsSinceEpoch() - minTime ) / static_cast< qreal >( 24 * 60 * 60 );
        const QPointF point( xValue, 0.0 );
        drawFluidicsPackChangedSymbol( ctx, point );
    }

    for ( const QDateTime& dt : qAsConst(d->sensorChanges) )
    {
        const qreal xValue = ( dt.toSecsSinceEpoch() - minTime ) / static_cast< qreal >( 24 * 60 * 60 );
        const QPointF point( xValue, 0.0 );
        drawSensorChangedSymbol( ctx, point );
    }
}

void LeveyJenningsDiagram::paint( PaintContext* ctx )
{
    d->reverseMapper.clear();

    // note: Not having any data model assigned is no bug
    //       but we can not draw a diagram then either.
    if ( !checkInvariants( true ) ) return;
    if ( !AbstractGrid::isBoundariesValid(dataBoundaries()) ) return;

    QPainter* const painter = ctx->painter();
    const PainterSaver p( painter );
    if ( model()->rowCount( rootIndex() ) == 0 || model()->columnCount( rootIndex() ) < 4 )
        return; // nothing to paint for us

    AbstractCoordinatePlane* const plane = ctx->coordinatePlane();
    ctx->setCoordinatePlane( plane->sharedAxisMasterPlane( painter ) );

    const QAbstractItemModel& m = *model();
    const int rowCount = m.rowCount( rootIndex() );

    const unsigned int minTime = timeRange().first.toSecsSinceEpoch();

    painter->setRenderHint( QPainter::Antialiasing, true );

    int prevLot = -1;
    QPointF prevPoint;
    bool hadMissingValue = false;

    for ( int row = 0; row < rowCount; ++row )
    {
        const QModelIndex lotIndex = m.index( row, 0, rootIndex() );
        const QModelIndex valueIndex = m.index( row, 1, rootIndex() );
        const QModelIndex okIndex = m.index( row, 2, rootIndex() );
        const QModelIndex timeIndex = m.index( row, 3, rootIndex() );
        const QModelIndex expectedMeanIndex = m.index( row, 4, rootIndex() );
        const QModelIndex expectedSDIndex = m.index( row, 5, rootIndex() );

        painter->setPen( pen( lotIndex ) );

        QVariant vValue = m.data( valueIndex );
        qreal value = vValue.toReal();
        const int lot = m.data( lotIndex ).toInt();
        const bool ok = m.data( okIndex ).toBool();
        const QDateTime time = m.data( timeIndex ).toDateTime();
        const qreal xValue = ( time.toSecsSinceEpoch() - minTime ) / static_cast< qreal >( 24 * 60 * 60 );

        QVariant vExpectedMean = m.data( expectedMeanIndex );
        const qreal expectedMean = vExpectedMean.toReal();
        QVariant vExpectedSD = m.data( expectedSDIndex );
        const qreal expectedSD = vExpectedSD.toReal();

        QPointF point = ctx->coordinatePlane()->translate( QPointF( xValue, value ) );

        if ( vValue.isNull() )
        {
            hadMissingValue = true;
        }
        else
        {
            if ( !vExpectedMean.isNull() && !vExpectedSD.isNull() )
            {
                // this calculates the 'logical' value relative to the expected mean and SD of this point
                value -= expectedMean;
                value /= expectedSD;
                value *= d->expectedStandardDeviation;
                value += d->expectedMeanValue;
                point = ctx->coordinatePlane()->translate( QPointF( xValue, value ) );
            }

            if ( prevLot == lot )
            {
                const QPen pen = painter->pen();
                QPen newPen = pen;

                if ( hadMissingValue )
                {
                    newPen.setDashPattern( QVector< qreal >() << 4.0 << 4.0 );
                }

                painter->setPen( newPen );
                painter->drawLine( prevPoint, point );
                painter->setPen( pen );
                // d->reverseMapper.addLine( valueIndex.row(), valueIndex.column(), prevPoint, point );
            }
            else if ( row > 0 )
            {
                drawLotChangeSymbol( ctx, QPointF( xValue, value ) );
            }

            if ( value <= d->expectedMeanValue + 4 * d->expectedStandardDeviation &&
                value >= d->expectedMeanValue - 4 * d->expectedStandardDeviation )
            {
                const QPointF location( xValue, value );
                drawDataPointSymbol( ctx, location, ok );
                d->reverseMapper.addCircle( valueIndex.row(),
                                            valueIndex.column(),
                                            ctx->coordinatePlane()->translate( location ),
                                            iconRect().size() );
            }
            prevLot = lot;
            prevPoint = point;
            hadMissingValue = false;
        }

        const QModelIndex current = selectionModel()->currentIndex();
        if ( selectionModel()->rowIntersectsSelection( lotIndex.row(), lotIndex.parent() ) || current.sibling( current.row(), 0 ) == lotIndex )
        {
            const QPen pen = ctx->painter()->pen();
            painter->setPen( d->scanLinePen );
            painter->drawLine( ctx->coordinatePlane()->translate( QPointF( xValue, d->expectedMeanValue - 4 *
                                                                                   d->expectedStandardDeviation ) ),
                               ctx->coordinatePlane()->translate( QPointF( xValue, d->expectedMeanValue + 4 *
                                                                                   d->expectedStandardDeviation ) ) );
            painter->setPen( pen );
        }
    }

    drawChanges( ctx );

    ctx->setCoordinatePlane( plane );
}

void LeveyJenningsDiagram::drawDataPointSymbol( PaintContext* ctx, const QPointF& pos, bool ok )
{
    const Symbol type = ok ? OkDataPoint : NotOkDataPoint;

    QPainter* const painter = ctx->painter();
    const PainterSaver ps( painter );
    const QPointF transPos = ctx->coordinatePlane()->translate( pos ).toPoint();
    painter->translate( transPos );

    painter->setClipping( false );
    iconRenderer( type )->render( painter, iconRect() );
}

void LeveyJenningsDiagram::drawLotChangeSymbol( PaintContext* ctx, const QPointF& pos )
{
    const QPointF transPos = ctx->coordinatePlane()->translate(
        QPointF( pos.x(), d->lotChangedPosition & Qt::AlignTop ? d->expectedMeanValue +
                                                                 4 * d->expectedStandardDeviation
                                                               : d->expectedMeanValue -
                                                                 4 * d->expectedStandardDeviation ) );


    QPainter* const painter = ctx->painter();
    const PainterSaver ps( painter );
    painter->setClipping( false );
    painter->translate( transPos );
    iconRenderer( LotChanged )->render( painter, iconRect() );
}

void LeveyJenningsDiagram::drawSensorChangedSymbol( PaintContext* ctx, const QPointF& pos )
{
    const QPointF transPos = ctx->coordinatePlane()->translate(
        QPointF( pos.x(), d->sensorChangedPosition & Qt::AlignTop ? d->expectedMeanValue +
                                                                    4 * d->expectedStandardDeviation
                                                                  : d->expectedMeanValue -
                                                                    4 * d->expectedStandardDeviation ) );

    QPainter* const painter = ctx->painter();
    const PainterSaver ps( painter );
    painter->setClipping( false );
    painter->translate( transPos );
    iconRenderer( SensorChanged )->render( painter, iconRect() );
}

void LeveyJenningsDiagram::drawFluidicsPackChangedSymbol( PaintContext* ctx, const QPointF& pos )
{
    const QPointF transPos = ctx->coordinatePlane()->translate(
        QPointF( pos.x(), d->fluidicsPackChangedPosition & Qt::AlignTop ? d->expectedMeanValue +
                                                                          4 * d->expectedStandardDeviation
                                                                        : d->expectedMeanValue -
                                                                          4 * d->expectedStandardDeviation ) );

    QPainter* const painter = ctx->painter();
    const PainterSaver ps( painter );
    painter->setClipping( false );
    painter->translate( transPos );
    iconRenderer( FluidicsPackChanged )->render( painter, iconRect() );
}

QRectF LeveyJenningsDiagram::iconRect() const
{
    const Measure m( 12.5, KChartEnums::MeasureCalculationModeAuto, KChartEnums::MeasureOrientationAuto );
    TextAttributes test;
    test.setFontSize( m );
    const QFontMetrics fm( test.calculatedFont( coordinatePlane()->parent(), KChartEnums::MeasureOrientationAuto ) );
    const qreal height = fm.height() / 1.2;
    return QRectF( -height / 2.0, -height / 2.0, height, height );
}

QSvgRenderer* LeveyJenningsDiagram::iconRenderer( Symbol symbol )
{
    if ( d->iconRenderer[ symbol ] == nullptr )
        d->iconRenderer[ symbol ] = new QSvgRenderer( d->icons[ symbol ], this );

    return d->iconRenderer[ symbol ];
}
