/* -*- 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 "qwt_plot_rasteritem.h"
#include "qwt_legend.h"
#include "qwt_legend_item.h"
#include "qwt_scale_map.h"
#include "qwt_painter.h"
#include <qapplication.h>
#include <qdesktopwidget.h>
#include <qpainter.h>
#include <qpaintengine.h>
#include <float.h>

class QwtPlotRasterItem::PrivateData
{
public:
    PrivateData():
        alpha( -1 ),
        paintAttributes( QwtPlotRasterItem::PaintInDeviceResolution )
    {
        cache.policy = QwtPlotRasterItem::NoCache;
    }

    int alpha;
    QwtPlotRasterItem::PaintAttributes paintAttributes;

    struct ImageCache
    {
        QwtPlotRasterItem::CachePolicy policy;
        QRectF area;
        QSizeF size;
        QImage image;
    } cache;
};


static QRectF qwtAlignRect(const QRectF &rect)
{
    QRectF r;
    r.setLeft( qRound( rect.left() ) );
    r.setRight( qRound( rect.right() ) );
    r.setTop( qRound( rect.top() ) );
    r.setBottom( qRound( rect.bottom() ) );

    return r;
}

static QRectF qwtStripRect(const QRectF &rect, const QRectF &area,
    const QwtScaleMap &xMap, const QwtScaleMap &yMap,
    const QwtInterval &xInterval, const QwtInterval &yInterval)
{
    QRectF r = rect;
    if ( xInterval.borderFlags() & QwtInterval::ExcludeMinimum )
    {
        if ( area.left() <= xInterval.minValue() )
        {
            if ( xMap.isInverting() )
                r.adjust(0, 0, -1, 0);
            else
                r.adjust(1, 0, 0, 0);
        }
    }

    if ( xInterval.borderFlags() & QwtInterval::ExcludeMaximum )
    {
        if ( area.right() >= xInterval.maxValue() )
        {
            if ( xMap.isInverting() )
                r.adjust(1, 0, 0, 0);
            else
                r.adjust(0, 0, -1, 0);
        }
    }

    if ( yInterval.borderFlags() & QwtInterval::ExcludeMinimum )
    {
        if ( area.top() <= yInterval.minValue() )
        {
            if ( yMap.isInverting() )
                r.adjust(0, 0, 0, -1);
            else
                r.adjust(0, 1, 0, 0);
        }
    }

    if ( yInterval.borderFlags() & QwtInterval::ExcludeMaximum )
    {
        if ( area.bottom() >= yInterval.maxValue() )
        {
            if ( yMap.isInverting() )
                r.adjust(0, 1, 0, 0);
            else
                r.adjust(0, 0, 0, -1);
        }
    }

    return r;
}

static QImage qwtExpandImage(const QImage &image,
    const QwtScaleMap &xMap, const QwtScaleMap &yMap,
    const QRectF &area, const QRectF &area2, const QRectF &paintRect,
    const QwtInterval &xInterval, const QwtInterval &yInterval )
{
    const QRectF strippedRect = qwtStripRect(paintRect, area2,
        xMap, yMap, xInterval, yInterval);
    const QSize sz = strippedRect.toRect().size();

    const int w = image.width();
    const int h = image.height();

    const QRectF r = QwtScaleMap::transform(xMap, yMap, area).normalized();
    const double pw = ( r.width() - 1) / w;
    const double ph = ( r.height() - 1) / h;

    double px0, py0;
    if ( !xMap.isInverting() )
    {
        px0 = xMap.transform( area2.left() );
        px0 = qRound( px0 );
        px0 = px0 - xMap.transform( area.left() );
    }
    else
    {
        px0 = xMap.transform( area2.right() );
        px0 = qRound( px0 );
        px0 -= xMap.transform( area.right() );

        px0 -= 1.0;
    }
    px0 += strippedRect.left() - paintRect.left();

    if ( !yMap.isInverting() )
    {
        py0 = yMap.transform( area2.top() );
        py0 = qRound( py0 );
        py0 -= yMap.transform( area.top() );
    }
    else
    {
        py0 = yMap.transform( area2.bottom() );
        py0 = qRound( py0 );
        py0 -= yMap.transform( area.bottom() );

        py0 -= 1.0;
    }
    py0 += strippedRect.top() - paintRect.top();

    QImage expanded(sz, image.format());

    switch( image.depth() )
    {
        case 32:
        {
            for ( int y1 = 0; y1 < h; y1++ )
            {
                int yy1;
                if ( y1 == 0 )
                {
                    yy1 = 0;
                }
                else
                {
                    yy1 = qRound( y1 * ph - py0 );
                    if ( yy1 < 0 )
                        yy1 = 0;
                }

                int yy2;
                if ( y1 == h - 1 )
                {
                    yy2 = sz.height();
                }
                else
                {
                    yy2 = qRound( ( y1 + 1 ) * ph - py0 );
                    if ( yy2 > sz.height() )
                        yy2 = sz.height();
                }

                const quint32 *line1 = (const quint32 *) image.scanLine( y1 );

                for ( int x1 = 0; x1 < w; x1++ )
                {
                    int xx1;
                    if ( x1 == 0 )
                    {
                        xx1 = 0;
                    }
                    else
                    {
                        xx1 = qRound( x1 * pw - px0 );
                        if ( xx1 < 0 )
                            xx1 = 0;
                    }

                    int xx2;
                    if ( x1 == w - 1 )
                    {
                        xx2 = sz.width();
                    }
                    else
                    {
                        xx2 = qRound( ( x1 + 1 ) * pw - px0 );
                        if ( xx2 > sz.width() )
                            xx2 = sz.width();
                    }

                    const quint32 rgb( line1[x1] );
                    for ( int y2 = yy1; y2 < yy2; y2++ )
                    {
                        quint32 *line2 = ( quint32 *) expanded.scanLine( y2 );
                        for ( int x2 = xx1; x2 < xx2; x2++ ) 
                            line2[x2] = rgb;
                    }       
                }   
            }   
            break;
        }
        case 8:
        {
            for ( int y1 = 0; y1 < h; y1++ )
            {
                int yy1;
                if ( y1 == 0 )
                {
                    yy1 = 0;
                }   
                else
                {
                    yy1 = qRound( y1 * ph - py0 );
                    if ( yy1 < 0 )
                        yy1 = 0; 
                }       
                
                int yy2;
                if ( y1 == h - 1 )
                {
                    yy2 = sz.height();
                }   
                else
                {
                    yy2 = qRound( ( y1 + 1 ) * ph - py0 );
                    if ( yy2 > sz.height() )
                        yy2 = sz.height();
                }
    
                const uchar *line1 = image.scanLine( y1 );

                for ( int x1 = 0; x1 < w; x1++ )
                {
                    int xx1;
                    if ( x1 == 0 )
                    {
                        xx1 = 0;
                    }
                    else
                    {
                        xx1 = qRound( x1 * pw - px0 );
                        if ( xx1 < 0 )
                            xx1 = 0;
                    }

                    int xx2;
                    if ( x1 == w - 1 )
                    {
                        xx2 = sz.width();
                    }
                    else
                    {
                        xx2 = qRound( ( x1 + 1 ) * pw - px0 );
                        if ( xx2 > sz.width() )
                            xx2 = sz.width();
                    }

                    for ( int y2 = yy1; y2 < yy2; y2++ )
                    {
                        uchar *line2 = expanded.scanLine( y2 );
                        memset( line2 + xx1, line1[x1], xx2 - xx1 );
                    }       
                }   
            }
            break;
        }
        default:
            expanded = image;
    }
    
    return expanded;
}   

static QRectF expandToPixels(const QRectF &rect, const QRectF &pixelRect)
{
    const double pw = pixelRect.width();
    const double ph = pixelRect.height();

    const double dx1 = pixelRect.left() - rect.left();
    const double dx2 = pixelRect.right() - rect.right();
    const double dy1 = pixelRect.top() - rect.top();
    const double dy2 = pixelRect.bottom() - rect.bottom();

    QRectF r;
    r.setLeft( pixelRect.left() - qCeil( dx1 / pw ) * pw );
    r.setTop( pixelRect.top() - qCeil( dy1 / ph ) * ph );
    r.setRight( pixelRect.right() - qFloor( dx2 / pw ) * pw );
    r.setBottom( pixelRect.bottom() - qFloor( dy2 / ph ) * ph );

    return r;
}

static void transformMaps( const QTransform &tr,
    const QwtScaleMap &xMap, const QwtScaleMap &yMap,
    QwtScaleMap &xxMap, QwtScaleMap &yyMap )
{
    const QPointF p1 = tr.map( QPointF( xMap.p1(), yMap.p1() ) );
    const QPointF p2 = tr.map( QPointF( xMap.p2(), yMap.p2() ) );

    xxMap = xMap;
    xxMap.setPaintInterval( p1.x(), p2.x() );

    yyMap = yMap;
    yyMap.setPaintInterval( p1.y(), p2.y() );
}

static void adjustMaps( QwtScaleMap &xMap, QwtScaleMap &yMap,
    const QRectF &area, const QRectF &paintRect)
{
    double sx1 = area.left();
    double sx2 = area.right();
    if ( xMap.isInverting() )
        qSwap(sx1, sx2);

    double sy1 = area.top();
    double sy2 = area.bottom();

    if ( yMap.isInverting() )
        qSwap(sy1, sy2);

    xMap.setPaintInterval(paintRect.left(), paintRect.right());
    xMap.setScaleInterval(sx1, sx2);

    yMap.setPaintInterval(paintRect.top(), paintRect.bottom());
    yMap.setScaleInterval(sy1, sy2);
}

static bool useCache( QwtPlotRasterItem::CachePolicy policy,
    const QPainter *painter )
{
    bool doCache = false;

    if ( policy == QwtPlotRasterItem::PaintCache )
    {
        // Caching doesn't make sense, when the item is
        // not painted to screen

        switch ( painter->paintEngine()->type() )
        {
            case QPaintEngine::SVG:
            case QPaintEngine::Pdf:
            case QPaintEngine::PostScript:
            case QPaintEngine::MacPrinter:
            case QPaintEngine::Picture:
                break;
            default:;
                doCache = true;
        }
    }

    return doCache;
}

static QImage toRgba( const QImage& image, int alpha )
{
    if ( alpha < 0 || alpha >= 255 )
        return image;

    QImage alphaImage( image.size(), QImage::Format_ARGB32 );

    const QRgb mask1 = qRgba( 0, 0, 0, alpha );
    const QRgb mask2 = qRgba( 255, 255, 255, 0 );
    const QRgb mask3 = qRgba( 0, 0, 0, 255 );

    const int w = image.size().width();
    const int h = image.size().height();

    if ( image.depth() == 8 )
    {
        for ( int y = 0; y < h; y++ )
        {
            QRgb* alphaLine = ( QRgb* )alphaImage.scanLine( y );
            const unsigned char *line = image.scanLine( y );

            for ( int x = 0; x < w; x++ )
                *alphaLine++ = ( image.color( *line++ ) & mask2 ) | mask1;
        }
    }
    else if ( image.depth() == 32 )
    {
        for ( int y = 0; y < h; y++ )
        {
            QRgb* alphaLine = ( QRgb* )alphaImage.scanLine( y );
            const QRgb* line = ( const QRgb* ) image.scanLine( y );

            for ( int x = 0; x < w; x++ )
            {
                const QRgb rgb = *line++;
                if ( rgb & mask3 ) // alpha != 0
                    *alphaLine++ = ( rgb & mask2 ) | mask1;
                else
                    *alphaLine++ = rgb;
            }
        }
    }

    return alphaImage;
}

//! Constructor
QwtPlotRasterItem::QwtPlotRasterItem( const QString& title ):
    QwtPlotItem( QwtText( title ) )
{
    init();
}

//! Constructor
QwtPlotRasterItem::QwtPlotRasterItem( const QwtText& title ):
    QwtPlotItem( title )
{
    init();
}

//! Destructor
QwtPlotRasterItem::~QwtPlotRasterItem()
{
    delete d_data;
}

void QwtPlotRasterItem::init()
{
    d_data = new PrivateData();

    setItemAttribute( QwtPlotItem::AutoScale, true );
    setItemAttribute( QwtPlotItem::Legend, false );

    setZ( 8.0 );
}

/*!
  Specify an attribute how to draw the raster item

  \param attribute Paint attribute
  \param on On/Off
  /sa PaintAttribute, testPaintAttribute()
*/
void QwtPlotRasterItem::setPaintAttribute( PaintAttribute attribute, bool on )
{
    if ( on )
        d_data->paintAttributes |= attribute;
    else
        d_data->paintAttributes &= ~attribute;
}

/*!
    \brief Return the current paint attributes
    \sa PaintAttribute, setPaintAttribute()
*/
bool QwtPlotRasterItem::testPaintAttribute( PaintAttribute attribute ) const
{
    return ( d_data->paintAttributes & attribute );
}

/*!
   \brief Set an alpha value for the raster data

   Often a plot has several types of raster data organized in layers.
   ( f.e a geographical map, with weather statistics ).
   Using setAlpha() raster items can be stacked easily.

   The alpha value is a value [0, 255] to
   control the transparency of the image. 0 represents a fully
   transparent color, while 255 represents a fully opaque color.

   \param alpha Alpha value

   - alpha >= 0\n
     All alpha values of the pixels returned by renderImage() will be set to
     alpha, beside those with an alpha value of 0 (invalid pixels).
   - alpha < 0
     The alpha values returned by renderImage() are not changed.

   The default alpha value is -1.

   \sa alpha()
*/
void QwtPlotRasterItem::setAlpha( int alpha )
{
    if ( alpha < 0 )
        alpha = -1;

    if ( alpha > 255 )
        alpha = 255;

    if ( alpha != d_data->alpha )
    {
        d_data->alpha = alpha;

        itemChanged();
    }
}

/*!
  \return Alpha value of the raster item
  \sa setAlpha()
*/
int QwtPlotRasterItem::alpha() const
{
    return d_data->alpha;
}

/*!
  Change the cache policy

  The default policy is NoCache

  \param policy Cache policy
  \sa CachePolicy, cachePolicy()
*/
void QwtPlotRasterItem::setCachePolicy(
    QwtPlotRasterItem::CachePolicy policy )
{
    if ( d_data->cache.policy != policy )
    {
        d_data->cache.policy = policy;

        invalidateCache();
        itemChanged();
    }
}

/*!
  \return Cache policy
  \sa CachePolicy, setCachePolicy()
*/
QwtPlotRasterItem::CachePolicy QwtPlotRasterItem::cachePolicy() const
{
    return d_data->cache.policy;
}

/*!
   Invalidate the paint cache
   \sa setCachePolicy()
*/
void QwtPlotRasterItem::invalidateCache()
{
    d_data->cache.image = QImage();
    d_data->cache.area = QRect();
    d_data->cache.size = QSize();
}

/*!
   \brief Pixel hint

   The geometry of a pixel is used to calculated the resolution and
   alignment of the rendered image. 

   Width and height of the hint need to be the horizontal  
   and vertical distances between 2 neighboured points. 
   The center of the hint has to be the position of any point 
   ( it doesn't matter which one ).

   Limiting the resolution of the image might significantly improve
   the performance and heavily reduce the amount of memory when rendering
   a QImage from the raster data. 

   The default implementation returns an empty rectangle (QRectF()),
   meaning, that the image will be rendered in target device ( f.e screen )
   resolution.

   \param area In most implementations the resolution of the data doesn't
               depend on the requested area.

   \return Bounding rectangle of a pixel

   \sa render(), renderImage()
*/
QRectF QwtPlotRasterItem::pixelHint( const QRectF &area ) const
{
    Q_UNUSED( area );
    return QRectF();
}

/*!
  \brief Draw the raster data
  \param painter Painter
  \param xMap X-Scale Map
  \param yMap Y-Scale Map
  \param canvasRect Contents rect of the plot canvas
*/
void QwtPlotRasterItem::draw( QPainter *painter,
    const QwtScaleMap &xMap, const QwtScaleMap &yMap,
    const QRectF &canvasRect ) const
{
    if ( canvasRect.isEmpty() || d_data->alpha == 0 )
        return;

    const bool doCache = useCache( d_data->cache.policy, painter );

    const QwtInterval xInterval = interval( Qt::XAxis );
    const QwtInterval yInterval = interval( Qt::YAxis );

    /*
        Scaling a rastered image always results in a loss of
        precision/quality. So we always render the image in
        paint device resolution.
    */

    QwtScaleMap xxMap, yyMap;
    transformMaps( painter->transform(), xMap, yMap, xxMap, yyMap );

    QRectF paintRect = painter->transform().mapRect( canvasRect );
    QRectF area = QwtScaleMap::invTransform( xxMap, yyMap, paintRect );

    const QRectF br = boundingRect();
    if ( br.isValid() && !br.contains( area ) )
    {
        area &= br;
        if ( !area.isValid() )
            return;

        paintRect = QwtScaleMap::transform( xxMap, yyMap, area );
    }

    QRectF imageRect;
    QImage image;

    QRectF pixelRect = pixelHint(area);
    if ( !pixelRect.isEmpty() )
    {
        const QRectF r = QwtScaleMap::invTransform( 
            xxMap, yyMap, QRectF(0, 0, 1, 1) ).normalized();

        if ( r.width() > pixelRect.width() &&
            r.height() > pixelRect.height() )
        {
            /*
              When the resolution of the data pixels is higher than
              the resolution of the target device we render in
              target device resolution.
             */
            pixelRect = QRectF();
        }
    }

    if ( pixelRect.isEmpty() )
    {
        if ( QwtPainter::roundingAlignment( painter ) )
        {
            // we want to have maps, where the boundaries of
            // the aligned paint rectangle exactly match the area

            paintRect = qwtAlignRect(paintRect);
            adjustMaps(xxMap, yyMap, area, paintRect);
        }

        // When we have no information about position and size of
        // data pixels we render in resolution of the paint device.

        image = compose(xxMap, yyMap, 
            area, paintRect, paintRect.size().toSize(), doCache);
        if ( image.isNull() )
            return;

        // Remove pixels at the boundaries, when explicitly
        // excluded in the intervals

        imageRect = qwtStripRect(paintRect, area, 
            xxMap, yyMap, xInterval, yInterval);

        if ( imageRect != paintRect )
        {
            const QRect r( 
                qRound( imageRect.x() - paintRect.x()),
                qRound( imageRect.y() - paintRect.y() ),
                qRound( imageRect.width() ),
                qRound( imageRect.height() ) );
                
            image = image.copy(r);
        }   
    }
    else
    {
        if ( QwtPainter::roundingAlignment( painter ) )
            paintRect = qwtAlignRect(paintRect);

        // align the area to the data pixels
        QRectF imageArea = expandToPixels(area, pixelRect);

        if ( imageArea.right() == xInterval.maxValue() &&
            !( xInterval.borderFlags() & QwtInterval::ExcludeMaximum ) )
        {
            imageArea.adjust(0, 0, pixelRect.width(), 0);
        }
        if ( imageArea.bottom() == yInterval.maxValue() &&
            !( yInterval.borderFlags() & QwtInterval::ExcludeMaximum ) )
        {
            imageArea.adjust(0, 0, 0, pixelRect.height() );
        }

        QSize imageSize;
        imageSize.setWidth( qRound( imageArea.width() / pixelRect.width() ) );
        imageSize.setHeight( qRound( imageArea.height() / pixelRect.height() ) );
        image = compose(xxMap, yyMap, 
            imageArea, paintRect, imageSize, doCache );
        if ( image.isNull() )
            return;

        imageRect = qwtStripRect(paintRect, area, 
            xxMap, yyMap, xInterval, yInterval);

        if ( ( image.width() > 1 || image.height() > 1 ) &&
            testPaintAttribute( PaintInDeviceResolution ) )
        {
            // Because of rounding errors the pixels 
            // need to be expanded manually to rectangles of 
            // different sizes

            image = qwtExpandImage(image, xxMap, yyMap, 
                imageArea, area, paintRect, xInterval, yInterval );
        }
    }

    painter->save();
    painter->setWorldTransform( QTransform() );
    
    QwtPainter::drawImage( painter, imageRect, image );

    painter->restore();
}

/*!
   \return Bounding interval for an axis

   This method is intended to be reimplemented by derived classes.
   The default implementation returns an invalid interval.
   
   \param axis X, Y, or Z axis
*/
QwtInterval QwtPlotRasterItem::interval(Qt::Axis axis) const
{
    Q_UNUSED( axis );
    return QwtInterval();
}

/*!
   \return Bounding rect of the data
   \sa QwtPlotRasterItem::interval()
*/
QRectF QwtPlotRasterItem::boundingRect() const
{
    const QwtInterval intervalX = interval( Qt::XAxis );
    const QwtInterval intervalY = interval( Qt::YAxis );

    if ( !intervalX.isValid() && !intervalY.isValid() )
        return QRectF(); // no bounding rect

    QRectF r;

    if ( intervalX.isValid() )
    {
        r.setLeft( intervalX.minValue() );
        r.setRight( intervalX.maxValue() );
    }
    else
    {
        r.setLeft(-0.5 * FLT_MAX);
        r.setWidth(FLT_MAX);
    }

    if ( intervalY.isValid() )
    {
        r.setTop( intervalY.minValue() );
        r.setBottom( intervalY.maxValue() );
    }
    else
    {
        r.setTop(-0.5 * FLT_MAX);
        r.setHeight(FLT_MAX);
    }

    return r.normalized();
}

QImage QwtPlotRasterItem::compose( 
    const QwtScaleMap &xMap, const QwtScaleMap &yMap,
    const QRectF &imageArea, const QRectF &paintRect, 
    const QSize &imageSize, bool doCache) const
{
    QImage image;
    if ( imageArea.isEmpty() || paintRect.isEmpty() || imageSize.isEmpty() )
        return image;

    if ( doCache )
    {
        if ( !d_data->cache.image.isNull()
            && d_data->cache.area == imageArea
            && d_data->cache.size == paintRect.size() )
        {
            image = d_data->cache.image;
        }
    }

    if ( image.isNull() )
    {
        double dx = 0.0;
        if ( paintRect.toRect().width() > imageSize.width() )
            dx = imageArea.width() / imageSize.width();

        const QwtScaleMap xxMap = 
            imageMap(Qt::Horizontal, xMap, imageArea, imageSize, dx);
        
        double dy = 0.0;
        if ( paintRect.toRect().height() > imageSize.height() )
            dy = imageArea.height() / imageSize.height();

        const QwtScaleMap yyMap = 
            imageMap(Qt::Vertical, yMap, imageArea, imageSize, dy);

        image = renderImage( xxMap, yyMap, imageArea, imageSize );

        if ( doCache )
        {
            d_data->cache.area = imageArea;
            d_data->cache.size = paintRect.size();
            d_data->cache.image = image;
        }
    }

    if ( d_data->alpha >= 0 && d_data->alpha < 255 )
        image = toRgba( image, d_data->alpha );

    return image;
}

/*!
   \brief Calculate a scale map for painting to an image

   \param orientation Orientation, Qt::Horizontal means a X axis
   \param map Scale map for rendering the plot item
   \param area Area to be painted on the image
   \param imageSize Image size
   \param pixelSize Width/Height of a data pixel
*/
QwtScaleMap QwtPlotRasterItem::imageMap(
    Qt::Orientation orientation,
    const QwtScaleMap &map, const QRectF &area,
    const QSize &imageSize, double pixelSize) const
{
    double p1, p2, s1, s2;

    if ( orientation == Qt::Horizontal )
    {
        p1 = 0.0;
        p2 = imageSize.width();
        s1 = area.left();
        s2 = area.right();
    }
    else
    {
        p1 = 0.0;
        p2 = imageSize.height();
        s1 = area.top();
        s2 = area.bottom();
    }

    if ( pixelSize > 0.0 )
    {
        double off = 0.5 * pixelSize;
        if ( map.isInverting() )
            off = -off;

        s1 += off;
        s2 += off;
    }
    else
    {
        p2--;
    }

    if ( map.isInverting() && ( s1 < s2 ) )
        qSwap( s1, s2 );

    QwtScaleMap newMap = map;
    newMap.setPaintInterval( p1, p2 );
    newMap.setScaleInterval( s1, s2 );

    return newMap;
}
