/*
 * 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 "KChartPlotter.h"
#include "KChartPlotter_p.h"

#include "KChartAbstractGrid.h"
#include "KChartPainterSaver_p.h"
#include "KChartMath_p.h"

#include "KChartNormalPlotter_p.h"
#include "KChartPercentPlotter_p.h"
#include "KChartStackedPlotter_p.h"

using namespace KChart;

Plotter::Private::Private()
    : implementor( nullptr )
    , normalPlotter( nullptr )
    , percentPlotter( nullptr )
    , stackedPlotter( nullptr )
{
}

Plotter::Private::~Private()
{
    delete normalPlotter;
    delete percentPlotter;
    delete stackedPlotter;
}


#define d d_func()


Plotter::Plotter( QWidget* parent, CartesianCoordinatePlane* plane ) :
    AbstractCartesianDiagram( new Private(), parent, plane )
{
    init();
}

void Plotter::init()
{
    d->diagram = this;
    d->normalPlotter = new NormalPlotter( this );
    d->percentPlotter = new PercentPlotter( this );
    d->stackedPlotter = new StackedPlotter( this );
    d->implementor = d->normalPlotter;
    QObject* test = d->implementor->plotterPrivate();
    connect( this, SIGNAL(boundariesChanged()), test, SLOT(changedProperties()) );
    // The signal is connected to the superclass's slot at this point because the connection happened
    // in its constructor when "its type was not Plotter yet".
    disconnect( this, SIGNAL(attributesModelAboutToChange(KChart::AttributesModel*,KChart::AttributesModel*)),
                this, SLOT(connectAttributesModel(KChart::AttributesModel*)) );
    connect( this, SIGNAL(attributesModelAboutToChange(KChart::AttributesModel*,KChart::AttributesModel*)),
             this, SLOT(connectAttributesModel(KChart::AttributesModel*)) );
    setDatasetDimensionInternal( 2 );
}

Plotter::~Plotter()
{
}

Plotter* Plotter::clone() const
{
    Plotter* newDiagram = new Plotter( new Private( *d ) );
    newDiagram->setType( type() );
    return newDiagram;
}

bool Plotter::compare( const Plotter* other ) const
{
    if ( other == this )
        return true;
    if ( other == nullptr )
        return false;
    return  // compare the base class
            ( static_cast< const AbstractCartesianDiagram* >( this )->compare( other ) ) &&
            // compare own properties
            ( type() == other->type() );
}

void Plotter::connectAttributesModel( AttributesModel* newModel )
{
    // Order of setting the AttributesModel in compressor and diagram is very important due to slot
    // invocation order. Refer to the longer comment in
    // AbstractCartesianDiagram::connectAttributesModel() for details.

    if ( useDataCompression() == Plotter::NONE )
    {
        d->plotterCompressor.setModel( nullptr );
        AbstractCartesianDiagram::connectAttributesModel( newModel );
    }
    else
    {
        d->compressor.setModel( nullptr );
        if ( attributesModel() != d->plotterCompressor.model() )
        {
            d->plotterCompressor.setModel( attributesModel() );
            connect( &d->plotterCompressor, SIGNAL(boundariesChanged()), this, SLOT(setDataBoundariesDirty()) );
            if ( useDataCompression() != Plotter::SLOPE )
            {
                connect( coordinatePlane(), SIGNAL(internal_geometryChanged(QRect,QRect)),
                         this, SLOT(setDataBoundariesDirty()) );
                connect( coordinatePlane(), SIGNAL(geometryChanged(QRect,QRect)),
                         this, SLOT(setDataBoundariesDirty()) );
                calcMergeRadius();
            }
        }
    }
}

Plotter::CompressionMode Plotter::useDataCompression() const
{    
    return d->implementor->useCompression();
}

void Plotter::setUseDataCompression( Plotter::CompressionMode value )
{
    if ( useDataCompression() != value )
    {
        d->implementor->setUseCompression( value );
        if ( useDataCompression() != Plotter::NONE )
        {
            d->compressor.setModel( nullptr );
            if ( attributesModel() != d->plotterCompressor.model() )                
                d->plotterCompressor.setModel( attributesModel() );
        }
    }
}

qreal Plotter::maxSlopeChange() const
{
    return d->plotterCompressor.maxSlopeChange();
}

void Plotter::setMaxSlopeChange( qreal value )
{
    d->plotterCompressor.setMaxSlopeChange( value );
}

qreal Plotter::mergeRadiusPercentage() const
{
    return d->mergeRadiusPercentage;
}

void Plotter::setMergeRadiusPercentage( qreal value )
{
    if ( d->mergeRadiusPercentage != value )
    {
        d->mergeRadiusPercentage = value;
        //d->plotterCompressor.setMergeRadiusPercentage( value );
        //update();
    }
}

void Plotter::setType( const PlotType type )
{
    if ( d->implementor->type() == type ) {
        return;
    }
    if ( datasetDimension() != 2 )  {
        Q_ASSERT_X ( false, "setType()",
                     "This line chart type can only be used with two-dimensional data." );
        return;
    }
    switch ( type ) {
    case Normal:
        d->implementor = d->normalPlotter;
        break;
    case Percent:
        d->implementor = d->percentPlotter;
        break;
    case Stacked:
        d->implementor = d->stackedPlotter;
        break;
    default:
        Q_ASSERT_X( false, "Plotter::setType", "unknown plotter subtype" );
    }
    bool connection = connect( this, SIGNAL(boundariesChanged()),
                               d->implementor->plotterPrivate(), SLOT(changedProperties()) );
    Q_ASSERT( connection );
    Q_UNUSED( connection );

    // d->lineType = type;
    Q_ASSERT( d->implementor->type() == type );

    setDataBoundariesDirty();
    Q_EMIT layoutChanged( this );
    Q_EMIT propertiesChanged();
}

Plotter::PlotType Plotter::type() const
{
    return d->implementor->type();
}

void Plotter::setLineAttributes( const LineAttributes& la )
{
    d->attributesModel->setModelData( QVariant::fromValue( la ), LineAttributesRole );
    Q_EMIT propertiesChanged();
}

void Plotter::setLineAttributes( int column, const LineAttributes& la )
{
    d->setDatasetAttrs( column, QVariant::fromValue( la ), LineAttributesRole );
    Q_EMIT propertiesChanged();
}

void Plotter::resetLineAttributes( int column )
{
    d->resetDatasetAttrs( column, LineAttributesRole );
    Q_EMIT propertiesChanged();
}

void Plotter::setLineAttributes( const QModelIndex & index, const LineAttributes& la )
{
    d->attributesModel->setData( d->attributesModel->mapFromSource( index ),
                                 QVariant::fromValue( la ), LineAttributesRole );
    Q_EMIT propertiesChanged();
}

void Plotter::resetLineAttributes( const QModelIndex & index )
{
    d->attributesModel->resetData(
            d->attributesModel->mapFromSource(index), LineAttributesRole );
    Q_EMIT propertiesChanged();
}

LineAttributes Plotter::lineAttributes() const
{
    return d->attributesModel->data( KChart::LineAttributesRole ).value<LineAttributes>();
}

LineAttributes Plotter::lineAttributes( int column ) const
{
    const QVariant attrs( d->datasetAttrs( column, LineAttributesRole ) );
    if ( attrs.isValid() )
        return attrs.value<LineAttributes>();
    return lineAttributes();
}

LineAttributes Plotter::lineAttributes( const QModelIndex& index ) const
{
    return d->attributesModel->data(
        d->attributesModel->mapFromSource( index ), KChart::LineAttributesRole ).value<LineAttributes>();
}

void Plotter::setThreeDLineAttributes( const ThreeDLineAttributes& la )
{
    setDataBoundariesDirty();
    d->attributesModel->setModelData( QVariant::fromValue( la ), ThreeDLineAttributesRole );
    Q_EMIT propertiesChanged();
}

void Plotter::setThreeDLineAttributes( int column, const ThreeDLineAttributes& la )
{
    setDataBoundariesDirty();
    d->setDatasetAttrs( column, QVariant::fromValue( la ), ThreeDLineAttributesRole );
    Q_EMIT propertiesChanged();
}

void Plotter::setThreeDLineAttributes( const QModelIndex& index, const ThreeDLineAttributes& la )
{
    setDataBoundariesDirty();
    d->attributesModel->setData( d->attributesModel->mapFromSource( index ), QVariant::fromValue( la ),
                                 ThreeDLineAttributesRole );
    Q_EMIT propertiesChanged();
}

ThreeDLineAttributes Plotter::threeDLineAttributes() const
{
    return d->attributesModel->data( KChart::ThreeDLineAttributesRole ).value<ThreeDLineAttributes>();
}

ThreeDLineAttributes Plotter::threeDLineAttributes( int column ) const
{
    const QVariant attrs( d->datasetAttrs( column, ThreeDLineAttributesRole ) );
    if ( attrs.isValid() ) {
        return attrs.value<ThreeDLineAttributes>();
    }
    return threeDLineAttributes();
}

ThreeDLineAttributes Plotter::threeDLineAttributes( const QModelIndex& index ) const
{
    return d->attributesModel->data(
        d->attributesModel->mapFromSource( index ), KChart::ThreeDLineAttributesRole ).value<ThreeDLineAttributes>();
}

qreal Plotter::threeDItemDepth( const QModelIndex & index ) const
{
    return threeDLineAttributes( index ).validDepth();
}

qreal Plotter::threeDItemDepth( int column ) const
{
    return threeDLineAttributes( column ).validDepth();
}

void Plotter::setValueTrackerAttributes( const QModelIndex & index, const ValueTrackerAttributes & va )
{
    d->attributesModel->setData( d->attributesModel->mapFromSource( index ),
                                 QVariant::fromValue( va ), KChart::ValueTrackerAttributesRole );
    Q_EMIT propertiesChanged();
}

ValueTrackerAttributes Plotter::valueTrackerAttributes( const QModelIndex & index ) const
{
    return d->attributesModel->data(
        d->attributesModel->mapFromSource( index ), KChart::ValueTrackerAttributesRole ).value<ValueTrackerAttributes>();
}

void Plotter::resizeEvent ( QResizeEvent* )
{
}

const QPair< QPointF, QPointF > Plotter::calculateDataBoundaries() const
{
    if ( !checkInvariants( true ) )
        return QPair< QPointF, QPointF >( QPointF( 0, 0 ), QPointF( 0, 0 ) );

    // note: calculateDataBoundaries() is ignoring the hidden flags.
    //       That's not a bug but a feature: Hiding data does not mean removing them.
    // For totally removing data from KD Chart's view people can use e.g. a proxy model ...

    // calculate boundaries for different line types Normal - Stacked - Percent - Default Normal
    return d->implementor->calculateDataBoundaries();
}


void Plotter::paintEvent ( QPaintEvent*)
{
    QPainter painter ( viewport() );
    PaintContext ctx;
    ctx.setPainter ( &painter );
    ctx.setRectangle ( QRectF ( 0, 0, width(), height() ) );
    paint ( &ctx );
}

void Plotter::paint( PaintContext* ctx )
{
    // note: Not having any data model assigned is no bug
    //       but we can not draw a diagram then either.
    if ( !checkInvariants( true ) ) return;

    AbstractCoordinatePlane* const plane = ctx->coordinatePlane();
    if ( ! plane ) return;
    d->setCompressorResolution( size(), plane );

    if ( !AbstractGrid::isBoundariesValid(dataBoundaries()) ) return;

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

    ctx->setCoordinatePlane( plane->sharedAxisMasterPlane( ctx->painter() ) );

    // paint different line types Normal - Stacked - Percent - Default Normal
    d->implementor->paint( ctx );

    ctx->setCoordinatePlane( plane );
}

void Plotter::resize ( const QSizeF& size )
{
    d->setCompressorResolution( size, coordinatePlane() );
    if ( useDataCompression() == Plotter::BOTH || useDataCompression() == Plotter::DISTANCE )
    {
        d->plotterCompressor.cleanCache();
        calcMergeRadius();
    }
    setDataBoundariesDirty();
    AbstractCartesianDiagram::resize( size );
}

void Plotter::setDataBoundariesDirty()
{
    AbstractCartesianDiagram::setDataBoundariesDirty();
    if ( useDataCompression() == Plotter::DISTANCE || useDataCompression() == Plotter::BOTH )
    {
        calcMergeRadius();
        //d->plotterCompressor.setMergeRadiusPercentage( d->mergeRadiusPercentage );
    }
}

void Plotter::calcMergeRadius()
{
    CartesianCoordinatePlane *plane = dynamic_cast< CartesianCoordinatePlane* >( coordinatePlane() );
    Q_ASSERT( plane );
    //Q_ASSERT( plane->translate( plane->translateBack( plane->visibleDiagramArea().topLeft() ) ) == plane->visibleDiagramArea().topLeft() );
    QRectF range = plane->visibleDataRange();
    //qDebug() << range;
    const qreal radius = std::sqrt( ( range.x() + range.width() ) * ( range.y() +  range.height() ) );
    //qDebug() << radius;
    //qDebug() << radius * d->mergeRadiusPercentage;
    //qDebug() << d->mergeRadiusPercentage;
    d->plotterCompressor.setMergeRadius( radius * d->mergeRadiusPercentage );
}

#if defined(Q_COMPILER_MANGLES_RETURN_TYPE)
const
#endif
int Plotter::numberOfAbscissaSegments () const
{
    return d->attributesModel->rowCount( attributesModelRootIndex() );
}

#if defined(Q_COMPILER_MANGLES_RETURN_TYPE)
const
#endif
int Plotter::numberOfOrdinateSegments () const
{
    return d->attributesModel->columnCount( attributesModelRootIndex() );
}
