/* ============================================================
 *
 * This file is a part of kipi-plugins project
 * http://www.kipi-plugins.org
 *
 * Date        : 2007-11-14
 * Description : a kipi plugin to slide images.
 *
 * Copyright (C) 2007 by Valerio Fuoglio <valerio dot fuoglio at gmail dot com>
 *
 * Parts of this code are based on smoothslidesaver by Carsten Weinhold 
 * <carsten dot weinhold at gmx dot de> and slideshowgl.{cpp|h} by Renchi Raju     
 * <renchi@pooh.tam.uiuc.edu>                                           
 *
 * 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 <cassert>
#include <cmath>

// TQt includes.

#include <tqapplication.h>
#include <tqimage.h>
#include <tqdatetime.h>
#include <tqpainter.h>
#include <tqobject.h>
#include <tqfont.h>
#include <tqcursor.h>

// KDE includes.

#include <tdeconfig.h>
#include <tdeglobal.h>
#include <tdelocale.h>
#include <tdeversion.h>
#include <tdeglobalsettings.h>

// Local includes.

#include "slideshowkb.h"
#include "imageloadthread.h"
#include "slideshowkb.moc"

namespace KIPISlideShowPlugin
{
  // -------------------------------------------------------------------------

  ViewTrans::ViewTrans(bool zoomIn, float relAspect) {

      int i;

      // randomly select sizes of start end end viewport
      double s[2];
      i = 0;
      do {
          s[0]  = 0.3 * rnd() + 1.0;
          s[1]  = 0.3 * rnd() + 1.0;        
      } while (fabs(s[0] - s[1]) < 0.15 && ++i < 10);

      if (zoomIn xor s[0] > s[1]) {
          double tmp = s[0];
          s[0]       = s[1];
          s[1]       = tmp;
      }

      m_deltaScale = s[1] / s[0] - 1.0;
      m_baseScale  = s[0];

      // additional scale factors to ensure proper m_aspect of the displayed image
      double x[2], y[2], xMargin[2], yMargin[2], bestDist;
      double sx, sy;
      if (relAspect > 1.0) {
          sx = 1.0;
          sy = relAspect;
      } else {
          sx = 1.0 / relAspect;
          sy = 1.0;
      }
      m_xScale = sx;
      m_yScale = sy;

      // calculate path
      xMargin[0] = (s[0] * sx - 1.0) / 2.0;
      yMargin[0] = (s[0] * sy - 1.0) / 2.0;
      xMargin[1] = (s[1] * sx - 1.0) / 2.0;
      yMargin[1] = (s[1] * sy - 1.0) / 2.0;

      i = 0;
      bestDist = 0.0;
      do {
          double sign = rndSign();
          x[0] = xMargin[0] * (0.2 * rnd() + 0.8) *  sign;
          y[0] = yMargin[0] * (0.2 * rnd() + 0.8) * -sign;
          x[1] = xMargin[1] * (0.2 * rnd() + 0.8) * -sign;
          y[1] = yMargin[1] * (0.2 * rnd() + 0.8) *  sign;

          if (fabs(x[1] - x[0]) + fabs(y[1] - y[0]) > bestDist) {
              m_baseX  = x[0];
              m_baseY  = y[0];
              m_deltaX = x[1] - x[0];
              m_deltaY = y[1] - y[0];
              bestDist = fabs(m_deltaX) + fabs(m_deltaY);
          }

      } while (bestDist < 0.3 && ++i < 10);
  }

  // -------------------------------------------------------------------------

  Image::Image(ViewTrans *viewTrans, float aspect) {

      this->m_viewTrans = viewTrans;
      this->m_aspect    = aspect;
      this->m_pos       = 0.0;
      this->m_opacity   = 0.0;
      this->m_paint     = (m_viewTrans) ? true : false;    
      this->m_texture   = 0;
  }

  Image::~Image() {

      delete m_viewTrans;
      if (glIsTexture(m_texture))
          glDeleteTextures(1, &m_texture);
  }

  // -------------------------------------------------------------------------
  SlideShowKB::SlideShowKB(const TQValueList<TQPair<TQString, int> >& fileList,
                           const TQStringList& commentsList, bool ImagesHasComments)
  : TQGLWidget(0, 0, 0, WStyle_StaysOnTop | WType_Popup |
      WX11BypassWM | WDestructiveClose)
  {
  #if KDE_IS_VERSION(3,2,0)
      TQRect deskRect = TDEGlobalSettings::desktopGeometry(this);
      m_deskX      = deskRect.x();
      m_deskY      = deskRect.y();
      m_deskWidth  = deskRect.width();
      m_deskHeight = deskRect.height();
  #else
      TQRect deskRect = TQApplication::desktop()->screenGeometry(this);
      m_deskX      = deskRect.x();
      m_deskY      = deskRect.y();
      m_deskWidth  = deskRect.width();
      m_deskHeight = deskRect.height();
  #endif

      move(m_deskX, m_deskY);
      resize(m_deskWidth, m_deskHeight);

      // =======================================================
      // Avoid boring compile time "unused parameter" warning :P
      // These parameters could be useful for future implementations
      m_commentsList = commentsList;
      m_imagesHasComments = ImagesHasComments;
      // =======================================================

      srand(TQTime::currentTime().msec());

      m_config = new TDEConfig("kipirc");
      m_config->setGroup("SlideShow Settings");
      readSettings();

      m_screen = new ScreenProperties(this);
      m_screen->enableVSync();

      unsigned frameRate;
      if (m_forceFrameRate == 0)
          frameRate = m_screen->suggestFrameRate() * 2;
      else
          frameRate = m_forceFrameRate;

      m_image[0]    = new Image(0);
      m_image[1]    = new Image(0);
      m_effect      = 0;
      m_step        = 1.0 / ((float) (m_delay * frameRate));
      m_zoomIn      = rand() < RAND_MAX / 2;
      m_initialized = false;
      m_haveImages  = true;

      TQValueList<TQPair<TQString, int> > m_fileList = fileList;

      m_imageLoadThread = new ImageLoadThread(m_fileList, width(), height());
      m_timer           = new TQTimer(this);

      m_endOfShow = false;
      m_showingEnd = false;

      connect(m_timer, TQT_SIGNAL(timeout(void)), this, TQT_SLOT(moveSlot()));
      connect(m_imageLoadThread, TQT_SIGNAL(endOfShow(void)), this, TQT_SLOT(slotEndOfShow()));

      // -- hide cursor when not moved --------------------

      m_mouseMoveTimer = new TQTimer;
      connect(m_mouseMoveTimer, TQT_SIGNAL(timeout()),
              TQT_SLOT(slotMouseMoveTimeOut()));

      setMouseTracking(true);
      slotMouseMoveTimeOut();

      m_imageLoadThread->start();
      m_timer->start(1000 / frameRate);
  }
 
  SlideShowKB::~SlideShowKB() {

      delete m_effect;
      delete m_image[0];
      delete m_image[1];

      m_imageLoadThread->quit();
      bool terminated = m_imageLoadThread->wait(10000);

      if ( !terminated) {
          m_imageLoadThread->terminate();
          terminated = m_imageLoadThread->wait(3000);
      }

      if (terminated)
          delete m_imageLoadThread;

      delete m_mouseMoveTimer;
      delete m_timer;
      delete m_screen;
  }


  void SlideShowKB::setNewKBEffect() {

      KBEffect::Type type;
      bool needFadeIn = (m_effect == 0 || m_effect->type() == KBEffect::Fade);

      // we currently only have two effects
      if (m_disableFadeInOut)
          type = KBEffect::Blend;
      else if (m_disableCrossFade)
          type = KBEffect::Fade;
      else
          type = KBEffect::chooseKBEffect((m_effect) ? m_effect->type() : KBEffect::Fade);

      delete m_effect;
      switch (type) {
          case KBEffect::Fade:
              m_effect = new FadeKBEffect(this, needFadeIn);
              break;
          case KBEffect::Blend:
              m_effect = new BlendKBEffect(this, needFadeIn);
              break;
          default:
              tqDebug("Unknown transition effect, falling back to crossfade");
              m_effect = new BlendKBEffect(this, needFadeIn);
      }
  }


  void SlideShowKB::moveSlot() {

      if (m_initialized) {
 
          if (m_effect->done()) {
            setNewKBEffect();
            m_imageLoadThread->requestNewImage();
          }
          m_effect->advanceTime(m_step);
      }

      updateGL();
  }


  bool SlideShowKB::setupNewImage(int idx) {

      assert(idx >= 0 && idx < 2);

      if ( !m_haveImages)
          return false;

      bool ok = false;
      m_zoomIn  = !m_zoomIn;

      if (m_imageLoadThread->grabImage()) {

          delete m_image[idx];

          // we have the image lock and there is an image
          float imageAspect    = m_imageLoadThread->imageAspect();
          ViewTrans *viewTrans = new ViewTrans(m_zoomIn, aspect() / imageAspect);
          m_image[idx]           = new Image(viewTrans, imageAspect);

          applyTexture(m_image[idx], m_imageLoadThread->image());
          ok = true;

      } 
      else {
          m_haveImages = false;
      }

      // don't forget to release the lock on the copy of the image
      // owned by the image loader thread
      m_imageLoadThread->ungrabImage();

      return ok;
  }

  void SlideShowKB::startSlideShowOnce() {
      // when the image loader thread is ready, it will already have loaded
      // the first image
      if (m_initialized == false && m_imageLoadThread->ready()) {

          setupNewImage(0);                   // setup the first image and
          m_imageLoadThread->requestNewImage(); // load the next one in background
          setNewKBEffect();                     // set the initial effect

          m_initialized = true;
      }
  }


  void SlideShowKB::swapImages() {

      Image *tmp = m_image[0];
      m_image[0] = m_image[1];
      m_image[1] = tmp;
  }


  void SlideShowKB::initializeGL() {

    // Enable Texture Mapping
    glEnable(GL_TEXTURE_2D);

    // Clear The Background Color
    glClearColor(0.0, 0.0, 0.0, 1.0f);

    glEnable (GL_TEXTURE_2D);
    glShadeModel (GL_SMOOTH);

    // Turn Blending On
    glEnable(GL_BLEND);
    // Blending Function For Translucency Based On Source Alpha Value
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // Enable perspective vision
    glClearDepth(1.0f);
  }


  void SlideShowKB::paintGL() {

    startSlideShowOnce();

    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);

    // only clear the color buffer, if none of the active images is fully opaque
    if ( !((m_image[0]->m_paint && m_image[0]->m_opacity == 1.0) ||
          (m_image[1]->m_paint && m_image[1]->m_opacity == 1.0)) )
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glLoadIdentity();

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    if (m_endOfShow && m_image[0]->m_paint && m_image[1]->m_paint)
    {
      endOfShow();
      m_timer->stop();
    }
    else
    {
      if (m_image[1]->m_paint)
          paintTexture(m_image[1]);
      if (m_image[0]->m_paint)
          paintTexture(m_image[0]);
    }
    glFlush();
  }


  void SlideShowKB::resizeGL(int w, int h) {
      glViewport(0, 0, (GLint) w, (GLint) h);
  }



  void SlideShowKB::applyTexture(Image *img, const TQImage &texture) {

      /* create the texture */
      glGenTextures(1, &img->m_texture);
      glBindTexture(GL_TEXTURE_2D, img->m_texture);

      /* actually generate the texture */
      glTexImage2D(GL_TEXTURE_2D, 0, 3, texture.width(), texture.height(), 0,
                  GL_RGBA, GL_UNSIGNED_BYTE, texture.bits());
      /* enable linear filtering  */
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
  }


  void SlideShowKB::paintTexture(Image *img) {

      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();

      float sx = img->m_viewTrans->m_xScaleCorrect();
      float sy = img->m_viewTrans->m_yScaleCorrect();
      
      glTranslatef(img->m_viewTrans->transX(img->m_pos) * 2.0,
                  img->m_viewTrans->transY(img->m_pos) * 2.0, 0.0);
      glScalef(img->m_viewTrans->scale(img->m_pos),
              img->m_viewTrans->scale(img->m_pos), 0.0);
      
      GLuint& tex = img->m_texture;
  
      glBindTexture(GL_TEXTURE_2D, tex);
      glBegin(GL_QUADS);
      {
          glColor4f(1.0, 1.0, 1.0, img->m_opacity);
          glTexCoord2f(0, 0);
          glVertex3f(-sx, -sy, 0);
  
          glTexCoord2f(1, 0);
          glVertex3f(sx, -sy, 0);
  
          glTexCoord2f(1, 1);
          glVertex3f(sx, sy, 0);
  
          glTexCoord2f(0, 1);
          glVertex3f(-sx, sy, 0);
      }
      glEnd();
     
  }
  

  void SlideShowKB::readSettings() {
      m_delay            = m_config->readUnsignedNumEntry("Delay", 8000)/1000;
      m_disableFadeInOut = m_config->readBoolEntry("KB Disable FadeInOut", false);
      m_disableCrossFade = m_config->readBoolEntry("KB Disable Crossfade", false);
      m_forceFrameRate   = m_config->readUnsignedNumEntry("KB Force Framerate", 0);
  
      if (m_delay < 5)  m_delay = 5;
//       if (m_delay > 20) m_delay = 20;
      if (m_forceFrameRate > 120) m_forceFrameRate = 120;
  }
  
  void SlideShowKB::endOfShow() 
  {
    TQPixmap pix(512,512);
    pix.fill(TQt::black);

    TQFont fn(font());
    fn.setPointSize(fn.pointSize()+10);
    fn.setBold(true);

    TQPainter p(&pix);
    p.setPen(TQt::white);
    p.setFont(fn);
    p.drawText(20, 50, i18n("SlideShow Completed."));
    p.drawText(20, 100, i18n("Click To Exit..."));
    p.end();

    TQImage image(pix.convertToImage());
    TQImage t = convertToGLFormat(image);

    GLuint tex;

    /* create the texture */
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_2D, tex);

    /* actually generate the texture */
    glTexImage2D( GL_TEXTURE_2D, 0, 3, t.width(), t.height(), 0,
                  GL_RGBA, GL_UNSIGNED_BYTE, t.bits() );
    /* enable linear filtering  */
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

    /* paint the texture */

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glBindTexture(GL_TEXTURE_2D, tex);
    glBegin(GL_QUADS);
    {
      glColor4f(1.0, 1.0, 1.0, 1.0);
      glTexCoord2f(0, 0);
      glVertex3f(-1.0, -1.0, 0);

      glTexCoord2f(1, 0);
      glVertex3f(1.0, -1.0, 0);

      glTexCoord2f(1, 1);
      glVertex3f(1.0, 1.0, 0);

      glTexCoord2f(0, 1);
      glVertex3f(-1.0, 1.0, 0);
    }
    glEnd();

    m_showingEnd = true;
  }

  TQStringList SlideShowKB::effectNames()
  {
    TQStringList effects;

    effects.append("Ken Burns");
    return effects;
  }

  TQMap<TQString,TQString> SlideShowKB::effectNamesI18N()
  {
    TQMap<TQString,TQString> effects;

    effects["Ken Burns"]    = i18n("Ken Burns");
    
    return effects;
  }
  
  void SlideShowKB::mousePressEvent(TQMouseEvent *e)
  {
    // =======================================================
    // Avoid boring compile time "unused parameter" warning :P
    // This parameter could be useful for future implementations
    if ( !e ) { /* TODO */ }
    // =======================================================
    
    if (m_endOfShow && m_showingEnd)
      slotClose();
  }
  
  void SlideShowKB::mouseMoveEvent(TQMouseEvent *e)
  {
    setCursor(TQCursor(TQt::ArrowCursor));
    m_mouseMoveTimer->start(1000, true);
    
    TQPoint pos(e->pos());
  }
  
  void SlideShowKB::slotEndOfShow()
  {
    m_endOfShow = true;    
  }
  
  void SlideShowKB::slotMouseMoveTimeOut()
  {
    TQPoint pos(TQCursor::pos());
    if ((pos.y() < (m_deskY+20)) ||
         (pos.y() > (m_deskY+m_deskHeight-20-1)))
      return;

    setCursor(TQCursor(TQt::BlankCursor));
  }
  
  void SlideShowKB::slotClose()
  {
    close();    
  }

}  // NameSpace KIPISlideShowPlugin
