/* -*- mode: C++ ; c-file-style: "stroustrup" -*- *****************************
 * 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 <qevent.h>
#include <qdrawutil.h>
#include <qpainter.h>
#include <qstyle.h>
#include "qwt_math.h"
#include "qwt_painter.h"
#include "qwt_paint_buffer.h"
#include "qwt_wheel.h"

#define NUM_COLORS 30

class QwtWheel::PrivateData
{
public:
    PrivateData()
    {
        viewAngle = 175.0;
        totalAngle = 360.0;
        tickCnt = 10;
        intBorder = 2;
        borderWidth = 2;
        wheelWidth = 20;
#if QT_VERSION < 0x040000
        allocContext = 0;
#endif
    };

    QRect sliderRect;
    double viewAngle;
    double totalAngle;
    int tickCnt;
    int intBorder;
    int borderWidth;
    int wheelWidth;
#if QT_VERSION < 0x040000
    int allocContext;
#endif
    QColor colors[NUM_COLORS];
};

//! Constructor
QwtWheel::QwtWheel(QWidget *parent): 
    QwtAbstractSlider(Qt::Horizontal, parent)
{
    initWheel();
}

#if QT_VERSION < 0x040000
QwtWheel::QwtWheel(QWidget *parent, const char *name): 
    QwtAbstractSlider(Qt::Horizontal, parent)
{
    setName(name);
    initWheel();
}
#endif

void QwtWheel::initWheel()
{
    d_data = new PrivateData;

#if QT_VERSION < 0x040000
    setWFlags(Qt::WNoAutoErase);
#endif

    setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);

#if QT_VERSION >= 0x040000
    setAttribute(Qt::WA_WState_OwnSizePolicy, false);
#else
    clearWState( WState_OwnSizePolicy );
#endif

    setUpdateTime(50);
}

//! Destructor
QwtWheel::~QwtWheel()  
{
#if QT_VERSION < 0x040000
    if ( d_data->allocContext )
        QColor::destroyAllocContext( d_data->allocContext );
#endif
    delete d_data;
}

//! Set up the color array for the background pixmap.
void QwtWheel::setColorArray()
{
    if ( !d_data->colors ) 
        return;

#if QT_VERSION < 0x040000
    const QColor light = colorGroup().light();
    const QColor dark = colorGroup().dark();
#else
    const QColor light = palette().color(QPalette::Light);
    const QColor dark = palette().color(QPalette::Dark);
#endif

    if ( !d_data->colors[0].isValid() ||
        d_data->colors[0] != light ||
        d_data->colors[NUM_COLORS - 1] != dark )
    {
#if QT_VERSION < 0x040000
        if ( d_data->allocContext )
            QColor::destroyAllocContext( d_data->allocContext );

        d_data->allocContext = QColor::enterAllocContext();
#endif

        d_data->colors[0] = light;
        d_data->colors[NUM_COLORS - 1] = dark;

        int dh, ds, dv, lh, ls, lv;
#if QT_VERSION < 0x040000
        d_data->colors[0].rgb(&lh, &ls, &lv);
        d_data->colors[NUM_COLORS - 1].rgb(&dh, &ds, &dv);
#else
        d_data->colors[0].getRgb(&lh, &ls, &lv);
        d_data->colors[NUM_COLORS - 1].getRgb(&dh, &ds, &dv);
#endif

        for ( int i = 1; i < NUM_COLORS - 1; ++i )
        {
            const double factor = double(i) / double(NUM_COLORS);

            d_data->colors[i].setRgb( lh + int( double(dh - lh) * factor ),
                      ls + int( double(ds - ls) * factor ),
                      lv + int( double(dv - lv) * factor ));
        }
#if QT_VERSION < 0x040000
        QColor::leaveAllocContext();
#endif
    }
}

/*!
  \brief Adjust the number of grooves in the wheel's surface.

  The number of grooves is limited to 6 <= cnt <= 50.
  Values outside this range will be clipped.
  The default value is 10.
  \param cnt Number of grooves per 360 degrees
*/
void QwtWheel::setTickCnt(int cnt)
{
    d_data->tickCnt = qwtLim( cnt, 6, 50 );
    update();
}

int QwtWheel::tickCnt() const 
{
    return d_data->tickCnt;
}

/*!
    \return mass
*/
double QwtWheel::mass() const
{
    return QwtAbstractSlider::mass();
}

/*!
  \brief Set the internal border width of the wheel.

  The internal border must not be smaller than 1
  and is limited in dependence on the wheel's size.
  Values outside the allowed range will be clipped.

  The internal border defaults to 2.
  \param w border width
*/
void QwtWheel::setInternalBorder( int w )
{
    const int d = qwtMin( width(), height() ) / 3;
    w = qwtMin( w, d );
    d_data->intBorder = qwtMax( w, 1 );
    layoutWheel();
}

int QwtWheel::internalBorder() const 
{
    return d_data->intBorder;
}

//! Draw the Wheel's background gradient
void QwtWheel::drawWheelBackground( QPainter *p, const QRect &r )
{
    p->save();

    //
    // initialize pens
    //
#if QT_VERSION < 0x040000
    const QColor light = colorGroup().light();
    const QColor dark = colorGroup().dark();
#else
    const QColor light = palette().color(QPalette::Light);
    const QColor dark = palette().color(QPalette::Dark);
#endif

    QPen lightPen;
    lightPen.setColor(light);
    lightPen.setWidth(d_data->intBorder);

    QPen darkPen;
    darkPen.setColor(dark);
    darkPen.setWidth(d_data->intBorder);

    setColorArray();

    //
    // initialize auxiliary variables
    //

    const int nFields = NUM_COLORS * 13 / 10;
    const int hiPos = nFields - NUM_COLORS + 1;

    if ( orientation() == Qt::Horizontal )
    {
        const int rx = r.x();
        int ry = r.y() + d_data->intBorder;
        const int rh = r.height() - 2* d_data->intBorder;
        const int rw = r.width();
        //
        //  draw shaded background
        //
        int x1 = rx;
        for (int i = 1; i < nFields; i++ )
        {
            const int x2 = rx + (rw * i) / nFields;
            p->fillRect(x1, ry, x2-x1 + 1 ,rh, d_data->colors[qwtAbs(i-hiPos)]);
            x1 = x2 + 1;
        }
        p->fillRect(x1, ry, rw - (x1 - rx), rh, d_data->colors[NUM_COLORS - 1]);

        //
        // draw internal border
        //
        p->setPen(lightPen);
        ry = r.y() + d_data->intBorder / 2;
        p->drawLine(r.x(), ry, r.x() + r.width() , ry);

        p->setPen(darkPen);
        ry = r.y() + r.height() - (d_data->intBorder - d_data->intBorder / 2);
        p->drawLine(r.x(), ry , r.x() + r.width(), ry);
    }
    else // Qt::Vertical
    {
        int rx = r.x() + d_data->intBorder;
        const int ry = r.y();
        const int rh = r.height();
        const int rw = r.width() - 2 * d_data->intBorder;

        //
        // draw shaded background
        //
        int y1 = ry;
        for ( int i = 1; i < nFields; i++ )
        {
            const int y2 = ry + (rh * i) / nFields;
            p->fillRect(rx, y1, rw, y2-y1 + 1, d_data->colors[qwtAbs(i-hiPos)]);
            y1 = y2 + 1;
        }
        p->fillRect(rx, y1, rw, rh - (y1 - ry), d_data->colors[NUM_COLORS - 1]);

        //
        //  draw internal borders
        //
        p->setPen(lightPen);
        rx = r.x() + d_data->intBorder / 2;
        p->drawLine(rx, r.y(), rx, r.y() + r.height());

        p->setPen(darkPen);
        rx = r.x() + r.width() - (d_data->intBorder - d_data->intBorder / 2);
        p->drawLine(rx, r.y(), rx , r.y() + r.height());
    }

    p->restore();
}


/*!
  \brief Set the total angle which the wheel can be turned.

  One full turn of the wheel corresponds to an angle of
  360 degrees. A total angle of n*360 degrees means
  that the wheel has to be turned n times around its axis
  to get from the minimum value to the maximum value.

  The default setting of the total angle is 360 degrees.
  \param angle total angle in degrees
*/
void QwtWheel::setTotalAngle(double angle)
{
    if ( angle < 0.0 )
        angle = 0.0;

    d_data->totalAngle = angle;
    update();
}

double QwtWheel::totalAngle() const 
{
    return d_data->totalAngle;
}

/*!
  \brief Set the wheel's orientation.
  \param o Orientation. Allowed values are
           Qt::Horizontal and Qt::Vertical.
   Defaults to Qt::Horizontal.
  \sa QwtAbstractSlider::orientation()
*/
void QwtWheel::setOrientation(Qt::Orientation o)
{
    if ( orientation() == o )
        return;

#if QT_VERSION >= 0x040000
    if ( !testAttribute(Qt::WA_WState_OwnSizePolicy) )
#else
    if ( !testWState( WState_OwnSizePolicy ) ) 
#endif
    {
        QSizePolicy sp = sizePolicy();
        sp.transpose();
        setSizePolicy(sp);

#if QT_VERSION >= 0x040000
        setAttribute(Qt::WA_WState_OwnSizePolicy, false);
#else
        clearWState( WState_OwnSizePolicy );
#endif
    }

    QwtAbstractSlider::setOrientation(o);
    layoutWheel();
}

/*!
  \brief Specify the visible portion of the wheel.

  You may use this function for fine-tuning the appearance of
  the wheel. The default value is 175 degrees. The value is
  limited from 10 to 175 degrees.
  \param angle Visible angle in degrees
*/
void QwtWheel::setViewAngle(double angle)
{
    d_data->viewAngle = qwtLim( angle, 10.0, 175.0 );
    update();
}

double QwtWheel::viewAngle() const 
{
    return d_data->viewAngle;
}

/*!
  \brief Redraw the wheel
  \param p painter
  \param r contents rectangle
*/
void QwtWheel::drawWheel( QPainter *p, const QRect &r )
{
    //
    // draw background gradient
    //
    drawWheelBackground( p, r );

    if ( maxValue() == minValue() || d_data->totalAngle == 0.0 )
        return;

#if QT_VERSION < 0x040000
    const QColor light = colorGroup().light();
    const QColor dark = colorGroup().dark();
#else
    const QColor light = palette().color(QPalette::Light);
    const QColor dark = palette().color(QPalette::Dark);
#endif

    const double sign = (minValue() < maxValue()) ? 1.0 : -1.0;
    double cnvFactor = qwtAbs(d_data->totalAngle / (maxValue() - minValue()));
    const double halfIntv = 0.5 * d_data->viewAngle / cnvFactor;
    const double loValue = value() - halfIntv;
    const double hiValue = value() + halfIntv;
    const double tickWidth = 360.0 / double(d_data->tickCnt) / cnvFactor;
    const double sinArc = sin(d_data->viewAngle * M_PI / 360.0);
    cnvFactor *= M_PI / 180.0;


    //
    // draw grooves
    //
    if ( orientation() == Qt::Horizontal )
    {
        const double halfSize = double(r.width()) * 0.5;

        int l1 = r.y() + d_data->intBorder;
        int l2 = r.y() + r.height() - d_data->intBorder - 1;

        // draw one point over the border if border > 1
        if ( d_data->intBorder > 1 )
        {
            l1 --;
            l2 ++;
        }

        const int maxpos = r.x() + r.width() - 2;
        const int minpos = r.x() + 2;

        //
        // draw tick marks
        //
        for ( double tickValue = ceil(loValue / tickWidth) * tickWidth;
            tickValue < hiValue; tickValue += tickWidth )
        {
            //
            //  calculate position
            //
            const int tickPos = r.x() + r.width()
                - int( halfSize
                    * (sinArc + sign *  sin((tickValue - value()) * cnvFactor))
                    / sinArc);
            //
            // draw vertical line
            //
            if ( (tickPos <= maxpos) && (tickPos > minpos) )
            {
                p->setPen(dark);
                p->drawLine(tickPos -1 , l1, tickPos - 1,  l2 );  
                p->setPen(light);
                p->drawLine(tickPos, l1, tickPos, l2);  
            }
        }
    }
    else if ( orientation() == Qt::Vertical )
    {
        const double halfSize = double(r.height()) * 0.5;

        int l1 = r.x() + d_data->intBorder;
        int l2 = r.x() + r.width() - d_data->intBorder - 1;

        if ( d_data->intBorder > 1 )
        {
            l1--;
            l2++;
        }

        const int maxpos = r.y() + r.height() - 2;
        const int minpos = r.y() + 2;

        //
        // draw tick marks
        //
        for ( double tickValue = ceil(loValue / tickWidth) * tickWidth;
            tickValue < hiValue; tickValue += tickWidth )
        {

            //
            // calculate position
            //
            const int tickPos = r.y() + int( halfSize *
                (sinArc + sign * sin((tickValue - value()) * cnvFactor))
                / sinArc);

            //
            //  draw horizontal line
            //
            if ( (tickPos <= maxpos) && (tickPos > minpos) )
            {
                p->setPen(dark);
                p->drawLine(l1, tickPos - 1 ,l2, tickPos - 1);  
                p->setPen(light);
                p->drawLine(l1, tickPos, l2, tickPos);  
            }
        }
    }
}


//! Determine the value corresponding to a specified point
double QwtWheel::getValue( const QPoint &p )
{
    // The reference position is arbitrary, but the
    // sign of the offset is important
    int w, dx;
    if ( orientation() == Qt::Vertical )
    {
        w = d_data->sliderRect.height();
        dx = d_data->sliderRect.y() - p.y();
    }
    else
    {
        w = d_data->sliderRect.width();
        dx = p.x() - d_data->sliderRect.x();
    }

    // w pixels is an arc of viewAngle degrees,
    // so we convert change in pixels to change in angle
    const double ang = dx * d_data->viewAngle / w;

    // value range maps to totalAngle degrees,
    // so convert the change in angle to a change in value
    const double val = ang * ( maxValue() - minValue() ) / d_data->totalAngle;

    // Note, range clamping and rasterizing to step is automatically
    // handled by QwtAbstractSlider, so we simply return the change in value
    return val;
}

//! Qt Resize Event
void QwtWheel::resizeEvent(QResizeEvent *)
{
    layoutWheel( false );
}

//! Recalculate the slider's geometry and layout based on
//  the current rect and fonts.
//  \param update_geometry  notify the layout system and call update
//         to redraw the scale
void QwtWheel::layoutWheel( bool update_geometry )
{
    const QRect r = this->rect();
    d_data->sliderRect.setRect(r.x() + d_data->borderWidth, r.y() + d_data->borderWidth,
        r.width() - 2*d_data->borderWidth, r.height() - 2*d_data->borderWidth);

    if ( update_geometry )
    {
        updateGeometry();
        update();
    }
}

//! Qt Paint Event
void QwtWheel::paintEvent(QPaintEvent *e)
{
    // Use double-buffering
    const QRect &ur = e->rect();
    if ( ur.isValid() )
    {
#if QT_VERSION < 0x040000
        QwtPaintBuffer paintBuffer(this, ur);
        draw(paintBuffer.painter(), ur);
#else
        QPainter painter(this);
        draw(&painter, ur);
#endif
    }
}

//! Redraw panel and wheel
void QwtWheel::draw(QPainter *painter, const QRect&)
{
    qDrawShadePanel( painter, rect().x(), rect().y(),
        rect().width(), rect().height(),
#if QT_VERSION < 0x040000
        colorGroup(), 
#else
        palette(), 
#endif
        true, d_data->borderWidth );

    drawWheel( painter, d_data->sliderRect );

    if ( hasFocus() )
        QwtPainter::drawFocusRect(painter, this);
}

//! Notify value change 
void QwtWheel::valueChange()
{
    QwtAbstractSlider::valueChange();
    update();
}


/*!
  \brief Determine the scrolling mode and direction corresponding
         to a specified point
  \param p point
  \param scrollMode scrolling mode
  \param direction direction
*/
void QwtWheel::getScrollMode( const QPoint &p, int &scrollMode, int &direction)
{
    if ( d_data->sliderRect.contains(p) )
        scrollMode = ScrMouse;
    else
        scrollMode = ScrNone;

    direction = 0;
}

/*!
  \brief Set the mass of the wheel

  Assigning a mass turns the wheel into a flywheel.
  \param val the wheel's mass
*/
void QwtWheel::setMass(double val)
{
    QwtAbstractSlider::setMass(val);
}

/*!
  \brief Set the width of the wheel

  Corresponds to the wheel height for horizontal orientation,
  and the wheel width for vertical orientation.
  \param w the wheel's width
*/
void QwtWheel::setWheelWidth(int w)
{
    d_data->wheelWidth = w;
    layoutWheel();
}

/*!
  \return a size hint
*/
QSize QwtWheel::sizeHint() const
{
    return minimumSizeHint();
}

/*!
  \brief Return a minimum size hint
  \warning The return value is based on the wheel width.
*/
QSize QwtWheel::minimumSizeHint() const
{
    QSize sz( 3*d_data->wheelWidth + 2*d_data->borderWidth,
    d_data->wheelWidth + 2*d_data->borderWidth );
    if ( orientation() != Qt::Horizontal )
        sz.transpose();
    return sz;
}

/*!
  \brief Call update() when the palette changes
*/
void QwtWheel::paletteChange( const QPalette& )
{
    update();
}