/*
    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) 2013  <copyright holder> <email>

    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 of the License, 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.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/


#include "imageview.h"
#include <QWheelEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QTimer>
#include <QPolygon>
#include <QStyle>
#include <QLabel>
#include <QGraphicsProxyWidget>
#include <QGraphicsSvgItem>

#define CURSOR_HIDE_DELY 3000

namespace LxImage {

ImageView::ImageView(QWidget* parent):
  QGraphicsView(parent),
  scene_(new GraphicsScene(this)),
  imageItem_(new QGraphicsRectItem()),
  gifMovie_(nullptr),
  cacheTimer_(nullptr),
  cursorTimer_(nullptr),
  scaleFactor_(1.0),
  autoZoomFit_(false),
  isSVG(false) {

  setViewportMargins(0, 0, 0, 0);
  setContentsMargins(0, 0, 0, 0);
  setLineWidth(0);

  setScene(scene_);
  connect(scene_, &GraphicsScene::fileDropped, this, &ImageView::onFileDropped);
  imageItem_->hide();
  imageItem_->setPen(QPen(Qt::NoPen)); // remove the border
  scene_->addItem(imageItem_);
}

ImageView::~ImageView() {
  scene_->clear(); // deletes all items
  if(gifMovie_)
    delete gifMovie_;
  if(cacheTimer_) {
    cacheTimer_->stop();
    delete cacheTimer_;
  }
  if(cursorTimer_) {
    cursorTimer_->stop();
    delete cursorTimer_;
  }
}

void ImageView::onFileDropped(const QString file) {
    Q_EMIT fileDropped(file);
}

void ImageView::wheelEvent(QWheelEvent* event) {
  int delta = event->delta();
  // Ctrl key is pressed
  if(event->modifiers() & Qt::ControlModifier) {
    if(delta > 0) { // forward
      zoomIn();
    }
    else { // backward
      zoomOut();
    }
  }
  else {
    // The default handler QGraphicsView::wheelEvent(event) tries to
    // scroll the view, which is not what we need.
    // Skip the default handler and use its parent QWidget's handler here.
    QWidget::wheelEvent(event);
  }
}

void ImageView::mouseDoubleClickEvent(QMouseEvent* event) {
  // The default behaviour of QGraphicsView::mouseDoubleClickEvent() is
  // not needed for us. We call its parent class instead so the event can be
  // filtered by event filter installed on the view.
  // QGraphicsView::mouseDoubleClickEvent(event);
  QAbstractScrollArea::mouseDoubleClickEvent(event);
}

void ImageView::mousePressEvent(QMouseEvent * event) {
  QGraphicsView::mousePressEvent(event);
  if(cursorTimer_) cursorTimer_->stop();
}

void ImageView::mouseReleaseEvent(QMouseEvent* event) {
  QGraphicsView::mouseReleaseEvent(event);
  if(cursorTimer_) cursorTimer_->start(CURSOR_HIDE_DELY);
}

void ImageView::mouseMoveEvent(QMouseEvent* event) {
  QGraphicsView::mouseMoveEvent(event);
   if(cursorTimer_ && (viewport()->cursor().shape() == Qt::BlankCursor
                       || viewport()->cursor().shape() == Qt::OpenHandCursor)) {
    cursorTimer_->start(CURSOR_HIDE_DELY); // restart timer
    viewport()->setCursor(Qt::OpenHandCursor);
  }
}

void ImageView::focusInEvent(QFocusEvent* event) {
  QGraphicsView::focusInEvent(event);
  if(cursorTimer_ && (viewport()->cursor().shape() == Qt::BlankCursor
                      || viewport()->cursor().shape() == Qt::OpenHandCursor)) {
    cursorTimer_->start(CURSOR_HIDE_DELY); // restart timer
    viewport()->setCursor(Qt::OpenHandCursor);
  }
}

void ImageView::resizeEvent(QResizeEvent* event) {
  QGraphicsView::resizeEvent(event);
  if(autoZoomFit_)
    zoomFit();
}

void ImageView::zoomFit() {
  if(!image_.isNull()) {
    // if the image is smaller than our view, use its original size
    // instead of scaling it up.
    if(image_.width() <= width() && image_.height() <= height()) {
      zoomOriginal();
      return;
    }
  }
  fitInView(scene_->sceneRect(), Qt::KeepAspectRatio);
  scaleFactor_ = transform().m11();
  queueGenerateCache();
}

void ImageView::zoomIn() {
  autoZoomFit_ = false;
  if(!image_.isNull()) {
    resetTransform();
    scaleFactor_ *= 1.1;
    scale(scaleFactor_, scaleFactor_);
    queueGenerateCache();
  }
}

void ImageView::zoomOut() {
  autoZoomFit_ = false;
  if(!image_.isNull()) {
    resetTransform();
    scaleFactor_ /= 1.1;
    scale(scaleFactor_, scaleFactor_);
    queueGenerateCache();
  }
}

void ImageView::zoomOriginal() {
  resetTransform();
  scaleFactor_ = 1.0;
  autoZoomFit_ = false;
  queueGenerateCache();
}

void ImageView::setImage(QImage image, bool show) {
  if(show && (gifMovie_ || isSVG)) { // a gif animation or SVG file was shown before
    scene_->clear();
    isSVG = false;
    if(gifMovie_) { // should be deleted explicitly
      delete gifMovie_;
      gifMovie_ = nullptr;
    }
    // recreate the rect item
    imageItem_ = new QGraphicsRectItem();
    imageItem_->hide();
    imageItem_->setPen(QPen(Qt::NoPen));
    scene_->addItem(imageItem_);
  }

  image_ = image;
  if(image.isNull()) {
    imageItem_->hide();
    imageItem_->setBrush(QBrush());
    scene_->setSceneRect(0, 0, 0, 0);
  }
  else {
    if(show) {
      imageItem_->setRect(0, 0, image_.width(), image_.height());
      imageItem_->setBrush(image_);
      imageItem_->show();
    }
    scene_->setSceneRect(0, 0, image_.width(), image_.height());
  }

  if(autoZoomFit_)
    zoomFit();
  queueGenerateCache();
}

void ImageView::setGifAnimation(QString fileName) {
  /* the built-in gif reader gives the first frame, which won't
     be shown but is used for tracking position and dimensions */
  image_ = QImage(fileName);
  if(image_.isNull()) {
    if(imageItem_) {
      imageItem_->hide();
      imageItem_->setBrush(QBrush());
    }
    scene_->setSceneRect(0, 0, 0, 0);
  }
  else {
    scene_->clear();
    imageItem_ = nullptr; // it's deleted by clear();
    if(gifMovie_) {
      delete gifMovie_;
      gifMovie_ = nullptr;
    }
    QPixmap pix(image_.size());
    pix.fill(Qt::transparent);
    QGraphicsItem *gifItem = new QGraphicsPixmapItem(pix);
    QLabel *gifLabel = new QLabel();
    gifMovie_ = new QMovie(fileName);
    QGraphicsProxyWidget* gifWidget = new QGraphicsProxyWidget(gifItem);
    gifLabel->setAttribute(Qt::WA_NoSystemBackground);
    gifLabel->setMovie(gifMovie_);
    gifWidget->setWidget(gifLabel);
    gifMovie_->start();
    scene_->addItem(gifItem);
    scene_->setSceneRect(gifItem->boundingRect());
  }

  if(autoZoomFit_)
    zoomFit();
  queueGenerateCache(); // deletes the cache timer in this case
}

void ImageView::setSVG(QString fileName) {
  image_ = QImage(fileName); // for tracking position and dimensions
  if(image_.isNull()) {
    if(imageItem_) {
      imageItem_->hide();
      imageItem_->setBrush(QBrush());
    }
    scene_->setSceneRect(0, 0, 0, 0);
  }
  else {
    scene_->clear();
    imageItem_ = nullptr;
    isSVG = true;
    QGraphicsSvgItem *svgItem = new QGraphicsSvgItem(fileName);
    scene_->addItem(svgItem);
    scene_->setSceneRect(svgItem->boundingRect());
  }

  if(autoZoomFit_)
    zoomFit();
  queueGenerateCache(); // deletes the cache timer in this case
}

void ImageView::setScaleFactor(double factor) {
  if(factor != scaleFactor_) {
    scaleFactor_ = factor;
    resetTransform();
    scale(factor, factor);
    queueGenerateCache();
  }
}

void ImageView::paintEvent(QPaintEvent* event) {
  // if the image is scaled and we have a high quality cached image
  if(imageItem_ && scaleFactor_ != 1.0 && !cachedPixmap_.isNull()) {
    // rectangle of the whole image in viewport coordinate
    QRect viewportImageRect = sceneToViewport(imageItem_->rect());
    // the visible part of the image.
    QRect desiredCachedRect = viewportToScene(viewportImageRect.intersected(viewport()->rect()));
    // check if the cached area is what we need and if the cache is out of date
    if(cachedSceneRect_ == desiredCachedRect) {
      // rect of the image area that needs repaint, in viewport coordinate
      QRect repaintImageRect = viewportImageRect.intersected(event->rect());
      // see if the part asking for repaint is contained by our cache.
      if(cachedRect_.contains(repaintImageRect)) {
        QPainter painter(viewport());
        painter.fillRect(event->rect(), backgroundBrush());
        painter.drawPixmap(repaintImageRect, cachedPixmap_);
        return;
      }
    }
  }
  if(!image_.isNull()) { // we don't have a cache yet or it's out of date already, generate one
    queueGenerateCache();
  }
  QGraphicsView::paintEvent(event);
}

void ImageView::queueGenerateCache() {
  if(!cachedPixmap_.isNull()) // clear the old pixmap if there's any
    cachedPixmap_ = QPixmap();

  // we don't need to cache the scaled image if its the same as the original image (scale:1.0)
  // no cache for gif animations or SVG images either
  if(scaleFactor_ == 1.0 || gifMovie_ || isSVG) {
    if(cacheTimer_) {
      cacheTimer_->stop();
      delete cacheTimer_;
      cacheTimer_ = nullptr;
    }
    return;
  }

  if(!cacheTimer_) {
    cacheTimer_ = new QTimer();
    cacheTimer_->setSingleShot(true);
    connect(cacheTimer_, &QTimer::timeout, this, &ImageView::generateCache);
  }
  if(cacheTimer_)
    cacheTimer_->start(200); // restart the timer
}

// really generate the cache
void ImageView::generateCache() {
  // disable the one-shot timer
  cacheTimer_->deleteLater();
  cacheTimer_ = nullptr;

  if(!imageItem_ || image_.isNull()) return;

  // generate a cache for "the visible part" of the scaled image
  // rectangle of the whole image in viewport coordinate
  QRect viewportImageRect = sceneToViewport(imageItem_->rect());
  // rect of the image area that's visible in the viewport (in viewport coordinate)
  cachedRect_ = viewportImageRect.intersected(viewport()->rect());

  // convert to the coordinate of the original image
  cachedSceneRect_ = viewportToScene(cachedRect_);
  // create a sub image of the visible without real data copy
  // Reference: http://stackoverflow.com/questions/12681554/dividing-qimage-to-smaller-pieces
  QRect subRect = image_.rect().intersected(cachedSceneRect_);
  const uchar* bits = image_.constBits();
  unsigned int offset = subRect.x() * image_.depth() / 8 + subRect.y() * image_.bytesPerLine();
  QImage subImage = QImage(bits + offset, subRect.width(), subRect.height(), image_.bytesPerLine(), image_.format());

  // If the original image has a color table, also use it for the subImage
  QVector<QRgb> colorTable = image_.colorTable();
  if (!colorTable.empty())
    subImage.setColorTable(colorTable);

  // QImage scaled = subImage.scaled(subRect.width() * scaleFactor_, subRect.height() * scaleFactor_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
  QImage scaled = subImage.scaled(cachedRect_.width(), cachedRect_.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);

  // convert the cached scaled image to pixmap
  cachedPixmap_ = QPixmap::fromImage(scaled);
  viewport()->update();
}

// convert viewport coordinate to the original image (not scaled).
QRect ImageView::viewportToScene(const QRect& rect) {
  // QPolygon poly = mapToScene(imageItem_->rect());
  QPoint topLeft = mapToScene(rect.topLeft()).toPoint();
  QPoint bottomRight = mapToScene(rect.bottomRight()).toPoint();
  return QRect(topLeft, bottomRight);
}

QRect ImageView::sceneToViewport(const QRectF& rect) {
  QPoint topLeft = mapFromScene(rect.topLeft());
  QPoint bottomRight = mapFromScene(rect.bottomRight());
  return QRect(topLeft, bottomRight);
}

void ImageView::blankCursor() {
    viewport()->setCursor(Qt::BlankCursor);
}

void ImageView::hideCursor(bool enable) {
  if(enable) {
    if(cursorTimer_) delete cursorTimer_;
    cursorTimer_ = new QTimer(this);
    cursorTimer_->setSingleShot(true);
    connect(cursorTimer_, &QTimer::timeout, this, &ImageView::blankCursor);
    if(viewport()->cursor().shape() == Qt::OpenHandCursor)
        cursorTimer_->start(CURSOR_HIDE_DELY);
  }
  else if (cursorTimer_) {
    cursorTimer_->stop();
    delete cursorTimer_;
    cursorTimer_ = nullptr;
    if(viewport()->cursor().shape() == Qt::BlankCursor)
        viewport()->setCursor(Qt::OpenHandCursor);
  }
}

} // namespace LxImage
