//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//=========================================================================

#include "qtProgressDialog.h"
#include "ui_qtProgressDialog.h"

#include <QApplication>
#include <QBuffer>
#include <QCommonStyle>
#include <QCoreApplication>
#include <QMutex>
#include <QProgressDialog>

// minimal constructor
qtProgressDialog::qtProgressDialog(QWidget* parent)
  : QDialog(parent)
  , ui(new Ui::qtProgressDialog)
{
  this->initialize();
}

// constructor
qtProgressDialog::qtProgressDialog(
  QWidget* parent,
  uint minProgress,
  uint maxProgress,
  QString windowTitle)
  : QDialog(parent)
  , ui(new Ui::qtProgressDialog)
{
  this->initialize();

  if (!windowTitle.isEmpty())
  {
    this->setWindowTitle(windowTitle);
  }

  this->ui->progressBar->setMinimum(minProgress);
  this->ui->progressBar->setMaximum(maxProgress);
  m_maxProgress = maxProgress;
}

namespace
{
QPixmap grayedOut(QPixmap input)
{
  QImage im = input.toImage().convertToFormat(QImage::Format_ARGB32);
  for (int y = 0; y < im.height(); ++y)
  {
    QRgb* scanLine = (QRgb*)im.scanLine(y);
    for (int x = 0; x < im.width(); ++x)
    {
      QRgb pixel = *scanLine;
      uint ci = uint(qGray(pixel));
      *scanLine = qRgba(ci, ci, ci, qAlpha(pixel) / 3);
      ++scanLine;
    }
  }
  return QPixmap::fromImage(im);
}

QString toBase64(const QIcon& icon, const QSize& size)
{
  QCommonStyle style;
  QPixmap pixmap = icon.pixmap(size);
  QImage image(pixmap.toImage());
  QByteArray byteArray;
  QBuffer buffer(&byteArray);
  image.save(&buffer, "PNG");
  return QString::fromLatin1(byteArray.toBase64().data());
}
} // namespace

// common initialization
void qtProgressDialog::initialize()
{
  this->ui->setupUi(this);

  this->setWindowTitle("Progress");

  // hide the question mark in the Window Title Bar
  this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);

  m_bAutoClose = false;
  m_bAutoDisableCancelButton = true;
  m_startTime = QTime();
  m_autoCloseDelay = 0;
  m_bShowMessageIcons = true;
  m_autoCloseTimer = nullptr;

  m_pMessageMutex = new QMutex();

  this->setMinimumSize(300, 260);

  // setup the normal & grayed out versions of the filter icons
  QCommonStyle style;
  QSize pixmapSize(48, 48);
  m_infoPixmap = style.standardIcon(QStyle::SP_MessageBoxInformation).pixmap(pixmapSize);
  m_warningPixmap = style.standardIcon(QStyle::SP_MessageBoxWarning).pixmap(pixmapSize);
  m_errorPixmap = style.standardIcon(QStyle::SP_MessageBoxCritical).pixmap(pixmapSize);
  m_noInfoPixmap = grayedOut(m_infoPixmap);
  m_noWarningPixmap = grayedOut(m_warningPixmap);
  m_noErrorPixmap = grayedOut(m_errorPixmap);
  this->ui->pushButton_Info->setIcon(QIcon(m_infoPixmap));
  this->ui->pushButton_Warnings->setIcon(QIcon(m_warningPixmap));
  this->ui->pushButton_Errors->setIcon(QIcon(m_errorPixmap));

  // setup the inline comment icons
  int fontSize = ui->textBrowser->font().pointSize();
  QSize pixmapSize2(fontSize * 1.25, fontSize * 1.25);
  QString infoBase64 = toBase64(style.standardIcon(QStyle::SP_MessageBoxInformation), pixmapSize2);
  QString warningBase64 = toBase64(style.standardIcon(QStyle::SP_MessageBoxWarning), pixmapSize2);
  QString errorBase64 = toBase64(style.standardIcon(QStyle::SP_MessageBoxCritical), pixmapSize2);
  m_infoIconHTML = QString("<img src=\"data:image/png;base64,%1\" />").arg(infoBase64);
  m_warningIconHTML = QString("<img src=\"data:image/png;base64,%1\" />").arg(warningBase64);
  m_errorIconHTML = QString("<img src=\"data:image/png;base64,%1\" />").arg(errorBase64);
}

// destructor
qtProgressDialog::~qtProgressDialog()
{
  delete ui;
}

// sets the label text above the progress bar
void qtProgressDialog::setLabelText(QString text)
{
  this->ui->label_progress->setText(text);
}

void qtProgressDialog::showEvent(QShowEvent* event)
{
  QRect parentRect(parentWidget()->mapToGlobal(QPoint(0, 0)), parentWidget()->size());
  this->move(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), parentRect).topLeft());

  QWidget::showEvent(event);
}

// updates the progress bar with the current progress value
void qtProgressDialog::setValue(int progressValue)
{
  if (m_startTime.isNull())
  {
    m_startTime = QTime::currentTime();
  }

  this->ui->progressBar->setValue((int)progressValue);

  if (progressValue == m_maxProgress)
  {
    if (m_bAutoDisableCancelButton)
    {
      this->ui->pushButton_Cancel->setEnabled(false);
    }
    if (m_bAutoClose)
    {
      this->progressFinished();
    }
  }
}

// triggers mechanisms associated with progress bar completion
void qtProgressDialog::progressFinished()
{
  if (m_bAutoDisableCancelButton)
  {
    this->ui->pushButton_Cancel->setEnabled(false);
  }

  // don't auto-close if m_bAutoClose isn't currently selected
  if (!m_bAutoClose)
  {
    return;
  }

  // now, start the auto-close process
  int elapsedTime = m_startTime.secsTo(QTime::currentTime());
  int remainingMinDuration = m_minDuration - elapsedTime;

  m_functionalAutoCloseDelay = m_autoCloseDelay;
  if (remainingMinDuration > (int)m_functionalAutoCloseDelay)
  {
    m_functionalAutoCloseDelay = remainingMinDuration;
  }

  this->processAutoClose();
}

// matching QProgressBar function calls
void qtProgressDialog::setRange(int minProgress, int maxProgress)
{
  this->ui->progressBar->setRange(minProgress, maxProgress);
  m_maxProgress = maxProgress;
}

// matching QProgressBar function calls
void qtProgressDialog::setMinimum(int minProgress)
{
  this->ui->progressBar->setMinimum(minProgress);
}

// matching QProgressBar function calls
void qtProgressDialog::setMaximum(int maxProgress)
{
  this->ui->progressBar->setMaximum(maxProgress);
  m_maxProgress = maxProgress;
}

// show/hide the Cancel button
void qtProgressDialog::setCancelButtonVisible(bool bVisible)
{
  this->ui->pushButton_Cancel->setVisible(bVisible);
}
// controls auto-disabling of Cancel button
void qtProgressDialog::setAutoDisableCancelButton(bool bAutoDisable)
{
  m_bAutoDisableCancelButton = bAutoDisable;
}

// sets the dialog to automatically close upon progress bar completion
void qtProgressDialog::setAutoClose(bool bAutoClose)
{
  m_bAutoClose = bAutoClose;
}

// when auto-close is in use, this forces the dialog to stay open for a minimum amount of time, even if it is already finished
void qtProgressDialog::setMinDuration(uint seconds)
{
  m_minDuration = seconds;
}

// imposes a delay after the completion of a run, before auto-close is triggered
void qtProgressDialog::setAutoCloseDelay(uint delay)
{
  m_autoCloseDelay = delay;
}

// show/hide the Progress Message Box
void qtProgressDialog::setMessageBoxVisible(bool bBoxVisible, bool bFiltersVisible)
{
  // don't allow the filters to be visible, if the Message Box is not visible
  bFiltersVisible &= bBoxVisible;

  this->ui->line->setVisible(bBoxVisible);
  this->ui->textBrowser->setVisible(bBoxVisible);
  this->ui->label_Filter->setVisible(bBoxVisible);
  this->ui->pushButton_Info->setVisible(bFiltersVisible);
  this->ui->pushButton_Warnings->setVisible(bFiltersVisible);
  this->ui->pushButton_Errors->setVisible(bFiltersVisible);
  if (bFiltersVisible)
  {
    this->ui->label_Filter->setText("Filters");
  }
  else
  {
    this->ui->label_Filter->setText("Messages");
  }

  int width = 100;
  int height = 100;
  if (bBoxVisible)
  {
    width = 500;
    height = 400;
    this->setMinimumHeight(260);
  }
  else
  {
    width = 400;
    height = 150;
    this->setMinimumHeight(130);
  }

  // resize the window, and center on the parent window
  int x = parentWidget()->pos().x() + parentWidget()->width() / 2 - width / 2;
  int y = parentWidget()->pos().y() + parentWidget()->height() / 2 - height / 2;
  this->setGeometry(x, y, width, height);
}

// enable/disable word wrap in the Progress Message Box
void qtProgressDialog::setMessageBoxWordWrap(bool bWrap)
{
  if (bWrap)
  {
    this->ui->textBrowser->setLineWrapMode(QTextEdit::WidgetWidth);
  }
  else
  {
    this->ui->textBrowser->setLineWrapMode(QTextEdit::NoWrap);
  }
}

void qtProgressDialog::setShowMessageIcons(bool bShow)
{
  m_bShowMessageIcons = bShow;
}

// set both the progress value, and a progress message
void qtProgressDialog::setProgress(
  uint progressValue,
  QString progressMessage,
  MessageType messageType)
{
  this->setValue(progressValue);
  this->setProgressText(progressMessage, messageType);
}

void qtProgressDialog::setInfoText(QString progressMessage)
{
  this->setProgressText(progressMessage, MessageType::Info);
}

void qtProgressDialog::setWarningText(QString progressMessage)
{
  this->setProgressText(progressMessage, MessageType::Warning);
}

void qtProgressDialog::setErrorText(QString progressMessage)
{
  this->setProgressText(progressMessage, MessageType::Error);
}

// add a progress message to the Progress Message Box
void qtProgressDialog::setProgressText(QString progressMessage, MessageType messageType)
{
  // if the mutex is not available, queue the message for later processing
  if (!m_pMessageMutex->tryLock(0))
  {
    if (progressMessage != "")
    {
      m_messageQueue.push_back(QPair<QString, MessageType>(progressMessage, messageType));
    }
    QTimer::singleShot(5, this, SLOT(setProgressText()));
    return;
  }

  // this assigns the input variables when this function is run by the mutex timer
  if (progressMessage == "")
  {
    progressMessage = m_messageQueue[0].first;
    messageType = m_messageQueue[0].second;
    m_messageQueue.pop_front();
  }

  // store the message for later gui-based filtering
  m_progressMessages.push_back(QPair<QString, MessageType>(progressMessage, messageType));

  bool bPostMessage = false;
  if (messageType == MessageType::Info)
  {
    if (this->ui->pushButton_Info->isChecked())
    {
      bPostMessage = true;
    }
  }
  else if (messageType == MessageType::Warning)
  {
    if (this->ui->pushButton_Warnings->isChecked())
    {
      bPostMessage = true;
    }
  }
  else if (messageType == MessageType::Error)
  {
    if (this->ui->pushButton_Errors->isChecked())
    {
      bPostMessage = true;
    }
  }
  if (bPostMessage)
  {
    addMessageText(progressMessage, messageType);
  }

  m_pMessageMutex->unlock();
}

// private function that adds color coding to progress messages
void qtProgressDialog::addMessageText(QString message, MessageType messageType)
{
  QString textColor = QColor(Qt::darkGreen).name();
  QString iconHTML = "";
  if (m_bShowMessageIcons)
  {
    iconHTML = m_infoIconHTML;
  }
  if (messageType == MessageType::Warning)
  {
    textColor = "#d1a700"; // yellow
    if (m_bShowMessageIcons)
    {
      iconHTML = m_warningIconHTML;
    }
  }
  else if (messageType == MessageType::Error)
  {
    textColor = QColor(Qt::darkRed).name();
    if (m_bShowMessageIcons)
    {
      iconHTML = m_errorIconHTML;
    }
  }

  QString messageHTML =
    QString("<p style=\"margin:2px;color:%2;\">%1 %3</p>").arg(iconHTML, textColor, message);

  this->ui->textBrowser->append(messageHTML);
}

void qtProgressDialog::clearProgressMessages()
{
  // if the mutex is not available, queue the message for later processing
  if (!m_pMessageMutex->tryLock(0))
  {
    QTimer::singleShot(5, this, SLOT(clearProgressMessages()));
    return;
  }

  m_progressMessages.clear();
  this->ui->textBrowser->clear();

  m_pMessageMutex->unlock();
}

void qtProgressDialog::on_pushButton_Close_clicked()
{
  this->close();
  this->clearProgressMessages();

  if (m_autoCloseTimer && m_autoCloseTimer->isActive())
  {
    // turn off the timer, and restore the button text in case the qtProgressDialog object is reused
    m_autoCloseTimer->stop();
    this->ui->pushButton_Close->setText("Close");
  }

  Q_EMIT closeClicked();
}

void qtProgressDialog::on_pushButton_Cancel_clicked()
{
  this->close();
  this->clearProgressMessages();

  Q_EMIT cancelClicked();
}

// changes the visibility of messages in the Progress Message Box
void qtProgressDialog::on_pushButton_Info_clicked()
{
  Icons icons = None;

  if (!this->ui->pushButton_Info->isChecked())
  {
    icons |= NoInfoIcon;

    // ensure that at least one filter button is on
    if (!this->ui->pushButton_Warnings->isChecked() && !this->ui->pushButton_Errors->isChecked())
    {
      this->ui->pushButton_Warnings->setChecked(true);
      icons |= WarningIcon;
    }
  }
  else
  {
    icons |= InfoIcon;
  }

  this->setButtonIcons(icons);
  this->rebuildMessageHistory();
}

// changes the visibility of messages in the Progress Message Box
void qtProgressDialog::on_pushButton_Warnings_clicked()
{
  Icons icons = None;

  if (!this->ui->pushButton_Warnings->isChecked())
  {
    icons |= NoWarningIcon;

    // ensure that at least one filter button is on
    if (!this->ui->pushButton_Info->isChecked() && !this->ui->pushButton_Errors->isChecked())
    {
      this->ui->pushButton_Info->setChecked(true);
      icons |= InfoIcon;
    }
  }
  else
  {
    icons |= WarningIcon;
  }

  this->setButtonIcons(icons);
  this->rebuildMessageHistory();
}

// changes the visibility of messages in the Progress Message Box
void qtProgressDialog::on_pushButton_Errors_clicked()
{
  Icons icons = None;

  if (!this->ui->pushButton_Errors->isChecked())
  {
    icons |= NoErrorIcon;

    // ensure that at least one filter button is on
    if (!this->ui->pushButton_Info->isChecked() && !this->ui->pushButton_Warnings->isChecked())
    {
      this->ui->pushButton_Info->setChecked(true);
      icons |= InfoIcon;
    }
  }
  else
  {
    icons |= ErrorIcon;
  }

  setButtonIcons(icons);
  rebuildMessageHistory();
}

// private function to change the message filter button icons
void qtProgressDialog::setButtonIcons(Icons icons)
{
  if (icons & InfoIcon)
  {
    this->ui->pushButton_Info->setIcon(QIcon(m_infoPixmap));
  }
  else if (icons & NoInfoIcon)
  {
    this->ui->pushButton_Info->setIcon(QIcon(m_noInfoPixmap));
  }

  if (icons & WarningIcon)
  {
    this->ui->pushButton_Warnings->setIcon(QIcon(m_warningPixmap));
  }
  else if (icons & NoWarningIcon)
  {
    this->ui->pushButton_Warnings->setIcon(QIcon(m_noWarningPixmap));
  }

  if (icons & ErrorIcon)
  {
    this->ui->pushButton_Errors->setIcon(QIcon(m_errorPixmap));
  }
  else if (icons & NoErrorIcon)
  {
    this->ui->pushButton_Errors->setIcon(QIcon(m_noErrorPixmap));
  }
}

// private function to rebuild the messages in the Progress Message Box based on the current message filters
void qtProgressDialog::rebuildMessageHistory()
{
  if (!m_pMessageMutex->tryLock())
  {
    QTimer::singleShot(5, this, SLOT(rebuildMessageHistory()));
    return;
  }

  bool bInfo = this->ui->pushButton_Info->isChecked();
  bool bWarnings = this->ui->pushButton_Warnings->isChecked();
  bool bErrors = this->ui->pushButton_Errors->isChecked();

  this->ui->textBrowser->clear();
  for (const QPair<QString, MessageType>& message : qAsConst(m_progressMessages))
  {
    if (bInfo && message.second == MessageType::Info)
    {
      addMessageText(message.first, message.second);
    }
    if (bWarnings && message.second == MessageType::Warning)
    {
      addMessageText(message.first, message.second);
    }
    if (bErrors && message.second == MessageType::Error)
    {
      addMessageText(message.first, message.second);
    }
  }

  m_pMessageMutex->unlock();
}

// process auto-close timer ticks
void qtProgressDialog::processAutoCloseTimer()
{
  m_remainingAutoCloseSeconds -= 1;
  if (m_remainingAutoCloseSeconds == 0)
  {
    on_pushButton_Close_clicked();
    QTimer* timer = dynamic_cast<QTimer*>(this->sender());
    timer->stop();
    this->ui->pushButton_Close->setText(QString("Close"));
  }
  else
  {
    this->ui->pushButton_Close->setText(QString("Close (%1)").arg(m_remainingAutoCloseSeconds));
  }
}

// called once to start the auto-close process
void qtProgressDialog::processAutoClose()
{
  if (m_functionalAutoCloseDelay == 0)
  {
    this->close();
  }
  else
  {
    if (m_autoCloseTimer)
    {
      m_autoCloseTimer->deleteLater();
      m_autoCloseTimer = nullptr;
    }
    m_autoCloseTimer = new QTimer(this);
    connect(m_autoCloseTimer, SIGNAL(timeout()), this, SLOT(processAutoCloseTimer()));

    m_remainingAutoCloseSeconds = m_functionalAutoCloseDelay;
    this->ui->pushButton_Close->setText(QString("Close (%1)").arg(m_remainingAutoCloseSeconds));

    m_autoCloseTimer->start(1000);
  }
}
