/****************************************************************************
**
**  indeview_widget.cpp  of the IndeView project, see http://www.indeview.org
**
**
**  The IndeView application is a stand-alone viewer to display presentations
**  created by IndeView creator modules like IndeViewExport.bas or kpr2iv.sh.
**  -------------------------------------------------------------------------
**  This enables you to show OpenOffice/StarOffice or KOffice presentations
**  on machines where the respective presentation program is not available.
**
**
**  Copyright (C) 2003..2004 Michael & Karl-Heinz Zimmer.
**  All rights reserved.
**
**  This file is distributed under the terms of the
**  GNU Lesser General Public License version 2.1 as published by the
**  Free Software Foundation and appearing in the file LICENSE included
**  in the packaging of this file.
**
**  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
**  THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
**  PURPOSE.
**
**  See http://www.indeview.org for further information.
**
**  Contact info@indeview.org if any conditions of this
**  licensing are not clear to you.
**
**
**  Please consider sending
**  a little picture postcard to   Michael & Karl-Heinz Zimmer
**                                 Butzengasse 15
**                                 54343 Föhren
**                                 Germany
**
**  Your feedback will be very much appreciated: we would be glad about
**  receiving a visual representation of where on earth you are living
**  using our little module.
**
*****************************************************************************/

#include <qapplication.h>
#include <qlayout.h>
#include <qvbox.h>
#include <qdialog.h>
#include <qtextedit.h>
#include <qlabel.h>
#include <qpainter.h>
#include <qcursor.h>
#include <qfile.h>
#include <qurl.h>
#include <qptrlist.h>
#include <qvaluelist.h>
#include <qtimer.h>

#include <qmessagebox.h>

#include "indeview_widget.h"
#include "indeview_slide.h"


IndeViewWidget::IndeViewWidget( IndeViewSlides* slides,
                                bool deleteSlidesWhenDone,
                                QWidget *parent, const char *name, int wFlags )
  : QWidget( parent, name, wFlags ),
    mSlides( slides ),
    mDeleteSlidesWhenDone( deleteSlidesWhenDone ),
    mCurSlide( 0 ),
    mBGColor( Qt::black ),
    mOptionDontScale( false ),
    mOptionDontStretch( false ),
    mMinHorVisible( 32),
    mMinVertVisible(20),
    mDX(0),
    mDY(0),
    mZoom(0),
    mScaledPix(QPixmap())
{
  setBackgroundMode( Qt::NoBackground );
  if( mSlides )
    mCurSlide = slides->rootSlide();
}

IndeViewWidget::~IndeViewWidget()
{
  if( mDeleteSlidesWhenDone && mSlides ){
    delete mSlides;
    mSlides = 0;
    mCurSlide = 0;
  }
}

void IndeViewWidget::setSlides( IndeViewSlides* slides,
                                bool deleteSlidesWhenDone,
                                bool resetMove,
                                bool resetZoom )
{
  if( mDeleteSlidesWhenDone && mSlides )
    delete mSlides;
  mSlides = slides;
  mDeleteSlidesWhenDone = deleteSlidesWhenDone;
  resetPresentation( resetMove, resetZoom );
}

void IndeViewWidget::resetPresentation(bool _resetMove, bool _resetZoom)
{
  mCurSlide = mSlides ? mSlides->rootSlide() : 0;
  bool bDummy;
  if( _resetMove )
    resetMove( bDummy );
  if( _resetZoom )
    resetZoom( bDummy );
  update();
}

void IndeViewWidget::processPainterBeforeDrawing( QPainter& )
{
  // implementation left empty intentionally
}

void IndeViewWidget::processPainterAfterDrawing( QPainter& )
{
  // implementation left empty intentionally
}

void IndeViewWidget::setBackgroundColor( const QColor& bgColor )
{
  mBGColor = bgColor;
}

void IndeViewWidget::setMinHorVisible( int minHorVisible )
{
  mMinHorVisible = QMAX( minHorVisible, 0 );
}

void IndeViewWidget::setMinVertVisible( int minVertVisible )
{
  mMinVertVisible = QMAX( minVertVisible, 0 );
}

void IndeViewWidget::setOptionDontScale( bool optionDontScale )
{
  mOptionDontScale = optionDontScale;
}

void IndeViewWidget::setOptionDontStretch( bool optionDontStretch )
{
  mOptionDontStretch = optionDontStretch;
}

void IndeViewWidget::gotoNextSlide( bool& bPageWasChanged )
{
  bPageWasChanged = false;
  if( mCurSlide )
    gotoPage( mCurSlide->pNext, bPageWasChanged );
}

void IndeViewWidget::gotoPrevSlide( bool& bPageWasChanged )
{
  bPageWasChanged = false;
  if( mCurSlide )
    gotoPage( mCurSlide->pPrev, bPageWasChanged );
}

void IndeViewWidget::gotoFirstSlide( bool& bPageWasChanged )
{
  gotoPage( rootSlide(), bPageWasChanged );
}

void IndeViewWidget::gotoLastSlide( bool& bPageWasChanged )
{
  gotoPage( lastSlide(), bPageWasChanged );
}

void IndeViewWidget::gotoSlide( int slideIdx,
                                bool& bPageWasChanged )
{
  bPageWasChanged = false;
  if( 0 <= slideIdx ) {
    const IndeViewSlide* p = rootSlide();
    const IndeViewSlide* oldP = 0;
    int idx = 0;
    while( p && oldP != lastSlide() ){
      if( idx == slideIdx ){
        gotoPage( p, bPageWasChanged );
        break;
      }
      oldP = p;
      p = p->pNext;
      ++idx;
    }
  }
}

const IndeViewSlide* IndeViewWidget::rootSlide()const
{
  return mSlides ? mSlides->rootSlide() : 0;
}
const IndeViewSlide* IndeViewWidget::lastSlide()const
{
  return mSlides ? mSlides->lastSlide() : 0;
}
const IndeViewSlide* IndeViewWidget::curSlide()const
{
  return mCurSlide;
}

QStringList IndeViewWidget::slideNames()const
{
  return mSlides ? mSlides->slideNames() : QStringList();
}

int IndeViewWidget::curSlideIdx()const
{
  const IndeViewSlide* p = rootSlide();
  const IndeViewSlide* oldP = 0;
  int i = 0;
  while( p && oldP != lastSlide() ){
    if( p == curSlide() )
      return i;
    else
      ++i;
    oldP = p;
    p = p->pNext;
  };
  return 0;
}

bool IndeViewWidget::optionDontScale()const
{
  return mOptionDontScale;
}

bool IndeViewWidget::optionDontStretch()const
{
  return mOptionDontStretch;
}

int IndeViewWidget::zoomLevel()const
{
  return mZoom;
}

int IndeViewWidget::movedByX()const
{
  return mDX;
}

int IndeViewWidget::movedByY()const
{
  return mDY;
}


const IndeViewSizeAndFile* findMatchingImage( IndeViewWidget& widget,
                                              bool& scaleToFitSpace,
                                              const IndeViewSlide* slide )
{
  bool bScaleToFitSpace = false;
  const IndeViewSizeAndFile* img = 0;
  if( slide && !slide->images.empty() ){
    // find matching image
    const int width  = widget.size().width();
    const int height = widget.size().height();
    img = &(*slide->images.begin());
    IndeViewImageList::ConstIterator it;
    for ( it = slide->images.begin(); it != slide->images.end(); ++it )
      if(    width  >= (*it).size().width()
          && height >= (*it).size().height()
          && img->size().width()  < (*it).size().width()
          && img->size().height() < (*it).size().height() )
        img = &(*it);

    // determine if the image must be scaled up or down
    const bool imgTooSmall = !widget.optionDontScale() && (img->size().width() < width && img->size().height() < height);
    const bool imgTooLarge = !widget.optionDontScale() && (img->size().width() > width || img->size().height() > height);

    // we only scale (or stretch, resp.) at zoom level 0
    if( 0 == widget.zoomLevel() ){
      // prepare scaling of the image (or of the next larger one, resp.)
      if( imgTooSmall || imgTooLarge ){
        bScaleToFitSpace = true;
      }
      if( imgTooSmall ){
        // we take the next larger one and scale it down
        const IndeViewSizeAndFile* oldImg = img;
        img = 0;
        for ( it = slide->images.begin(); it != slide->images.end(); ++it ){
          if(    (    !img
                   || (    img->size().width()  > (*it).size().width()
                        && img->size().height() > (*it).size().height()
                      )
                 )
              && oldImg->size().width()  < (*it).size().width()
              && oldImg->size().height() < (*it).size().height() ){
            img = &(*it);
          }
        }
        if( 0 == img )
          img = oldImg;
      }
    }else if( 0 < widget.zoomLevel() ){
      // magnification wanted
      const int nZoom = imgTooLarge ? widget.zoomLevel() - 1 : widget.zoomLevel();
      const IndeViewSizeAndFile* oldImg = img;
      for(int iZoom = 0; iZoom < nZoom; ++iZoom ){
        img = 0;
        for ( it = slide->images.begin(); it != slide->images.end(); ++it ){
          if(    (    !img
                   || (    img->size().width()  > (*it).size().width()
                        && img->size().height() > (*it).size().height()
                      )
                 )
              && oldImg->size().width()  < (*it).size().width()
              && oldImg->size().height() < (*it).size().height() )
            img = &(*it);
        }
        oldImg = img;
      }
      if( 0 == img )
        img = oldImg;
    }else if( 0 > widget.zoomLevel() ){
      // minorification wanted
      const int nZoom = imgTooSmall ? widget.zoomLevel() + 1 : widget.zoomLevel();
      const IndeViewSizeAndFile* oldImg = img;
      for(int iZoom = 0; iZoom > nZoom; --iZoom ){
        img = 0;
        for ( it = slide->images.begin(); it != slide->images.end(); ++it ){
          if( (    !img
                || (    img->size().width()  < (*it).size().width()
                     && img->size().height() < (*it).size().height()
                   )
              )
              && oldImg->size().width()  > (*it).size().width()
              && oldImg->size().height() > (*it).size().height() )
            img = &(*it);
        }
        oldImg = img;
      }
      if( 0 == img )
        img = oldImg;
    }
  }
  scaleToFitSpace = bScaleToFitSpace;
  return img;
}


// It may happen that we zoom by less than the asked amount of steps
// if there are no matching image files available
void IndeViewWidget::changeZoom( int askedDelta,
                                 int& realDelta )
{
  const int oldZoom = mZoom;
  if( mCurSlide && askedDelta ){
    int delta = askedDelta;
    mZoom += delta;
    bool bDummy;
    while( delta && !findMatchingImage( *this, bDummy, mCurSlide ) ){
      if( 0 < askedDelta )
        --delta;
      else
        ++delta;
      mZoom = oldZoom + delta;
    }
    if( delta )
      update();
    else
      mZoom = oldZoom;
  }
  realDelta = mZoom - oldZoom;
}

void IndeViewWidget::resetZoom( bool& bWasZoom )
{
  bWasZoom = 0 != mZoom;
  if( bWasZoom ){
    mZoom = 0;
    if( mCurSlide )
      update();
  }
}


int IndeViewWidget::defaultX1( int width )
{
  if( width != size().width() )
    return (size().width() - width) / 2;
  else
    return 0;
}
int IndeViewWidget::defaultX2( int width )
{
  return defaultX1( width ) + width - 1;
}
int IndeViewWidget::defaultY1( int height )
{
  if( height != size().height() )
    return (size().height() - height) / 2;
  else
    return 0;
}
int IndeViewWidget::defaultY2( int height )
{
  return defaultY1( height ) + height - 1;
}

// It may happen that we move by less than the asked amount
// if the image otherwise would become completely invisible.
void IndeViewWidget::moveHorizontal( int askedMove,
                                   int& realMove,
                                   bool doUpdate )
{
  realMove = 0;

  if( mCurSlide ){
    bool bDummy;
    const IndeViewSizeAndFile* img = findMatchingImage( *this, bDummy, mCurSlide );
    if( img ){
      const int oldDX = mDX;
      const int defX1 = defaultX1( img->size().width() );
      const int defX2 = defaultX2( img->size().width() );
      if(    QABS( defX1 + mDX + askedMove )
          <= QABS( askedMove / 3 ) ){
        // left border is magnetic
        if( defX1 )
          mDX = -defX1;
        else
          mDX = 0;
      }else if(    QABS( (size().width() - 1) - (defX2 + mDX + askedMove) )
                <= QABS( askedMove / 3 ) ){
        // right border is magnetic
        if( defX2 != (size().width() - 1) )
          mDX = (size().width() - 1) - defX2;
        else
          mDX = 0;
      }else{
        // just move the picture by the desired amount
        mDX += askedMove;
      }
      // make sure the left border of the image is not at the right of the screen
      if(   defX1 + mDX
          > size().width() - 1 - mMinHorVisible )
        mDX = size().width() - 1 - defX1 - mMinHorVisible;
      // make sure the right border of the image is not at the left of the screen
      if(   defX2 + mDX
          < mMinHorVisible )
        mDX = 0 - defX2 + mMinHorVisible;

      realMove = mDX - oldDX;

      if( doUpdate && oldDX != mDX )
        update();
    }
  }
}
void IndeViewWidget::moveVertical( int askedMove,
                                 int& realMove,
                                 bool doUpdate )
{
  realMove = 0;

  if( mCurSlide ){
    bool bDummy;
    const IndeViewSizeAndFile* img = findMatchingImage( *this, bDummy, mCurSlide );
    if( img ){
      const int oldDY = mDY;
      const int defY1 = defaultY1( img->size().height() );
      const int defY2 = defaultY2( img->size().height() );
      if(    QABS( defY1 + mDY + askedMove )
          <= QABS( askedMove / 3 ) ){
        // top border is magnetic
        if( defY1 )
          mDY = -defY1;
        else
          mDY = 0;
      }else if(    QABS( (size().height() - 1) - (defY2 + mDY + askedMove) )
                <= QABS( askedMove / 3 ) ){
        // bottom border is magnetic
        if( defY2 != (size().height() - 1) )
          mDY = (size().height() - 1) - defY2;
        else
          mDY = 0;
      }else{
        // just move the picture by the desired amount
        mDY += askedMove;
      }
      // make sure the top border of the image is not below the bottom of the screen
      if(   defY1 + mDY
          > size().height() - 1 - mMinVertVisible )
        mDY = size().height() - 1 - defY1 - mMinVertVisible;
      // make sure the bottom border of the image is not above the top of the screen
      if(   defY2 + mDY
          < mMinVertVisible )
        mDY = 0 - defY2 + mMinVertVisible;

      realMove = mDY - oldDY;

      if( doUpdate && oldDY != mDY )
        update();
    }
  }
}

void IndeViewWidget::moveHoriVert( int askedX,
                                 int askedY,
                                 int& realMoveX,
                                 int& realMoveY )
{
  moveHorizontal(askedX, realMoveX, false );
  moveVertical(  askedY, realMoveY, false );
  if( realMoveX || realMoveY )
    update();
}

void IndeViewWidget::resetMove( bool& bWasMove )
{
  bWasMove = mDX || mDY;
  if( bWasMove ){
    mDX = 0;
    mDY = 0;
    if( mCurSlide )
      update();
  }
}

void IndeViewWidget::gotoPage( const IndeViewSlide* slide,
                               bool& bPageWasChanged )
{
  bPageWasChanged = slide && ( !mCurSlide || mCurSlide != slide );
  if( bPageWasChanged ){
    mCurSlide = slide;
    if( mCurSlide )
      update();
  }
}

void IndeViewWidget::preLoadNextImage()
{
  if( mCurSlide ){
    bool bDummy;
    const IndeViewSizeAndFile* img = findMatchingImage( *this, bDummy, mCurSlide->pNext );
    if( img ){
      //qDebug("\npreloading:");
      //qDebug(img->fileName());
      mCurSlide->pNext->getImage( img->size(), true );
    }
  }
}

void IndeViewWidget::paintEvent( QPaintEvent * )
{
  QPixmap* pix = 0;
  bool bScaleToFitSpace = false;
  {
    const IndeViewSizeAndFile* img = findMatchingImage( *this, bScaleToFitSpace, mCurSlide );
    if( img ){
      //qDebug("\nPAINTING:");
      //qDebug(img->fileName());
      pix = mCurSlide->getImage( img->size() );
    }
  }
  if( pix && !pix->isNull() ){
    if( bScaleToFitSpace ){
      const double pw = static_cast < double > ( pix->width() );
      const double ph = static_cast < double > ( pix->height() );
      const double w  = static_cast < double > ( width() );
      const double h  = static_cast < double > ( height() );
      const double factorW = pw/w;
      const double factorH = ph/h;
      QWMatrix matrix;
      if( !mOptionDontStretch &&
          ( 0.9 <= factorW / factorH ) &&
          ( 0.9 <= factorH / factorW )    ){
        // stretching
        matrix.scale( 1/factorW, 1/factorH );
      }else{
        // scaling
        double factor = 1 / QMAX( factorW, factorH );
        matrix.scale( factor, factor );
      }
      mScaledPix = pix->xForm( matrix );
      pix = &mScaledPix;
    }
  }else{
    mScaledPix = INDEVIEWERROR_Xpm;
    pix = &mScaledPix;
  }
  if( pix && !pix->isNull() ){
    QPixmap buffer( width()  + pix->width(),
                    height() + pix->height(),
                    pix->depth() );
    buffer.fill( mBGColor );
    QPainter painterBuffer( &buffer );
    processPainterBeforeDrawing( painterBuffer );
    painterBuffer.drawPixmap( defaultX1( pix->width()  ) + mDX,
                              defaultY1( pix->height() ) + mDY,
                              *pix );
    processPainterAfterDrawing( painterBuffer );
    QPainter painter( this );
    painter.drawPixmap( 0,0, buffer );

    // read next image while the current one is being viewed
    preLoadNextImage();
  }
}
