/******************************************************************************
 * Qwt Widget Library
 * Copyright (C) 1997   Josef Wilgen
 * Copyright (C) 2002   Uwe Rathmann
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the Qwt License, Version 1.0
 *****************************************************************************/

#include "qwt_knob.h"
#include "qwt_round_scale_draw.h"
#include "qwt_painter.h"
#include "qwt_scale_map.h"
#include "qwt_math.h"
#include "qwt.h"

#include <qpainter.h>
#include <qpalette.h>
#include <qstyle.h>
#include <qstyleoption.h>
#include <qevent.h>
#include <qmargins.h>
#include <qmath.h>

static QSize qwtKnobSizeHint( const QwtKnob* knob, int min )
{
    int knobWidth = knob->knobWidth();
    if ( knobWidth <= 0 )
        knobWidth = qMax( 3 * knob->markerSize(), min );

    // Add the scale radial thickness to the knobWidth
    const int extent = qwtCeil( knob->scaleDraw()->extent( knob->font() ) );
    const int d = 2 * ( extent + 4 ) + knobWidth;

    const QMargins m = knob->contentsMargins();
    return QSize( d + m.left() + m.right(), d + m.top() + m.bottom() );
}

static inline double qwtToScaleAngle( double angle )
{
    // the map is counter clockwise with the origin
    // at 90° using angles from -180° -> 180°

    double a = 90.0 - angle;
    if ( a <= -180.0 )
        a += 360.0;
    else if ( a >= 180.0 )
        a -= 360.0;

    return a;
}

static double qwtToDegrees( double value )
{
    return qwtNormalizeDegrees( 90.0 - value );
}

class QwtKnob::PrivateData
{
  public:
    PrivateData()
        : knobStyle( QwtKnob::Raised )
        , markerStyle( QwtKnob::Notch )
        , borderWidth( 2 )
        , borderDist( 4 )
        , scaleDist( 4 )
        , maxScaleTicks( 11 )
        , knobWidth( 0 )
        , alignment( Qt::AlignCenter )
        , markerSize( 8 )
        , totalAngle( 270.0 )
        , mouseOffset( 0.0 )
    {
    }

    QwtKnob::KnobStyle knobStyle;
    QwtKnob::MarkerStyle markerStyle;

    int borderWidth;
    int borderDist;
    int scaleDist;
    int maxScaleTicks;
    int knobWidth;
    Qt::Alignment alignment;
    int markerSize;

    double totalAngle;

    double mouseOffset;
};

/*!
   \brief Constructor

   Construct a knob with an angle of 270°. The style is
   QwtKnob::Raised and the marker style is QwtKnob::Notch.
   The width of the knob is set to 50 pixels.

   \param parent Parent widget

   \sa setTotalAngle()
 */
QwtKnob::QwtKnob( QWidget* parent )
    : QwtAbstractSlider( parent )
{
    m_data = new PrivateData;

    setScaleDraw( new QwtRoundScaleDraw() );

    setTotalAngle( 270.0 );

    setScale( 0.0, 10.0 );
    setValue( 0.0 );

    setSizePolicy( QSizePolicy::MinimumExpanding,
        QSizePolicy::MinimumExpanding );
}

//! Destructor
QwtKnob::~QwtKnob()
{
    delete m_data;
}

/*!
   \brief Set the knob type

   \param knobStyle Knob type
   \sa knobStyle(), setBorderWidth()
 */
void QwtKnob::setKnobStyle( KnobStyle knobStyle )
{
    if ( m_data->knobStyle != knobStyle )
    {
        m_data->knobStyle = knobStyle;
        update();
    }
}

/*!
    \return Marker type of the knob
    \sa setKnobStyle(), setBorderWidth()
 */
QwtKnob::KnobStyle QwtKnob::knobStyle() const
{
    return m_data->knobStyle;
}

/*!
   \brief Set the marker type of the knob

   \param markerStyle Marker type
   \sa markerStyle(), setMarkerSize()
 */
void QwtKnob::setMarkerStyle( MarkerStyle markerStyle )
{
    if ( m_data->markerStyle != markerStyle )
    {
        m_data->markerStyle = markerStyle;
        update();
    }
}

/*!
   \return Marker type of the knob
   \sa setMarkerStyle(), setMarkerSize()
 */
QwtKnob::MarkerStyle QwtKnob::markerStyle() const
{
    return m_data->markerStyle;
}

/*!
   \brief Set the total angle by which the knob can be turned
   \param angle Angle in degrees.

   The angle has to be between [10, 360] degrees. Angles above
   360 ( so that the knob can be turned several times around its axis )
   have to be set using setNumTurns().

   The default angle is 270 degrees.

   \sa totalAngle(), setNumTurns()
 */
void QwtKnob::setTotalAngle ( double angle )
{
    angle = qBound( 10.0, angle, 360.0 );

    if ( angle != m_data->totalAngle )
    {
        m_data->totalAngle = angle;

        scaleDraw()->setAngleRange( -0.5 * m_data->totalAngle,
            0.5 * m_data->totalAngle );

        updateGeometry();
        update();
    }
}

/*!
   \return the total angle
   \sa setTotalAngle(), setNumTurns(), numTurns()
 */
double QwtKnob::totalAngle() const
{
    return m_data->totalAngle;
}

/*!
   \brief Set the number of turns

   When numTurns > 1 the knob can be turned several times around its axis
   - otherwise the total angle is floored to 360°.

   \sa numTurns(), totalAngle(), setTotalAngle()
 */

void QwtKnob::setNumTurns( int numTurns )
{
    numTurns = qMax( numTurns, 1 );

    if ( numTurns == 1 && m_data->totalAngle <= 360.0 )
        return;

    const double angle = numTurns * 360.0;
    if ( angle != m_data->totalAngle )
    {
        m_data->totalAngle = angle;

        scaleDraw()->setAngleRange( -0.5 * m_data->totalAngle,
            0.5 * m_data->totalAngle );

        updateGeometry();
        update();
    }
}

/*!
   \return Number of turns.

   When the total angle is below 360° numTurns() is ceiled to 1.
   \sa setNumTurns(), setTotalAngle(), totalAngle()
 */
int QwtKnob::numTurns() const
{
    return qwtCeil( m_data->totalAngle / 360.0 );
}

/*!
   Change the scale draw of the knob

   For changing the labels of the scales, it
   is necessary to derive from QwtRoundScaleDraw and
   overload QwtRoundScaleDraw::label().

   \sa scaleDraw()
 */
void QwtKnob::setScaleDraw( QwtRoundScaleDraw* scaleDraw )
{
    setAbstractScaleDraw( scaleDraw );
    setTotalAngle( m_data->totalAngle );

    updateGeometry();
    update();
}

/*!
   \return the scale draw of the knob
   \sa setScaleDraw()
 */
const QwtRoundScaleDraw* QwtKnob::scaleDraw() const
{
    return static_cast< const QwtRoundScaleDraw* >( abstractScaleDraw() );
}

/*!
   \return the scale draw of the knob
   \sa setScaleDraw()
 */
QwtRoundScaleDraw* QwtKnob::scaleDraw()
{
    return static_cast< QwtRoundScaleDraw* >( abstractScaleDraw() );
}

/*!
   Calculate the bounding rectangle of the knob without the scale

   \return Bounding rectangle of the knob
   \sa knobWidth(), alignment(), QWidget::contentsRect()
 */
QRect QwtKnob::knobRect() const
{
    const QRect cr = contentsRect();

    const int extent = qwtCeil( scaleDraw()->extent( font() ) );
    const int d = extent + m_data->scaleDist;

    int w = m_data->knobWidth;
    if ( w <= 0 )
    {
        const int dim = qMin( cr.width(), cr.height() );

        w = dim - 2 * ( d );
        w = qMax( 0, w );
    }

    QRect r( 0, 0, w, w );

    if ( m_data->alignment & Qt::AlignLeft )
    {
        r.moveLeft( cr.left() + d );
    }
    else if ( m_data->alignment & Qt::AlignRight )
    {
        r.moveRight( cr.right() - d );
    }
    else
    {
        r.moveCenter( QPoint( cr.center().x(), r.center().y() ) );
    }

    if ( m_data->alignment & Qt::AlignTop )
    {
        r.moveTop( cr.top() + d );
    }
    else if ( m_data->alignment & Qt::AlignBottom )
    {
        r.moveBottom( cr.bottom() - d );
    }
    else
    {
        r.moveCenter( QPoint( r.center().x(), cr.center().y() ) );
    }

    return r;
}

/*!
   \brief Determine what to do when the user presses a mouse button.

   \param pos Mouse position

   \retval True, when pos is inside the circle of the knob.
   \sa scrolledTo()
 */
bool QwtKnob::isScrollPosition( const QPoint& pos ) const
{
    const QRect kr = knobRect();

    const QRegion region( kr, QRegion::Ellipse );
    if ( region.contains( pos ) && ( pos != kr.center() ) )
    {
        const double angle = QLineF( kr.center(), pos ).angle();
        const double valueAngle = qwtToDegrees( scaleMap().transform( value() ) );

        m_data->mouseOffset = qwtNormalizeDegrees( angle - valueAngle );

        return true;
    }

    return false;
}

/*!
   \brief Determine the value for a new position of the mouse

   \param pos Mouse position

   \return Value for the mouse position
   \sa isScrollPosition()
 */
double QwtKnob::scrolledTo( const QPoint& pos ) const
{
    double angle = QLineF( rect().center(), pos ).angle();
    angle = qwtNormalizeDegrees( angle - m_data->mouseOffset );

    if ( scaleMap().pDist() > 360.0 )
    {
        angle = qwtToDegrees( angle );

        const double v = scaleMap().transform( value() );

        int numTurns = qwtFloor( ( v - scaleMap().p1() ) / 360.0 );

        double valueAngle = qwtNormalizeDegrees( v );
        if ( qAbs( valueAngle - angle ) > 180.0 )
        {
            numTurns += ( angle > valueAngle ) ? -1 : 1;
        }

        angle += scaleMap().p1() + numTurns * 360.0;

        if ( !wrapping() )
        {
            const double boundedAngle =
                qBound( scaleMap().p1(), angle, scaleMap().p2() );

            m_data->mouseOffset += ( boundedAngle - angle );
            angle = boundedAngle;
        }
    }
    else
    {
        angle = qwtToScaleAngle( angle );

        double boundedAngle = qBound( scaleMap().p1(), angle, scaleMap().p2() );

        if ( !wrapping() )
        {
            const double currentAngle = scaleMap().transform( value() );

            if ( ( currentAngle > 90.0 ) && ( boundedAngle < -90.0 ) )
                boundedAngle = scaleMap().p2();
            else if ( ( currentAngle < -90.0 ) && ( boundedAngle > 90.0 ) )
                boundedAngle = scaleMap().p1();

            m_data->mouseOffset += ( boundedAngle - angle );
        }

        angle = boundedAngle;
    }

    return scaleMap().invTransform( angle );
}

/*!
   Handle QEvent::StyleChange and QEvent::FontChange;
   \param event Change event
 */
void QwtKnob::changeEvent( QEvent* event )
{
    switch( event->type() )
    {
        case QEvent::StyleChange:
        case QEvent::FontChange:
        {
            updateGeometry();
            update();
            break;
        }
        default:
            break;
    }
}

/*!
   Repaint the knob
   \param event Paint event
 */
void QwtKnob::paintEvent( QPaintEvent* event )
{
    const QRectF knobRect = this->knobRect();

    QPainter painter( this );
    painter.setClipRegion( event->region() );

    QStyleOption opt;
    opt.initFrom(this);
    style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);

    painter.setRenderHint( QPainter::Antialiasing, true );

    if ( !knobRect.contains( event->region().boundingRect() ) )
    {
        scaleDraw()->setRadius( 0.5 * knobRect.width() + m_data->scaleDist );
        scaleDraw()->moveCenter( knobRect.center() );

        scaleDraw()->draw( &painter, palette() );
    }

    drawKnob( &painter, knobRect );

    drawMarker( &painter, knobRect,
        qwtNormalizeDegrees( scaleMap().transform( value() ) ) );

    painter.setRenderHint( QPainter::Antialiasing, false );

    if ( hasFocus() )
        drawFocusIndicator( &painter );
}

/*!
   \brief Draw the knob

   \param painter painter
   \param knobRect Bounding rectangle of the knob (without scale)
 */
void QwtKnob::drawKnob( QPainter* painter, const QRectF& knobRect ) const
{
    double dim = qMin( knobRect.width(), knobRect.height() );
    dim -= m_data->borderWidth * 0.5;

    QRectF aRect( 0, 0, dim, dim );
    aRect.moveCenter( knobRect.center() );

    QPen pen( Qt::NoPen );
    if ( m_data->borderWidth > 0 )
    {
        QColor c1 = palette().color( QPalette::Light );
        QColor c2 = palette().color( QPalette::Dark );

        QLinearGradient gradient( aRect.topLeft(), aRect.bottomRight() );
        gradient.setColorAt( 0.0, c1 );
        gradient.setColorAt( 0.3, c1 );
        gradient.setColorAt( 0.7, c2 );
        gradient.setColorAt( 1.0, c2 );

        pen = QPen( gradient, m_data->borderWidth );
    }

    QBrush brush;
    switch( m_data->knobStyle )
    {
        case QwtKnob::Raised:
        {
            double off = 0.3 * knobRect.width();
            QRadialGradient gradient( knobRect.center(),
                knobRect.width(), knobRect.topLeft() + QPointF( off, off ) );

            gradient.setColorAt( 0.0, palette().color( QPalette::Midlight ) );
            gradient.setColorAt( 1.0, palette().color( QPalette::Button ) );

            brush = QBrush( gradient );

            break;
        }
        case QwtKnob::Styled:
        {
            QRadialGradient gradient(knobRect.center().x() - knobRect.width() / 3,
                knobRect.center().y() - knobRect.height() / 2,
                knobRect.width() * 1.3,
                knobRect.center().x(),
                knobRect.center().y() - knobRect.height() / 2);

            const QColor c = palette().color( QPalette::Button );
            gradient.setColorAt(0, c.lighter(110) );
            gradient.setColorAt( 0.5, c);
            gradient.setColorAt( 0.501, c.darker(102) );
            gradient.setColorAt(1, c.darker(115) );

            brush = QBrush( gradient );

            break;
        }
        case QwtKnob::Sunken:
        {
            QLinearGradient gradient(
                knobRect.topLeft(), knobRect.bottomRight() );
            gradient.setColorAt( 0.0, palette().color( QPalette::Mid ) );
            gradient.setColorAt( 0.5, palette().color( QPalette::Button ) );
            gradient.setColorAt( 1.0, palette().color( QPalette::Midlight ) );
            brush = QBrush( gradient );

            break;
        }
        case QwtKnob::Flat:
        default:
            brush = palette().brush( QPalette::Button );
    }

    painter->setPen( pen );
    painter->setBrush( brush );
    painter->drawEllipse( aRect );
}


/*!
   \brief Draw the marker at the knob's front

   \param painter Painter
   \param rect Bounding rectangle of the knob without scale
   \param angle Angle of the marker in degrees
               ( clockwise, 0 at the 12 o'clock position )
 */
void QwtKnob::drawMarker( QPainter* painter,
    const QRectF& rect, double angle ) const
{
    if ( m_data->markerStyle == NoMarker || !isValid() )
        return;

    const double radians = qwtRadians( angle );
    const double sinA = -qFastSin( radians );
    const double cosA = qFastCos( radians );

    const double xm = rect.center().x();
    const double ym = rect.center().y();
    const double margin = 4.0;

    double radius = 0.5 * ( rect.width() - m_data->borderWidth ) - margin;
    if ( radius < 1.0 )
        radius = 1.0;

    double markerSize = m_data->markerSize;
    if ( markerSize <= 0 )
        markerSize = qRound( 0.4 * radius );

    switch ( m_data->markerStyle )
    {
        case Notch:
        case Nub:
        {
            const double dotWidth = qwtMinF( markerSize, radius );

            const double dotCenterDist = radius - 0.5 * dotWidth;
            if ( dotCenterDist > 0.0 )
            {
                const QPointF center( xm - sinA * dotCenterDist,
                    ym - cosA * dotCenterDist );

                QRectF ellipse( 0.0, 0.0, dotWidth, dotWidth );
                ellipse.moveCenter( center );

                QColor c1 = palette().color( QPalette::Light );
                QColor c2 = palette().color( QPalette::Mid );

                if ( m_data->markerStyle == Notch )
                    qSwap( c1, c2 );

                QLinearGradient gradient(
                    ellipse.topLeft(), ellipse.bottomRight() );
                gradient.setColorAt( 0.0, c1 );
                gradient.setColorAt( 1.0, c2 );

                painter->setPen( Qt::NoPen );
                painter->setBrush( gradient );

                painter->drawEllipse( ellipse );
            }
            break;
        }
        case Dot:
        {
            const double dotWidth = qwtMinF( markerSize, radius);

            const double dotCenterDist = radius - 0.5 * dotWidth;
            if ( dotCenterDist > 0.0 )
            {
                const QPointF center( xm - sinA * dotCenterDist,
                    ym - cosA * dotCenterDist );

                QRectF ellipse( 0.0, 0.0, dotWidth, dotWidth );
                ellipse.moveCenter( center );

                painter->setPen( Qt::NoPen );
                painter->setBrush( palette().color( QPalette::ButtonText ) );
                painter->drawEllipse( ellipse );
            }

            break;
        }
        case Tick:
        {
            const double rb = qwtMaxF( radius - markerSize, 1.0 );
            const double re = radius;

            const QLineF line( xm - sinA * rb, ym - cosA * rb,
                xm - sinA * re, ym - cosA * re );

            QPen pen( palette().color( QPalette::ButtonText ), 0 );
            pen.setCapStyle( Qt::FlatCap );
            painter->setPen( pen );
            painter->drawLine ( line );

            break;
        }
        case Triangle:
        {
            const double rb = qwtMaxF( radius - markerSize, 1.0 );
            const double re = radius;

            painter->translate( rect.center() );
            painter->rotate( angle - 90.0 );

            QPolygonF polygon;
            polygon += QPointF( re, 0.0 );
            polygon += QPointF( rb, 0.5 * ( re - rb ) );
            polygon += QPointF( rb, -0.5 * ( re - rb ) );

            painter->setPen( Qt::NoPen );
            painter->setBrush( palette().color( QPalette::ButtonText ) );
            painter->drawPolygon( polygon );

            painter->resetTransform();

            break;
        }
        default:
            break;
    }
}

/*!
   Draw the focus indicator
   \param painter Painter
 */
void QwtKnob::drawFocusIndicator( QPainter* painter ) const
{
    const QRect cr = contentsRect();

    int w = m_data->knobWidth;
    if ( w <= 0 )
    {
        w = qMin( cr.width(), cr.height() );
    }
    else
    {
        const int extent = qCeil( scaleDraw()->extent( font() ) );
        w += 2 * ( extent + m_data->scaleDist );
    }

    QRect focusRect( 0, 0, w, w );
    focusRect.moveCenter( cr.center() );

    QwtPainter::drawFocusRect( painter, this, focusRect );
}

/*!
   \brief Set the alignment of the knob

   Similar to a QLabel::alignment() the flags decide how
   to align the knob inside of contentsRect().

   The default setting is Qt::AlignCenter

   \param alignment Or'd alignment flags

   \sa alignment(), setKnobWidth(), knobRect()
 */
void QwtKnob::setAlignment( Qt::Alignment alignment )
{
    if ( m_data->alignment != alignment )
    {
        m_data->alignment = alignment;
        update();
    }
}

/*!
   \return Alignment of the knob inside of contentsRect()
   \sa setAlignment(), knobWidth(), knobRect()
 */
Qt::Alignment QwtKnob::alignment() const
{
    return m_data->alignment;
}

/*!
   \brief Change the knob's width.

   Setting a fixed value for the diameter of the knob
   is helpful for aligning several knobs in a row.

   \param width New width

   \sa knobWidth(), setAlignment()
   \note Modifies the sizePolicy()
 */
void QwtKnob::setKnobWidth( int width )
{
    width = qMax( width, 0 );

    if ( width != m_data->knobWidth )
    {
        QSizePolicy::Policy policy;
        if ( width > 0 )
            policy = QSizePolicy::Minimum;
        else
            policy = QSizePolicy::MinimumExpanding;

        setSizePolicy( policy, policy );

        m_data->knobWidth = width;

        updateGeometry();
        update();
    }
}

//! Return the width of the knob
int QwtKnob::knobWidth() const
{
    return m_data->knobWidth;
}

/*!
   \brief Set the knob's border width
   \param borderWidth new border width
 */
void QwtKnob::setBorderWidth( int borderWidth )
{
    m_data->borderWidth = qMax( borderWidth, 0 );

    updateGeometry();
    update();
}

//! Return the border width
int QwtKnob::borderWidth() const
{
    return m_data->borderWidth;
}

/*!
   \brief Set the size of the marker

   When setting a size <= 0 the marker will
   automatically scaled to 40% of the radius of the knob.

   \sa markerSize(), markerStyle()
 */
void QwtKnob::setMarkerSize( int size )
{
    if ( m_data->markerSize != size )
    {
        m_data->markerSize = size;
        update();
    }
}

/*!
   \return Marker size
   \sa setMarkerSize()
 */
int QwtKnob::markerSize() const
{
    return m_data->markerSize;
}

/*!
   \return sizeHint()
 */
QSize QwtKnob::sizeHint() const
{
    const QSize hint = qwtKnobSizeHint( this, 50 );
    return qwtExpandedToGlobalStrut( hint );
}

/*!
   \return Minimum size hint
   \sa sizeHint()
 */
QSize QwtKnob::minimumSizeHint() const
{
    return qwtKnobSizeHint( this, 20 );
}

#if QWT_MOC_INCLUDE
#include "moc_qwt_knob.cpp"
#endif
