/*****************************************************************************
 * Qwt Examples - Copyright (C) 2002 Uwe Rathmann
 * This file may be used under the terms of the 3-clause BSD License
 *****************************************************************************/

#include "Plot.h"
#include "ScalePicker.h"
#include "CanvasPicker.h"

#include <QwtPlotLayout>
#include <QwtPlotCurve>
#include <QwtPlotMarker>
#include <QwtPlotRenderer>
#include <QwtSymbol>
#include <QwtScaleWidget>
#include <QwtWheel>
#include <QwtSplineLocal>
#include <QwtSplineCubic>
#include <QwtSplinePleasing>
#include <QwtSplineBasis>
#include <QwtSplineParametrization>
#include <QwtCurveFitter>
#include <QwtLegend>
#include <QwtLegendLabel>

#include <QEvent>
#include <QPrinter>
#include <QPrintDialog>
#include <QPainterPath>

namespace
{
    class Symbol : public QwtSymbol
    {
      public:
        Symbol()
            : QwtSymbol( QwtSymbol::Ellipse )
        {
            QColor c( Qt::gray );
            c.setAlpha( 100 );
            setBrush( c );

            setPen( Qt::black );
            setSize( 8 );
        }
    };

    class SplineFitter : public QwtCurveFitter
    {
      public:
        enum Mode
        {
            PChipSpline,
            AkimaSpline,
            CubicSpline,
            CardinalSpline,
            ParabolicBlendingSpline,
            PleasingSpline,
            BasisSpline
        };

        SplineFitter( Mode mode )
            : QwtCurveFitter( QwtCurveFitter::Path )
            , m_spline(NULL)
        {
            switch( mode )
            {
                case PleasingSpline:
                {
                    m_spline = new QwtSplinePleasing();
                    break;
                }
                case PChipSpline:
                {
                    m_spline = new QwtSplineLocal( QwtSplineLocal::PChip );
                    break;
                }
                case AkimaSpline:
                {
                    m_spline = new QwtSplineLocal( QwtSplineLocal::Akima );
                    break;
                }
                case CubicSpline:
                {
                    m_spline = new QwtSplineCubic();
                    break;
                }
                case CardinalSpline:
                {
                    m_spline = new QwtSplineLocal( QwtSplineLocal::Cardinal );
                    break;
                }
                case ParabolicBlendingSpline:
                {
                    m_spline = new QwtSplineLocal( QwtSplineLocal::ParabolicBlending );
                    break;
                }
                case BasisSpline:
                {
                    m_spline = new QwtSplineBasis();
                    break;
                }
            }
            if ( m_spline )
                m_spline->setParametrization( QwtSplineParametrization::ParameterX );
        }

        ~SplineFitter()
        {
            delete m_spline;
        }

        void setClosing( bool on )
        {
            if ( m_spline == NULL )
                return;

            m_spline->setBoundaryType(
                on ? QwtSpline::ClosedPolygon : QwtSpline::ConditionalBoundaries );
        }

        void setBoundaryCondition( const QString& condition )
        {
            QwtSplineC2* splineC2 = dynamic_cast< QwtSplineC2* >( m_spline );
            if ( splineC2 )
            {
                if ( condition == "Cubic Runout" )
                {
                    setBoundaryConditions( QwtSplineC2::CubicRunout );
                    return;
                }

                if ( condition == "Not a Knot" )
                {
                    setBoundaryConditions( QwtSplineC2::NotAKnot );
                    return;
                }
            }

            QwtSplineC1* splineC1 = dynamic_cast< QwtSplineC1* >( m_spline );
            if ( splineC1 )
            {
                if ( condition == "Linear Runout" )
                {
                    setBoundaryConditions( QwtSpline::LinearRunout, 0.0 );
                    return;
                }

                if ( condition == "Parabolic Runout" )
                {
                    // Parabolic Runout means clamping the 3rd derivative to 0.0
                    setBoundaryConditions( QwtSpline::Clamped3, 0.0 );
                    return;
                }
            }

            // Natural
            setBoundaryConditions( QwtSplineC1::Clamped2, 0.0 );
        }

        void setParametric( const QString& parameterType )
        {
            QwtSplineParametrization::Type type = QwtSplineParametrization::ParameterX;

            if ( parameterType == "Uniform" )
            {
                type = QwtSplineParametrization::ParameterUniform;
            }
            else if ( parameterType == "Centripetral" )
            {
                type = QwtSplineParametrization::ParameterCentripetal;
            }
            else if ( parameterType == "Chordal" )
            {
                type = QwtSplineParametrization::ParameterChordal;
            }
            else if ( parameterType == "Manhattan" )
            {
                type = QwtSplineParametrization::ParameterManhattan;
            }

            m_spline->setParametrization( type );
        }

        virtual QPolygonF fitCurve( const QPolygonF& points ) const QWT_OVERRIDE
        {
            return m_spline->polygon( points, 0.5 );
        }

        virtual QPainterPath fitCurvePath( const QPolygonF& points ) const QWT_OVERRIDE
        {
            return m_spline->painterPath( points );
        }

      private:
        void setBoundaryConditions( int condition, double value = 0.0 )
        {
            if ( m_spline == NULL )
                return;

            // always the same at both ends

            m_spline->setBoundaryCondition( QwtSpline::AtBeginning, condition );
            m_spline->setBoundaryValue( QwtSpline::AtBeginning, value );

            m_spline->setBoundaryCondition( QwtSpline::AtEnd, condition );
            m_spline->setBoundaryValue( QwtSpline::AtEnd, value );
        }

        QwtSpline* m_spline;
    };

    class Curve : public QwtPlotCurve
    {
      public:
        Curve( const QString& title, const QColor& color )
            : QwtPlotCurve( title )
        {
            setPaintAttribute( QwtPlotCurve::ClipPolygons, false );
            setCurveAttribute( QwtPlotCurve::Fitted, true );
            setRenderHint( QwtPlotItem::RenderAntialiased );

            setPen( color );
            setZ( 100 ); // on top of the marker
        }
    };

    class LineMarker : public QwtPlotMarker
    {
      public:
        LineMarker( const QString& title, const QColor& color )
            : QwtPlotMarker( title )
        {
            setLineStyle( QwtPlotMarker::VLine );
            setLinePen( QPen( color, 0, Qt::DotLine ) );

            QwtText text( "click on the axes" );
            text.setBackgroundBrush( Qt::white );
            text.setColor( Qt::darkRed );

            setLabel( text );
            setLabelOrientation( Qt::Vertical );
            setXValue( 5 );
        }
    };
}

Plot::Plot( bool parametric, QWidget* parent )
    : QwtPlot( parent )
    , m_boundaryCondition( QwtSplineC1::Clamped2 )
{
    setTitle( "Points can be dragged using the mouse" );

    setCanvasBackground( Qt::white );
#ifndef QT_NO_CURSOR
    canvas()->setCursor( Qt::PointingHandCursor );
#endif

    QwtLegend* legend = new QwtLegend;
    legend->setDefaultItemMode( QwtLegendData::Checkable );
    insertLegend( legend, QwtPlot::RightLegend );

    connect( legend, SIGNAL(checked(const QVariant&,bool,int)),
        SLOT(legendChecked(const QVariant&,bool)) );

    // Avoid jumping when label with 3 digits
    // appear/disappear when scrolling vertically

    QwtScaleDraw* sd = axisScaleDraw( QwtAxis::YLeft );
    sd->setMinimumExtent( sd->extent( axisWidget( QwtAxis::YLeft )->font() ) );

    // pointless marker
    m_marker = new LineMarker( "Marker", Qt::darkRed );
    m_marker->attach( this );

    // curves
    m_curve = new Curve( "Lines", QColor() );
    m_curve->setStyle( QwtPlotCurve::NoCurve );
    m_curve->setSymbol( new Symbol() );
    m_curve->setItemAttribute( QwtPlotItem::Legend, false );
    m_curve->setZ( 1000 );

    QPolygonF points;

    if ( parametric )
    {
        setAxisScale( QwtAxis::XBottom, 20.0, 80.0 );
        setAxisScale( QwtAxis::YLeft, -50.0, 100.0 );

        const QSizeF size( 40, 50 );
        const QPointF pos( 50, 70 );

        const double cos30 = 0.866025;

        const double dx = 0.5 * size.width() - cos30;
        const double dy = 0.25 * size.height();

        double x1 = pos.x() - dx;
        double y1 = pos.y() - 2 * dy;

        const double x2 = x1 + 1 * dx;
        const double x3 = x1 + 2 * dx;

        const double y2 = y1 + 1 * dy;
        const double y3 = y1 + 3 * dy;
        const double y4 = y1 + 4 * dy;

        points += QPointF( x2, y1 );
        points += QPointF( 0.5 * ( x2 + x3 ), y1 - 0.5 * ( y2 - y1 ) );
        points += QPointF( x3, y2 );
        points += QPointF( 0.5 * ( x2 + x3 ), 0.5 * ( y3 + y1 ) );
        points += QPointF( x3, y3 );
        points += QPointF( 0.5 * ( x2 + x3 ), y3 + 0.5 * ( y3 - y2 ) );
        points += QPointF( x2, y4 );
        points += QPointF( 0.5 * ( x1 + x2 ), y3 + 0.5 * ( y4 - y3 ) );
        points += QPointF( x1, y3 );
        points += QPointF( x1, y2 );
    }
    else
    {
        setAxisScale( QwtAxis::XBottom, 0.0, 100.0 );
        setAxisScale( QwtAxis::YLeft, -50.0, 100.0 );

        points << QPointF( 10, 30 ) << QPointF( 20, 90 ) << QPointF( 25, 60 )
               << QPointF( 35, 38 ) << QPointF( 42, 40 ) << QPointF( 55, 60 )
               << QPointF( 60, 50 ) << QPointF( 65, 80 ) << QPointF( 73, 30 )
               << QPointF( 82, 30 ) << QPointF( 87, 40 ) << QPointF( 95, 70 );
    }

    m_curve->setSamples( points );
    m_curve->attach( this );

    //

    Curve* curve;

    QVector< Curve* > curves;

    curve = new Curve( "Pleasing", "DarkGoldenRod" );
    curve->setCurveFitter( new SplineFitter( SplineFitter::PleasingSpline ) );
    curves += curve;

    curve = new Curve( "Cardinal", Qt::darkGreen);
    curve->setCurveFitter( new SplineFitter( SplineFitter::CardinalSpline ) );
    curves += curve;

    curve = new Curve( "PChip", Qt::darkYellow);
    curve->setCurveFitter( new SplineFitter( SplineFitter::PChipSpline ) );
    curves += curve;

    curve = new Curve( "Parabolic Blending", Qt::darkBlue);
    curve->setCurveFitter( new SplineFitter( SplineFitter::ParabolicBlendingSpline ) );
    curves += curve;

    curve = new Curve( "Akima", Qt::darkCyan);
    curve->setCurveFitter( new SplineFitter( SplineFitter::AkimaSpline ) );
    curves += curve;

    curve = new Curve( "Cubic", Qt::darkRed );
    curve->setCurveFitter( new SplineFitter( SplineFitter::CubicSpline ) );
    curves += curve;

    curve = new Curve( "Basis", QColor("DarkOliveGreen" ) );
    curve->setCurveFitter( new SplineFitter( SplineFitter::BasisSpline ) );
    curves += curve;

    for ( int i = 0; i < curves.size(); i++ )
    {
        curves[i]->attach( this );
        showCurve( curves[i], true );
    }

#if 0
    for ( int i = 0; i < curves.size(); i++ )
        showCurve( curves[i], false );

    showCurve( curves[0], true );
#endif

    setOverlaying( false );

    // ------------------------------------
    // The scale picker translates mouse clicks
    // on the bottom axis into clicked() signals
    // ------------------------------------

    ScalePicker* scalePicker = new ScalePicker( this );
    connect( scalePicker, SIGNAL(clicked(int,double)),
        this, SLOT(updateMarker(int,double)) );

    // ------------------------------------
    // The canvas picker handles all mouse and key
    // events on the plot canvas
    // ------------------------------------

    ( void ) new CanvasPicker( !parametric, this );

    // ------------------------------------
    // We add a wheel to the canvas
    // ------------------------------------

    m_wheel = new QwtWheel( canvas() );
    m_wheel->setOrientation( Qt::Vertical );
    m_wheel->setRange( -100, 100 );
    m_wheel->setValue( 0.0 );
    m_wheel->setMass( 0.2 );
    m_wheel->setTotalAngle( 4 * 360.0 );
    m_wheel->resize( 16, 60 );

    plotLayout()->setAlignCanvasToScale( QwtAxis::XTop, true );
    plotLayout()->setAlignCanvasToScale( QwtAxis::XBottom, true );
    plotLayout()->setAlignCanvasToScale( QwtAxis::YLeft, true );
    plotLayout()->setCanvasMargin( m_wheel->width() + 4, QwtAxis::YRight );

    connect( m_wheel, SIGNAL(valueChanged(double)),
        SLOT(scrollLeftAxis(double)) );

    // we need the resize events, to lay out the wheel
    canvas()->installEventFilter( this );

    m_wheel->setWhatsThis(
        "With the wheel you can move the visible area." );
    axisWidget( QwtAxis::XBottom )->setWhatsThis(
        "Selecting a value at the scale will insert a new curve." );
}

void Plot::scrollLeftAxis( double value )
{
    const double range = axisScaleDiv( QwtAxis::YLeft ).range();
    setAxisScale( QwtAxis::YLeft, value, value + range );
    replot();
}

bool Plot::eventFilter( QObject* object, QEvent* e )
{
    if ( e->type() == QEvent::Resize )
    {
        if ( object == canvas() )
        {
            const int margin = 2;

            const QRect cr = canvas()->contentsRect();
            m_wheel->move( cr.right() - margin - m_wheel->width(),
                cr.center().y() - ( m_wheel->height() ) / 2 );
        }
    }

    return QwtPlot::eventFilter( object, e );
}

void Plot::updateMarker( int axis, double value )
{
    if ( QwtAxis::isYAxis( axis ) )
    {
        m_marker->setLineStyle( QwtPlotMarker::HLine );
        m_marker->setLabelOrientation( Qt::Horizontal );
        m_marker->setYValue( value );
    }
    else
    {
        m_marker->setLineStyle( QwtPlotMarker::VLine );
        m_marker->setLabelOrientation( Qt::Vertical );
        m_marker->setXValue( value );
    }

    replot();
}


void Plot::legendChecked( const QVariant& itemInfo, bool on )
{
    QwtPlotItem* plotItem = infoToItem( itemInfo );
    if ( plotItem )
        showCurve( plotItem, on );
}

void Plot::showCurve( QwtPlotItem* item, bool on )
{
    item->setVisible( on );

    QwtLegend* lgd = qobject_cast< QwtLegend* >( legend() );

    QList< QWidget* > legendWidgets =
        lgd->legendWidgets( itemToInfo( item ) );

    if ( legendWidgets.size() == 1 )
    {
        QwtLegendLabel* legendLabel =
            qobject_cast< QwtLegendLabel* >( legendWidgets[0] );

        if ( legendLabel )
            legendLabel->setChecked( on );
    }

    replot();
}

void Plot::setClosed( bool on )
{
    QwtPlotItemList curves = itemList( QwtPlotItem::Rtti_PlotCurve );
    for ( int i = 0; i < curves.size(); i++ )
    {
        QwtPlotCurve* curve = dynamic_cast< QwtPlotCurve* >( curves[i] );

        SplineFitter* fitter = dynamic_cast< SplineFitter* >( curve->curveFitter() );
        if ( fitter )
            fitter->setClosing( on );
    }

    replot();
}

void Plot::setBoundaryCondition( const QString& condition )
{
    QwtPlotItemList curves = itemList( QwtPlotItem::Rtti_PlotCurve );
    for ( int i = 0; i < curves.size(); i++ )
    {
        QwtPlotCurve* curve = dynamic_cast< QwtPlotCurve* >( curves[i] );

        SplineFitter* fitter = dynamic_cast< SplineFitter* >( curve->curveFitter() );
        if ( fitter )
            fitter->setBoundaryCondition( condition );
    }

    replot();
}

void Plot::setParametric( const QString& parameterType )
{
    QwtPlotItemList curves = itemList( QwtPlotItem::Rtti_PlotCurve );
    for ( int i = 0; i < curves.size(); i++ )
    {
        QwtPlotCurve* curve = ( QwtPlotCurve*)curves[i];

        SplineFitter* fitter = dynamic_cast< SplineFitter* >( curve->curveFitter() );
        if ( fitter )
            fitter->setParametric( parameterType );
    }

    replot();
}

void Plot::setOverlaying( bool on )
{
    QPolygonF points;
    for ( size_t i = 0; i < m_curve->dataSize(); i++ )
        points += m_curve->sample( i );

    QwtPlotItemList curves = itemList( QwtPlotItem::Rtti_PlotCurve );

    for ( int i = 0; i < curves.size(); i++ )
    {
        QwtPlotCurve* curve = static_cast< QwtPlotCurve* >( curves[i] );
        if ( curve == m_curve )
            continue;

        QwtSymbol* symbol = NULL;

        if ( !on )
        {
            points.translate( 0.0, -10 );
            symbol = new Symbol();
        }

        curve->setSymbol( symbol );
        curve->setSamples( points );
    }

    m_curve->setVisible( on );

    replot();
}

#ifndef QT_NO_PRINTER

void Plot::printPlot()
{
    QPrinter printer( QPrinter::HighResolution );
#if QT_VERSION >= 0x050300
    printer.setPageOrientation( QPageLayout::Landscape );
#else
    printer.setOrientation( QPrinter::Landscape );
#endif
    printer.setOutputFileName( "spline.pdf" );

    QPrintDialog dialog( &printer );
    if ( dialog.exec() )
    {
        QwtPlotRenderer renderer;

        if ( printer.colorMode() == QPrinter::GrayScale )
        {
            renderer.setDiscardFlag( QwtPlotRenderer::DiscardBackground );
            renderer.setDiscardFlag( QwtPlotRenderer::DiscardCanvasBackground );
            renderer.setDiscardFlag( QwtPlotRenderer::DiscardCanvasFrame );
            renderer.setLayoutFlag( QwtPlotRenderer::FrameWithScales );
        }

        renderer.renderTo( this, printer );
    }
}

#endif

#include "moc_Plot.cpp"
