#include "runtimeqml.h"

#include <QXmlStreamReader>
#include <QFileInfo>
#include <QRegExp>
#include <QTimer>

/*!
 * \brief Construct a RuntimeQML object with a path to the qrc file.
 * \param engine App engine to reload.
 * \param qrcFilename File name of the QRC file for auto reload.
 * \param parent Pointer to a parent object.
 */
RuntimeQML::RuntimeQML(QQmlApplicationEngine* engine, const QString &qrcFilename, QObject *parent) :
    QObject(parent),
    m_engine(engine),
    m_qrcFilename(qrcFilename),
    m_mainQmlFilename("main.qml")
{
    m_allowedSuffixList << "qml";
}


/*!
 * \brief Returns the absolute path for the given qml file.
 * \param qmlFile Qml filename
 */
QString RuntimeQML::adjustPath(QString qmlFile)
{
    return m_selector.select(qrcAbsolutePath() + "/" + qmlFile);
}

/*!
 * \brief Returns the absolute path to the QRC file.
 */
QString RuntimeQML::qrcAbsolutePath() const
{
    return QFileInfo(m_qrcFilename).absolutePath();
}

/*!
 * \brief Filename of the QRC file.
 */
QString RuntimeQML::qrcFilename() const
{
    return m_qrcFilename;
}

/*!
 * \brief If true, files are watched for changes and auto-reloaded.
 * Otherwise, you need to trigger a reload manually from your code by calling reload().
 * \sa reload
 */
bool RuntimeQML::autoReload() const
{
    return m_autoReload;
}

/*!
 * \brief If true, all open windows will be closed upon reload.
 * \default true
 */
bool RuntimeQML::closeAllOnReload() const
{
    return m_closeAllOnReload;
}

/*!
 * \brief QRC prefixes that are ignored.
 */
const QList<QString>& RuntimeQML::prefixIgnoreList() const
{
    return m_prefixIgnoreList;
}

/*!
 * \brief Files that are ignored.
 */
const QList<QString> &RuntimeQML::fileIgnoreList() const
{
    return m_fileIgnoreList;
}

/*!
 * \brief Allowed suffixes to filter files to watch for changes.
 * By default contains only "qml".
 */
const QList<QString> &RuntimeQML::allowedSuffixes() const
{
    return m_allowedSuffixList;
}

/*!
 * \brief Call it if you don't want debug outputs from this class.
 */
void RuntimeQML::noDebug()
{
    if (m_noDebug)
        return;
    m_noDebug = true;
}


/*!
 * \brief Reload the window.
 */
void RuntimeQML::reload()
{
    QMetaObject::invokeMethod(this, "reloadQml", Qt::QueuedConnection);
}


/*!
 * \brief Call it from QML to set the current QQuickWindow.
 * You shouldn't need to call it as it is done automatically on reload.
 * \param window
 */
void RuntimeQML::setWindow(QQuickWindow* window)
{
    if (window == m_window)
        return;
    m_window = window;
}


/*!
 * \brief Set the QRC filename for files to watch for changes.
 * \param qrcFilename Path to a .qrc file.
 */
void RuntimeQML::setQrcFilename(QString qrcFilename)
{
    if (m_qrcFilename == qrcFilename)
        return;

    m_qrcFilename = qrcFilename;
    emit qrcFilenameChanged(qrcFilename);

    loadQrcFiles();
}

/*!
 * \brief Set the name of the main qml file.
 * Default is "main.qml".
 * \param filename The main qml filename.
 */
void RuntimeQML::setMainQmlFilename(QString filename)
{
    if (m_mainQmlFilename == filename)
        return;

    m_mainQmlFilename = filename;
}


/*!
 * \brief If true, files are watched for changes and auto-reloaded.
 * Otherwise, you need to trigger a reload manually from your code by calling reload().
 * \param reload True to auto-reload, false otherwise.
 */
void RuntimeQML::setAutoReload(bool autoReload)
{
    if (m_autoReload == autoReload)
        return;

    m_autoReload = autoReload;
    emit autoReloadChanged(autoReload);

    if (autoReload)
        loadQrcFiles();
    else
        unloadFileWatcher();
}

/*!
 * \brief If true, all open windows are closed upon reload. Otherwise, might cause "link" errors with QML components.
 * \param closeAllOnReload True to close all windows on reload, false otherwise.
 */
void RuntimeQML::setCloseAllOnReload(bool closeAllOnReload)
{
    if (m_closeAllOnReload == closeAllOnReload)
        return;

    m_closeAllOnReload = closeAllOnReload;
    emit closeAllOnReloadChanged(m_closeAllOnReload);
}

/*!
 * \brief Add a QRC prefix to ignore.
 * \note Relevant for auto-reload only.
 * \param prefix Prefix to ignore.
 */
void RuntimeQML::ignoreQrcPrefix(const QString& prefix)
{
    if (m_prefixIgnoreList.contains(prefix))
        return;

    m_prefixIgnoreList.append(prefix);

    if (m_autoReload)
        loadQrcFiles();
}

/*!
 * \brief Add a filename to ignore from changes.
 * Applies to the full filename in the QRC entry (i.e. the local "path"), with the prefix.
 * Supports "file globbing" matching using wildcards.
 * \note Relevant for auto-reload only.
 * \param filename Filename to ignore.
 */
void RuntimeQML::ignoreFile(const QString &filename)
{
    if (m_fileIgnoreList.contains(filename))
        return;

    m_fileIgnoreList.append(filename);

    if (m_autoReload)
        loadQrcFiles();
}

/*!
 * \brief Allow a file suffix to be watched for changes.
 * \note Relevant for auto-reload only.
 * \param suffix
 */
void RuntimeQML::addSuffix(const QString &suffix)
{
    if (m_allowedSuffixList.contains(suffix))
        return;

    m_allowedSuffixList.append(suffix);

    if (m_autoReload)
        loadQrcFiles();
}


/*!
 * \brief Reload the QML. Do not call it directly, use reload() instead.
 */
void RuntimeQML::reloadQml()
{
    if (m_mainQmlFilename.isEmpty()) {
        qWarning("No QML file specified.");
        return;
    }

    if (m_window) {
        if (m_closeAllOnReload) {
            // Find all child windows and close them
            auto const allWindows = m_window->findChildren<QQuickWindow*>();
            for (int i {0}; i < allWindows.size(); ++i) {
                QQuickWindow* w = qobject_cast<QQuickWindow*>(allWindows.at(i));
                if (w) {
                    w->close();
                    w->deleteLater();
                }
            }
        }

        m_window->close();
        m_window->deleteLater();
    }

    m_engine->clearComponentCache();
    // TODO: test with network files
    // TODO: QString path to QUrl doesn't work under Windows with load() (load fail)
    m_engine->load(m_selector.select(qrcAbsolutePath() + "/" + m_mainQmlFilename));
    // NOTE: QQmlApplicationEngine::rootObjects() isn't cleared, should it be?

    if (!m_engine->rootObjects().isEmpty()) {
        QQuickWindow* w = qobject_cast<QQuickWindow*>(m_engine->rootObjects().last());
        if (w) m_window = w;
    }

//    for (auto *o : m_engine->rootObjects()) {
//        qDebug() << "> " << o;
//    }
}

/*!
 * \brief Called when a watched file changed, from QFileSystemWatcher.
 * \param path Path/file that triggered the signal.
 */
void RuntimeQML::fileChanged(const QString& path)
{
    if (!m_noDebug)
        qDebug() << "Reloading qml:" << path;
    reload();

#if defined(Q_OS_WIN)
    // Deleted files are removed from the watcher, re-add the file for
    //  systems that delete files to update them
    if (m_fileWatcher)
        QTimer::singleShot(500, m_fileWatcher, [this,path](){
             m_fileWatcher->addPath(path);
        });
#endif
}


/*!
 * \brief Load qml from the QRC file to watch them.
 */
void RuntimeQML::loadQrcFiles()
{
    unloadFileWatcher();

    m_fileWatcher = new QFileSystemWatcher(this);
    connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &RuntimeQML::fileChanged);

    QFile file(m_qrcFilename);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qWarning("Unable to open resource file '%s', RuntimeQML will not work! Error: %s",
                 qPrintable(m_qrcFilename), qPrintable(file.errorString()));
        return;
    }

    QString const basePath = qrcAbsolutePath() + "/";
    QString currentPrefix;

    // Read each entry
    QXmlStreamReader inputStream(&file);
    while (!inputStream.atEnd() && !inputStream.hasError()) {
        inputStream.readNext();
        if (inputStream.isStartElement()) {
            QString name { inputStream.name().toString() };

            // Check prefix
            if (name == "qresource") {
                if (inputStream.attributes().hasAttribute("prefix")) {
                    auto p = inputStream.attributes().value("prefix").toString();
                    if (m_prefixIgnoreList.contains(p)) {
                        // Ignore this prefix, loop through elements in this 'qresource' tag
                        while (!inputStream.atEnd() && !inputStream.hasError()) {
                            inputStream.readNext();
                            if (inputStream.isEndElement() && inputStream.name() == "qresource")
                                break;
                        }
                        continue;
                    }
                    currentPrefix = p;
                }
            }

            // Check file name
            if (name == "file") {
                QString const filename { inputStream.readElementText() };

                // Check ignore list
                QString const fullFilename { currentPrefix + filename };
                auto it = std::find_if(m_fileIgnoreList.cbegin(), m_fileIgnoreList.cend(), [&](QString const& pattern) {
                    QRegExp re(pattern);
                    re.setPatternSyntax(QRegExp::WildcardUnix);
                    return re.exactMatch(fullFilename);
                });

                if (it != m_fileIgnoreList.cend())
                    continue;

                QFileInfo const file { filename };

                // Add to the watch list if the file suffix is allowed
                if (m_allowedSuffixList.contains(file.suffix())) {
                    QString fp { m_selector.select(basePath + filename) };
                    m_fileWatcher->addPath(fp);
                    //qDebug() << "    " << file.absoluteFilePath() << fp;
                }
            }
        }
    }

    if (!m_noDebug) {
        qDebug("Watching QML files:");
        int const fileCount = m_fileWatcher->files().size();

        for (auto &f : m_fileWatcher->files()) {
            qDebug() << "    " << f;
        }

        if (fileCount > 0)
            qDebug("  Total: %d", fileCount);
        else
            qDebug("  None.");
    }
}

/*!
 * \brief Unload the file watcher.
 */
void RuntimeQML::unloadFileWatcher()
{
    if (m_fileWatcher) {
        disconnect(m_fileWatcher);
        delete m_fileWatcher;
        m_fileWatcher = nullptr;
    }
}