/* ============================================================
 *
 * This file is a part of digiKam project
 * http://www.digikam.org
 *
 * Date        : 2006-06-13
 * Description : a widget to display an image preview
 *
 * Copyright (C) 2006-2008 Gilles Caulier <caulier dot gilles at gmail dot com>
 *
 * This program is free software; you can redistribute it
 * and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation;
 * either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * ============================================================ */

// C++ includes.

#include <cmath>

// TQt includes.

#include <tqstring.h>
#include <tqcache.h>
#include <tqpainter.h>
#include <tqimage.h>
#include <tqpixmap.h>
#include <tqrect.h>
#include <tqtimer.h>
#include <tqguardedptr.h>

// KDE includes.

#include <kcursor.h>
#include <tdelocale.h>

// Local includes.

#include "ddebug.h"
#include "previewwidget.h"
#include "previewwidget.moc"

namespace Digikam
{

class PreviewWidgetPriv
{
public:

    PreviewWidgetPriv() :
        tileSize(128), zoomMultiplier(1.2) 
    {
        midButtonX = 0;
        midButtonY = 0;
        autoZoom   = false;
        fullScreen = false;
        zoom       = 1.0;
        minZoom    = 0.1;
        maxZoom    = 12.0;
        zoomWidth  = 0;
        zoomHeight = 0;
        tileTmpPix = new TQPixmap(tileSize, tileSize);

        tileCache.setMaxCost((10*1024*1024)/(tileSize*tileSize*4));
        tileCache.setAutoDelete(true);
    }

    bool            autoZoom;
    bool            fullScreen;

    const int       tileSize;
    int             midButtonX;
    int             midButtonY;
    int             zoomWidth;
    int             zoomHeight;

    double          zoom;
    double          minZoom;
    double          maxZoom;
    const double    zoomMultiplier;

    TQPoint          centerZoomPoint;

    TQRect           pixmapRect;

    TQCache<TQPixmap> tileCache;

    TQPixmap*        tileTmpPix;

    TQColor          bgColor;
};

PreviewWidget::PreviewWidget(TQWidget *parent)
             : TQScrollView(parent, 0, TQt::WDestructiveClose)
{
    d = new PreviewWidgetPriv;
    d->bgColor.setRgb(0, 0, 0);
    m_movingInProgress = false;

    viewport()->setBackgroundMode(TQt::NoBackground);
    viewport()->setMouseTracking(false);

    horizontalScrollBar()->setLineStep( 1 );
    horizontalScrollBar()->setPageStep( 1 );
    verticalScrollBar()->setLineStep( 1 );
    verticalScrollBar()->setPageStep( 1 );

    setFrameStyle(TQFrame::GroupBoxPanel|TQFrame::Plain); 
    setMargin(0); 
    setLineWidth(1);
}

PreviewWidget::~PreviewWidget()
{
    delete d->tileTmpPix;
    delete d;
}

void PreviewWidget::setBackgroundColor(const TQColor& color)
{
    if (d->bgColor == color)
        return;

    d->bgColor = color;
    viewport()->update();
}

void PreviewWidget::slotReset()
{
    d->tileCache.clear();
    resetPreview();
}

TQRect PreviewWidget::previewRect()
{
    return d->pixmapRect;
}

int PreviewWidget::tileSize()
{
    return d->tileSize;
}

int PreviewWidget::zoomWidth()
{
    return d->zoomWidth;
}

int PreviewWidget::zoomHeight()
{
    return d->zoomHeight;
}

double PreviewWidget::zoomMax()
{
    return d->maxZoom;
}

double PreviewWidget::zoomMin()
{
    return d->minZoom;
}

void PreviewWidget::setZoomMax(double z)
{
    d->maxZoom = ceilf(z * 10000.0) / 10000.0;
}

void PreviewWidget::setZoomMin(double z)
{
    d->minZoom = floor(z * 10000.0) / 10000.0;
}

bool PreviewWidget::maxZoom()
{
    return (d->zoom >= d->maxZoom);
}

bool PreviewWidget::minZoom()
{
    return (d->zoom <= d->minZoom);
}

double PreviewWidget::snapZoom(double zoom)
{
    // If the zoom value gets changed from d->zoom to zoom
    // across 50%, 100% or fit-to-window, then return the
    // the corresponding special value. Otherwise zoom is returned unchanged.
    double fit = calcAutoZoomFactor(ZoomInOrOut);
    TQValueList<double> snapValues;
    snapValues.append(0.5);
    snapValues.append(1.0);
    snapValues.append(fit);
    qHeapSort(snapValues);
    TQValueList<double>::const_iterator it;

    if (d->zoom < zoom) 
    {
        for(it = snapValues.constBegin(); it != snapValues.constEnd(); ++it)
        {
            double z = *it;
            if ((d->zoom < z) && (zoom > z))
            {
                 zoom = z;
                 break;
            }
        }
    } 
    else
    {
        for(it = snapValues.constEnd(); it != snapValues.constBegin(); --it)
        {
            double z = *it;
            if ((d->zoom > z) && (zoom < z))
            {
                 zoom = z;
                 break;
            }
        }
    }

    return zoom;
}

void PreviewWidget::slotIncreaseZoom()
{
    double zoom = d->zoom * d->zoomMultiplier;
    zoom = snapZoom(zoom > zoomMax() ? zoomMax() : zoom);
    setZoomFactor(zoom);
}

void PreviewWidget::slotDecreaseZoom()
{
    double zoom = d->zoom / d->zoomMultiplier;
    zoom = snapZoom(zoom < zoomMin() ? zoomMin() : zoom);
    setZoomFactor(zoom);
}

void PreviewWidget::setZoomFactorSnapped(double zoom)
{
    double fit = calcAutoZoomFactor(ZoomInOrOut);
    if (fabs(zoom-1.0) < 0.05) 
    {
        zoom = 1.0;
    }
    if (fabs(zoom-0.5) < 0.05) 
    {
        zoom = 0.5;
    }
    if (fabs(zoom-fit) < 0.05) 
    {
        zoom = fit;
    }

    setZoomFactor(zoom);
}

void PreviewWidget::setZoomFactor(double zoom)
{
    setZoomFactor(zoom, false);
}

void PreviewWidget::setZoomFactor(double zoom, bool centerView)
{
    // Zoom using center of canvas and given zoom factor.

    double oldZoom = d->zoom;
    double cpx, cpy;

    if (d->centerZoomPoint.isNull())
    {
        // center on current center
        // store old center pos
        cpx = contentsX() + visibleWidth()  / 2.0;
        cpy = contentsY() + visibleHeight() / 2.0;

        cpx = ( cpx / d->tileSize ) * floor(d->tileSize / d->zoom);
        cpy = ( cpy / d->tileSize ) * floor(d->tileSize / d->zoom);
    }
    else
    {
        // keep mouse pointer position constant
        // store old content pos
        cpx = contentsX();
        cpy = contentsY();
    }

    // To limit precision of zoom value and reduce error with check of max/min zoom.
    d->zoom       = floor(zoom * 10000.0) / 10000.0;
    d->zoomWidth  = (int)(previewWidth()  * d->zoom);
    d->zoomHeight = (int)(previewHeight() * d->zoom);

    updateContentsSize();

    // adapt step size to zoom factor. Overall, using a finer step size than scrollbar default.
    int step = TQMAX(2, 2*lround(d->zoom));
    horizontalScrollBar()->setLineStep( step );
    horizontalScrollBar()->setPageStep( step * 10 );
    verticalScrollBar()->setLineStep( step );
    verticalScrollBar()->setPageStep( step * 10 );

    viewport()->setUpdatesEnabled(false);
    if (d->centerZoomPoint.isNull())
    {
        cpx = ( cpx * d->tileSize ) / floor(d->tileSize / d->zoom);
        cpy = ( cpy * d->tileSize ) / floor(d->tileSize / d->zoom);

        if (centerView)
        {
            cpx = d->zoomWidth/2.0;
            cpy = d->zoomHeight/2.0;
        }

        center((int)cpx, (int)(cpy));
    }
    else
    {
        cpx = d->zoom * d->centerZoomPoint.x() / oldZoom - d->centerZoomPoint.x() + cpx;
        cpy = d->zoom * d->centerZoomPoint.y() / oldZoom - d->centerZoomPoint.y() + cpy;

        setContentsPos((int)cpx, (int)(cpy));
    }
    viewport()->setUpdatesEnabled(true);
    viewport()->update();

    zoomFactorChanged(d->zoom);
}

double PreviewWidget::zoomFactor()
{
    return d->zoom; 
}

bool PreviewWidget::isFitToWindow()
{
    return d->autoZoom;
}

void PreviewWidget::fitToWindow()
{
    updateAutoZoom();
    updateContentsSize();
    zoomFactorChanged(d->zoom);
    viewport()->update();
}

void PreviewWidget::toggleFitToWindow()
{
    d->autoZoom = !d->autoZoom;

    if (d->autoZoom)
    {
        updateAutoZoom();
    }
    else
    {
        d->zoom = 1.0;
        zoomFactorChanged(d->zoom);
    }

    updateContentsSize();
    viewport()->update();
}

void PreviewWidget::toggleFitToWindowOr100()
{
    // If the current zoom is 100%, then fit to window.
    if (d->zoom == 1.0) 
    {
        fitToWindow();
    }
    else
    {
        setZoomFactor(1.0, true);
    }
}

void PreviewWidget::updateAutoZoom(AutoZoomMode mode)
{
    d->zoom       = calcAutoZoomFactor(mode);
    d->zoomWidth  = (int)(previewWidth()  * d->zoom);
    d->zoomHeight = (int)(previewHeight() * d->zoom);

    zoomFactorChanged(d->zoom);
}

double PreviewWidget::calcAutoZoomFactor(AutoZoomMode mode)
{
    if (previewIsNull()) return d->zoom;

    double srcWidth  = previewWidth();
    double srcHeight = previewHeight();
    double dstWidth  = contentsRect().width();
    double dstHeight = contentsRect().height();

    double zoom = TQMIN(dstWidth/srcWidth, dstHeight/srcHeight);
    // limit precision as above
    zoom = floor(zoom * 10000.0) / 10000.0;
    if (mode == ZoomInOrOut)
        // fit to available space, scale up or down
        return zoom;
    else
        // ZoomInOnly: accept that an image is smaller than available space, dont scale up
        return TQMIN(1.0, zoom);
}

void PreviewWidget::updateContentsSize()
{
    viewport()->setUpdatesEnabled(false);

    if (visibleWidth() > d->zoomWidth || visibleHeight() > d->zoomHeight)
    {
        // Center the image
        int centerx = contentsRect().width()/2;
        int centery = contentsRect().height()/2;
        int xoffset = int(centerx - d->zoomWidth/2);
        int yoffset = int(centery - d->zoomHeight/2);
        xoffset     = TQMAX(xoffset, 0);
        yoffset     = TQMAX(yoffset, 0);

        d->pixmapRect = TQRect(xoffset, yoffset, d->zoomWidth, d->zoomHeight);
    }
    else
    {
        d->pixmapRect = TQRect(0, 0, d->zoomWidth, d->zoomHeight);
    }

    d->tileCache.clear();
    setContentsSize();
    viewport()->setUpdatesEnabled(true);
}

void PreviewWidget::setContentsSize()
{
    resizeContents(d->zoomWidth, d->zoomHeight);
}

void PreviewWidget::resizeEvent(TQResizeEvent* e)
{
    if (!e) return;

    TQScrollView::resizeEvent(e);

    if (d->autoZoom)
        updateAutoZoom();

    updateContentsSize();

    // No need to repaint. its called   
    // automatically after resize

    // To be sure than corner widget used to pan image will be hide/show 
    // accordinly with resize event.
    zoomFactorChanged(d->zoom);
}

void PreviewWidget::viewportPaintEvent(TQPaintEvent *e)
{
    TQRect er(e->rect());
    er = TQRect(TQMAX(er.x()      - 1, 0),
               TQMAX(er.y()      - 1, 0),
               TQMIN(er.width()  + 2, contentsRect().width()),
               TQMIN(er.height() + 2, contentsRect().height()));
    
    bool antialias = (d->zoom <= 1.0) ? true : false;

    TQRect o_cr(viewportToContents(er.topLeft()), viewportToContents(er.bottomRight()));
    TQRect cr = o_cr;

    TQRegion clipRegion(er);
    cr = d->pixmapRect.intersect(cr);

    if (!cr.isEmpty() && !previewIsNull())
    {
        clipRegion -= TQRect(contentsToViewport(cr.topLeft()), cr.size());

        TQRect pr = TQRect(cr.x() - d->pixmapRect.x(), cr.y() - d->pixmapRect.y(),
                         cr.width(), cr.height());

        int x1 = (int)floor((double)pr.x()      / (double)d->tileSize) * d->tileSize;
        int y1 = (int)floor((double)pr.y()      / (double)d->tileSize) * d->tileSize;
        int x2 = (int)ceilf((double)pr.right()  / (double)d->tileSize) * d->tileSize;
        int y2 = (int)ceilf((double)pr.bottom() / (double)d->tileSize) * d->tileSize;

        TQPixmap pix(d->tileSize, d->tileSize);
        int sx, sy, sw, sh;
        int step = (int)floor(d->tileSize / d->zoom); 

        for (int j = y1 ; j < y2 ; j += d->tileSize)
        {
            for (int i = x1 ; i < x2 ; i += d->tileSize)
            {
                TQString key  = TQString("%1,%2").arg(i).arg(j);
                TQPixmap *pix = d->tileCache.find(key);
                
                if (!pix)
                {
                    if (antialias)
                    {
                        pix = new TQPixmap(d->tileSize, d->tileSize);
                        d->tileCache.insert(key, pix);
                    }
                    else
                    {
                        pix = d->tileTmpPix;
                    }

                    pix->fill(d->bgColor);

                    sx = (int)floor((double)i / d->tileSize ) * step;
                    sy = (int)floor((double)j / d->tileSize ) * step;
                    sw = step;
                    sh = step;

                    paintPreview(pix, sx, sy, sw, sh);
                }

                TQRect  r(i, j, d->tileSize, d->tileSize);
                TQRect  ir = pr.intersect(r);
                TQPoint pt(contentsToViewport(TQPoint(ir.x() + d->pixmapRect.x(),
                                                    ir.y() + d->pixmapRect.y())));

                bitBlt(viewport(), pt.x(), pt.y(),
                       pix,
                       ir.x()-r.x(), ir.y()-r.y(),
                       ir.width(), ir.height());
            }
        }
    }

    TQPainter p(viewport());
    p.setClipRegion(clipRegion);
    p.fillRect(er, d->bgColor);
    p.end();

    viewportPaintExtraData();
}

void PreviewWidget::contentsMousePressEvent(TQMouseEvent *e)
{
    if (!e || e->button() == TQt::RightButton)
        return;

    m_movingInProgress = false;

    if (e->button() == TQt::LeftButton)
    {
        emit signalLeftButtonClicked();
    }
    else if (e->button() == TQt::MidButton)
    {
        if (visibleWidth()  < d->zoomWidth ||
            visibleHeight() < d->zoomHeight)
        {
            m_movingInProgress = true;
            d->midButtonX      = e->x();
            d->midButtonY      = e->y();
            viewport()->repaint(false);
            viewport()->setCursor(TQt::SizeAllCursor);            
        }
        return;
    }
    
    viewport()->setMouseTracking(false);
}

void PreviewWidget::contentsMouseMoveEvent(TQMouseEvent *e)
{
    if (!e) return;

    if (e->state() & TQt::MidButton)
    {
        if (m_movingInProgress)
        {
            scrollBy(d->midButtonX - e->x(),
                     d->midButtonY - e->y());
            emit signalContentsMovedEvent(false);
        }
    }
}
    
void PreviewWidget::contentsMouseReleaseEvent(TQMouseEvent *e)
{
    if (!e) return;

    m_movingInProgress = false;

    if (e->button() == TQt::MidButton)
    {
        emit signalContentsMovedEvent(true);
        viewport()->unsetCursor();
        viewport()->repaint(false);
    }

    if (e->button() == TQt::RightButton)
    {
        emit signalRightButtonClicked();
    }
}

void PreviewWidget::contentsWheelEvent(TQWheelEvent *e)
{
    e->accept();

    if (e->state() & TQt::ShiftButton)
    {
        if (e->delta() < 0)
            emit signalShowNextImage();
        else if (e->delta() > 0)
            emit signalShowPrevImage();
        return;
    }
    else if (e->state() & TQt::ControlButton)
    {
        // When zooming with the mouse-wheel, the image center is kept fixed.
        d->centerZoomPoint = e->pos();
        if (e->delta() < 0 && !minZoom())
            slotDecreaseZoom();
        else if (e->delta() > 0 && !maxZoom())
            slotIncreaseZoom();
        d->centerZoomPoint = TQPoint();
        return;
    }

    TQScrollView::contentsWheelEvent(e);
}

void PreviewWidget::zoomFactorChanged(double zoom)
{
    emit signalZoomFactorChanged(zoom);
}

}  // NameSpace Digikam
