/*
 *   This file is part of Dianara
 *   Copyright 2012-2019  JanKusanagi JRR <jancoding@gmx.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 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 "imageviewer.h"


ImageViewer::ImageViewer(QString url, QSize imageSize,
                         QString title, QString suggestedFilename,
                         bool isAnimated,
                         QWidget *parent) : QWidget(parent)
{
    if (title.isEmpty())
    {
        title = "<" + tr("Untitled") + ">";
    }
    this->setWindowTitle(tr("Image") + ": " + title + " - Dianara");
    this->setWindowIcon(QIcon::fromTheme("folder-image",
                                         QIcon(":/images/attached-image.png")));
    this->setWindowFlags(Qt::Window);
    this->setMinimumSize(420, 420);

    m_imageUrl = url;

    m_suggestedFilename = suggestedFilename;
    m_imageIsAnimated = isAnimated;

    m_zoomLevel = 1.000;
    m_rotationAngle = 0.000;
    m_fitToWindow = false;

    m_loadingLabel = new QLabel(this);
    m_loadingLabel->setAlignment(Qt::AlignCenter);

    m_graphicsPixmapItem = new QGraphicsPixmapItem(nullptr);
    m_graphicsScene = new QGraphicsScene(this);

    // This label needs to NOT have a parent, to work properly inside the QGraphicsScene
    m_movieLabel = new QLabel(); // Will be deleted in destructor
    m_movieLabel->setCursor(Qt::OpenHandCursor); // To match the static GraphicsPixmapItem

    m_ivGraphicsView = new IvGraphicsView(m_graphicsScene, this);
    m_ivGraphicsView->setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
    connect(m_ivGraphicsView, &IvGraphicsView::zoomableModeRequested,
            this, &ImageViewer::setFullMode);


    // Buttons
    m_saveButton = new QPushButton(QIcon::fromTheme("document-save-as",
                                                    QIcon(":/images/button-save.png")),
                                   tr("&Save As..."),
                                   this);
    m_saveButton->setDisabled(true);
    connect(m_saveButton, &QAbstractButton::clicked,
            this, &ImageViewer::saveImage);


    if (m_imageIsAnimated)
    {
        m_movie = new QMovie(this);

        m_restartButton = new QPushButton(QIcon::fromTheme("view-refresh",
                                                           QIcon(":/images/menu-refresh.png")),
                                          tr("&Restart",
                                             "Restart animation"),
                                          this);
        m_restartButton->setDisabled(true);
        connect(m_restartButton, &QAbstractButton::clicked,
                this, &ImageViewer::restartAnimation);
    }


    m_fitButton = new QPushButton(QIcon::fromTheme("zoom-fit-best"),
                                  tr("Fit",
                                     "As in: fit image to window"),
                                  this);
    connect(m_fitButton, &QAbstractButton::clicked,
            this, &ImageViewer::zoomToFit);


    m_zoomLabel = new QLabel("000%", this);
    m_zoomLabel->setAlignment(Qt::AlignCenter);
    m_zoomLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
    m_zoomLabel->setMinimumWidth(m_zoomLabel->sizeHint().width());
    m_zoomLabel->clear();


    m_fullButton = new QPushButton(QIcon::fromTheme("zoom-original"),
                                   QStringLiteral("100%"),
                                   this);
    connect(m_fullButton, &QAbstractButton::clicked,
            this, &ImageViewer::zoomToFull);


    m_zoomInButton = new QPushButton(QIcon::fromTheme("zoom-in"),
                                     QStringLiteral("+"),
                                     this);
    m_zoomInButton->setShortcut(QKeySequence(Qt::Key_Plus));
    m_zoomInButton->setDisabled(true);
    connect(m_zoomInButton, &QAbstractButton::clicked,
            this, &ImageViewer::zoomIn);
    connect(m_ivGraphicsView, &IvGraphicsView::zoomInRequested,
            m_zoomInButton, &QAbstractButton::click);


    m_zoomOutButton = new QPushButton(QIcon::fromTheme("zoom-out"),
                                      QStringLiteral("-"),
                                      this);
    m_zoomOutButton->setShortcut(QKeySequence(Qt::Key_Minus));
    m_zoomOutButton->setDisabled(true);
    connect(m_zoomOutButton, &QAbstractButton::clicked,
            this, &ImageViewer::zoomOut);
    connect(m_ivGraphicsView, &IvGraphicsView::zoomOutRequested,
            m_zoomOutButton, &QAbstractButton::click);


    m_rotateLeftButton = new QPushButton(QIcon::fromTheme("object-rotate-left",
                                                          QIcon(":/images/button-rotleft.png")),
                                         QString(),
                                         this);
    m_rotateLeftButton->setToolTip(tr("Rotate image to the left",
                                      "RTL: This actually means LEFT, anticlockwise"));
    m_rotateLeftButton->setDisabled(true);
    connect(m_rotateLeftButton, &QAbstractButton::clicked,
            this, &ImageViewer::rotateLeft);

    m_rotateRightButton = new QPushButton(QIcon::fromTheme("object-rotate-right",
                                                           QIcon(":/images/button-rotright.png")),
                                          QString(),
                                          this);
    m_rotateRightButton->setToolTip(tr("Rotate image to the right",
                                       "RTL: This actually means RIGHT, clockwise"));
    m_rotateRightButton->setDisabled(true);
    connect(m_rotateRightButton, &QAbstractButton::clicked,
            this, &ImageViewer::rotateRight);


    m_infoLabel = new QLabel("~    ~    ~    ~    ~    ~    ~", this);
    m_infoLabel->setAlignment(Qt::AlignCenter);


    m_closeButton = new QPushButton(QIcon::fromTheme("window-close",
                                                     QIcon(":/images/button-close.png")),
                                    tr("&Close"),
                                    this);
    connect(m_closeButton, &QAbstractButton::clicked,
            this, &QWidget::close);


    // This needs to be created after the buttons, since it uses info from them
    this->createContextMenu();


    // Layout
    m_fitFullLayout = new QHBoxLayout();
    m_fitFullLayout->setContentsMargins(0, 0, 0, 0);
    m_fitFullLayout->setSpacing(0);
    m_fitFullLayout->addWidget(m_fitButton);
    m_fitFullLayout->addWidget(m_zoomLabel);
    m_fitFullLayout->addWidget(m_fullButton);

    m_buttonsLayout = new QHBoxLayout();
    m_buttonsLayout->addWidget(m_saveButton);
    if (m_imageIsAnimated)
    {
        m_buttonsLayout->addWidget(m_restartButton);
    }
    m_buttonsLayout->addLayout(m_fitFullLayout);
    m_buttonsLayout->addWidget(m_zoomInButton);
    m_buttonsLayout->addWidget(m_zoomOutButton);
    m_buttonsLayout->addWidget(m_rotateLeftButton);
    m_buttonsLayout->addWidget(m_rotateRightButton);
    m_buttonsLayout->addWidget(m_infoLabel, 1);
    m_buttonsLayout->addWidget(m_closeButton);

    m_mainLayout = new QVBoxLayout();
    m_mainLayout->addWidget(m_loadingLabel);
    m_mainLayout->addWidget(m_ivGraphicsView);
    m_mainLayout->addLayout(m_buttonsLayout);
    this->setLayout(m_mainLayout);


    this->updateButtons();

    // For big enough images, "Fit mode" will be automatically enabled by timer
    m_autoFitTimer = new QTimer(this);
    m_autoFitTimer->setSingleShot(true);
    m_autoFitTimer->setInterval(50);
    connect(m_autoFitTimer, &QTimer::timeout,
            this, &ImageViewer::checkAutoFit);


    this->loadImage(m_imageUrl, imageSize);


    // Set initial window size according to image size and desktop (screen) size
    QDesktopWidget desktopWidget;
    int desktopWidth = desktopWidget.availableGeometry().width();
    int desktopHeight = desktopWidget.availableGeometry().height();
    // FIXME: QDesktopWidget::availableGeometry() is deprecated in 5.11:
    //        Use QGuiApplication::screens()
    qDebug() << "Available desktop area: "
             << desktopWidth << "x" << desktopHeight;

    int imageWidth = imageSize.width();
    int imageHeight = imageSize.height();
    // If image is already loaded, use its actual w/h values, just in case
    if (!m_originalPixmap.isNull())
    {
        imageWidth = m_originalPixmap.width();
        imageHeight = m_originalPixmap.height();
    }

    int windowWidth = qMin(imageWidth + 200, // Some margin for the window itself,
                           desktopWidth);    // and extra width for the buttons
    int windowHeight = qMin(imageHeight + 160,
                            desktopHeight);


    // Calculate optimal minimum width for the window,
    // in order for all the buttons to be readable
    int desiredWidth = m_saveButton->sizeHint().width()
                     + m_fitButton->sizeHint().width()
                     + m_zoomLabel->sizeHint().width()
                     + m_fullButton->sizeHint().width()
                     + m_zoomInButton->sizeHint().width()
                     + m_zoomOutButton->sizeHint().width()
                     + m_rotateLeftButton->sizeHint().width()
                     + m_rotateRightButton->sizeHint().width()
                     + m_infoLabel->sizeHint().width()
                     + m_closeButton->sizeHint().width()
                     + (m_buttonsLayout->spacing() * 12); // With a few extra spacings
    if (m_imageIsAnimated)
    {
        desiredWidth += m_restartButton->sizeHint().width()
                        + m_buttonsLayout->spacing();
    }
    qDebug() << "Sum of button sizeHints, minimal optimal width:"
             << desiredWidth;

    if (desiredWidth > windowWidth && desiredWidth < desktopWidth)
    {
        windowWidth = desiredWidth;
    }

    // Resize window to our desired size, bigger than the image if the image is small
    this->resize(windowWidth, windowHeight);


    // Ensure these are disabled until image is actually loaded
    m_fitButton->setDisabled(true);
    m_fullButton->setDisabled(true);
    m_zoomInButton->setDisabled(true);
    m_zoomOutButton->setDisabled(true);

    m_autoFitTimer->start();

    qDebug() << "ImageViewer created";
}


ImageViewer::~ImageViewer()
{
    delete m_movieLabel; // Since it needed to have no parent
    delete m_graphicsPixmapItem;
    qDebug() << "ImageViewer destroyed";
}


/*
 * Context menu entries are connected to the corresponding buttons simulated
 * click, to provide visual feedback.
 *
 * Icons will be taken from them where possible.
 *
 */
void ImageViewer::createContextMenu()
{
    m_saveAction = new QAction(m_saveButton->icon(),
                               tr("Save Image..."),
                               this);
    m_saveAction->setShortcut(QKeySequence("Ctrl+S"));
    m_saveAction->setDisabled(true);
    connect(m_saveAction, &QAction::triggered,
            m_saveButton, &QAbstractButton::animateClick);

    // This one gets icon and name from Fit button initially,
    // and from 100% button when toggled
    m_toggleFitFullAction = new QAction(m_fitButton->icon(),
                                        m_fitButton->text(),
                                        this);
    m_toggleFitFullAction->setShortcut(QKeySequence(Qt::Key_F));
    m_toggleFitFullAction->setDisabled(true);
    connect(m_toggleFitFullAction, &QAction::triggered,
            this, &ImageViewer::onFitFullToggled);
    connect(m_ivGraphicsView, &IvGraphicsView::doubleClicked,
            m_toggleFitFullAction, &QAction::trigger);


    // Name of this one comes from the rotate-left button's tooltip, already created
    m_rotateLeftAction = new QAction(m_rotateLeftButton->icon(),
                                     m_rotateLeftButton->toolTip(),
                                     this);
    m_rotateLeftAction->setShortcut(QKeySequence("Ctrl+Left"));
    m_rotateLeftAction->setDisabled(true);
    connect(m_rotateLeftAction, &QAction::triggered,
            m_rotateLeftButton, &QAbstractButton::animateClick);

    // Name of this action comes from the rotate-right button, already created
    m_rotateRightAction = new QAction(m_rotateRightButton->icon(),
                                      m_rotateRightButton->toolTip(),
                                      this);
    m_rotateRightAction->setShortcut(QKeySequence("Ctrl+Right"));
    m_rotateRightAction->setDisabled(true);
    connect(m_rotateRightAction, &QAction::triggered,
            m_rotateRightButton, &QAbstractButton::animateClick);


    m_closeAction = new QAction(m_closeButton->icon(),
                                tr("Close Viewer"),
                                this);
    m_closeAction->setShortcut(QKeySequence(Qt::Key_Escape));
    connect(m_closeAction, &QAction::triggered,
            m_closeButton, &QAbstractButton::animateClick);


    m_contextMenu = new QMenu(QStringLiteral("*imageViewerMenu*"), this);
    m_contextMenu->addAction(m_saveAction);
    m_contextMenu->addSeparator();
    m_contextMenu->addAction(m_toggleFitFullAction);
    m_contextMenu->addAction(m_rotateLeftAction);
    m_contextMenu->addAction(m_rotateRightAction);
    m_contextMenu->addSeparator();
    m_contextMenu->addAction(m_closeAction);

    // Make the shortcuts work
    this->addAction(m_saveAction);
    this->addAction(m_toggleFitFullAction);
    this->addAction(m_rotateLeftAction);
    this->addAction(m_rotateRightAction);
    this->addAction(m_closeAction);
}


void ImageViewer::loadImage(QString url, QSize expectedSize)
{
    m_originalFileUri = MiscHelpers::getCachedImageFilename(url);

    m_originalPixmap = QPixmap(m_originalFileUri);

    if (m_originalPixmap.isNull())
    {
        m_ivGraphicsView->hide();

        QString expectedResolution = MiscHelpers::resolutionString(expectedSize.width(),
                                                                   expectedSize.height());

        m_loadingLabel->setText("<img src=\":/images/image-loading.png\" />"
                                "<br /><br /><br />"
                                "<big>"
                                + tr("Downloading full image...")
                                + "</big>"
                                  "<br /><br />"
                                  "(" + expectedResolution + ")");
    }
    else
    {
        m_loadingLabel->clear();
        m_loadingLabel->hide();
        m_ivGraphicsView->show();

        QString resolution = MiscHelpers::resolutionString(m_originalPixmap.width(),
                                                           m_originalPixmap.height());

        QString imageDetails = resolution + "  ~  "
                               + MiscHelpers::fileSizeString(m_originalFileUri);

        m_infoLabel->setText(imageDetails);

        if (m_imageIsAnimated)
        {
            m_movie->setFileName(m_originalFileUri);
            m_movie->start();

            m_movieLabel->setMovie(m_movie);
            m_labelProxyWidget = m_graphicsScene->addWidget(m_movieLabel);

            m_restartButton->setEnabled(true);
        }
        else
        {
            m_graphicsPixmapItem->setPixmap(m_originalPixmap);
            m_graphicsScene->addItem(m_graphicsPixmapItem);
        }


        this->drawImage();

        m_saveButton->setEnabled(true);
        m_saveAction->setEnabled(true);

        m_rotateLeftButton->setEnabled(true);
        m_rotateRightButton->setEnabled(true);

        m_rotateLeftAction->setEnabled(true);
        m_rotateRightAction->setEnabled(true);

        m_toggleFitFullAction->setEnabled(true);

        m_autoFitTimer->start();
    }
}



void ImageViewer::drawImage()
{
    m_ivGraphicsView->resetTransform(); // Reset and re-scaled+re-rotated every time
    //// FIXME: This reset causes issues when zooming towards a corner,
    ///         and then zooming towards the opposite corner

    m_ivGraphicsView->rotate(m_rotationAngle);

    if (m_fitToWindow)
    {
        m_zoomLevel = m_ivGraphicsView->getScaleToFit(m_originalPixmap.width(),
                                                      m_originalPixmap.height(),
                                                      m_rotationAngle);
    }
    else
    {
        toggleZoomButtons();
    }


    m_ivGraphicsView->scale(m_zoomLevel, m_zoomLevel);

    m_zoomLabel->setText(QString("%1%").arg(m_zoomLevel * 100, 3, 'f', 0));

    this->updateButtons();
}


void ImageViewer::toggleZoomButtons()
{
    m_zoomInButton->setDisabled(m_zoomLevel >= 5.00);
    // Some rounding issue sometimes makes this fail with <= 0.05, so...
    m_zoomOutButton->setDisabled(m_zoomLevel < 0.06);

    m_fullButton->setDisabled(m_zoomLevel == 1.0);
}


void ImageViewer::updateButtons()
{
    if (m_fitToWindow)
    {
        m_fitButton->setDisabled(true);
        m_fullButton->setEnabled(true);

        m_zoomInButton->setVisible(false);
        m_zoomInButton->setEnabled(false);
        m_zoomOutButton->setVisible(false);
        m_zoomOutButton->setEnabled(false);

        m_toggleFitFullAction->setIcon(m_fullButton->icon());
        m_toggleFitFullAction->setText(m_fullButton->text());
    }
    else
    {
        m_fullButton->setDisabled(true);
        m_fitButton->setEnabled(true);

        m_zoomInButton->setVisible(true);
        m_zoomOutButton->setVisible(true);

        m_toggleFitFullAction->setIcon(m_fitButton->icon());
        m_toggleFitFullAction->setText(m_fitButton->text());

        this->toggleZoomButtons();
    }
}



/****************************************************************************/
/************************************ SLOTS *********************************/
/****************************************************************************/



void ImageViewer::reloadImage(QString url)
{
    if (url == m_imageUrl)
    {
        this->loadImage(url, QSize());
    }
}

void ImageViewer::onImageFailed(QString url)
{
    if (url == m_imageUrl)
    {
        m_loadingLabel->setText("<img src=\":/images/image-missing.png\" />"
                                "<br /><br /><br />"
                                "<big>"
                                + tr("Error downloading image!")
                                + "<br /><br />"
                                + tr("Try again later.")
                                + "</big>");
    }
}


void ImageViewer::saveImage()
{
    bool savedCorrectly;

    QString filename;
    filename = QFileDialog::getSaveFileName(this, tr("Save Image As..."),
                                            QDir::homePath()
                                            + "/" + m_suggestedFilename,
                                            tr("Image files") + " (*.jpg *.png);;"
                                            + tr("All files") + " (*)");

    if (!filename.isEmpty())
    {
        // Save pixmap from original file
        savedCorrectly = QPixmap(m_originalFileUri).save(filename);

        // FIXME: change this to directly copy the unmodified original instead?
        if (!savedCorrectly)
        {
            QMessageBox::warning(this,
                                 tr("Error saving image"),
                                 tr("There was a problem while saving %1.\n\n"
                                    "Filename should end in .jpg "
                                    "or .png extensions.").arg(filename));
        }
    }
}


void ImageViewer::restartAnimation()
{
    if (m_imageIsAnimated)
    {
        m_movie->stop();
        m_movie->start();
    }
}


void ImageViewer::zoomToFit()
{
    m_fitToWindow = true;
    this->drawImage();
}


void ImageViewer::zoomToFull()
{
    m_fitToWindow = false;
    m_zoomLevel = 1.000;     // Reset

    this->drawImage();
}


/*
 * To be called on wheelEvent, instead of zoomToFit, not resetting zoom level
 *
 */
void ImageViewer::setFullMode()
{
    // Only if not already in full mode, to avoid weird effects with wheel events
    if (m_fitToWindow)
    {
        m_fitToWindow = false;
        this->drawImage();
    }
}


void ImageViewer::zoomIn()
{
    m_fitToWindow = false;

    if (m_zoomLevel < 5.0)
    {
        if (m_zoomLevel >= 1.0)
        {
            m_zoomLevel += 0.1;
            if (m_zoomLevel > 5.0)
            {
                m_zoomLevel = 5.0;
            }
        }
        else
        {
            m_zoomLevel += 0.05;
        }

        this->drawImage();
    }
}



void ImageViewer::zoomOut()
{
    m_fitToWindow = false;

    if (m_zoomLevel > 0.05)
    {
        if (m_zoomLevel >= 1.1)
        {
            m_zoomLevel -= 0.1;
        }
        else
        {
            m_zoomLevel -=0.05;
            if (m_zoomLevel < 0.05)
            {
                m_zoomLevel = 0.05;
            }
        }

        this->drawImage();
    }
}


void ImageViewer::checkAutoFit()
{
    if (m_originalPixmap.isNull())
    {
        // Avoid messing with the buttons until the image is loaded
        return;
    }


    zoomToFit();

    if (m_originalPixmap.width() < m_ivGraphicsView->viewport()->width()
     && m_originalPixmap.height() < m_ivGraphicsView->viewport()->height())
    {
        zoomToFull();
    }
}



void ImageViewer::rotateLeft()
{
    m_rotationAngle -= 90.0;
    if (m_rotationAngle == -360.0)
    {
        m_rotationAngle = 0.0;
    }

    this->drawImage();
}


void ImageViewer::rotateRight()
{
    m_rotationAngle += 90.0;
    if (m_rotationAngle == 360.0)
    {
        m_rotationAngle = 0.0;
    }

    this->drawImage();
}


void ImageViewer::onFitFullToggled()
{
    if (m_fitToWindow)
    {
        m_fullButton->animateClick();
    }
    else
    {
        m_fitButton->animateClick();
    }
}



/****************************************************************************/
/********************************** PROTECTED *******************************/
/****************************************************************************/


void ImageViewer::closeEvent(QCloseEvent *event)
{
    qDebug() << "ImageViewer::closeEvent(); hiding and destroying widget!";
    event->ignore();

    this->hide();
    this->deleteLater();
}

void ImageViewer::showEvent(QShowEvent *event)
{
    qDebug() << "ImageViewer::showEvent()";

    if (!m_originalPixmap.isNull())
    {
        this->drawImage();
    }

    event->accept();
}

void ImageViewer::hideEvent(QHideEvent *event)
{
    qDebug() << "ImageViewer::hideEvent()";
    event->accept();
}


void ImageViewer::resizeEvent(QResizeEvent *event)
{
    qDebug() << "ImageViewer::resizeEvent()";

    if (!m_originalPixmap.isNull())
    {
        this->drawImage();
    }

    event->accept();
}



void ImageViewer::contextMenuEvent(QContextMenuEvent *event)
{
    m_contextMenu->exec(event->globalPos());
}
