/***************************************************************************
 *   Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net>     *
 *                                                                         *
 *   This file is part of the FreeCAD CAx development system.              *
 *                                                                         *
 *   This library is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Library General Public           *
 *   License as published by the Free Software Foundation; either          *
 *   version 2 of the License, or (at your option) any later version.      *
 *                                                                         *
 *   This library  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 Library General Public License for more details.                  *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this library; see the file COPYING.LIB. If not,    *
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
 *   Suite 330, Boston, MA  02111-1307, USA                                *
 *                                                                         *
 ***************************************************************************/


#include <QApplication>
#include <QElapsedTimer>
#include <QKeyEvent>
#include <QMessageBox>
#include <QMetaObject>
#include <QThread>
#include <QTime>
#include <QTimer>
#include <QWindow>


#include "ProgressBar.h"
#include "MainWindow.h"
#include "ProgressDialog.h"
#include "WaitCursor.h"


using namespace Gui;


namespace Gui
{
struct SequencerBarPrivate
{
    ProgressBar* bar;
    WaitCursor* waitCursor;
    QElapsedTimer measureTime;
    QElapsedTimer progressTime;
    QElapsedTimer checkAbortTime;
    QString text;
    bool guiThread;
};

struct ProgressBarPrivate
{
    QTimer* delayShowTimer;
    int minimumDuration;
    int observeEventFilter;

    bool isModalDialog(QObject* o) const
    {
        QWidget* parent = qobject_cast<QWidget*>(o);
        if (!parent) {
            QWindow* window = qobject_cast<QWindow*>(o);
            if (window) {
                parent = QWidget::find(window->winId());
            }
        }
        while (parent) {
            auto* dlg = qobject_cast<QMessageBox*>(parent);
            if (dlg && dlg->isModal()) {
                return true;
            }
            auto* pd = qobject_cast<QProgressDialog*>(parent);
            if (pd) {
                return true;
            }
            parent = parent->parentWidget();
        }

        return false;
    }
};
}  // namespace Gui

SequencerBar* SequencerBar::_pclSingleton = nullptr;

SequencerBar* SequencerBar::instance()
{
    // not initialized?
    if (!_pclSingleton) {
        _pclSingleton = new SequencerBar();
    }

    return _pclSingleton;
}

SequencerBar::SequencerBar()
{
    d = new SequencerBarPrivate;
    d->bar = nullptr;
    d->waitCursor = nullptr;
    d->guiThread = true;
}

SequencerBar::~SequencerBar()
{
    delete d;
}

void SequencerBar::pause()
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread
    d->bar->leaveControlEvents(d->guiThread);
    if (thr != currentThread) {
        return;
    }

    // allow key handling of dialog and restore cursor
    d->waitCursor->restoreCursor();
    QApplication::setOverrideCursor(Qt::ArrowCursor);
}

void SequencerBar::resume()
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread
    if (thr == currentThread) {
        QApplication::restoreOverrideCursor();
        d->waitCursor->setWaitCursor();
    }

    // must be called as last to get control before WaitCursor
    d->bar->enterControlEvents(d->guiThread);  // grab again
}

void SequencerBar::startStep()
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread
    if (thr != currentThread) {
        d->guiThread = false;
        QMetaObject::invokeMethod(
            d->bar,
            "setRangeEx",
            Qt::QueuedConnection,
            Q_ARG(int, 0),
            Q_ARG(int, (int)nTotalSteps)
        );
        d->progressTime.start();
        d->checkAbortTime.start();
        d->measureTime.start();
        QMetaObject::invokeMethod(d->bar, "aboutToShow", Qt::QueuedConnection);
        d->bar->enterControlEvents(d->guiThread);
    }
    else {
        d->guiThread = true;
        d->bar->setRangeEx(0, (int)nTotalSteps);
        d->progressTime.start();
        d->checkAbortTime.start();
        d->measureTime.start();
        d->waitCursor = new Gui::WaitCursor;
        d->bar->enterControlEvents(d->guiThread);
        d->bar->aboutToShow();
    }
}

void SequencerBar::checkAbort()
{
    if (d->bar->thread() != QThread::currentThread()) {
        return;
    }
    if (!wasCanceled()) {
        if (d->checkAbortTime.elapsed() < 500) {
            return;
        }
        d->checkAbortTime.restart();
        qApp->processEvents();
        return;
    }
    // restore cursor
    pause();
    bool ok = d->bar->canAbort();
    // continue and show up wait cursor if needed
    resume();

    // force to abort the operation
    if (ok) {
        abort();
    }
    else {
        rejectCancel();
    }
}

void SequencerBar::nextStep(bool canAbort)
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread
    if (thr != currentThread) {
        if (wasCanceled() && canAbort) {
            abort();
        }
        else {
            setValue((int)nProgress + 1);
        }
    }
    else {
        if (wasCanceled() && canAbort) {
            // restore cursor
            pause();
            bool ok = d->bar->canAbort();
            // continue and show up wait cursor if needed
            resume();

            // force to abort the operation
            if (ok) {
                abort();
            }
            else {
                rejectCancel();
                setValue((int)nProgress + 1);
            }
        }
        else {
            setValue((int)nProgress + 1);
        }
    }
}

void SequencerBar::setProgress(size_t step)
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread
    if (thr != currentThread) {
        QMetaObject::invokeMethod(d->bar, "show", Qt::QueuedConnection);
    }
    else {
        d->bar->show();
    }

    setValue((int)step);
}

void SequencerBar::setValue(int step)
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread
    // if number of total steps is unknown then increment only by one
    if (nTotalSteps == 0) {
        int elapsed = d->progressTime.elapsed();
        // allow an update every 100 milliseconds only
        if (elapsed > 100) {
            d->progressTime.restart();
            if (thr != currentThread) {
                QMetaObject::invokeMethod(
                    d->bar,
                    "setValueEx",
                    Qt::/*Blocking*/ QueuedConnection,
                    Q_ARG(int, d->bar->value() + 1)
                );
            }
            else {
                d->bar->setValueEx(d->bar->value() + 1);
                qApp->processEvents();
            }
        }
    }
    else {
        int elapsed = d->progressTime.elapsed();
        // allow an update every 100 milliseconds only
        if (elapsed > 100) {
            d->progressTime.restart();
            if (thr != currentThread) {
                QMetaObject::invokeMethod(
                    d->bar,
                    "setValueEx",
                    Qt::/*Blocking*/ QueuedConnection,
                    Q_ARG(int, step)
                );
                if (d->bar->isVisible()) {
                    showRemainingTime();
                }
            }
            else {
                d->bar->setValueEx(step);
                if (d->bar->isVisible()) {
                    showRemainingTime();
                }
                d->bar->resetObserveEventFilter();
                qApp->processEvents();
            }
        }
    }
}

void SequencerBar::showRemainingTime()
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread

    int elapsed = d->measureTime.elapsed();
    int progress = d->bar->value();
    int totalSteps = d->bar->maximum() - d->bar->minimum();

    QString txt = d->text;
    // More than 5 percent complete or more than 5 secs have elapsed.
    if (progress * 20 > totalSteps || elapsed > 5000) {
        int rest = (int)((double)totalSteps / progress * elapsed) - elapsed;

        // more than 1 secs have elapsed and at least 100 ms are remaining
        if (elapsed > 1000 && rest > 100) {
            QTime time(0, 0, 0);
            time = time.addSecs(rest / 1000);
            QString remain = Gui::ProgressBar::tr("Remaining: %1").arg(time.toString());
            QString status = QStringLiteral("%1\t[%2]").arg(txt, remain);

            if (thr != currentThread) {
                QMetaObject::invokeMethod(
                    getMainWindow(),
                    "showMessage",
                    Qt::/*Blocking*/ QueuedConnection,
                    Q_ARG(QString, status)
                );
            }
            else {
                getMainWindow()->showMessage(status);
            }
        }
    }
}

void SequencerBar::resetData()
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread
    if (thr != currentThread) {
        QMetaObject::invokeMethod(d->bar, "resetEx", Qt::QueuedConnection);
        QMetaObject::invokeMethod(d->bar, "aboutToHide", Qt::QueuedConnection);
        QMetaObject::invokeMethod(
            getMainWindow(),
            "showMessage",
            Qt::/*Blocking*/ QueuedConnection,
            Q_ARG(QString, QString())
        );
        QMetaObject::invokeMethod(
            getMainWindow(),
            "setPaneText",
            Qt::/*Blocking*/ QueuedConnection,
            Q_ARG(int, 1),
            Q_ARG(QString, QString())
        );
        d->bar->leaveControlEvents(d->guiThread);
    }
    else {
        d->bar->resetEx();
        // Note: Under Qt 4.1.4 this forces to run QWindowsStyle::eventFilter() twice
        // handling the same event thus a warning is printed. Possibly, this is a bug
        // in Qt. The message is QEventDispatcherUNIX::unregisterTimer: invalid argument.
        d->bar->aboutToHide();
        delete d->waitCursor;
        d->waitCursor = nullptr;
        d->bar->leaveControlEvents(d->guiThread);
        getMainWindow()->setPaneText(1, QString());
        getMainWindow()->showMessage(QString());
    }

    SequencerBase::resetData();
}

void SequencerBar::abort()
{
    // resets
    resetData();
    Base::AbortException exc("User aborted");
    throw exc;
}

void SequencerBar::setText(const char* pszTxt)
{
    QThread* currentThread = QThread::currentThread();
    QThread* thr = d->bar->thread();  // this is the main thread

    // print message to the statusbar
    d->text = pszTxt ? QString::fromUtf8(pszTxt) : QLatin1String("");
    if (thr != currentThread) {
        QMetaObject::invokeMethod(
            getMainWindow(),
            "showMessage",
            Qt::/*Blocking*/ QueuedConnection,
            Q_ARG(QString, d->text)
        );
    }
    else {
        getMainWindow()->showMessage(d->text);
    }
}

bool SequencerBar::isBlocking() const
{
    return d->guiThread;
}

QProgressBar* SequencerBar::getProgressBar(QWidget* parent)
{
    if (!d->bar) {
        d->bar = new ProgressBar(this, parent);
    }
    return d->bar;
}

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

/* TRANSLATOR Gui::ProgressBar */

ProgressBar::ProgressBar(SequencerBar* s, QWidget* parent)
    : QProgressBar(parent)
    , sequencer(s)
{
#ifdef QT_WINEXTRAS_LIB
    m_taskbarButton = nullptr;
    m_taskbarButton = nullptr;
#endif
    d = new Gui::ProgressBarPrivate;
    d->minimumDuration = 2000;  // 2 seconds
    d->delayShowTimer = new QTimer(this);
    d->delayShowTimer->setSingleShot(true);
    connect(d->delayShowTimer, &QTimer::timeout, this, &ProgressBar::delayedShow);
    d->observeEventFilter = 0;

    setFixedWidth(120);

    // write percentage to the center
    setAlignment(Qt::AlignHCenter);
    hide();
}

ProgressBar::~ProgressBar()
{
    disconnect(d->delayShowTimer, &QTimer::timeout, this, &ProgressBar::delayedShow);
    delete d->delayShowTimer;
    delete d;
}

int ProgressBar::minimumDuration() const
{
    return d->minimumDuration;
}

void ProgressBar::resetEx()
{
    QProgressBar::reset();
#ifdef QT_WINEXTRAS_LIB
    setupTaskBarProgress();
    m_taskbarProgress->reset();
#endif
}

void ProgressBar::setRangeEx(int minimum, int maximum)
{
    QProgressBar::setRange(minimum, maximum);
#ifdef QT_WINEXTRAS_LIB
    setupTaskBarProgress();
    m_taskbarProgress->setRange(minimum, maximum);
#endif
}

void ProgressBar::setValueEx(int value)
{
    QProgressBar::setValue(value);
#ifdef QT_WINEXTRAS_LIB
    setupTaskBarProgress();
    m_taskbarProgress->setValue(value);
#endif
}

void ProgressBar::setMinimumDuration(int ms)
{
    if (value() == 0) {
        d->delayShowTimer->stop();
        d->delayShowTimer->start(ms);
    }

    d->minimumDuration = ms;
}

void ProgressBar::aboutToShow()
{
    // delay showing the bar
    d->delayShowTimer->start(d->minimumDuration);
#ifdef QT_WINEXTRAS_LIB
    setupTaskBarProgress();
    m_taskbarProgress->show();
#endif
}

void ProgressBar::delayedShow()
{
    if (!isVisible() && !sequencer->wasCanceled() && sequencer->isRunning()) {
        show();
    }
}

void ProgressBar::aboutToHide()
{
    hide();
#ifdef QT_WINEXTRAS_LIB
    setupTaskBarProgress();
    m_taskbarProgress->hide();
#endif
}

bool ProgressBar::canAbort() const
{
    auto ret = QMessageBox::question(
        getMainWindow(),
        tr("Aborting"),
        tr("Abort the operation?"),
        QMessageBox::Yes | QMessageBox::No,
        QMessageBox::No
    );

    return (ret == QMessageBox::Yes) ? true : false;
}

void ProgressBar::showEvent(QShowEvent* e)
{
    QProgressBar::showEvent(e);
    d->delayShowTimer->stop();
}

void ProgressBar::hideEvent(QHideEvent* e)
{
    QProgressBar::hideEvent(e);
    d->delayShowTimer->stop();
}

void ProgressBar::resetObserveEventFilter()
{
    d->observeEventFilter = 0;
}

void ProgressBar::enterControlEvents(bool grab)
{
    qApp->installEventFilter(this);

    // Make sure that we get the key events, otherwise the Inventor viewer usurps the key events
    // This also disables accelerators.
#if defined(Q_OS_LINUX)
    Q_UNUSED(grab)
#else
    if (grab) {
        grabKeyboard();
    }
#endif
}

void ProgressBar::leaveControlEvents(bool release)
{
    qApp->removeEventFilter(this);

#if defined(Q_OS_LINUX)
    Q_UNUSED(release)
#else
    // release the keyboard again
    if (release) {
        releaseKeyboard();
    }
#endif
}

#ifdef QT_WINEXTRAS_LIB
void ProgressBar::setupTaskBarProgress()
{
    if (!m_taskbarButton || !m_taskbarProgress) {
        m_taskbarButton = new QWinTaskbarButton(this);
        m_taskbarButton->setWindow(MainWindow::getInstance()->windowHandle());
        // m_myButton->setOverlayIcon(QIcon(""));

        m_taskbarProgress = m_taskbarButton->progress();
    }
}
#endif

bool ProgressBar::eventFilter(QObject* o, QEvent* e)
{
    if (sequencer->isRunning() && e) {
        QThread* currentThread = QThread::currentThread();
        QThread* thr = this->thread();  // this is the main thread
        if (thr != currentThread) {
            if (e->type() == QEvent::KeyPress) {
                auto ke = static_cast<QKeyEvent*>(e);
                if (ke->key() == Qt::Key_Escape) {
                    // cancel the operation
                    sequencer->tryToCancel();
                    return true;
                }
            }
            return QProgressBar::eventFilter(o, e);
        }

        // main thread
        switch (e->type()) {
            // check for ESC
            case QEvent::KeyPress: {
                auto ke = static_cast<QKeyEvent*>(e);
                if (ke->key() == Qt::Key_Escape) {
                    // eventFilter() was called from the application 50 times without performing a
                    // new step (app could hang)
                    if (d->observeEventFilter > 50) {
                        // tries to unlock the application if it hangs (probably due to incorrect
                        // usage of Base::Sequencer)
                        if (ke->modifiers() & (Qt::ControlModifier | Qt::AltModifier)) {
                            sequencer->resetData();
                            return true;
                        }
                    }

                    // cancel the operation
                    sequencer->tryToCancel();
                }

                return true;
            } break;

            // ignore all these events
            case QEvent::KeyRelease:
            case QEvent::Enter:
            case QEvent::Leave:
            case QEvent::MouseButtonDblClick:
            case QEvent::MouseButtonRelease:
            case QEvent::MouseMove:
            case QEvent::NativeGesture:
            case QEvent::ContextMenu: {
                if (!d->isModalDialog(o)) {
                    return true;
                }
            } break;

            // special case if the main window's close button was pressed
            case QEvent::Close: {
                // avoid to exit while app is working
                // note: all other widget types are allowed to be closed anyway
                if (o == getMainWindow()) {
                    e->ignore();
                    return true;
                }
            } break;

            // do a system beep and ignore the event
            case QEvent::MouseButtonPress: {
                if (!d->isModalDialog(o)) {
                    QApplication::beep();
                    return true;
                }
            } break;

            default: {
            } break;
        }

        d->observeEventFilter++;
    }

    return QProgressBar::eventFilter(o, e);
}


#include "moc_ProgressBar.cpp"
