/**************************************************************************
 **                                                                      **
 ** Copyright (C) 2011-2026 Lukas Spies                                  **
 ** Contact: https://photoqt.org                                         **
 **                                                                      **
 ** This file is part of PhotoQt.                                        **
 **                                                                      **
 ** PhotoQt 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.                                  **
 **                                                                      **
 ** PhotoQt 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 PhotoQt. If not, see <http://www.gnu.org/licenses/>.      **
 **                                                                      **
 **************************************************************************/

#include <QKeyEvent>
#include <QFileInfo>
#include <QDir>
#include <thread>
#include <iostream>
#include <qlogging.h>   // needed in this form to compile with Qt 6.2

#include <QQmlApplicationEngine>
#include <QLocalSocket>
#include <QLocalServer>
#include <QSqlError>
#include <QSqlQuery>
#include <QWindow>

#include <pqc_commandlineparser.h>
#include <pqc_singleinstance.h>
#include <pqc_notify_cpp.h>
#include <pqc_configfiles.h>
#include <pqc_filefoldermodelCPP.h>
#include <pqc_helper.h>

#ifdef PQMLIBARCHIVE
#include <archive.h>
#include <archive_entry.h>
#endif

PQCSingleInstance::PQCSingleInstance(int &argc, char *argv[]) : QApplication(argc, argv) {

    // Parse the command line arguments
    PQCCommandLineParser parser(*this);
    PQCCommandLineResult result = parser.getResult();

    QList<Actions> msg;
    QStringList receivedFiles;
    QString receivedShortcut = "";
    QString receivedSetting[2] = {"", ""};

    socket = nullptr;
    server = nullptr;

    forceModernInterface = false;
    forceIntegratedInterface = false;

    m_forceShowWizard = false;
    m_forceSkipWizard = false;

    if(result & PQCCommandLineFile) {
        bool first = true;
        for(const auto &f : std::as_const(parser.filenames)) {
            if(first) msg << Actions::File;
            first = false;
            receivedFiles.append(QFileInfo(f).canonicalFilePath());
        }
    }

    if(result & PQCCommandLineOpen) {
        msg << Actions::Open;
    }

    if(result & PQCCommandLineShow) {
        msg << Actions::Show;
    }

    if(result & PQCCommandLineHide) {
        msg << Actions::Hide;
    }

    if(result & PQCCommandLineQuit) {
        msg << Actions::Quit;
    }

    if(result & PQCCommandLineToggle) {
        msg << Actions::Toggle;
    }

    if(result & PQShortcutSequence) {
        msg << Actions::Shortcut;
        receivedShortcut = parser.shortcutSequence;
    }

    if(result & PQCCommandLineStartInTray)
        msg << Actions::StartInTray;

    if(result & PQCCommandLineEnableTray)
        msg << Actions::Tray;

    if(result & PQCCommandLineDisableTray)
        msg << Actions::NoTray;

    if(result & PQCCommandLineDebug)
        msg << Actions::Debug;

    if(result & PQCCommandLineNoDebug)
        msg << Actions::NoDebug;

    if(result & PQCCommandLineModernInterface)
        forceModernInterface = true;

    if(result & PQCCommandLineIntegratedInterface)
        forceIntegratedInterface = true;

    if(result & PQCCommandLineShowStartupWizard)
        m_forceShowWizard = true;

    if(result & PQCCommandLineDontShowStartupWizard)
        m_forceSkipWizard = true;

    if(result & PQCCommandLineSettingUpdate) {
        receivedSetting[0] = parser.settingUpdate[0];
        receivedSetting[1] = parser.settingUpdate[1];
        msg << Actions::Setting;
    }

    if(result & PQCCommandLineInstallExtension) {
        installExtensionFileName = parser.extensionFileName;
    }

    // validation requested
    checkConfig = false;
    if(result & PQCCommandLineCheckConfig) {
        checkConfig = true;
        socket = new QLocalSocket();
        server = new QLocalServer();
        return;
    }

    // reset defaults
    resetConfig = false;
    if(result & PQCCommandLineResetConfig) {
        resetConfig = true;
        socket = new QLocalSocket();
        server = new QLocalServer();
        return;
    }

    // show info
    showInfo = false;
    if(result & PQCCommandLineShowInfo) {
        showInfo = true;
        socket = new QLocalSocket();
        server = new QLocalServer();
        return;
    }

    // STANDALONE, EXPORT, IMPORT

    exportAndQuit = "";
    if(result & PQCCommandLineExport) {
        exportAndQuit = parser.exportFileName;
        socket = new QLocalSocket();
        server = new QLocalServer();
        return;
    }

    importAndQuit = "";
    if(result & PQCCommandLineImport) {
        importAndQuit = parser.importFileName;
        socket = new QLocalSocket();
        server = new QLocalServer();
        return;
    }

    // we need to figure out if multiple instances are allowed here WITHOUT using the PQCSettings class
    if(QFile::exists(PQCConfigFiles::get().USERSETTINGS_DB())) {

        // This database connection happens before the general setup in PQCStartupManager
        QSqlDatabase dbtmp;
        if(QSqlDatabase::contains("settings"))
            dbtmp = QSqlDatabase::database("settings");
        else {
            if(QSqlDatabase::isDriverAvailable("QSQLITE3"))
                dbtmp = QSqlDatabase::addDatabase("QSQLITE3", "settings");
            else if(QSqlDatabase::isDriverAvailable("QSQLITE"))
                dbtmp = QSqlDatabase::addDatabase("QSQLITE", "settings");
            dbtmp.setDatabaseName(PQCConfigFiles::get().USERSETTINGS_DB());
        }

        if(!dbtmp.open()) {
            qWarning() << "Unable to check how to handle multiple instances:" << dbtmp.lastError().text();
            qWarning() << "Assuming only a single instance is to be used";
        } else {
            QSqlQuery query(dbtmp);
            if(!query.exec("SELECT `value` FROM interface WHERE `name`='AllowMultipleInstances'"))
                qWarning() << "Unable to check for interfaceAllowMultipleInstances setting";
            else {
                if(query.next()) {
                    if(query.value(0).toBool()) {
                        m_receivedFiles = receivedFiles;
                        if(!receivedShortcut.isEmpty())
                            m_receivedShortcut = receivedShortcut;
                        if(!receivedSetting[0].isEmpty()) {
                            m_receivedSetting[0] = receivedSetting[0];
                            m_receivedSetting[1] = receivedSetting[1];
                        }
                        handleMessage(msg);
                        query.clear();
                        dbtmp.close();
                        return;
                    }
                }
            }
            query.clear();
            dbtmp.close();
        }
    }

    /*****************/
    /* Server/Socket */
    /*****************/

    // Create server name
    QString server_str = "org.photoqt.PhotoQt";

    // Connect to a Local Server (if available)
    socket = new QLocalSocket();
    socket->connectToServer(server_str);

    // If this is successful, then an instance is already running
    if(socket->waitForConnected(100)) {

        if(msg.size() == 0)
            msg << Actions::Show;

        QList<QByteArray> writeMessage;

        if(qApp->platformName() == "wayland") {

            writeMessage.reserve(msg.size()+1);

            QString token = qgetenv("XDG_ACTIVATION_TOKEN");
            if(!token.isEmpty())
                writeMessage.append(("_T_O_K_E_N_" % token % "\n").toUtf8());

        } else
            writeMessage.reserve(msg.size());

        for(const Actions &i : std::as_const(msg)) {
            writeMessage.append(QString::number(static_cast<int>(i)).toUtf8());
        }

        // Send composed message string
        if(receivedFiles.length())
            writeMessage.append(("_F_I_L_E_" % receivedFiles.join(":://:://::") % "\n").toUtf8());
        if(!receivedShortcut.isEmpty())
            writeMessage.append(("_S_H_O_R_T_C_U_T_" % receivedShortcut % "\n").toUtf8());
        if(!receivedSetting[0].isEmpty())
            writeMessage.append(("_S_E_T_T_I_N_G_" % receivedSetting[0] % ":" % receivedSetting[1] % "\n").toUtf8());

        socket->write(writeMessage.join('\n'));
        socket->flush();

        // Inform user
        std::cout << "Running instance of PhotoQt detected, connecting to existing instance." << std::endl;

        // Exit the code (need to use stdlib exit function to ensure an immediate exit)
        // We wait 100ms as otherwise this instance might return as a crash (even though it doesn't really)
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::exit(0);

    } else {

        /*********************************************************************/

        // in this case we check and (if found) perform any import
        importConfig();

        /*********************************************************************/

        // Create a new local server
        server = new QLocalServer();
        server->removeServer(server_str);
        server->listen(server_str);
        connect(server, &QLocalServer::newConnection, this, &PQCSingleInstance::newConnection);

        m_receivedFiles = receivedFiles;
        m_receivedSetting[0] = receivedSetting[0];
        m_receivedSetting[1] = receivedSetting[1];
        m_receivedShortcut = receivedShortcut;

        handleMessage(msg);
        m_startupMessageForProcessing = msg;
#if __cplusplus >= 202002L
        connect(&PQCNotifyCPP::get(), &PQCNotifyCPP::reprocessStartupMessage, this, [=, this]() { handleMessage(m_startupMessageForProcessing, false); });
#else
        connect(&PQCNotifyCPP::get(), &PQCNotifyCPP::reprocessStartupMessage, this, [=]() { handleMessage(m_startupMessageForProcessing, false); });
#endif

    }

}

void PQCSingleInstance::newConnection() {
    QLocalSocket *socket = server->nextPendingConnection();
    if(socket->waitForReadyRead(2000)) {
        const QList<QByteArray> reply = socket->readAll().split('\n');
        QList<Actions> handleAll;
        for(const QByteArray &rep : reply) {
            if(rep.startsWith("_T_O_K_E_N_")) {
                qputenv("XDG_ACTIVATION_TOKEN", rep.last(rep.length()-11));
            } else if(rep.startsWith("_F_I_L_E_")) {
                m_receivedFiles = QString(rep.last(rep.length()-9)).split(":://:://::");
                handleAll.append(Actions::File);
            } else if(rep.startsWith("_S_H_O_R_T_C_U_T_")) {
                m_receivedShortcut = rep.last(rep.length()-17);
                handleAll.append(Actions::Shortcut);
            } else if(rep.startsWith("_S_E_T_T_I_N_G_")) {
                const QList<QByteArray> tmp = rep.last(rep.length()-15).split(':');
                m_receivedSetting[0] = tmp[0];
                m_receivedSetting[1] = tmp[1];
                handleAll.append(Actions::Setting);
            } else {
                const QList<QByteArray> _reps = rep.split('/');
                for(const QByteArray &r : _reps) {
                    handleAll.append(static_cast<Actions>(r.toInt()));
                }
            }
        }
        handleMessage(handleAll);
    }
    socket->close();
    delete socket;
}

void PQCSingleInstance::handleMessage(const QList<Actions> msg, bool includeFileProcessing) {

    qDebug() << "args: msg";
    qDebug() << "args: includeFileProcessing =" << includeFileProcessing;

    // NOTE
    // file processing is handled directly during startup for faster displaying of files
    // all others might need to be re-processed once the ui is ready
    // that's why this call is also triggered from qml at the end of startup

    QStringList allfiles;
    QStringList allfolders;

    for(const Actions &m : std::as_const(msg)) {

        switch(m) {

        case Actions::File:

            if(includeFileProcessing) {

                for(const QString &f : std::as_const(m_receivedFiles)) {

                    // sort by files and folders
                    // that way we can make sure to always load the first specified file as initial image

                    QFileInfo info(f);

                    if(!info.exists())
                        continue;

                    if(info.isFile()) {
                        qDebug() << "received file:" << f;
                        allfiles.append(f);
                    } else if(info.isDir()) {
                        qDebug() << "received folder:" << f;
                        allfolders.append(f);
                    }

                }
            }

            break;

        case Actions::Open:

            qDebug() << "request action: open";
            Q_EMIT PQCNotifyCPP::get().cmdOpen();
            break;

        case Actions::Show:

            qDebug() << "request action: show";
            Q_EMIT PQCNotifyCPP::get().cmdShow();
            break;

        case Actions::Hide:

            qDebug() << "request action: hide";
            Q_EMIT PQCNotifyCPP::get().cmdHide();
            break;

        case Actions::Quit:

            qDebug() << "request action: quit";
            Q_EMIT PQCNotifyCPP::get().cmdQuit();
            break;

        case Actions::Toggle:

            qDebug() << "request action: toggle";
            Q_EMIT PQCNotifyCPP::get().cmdToggle();
            break;

        case Actions::StartInTray:

            qDebug() << "request action: strat in tray";
            PQCNotifyCPP::get().setStartInTray(true);
            break;

        case Actions::Tray:

            qDebug() << "request action: tray";
            Q_EMIT PQCNotifyCPP::get().cmdTray(true);
            break;

        case Actions::NoTray:

            qDebug() << "request action: no tray";
            Q_EMIT PQCNotifyCPP::get().cmdTray(false);
            break;

        case Actions::Shortcut:

            qDebug() << "request action: shortcut:" << m_receivedShortcut;
            Q_EMIT PQCNotifyCPP::get().cmdShortcutSequence(m_receivedShortcut);
            break;

        case Actions::Debug:

            qDebug() << "request action: debug";
            PQCNotifyCPP::get().setDebug(true);
            break;

        case Actions::NoDebug:

            qDebug() << "request action: no debug";
            PQCNotifyCPP::get().setDebug(false);
            break;

        case Actions::Setting:

            qDebug() << "request action: settings update:" << m_receivedSetting;
            PQCNotifyCPP::get().setSettingUpdate({m_receivedSetting[0], m_receivedSetting[1]});
            break;

        default:
            qWarning() << "Unknown action received:" << static_cast<int>(m);

        }

    }

    if(includeFileProcessing) {
        // if we have files and/or folders that were passed on
        if(allfiles.length() > 0 || allfolders.length() > 0) {

            if(allfiles.length()+allfolders.length() > 1) {
                PQCNotifyCPP::get().setVirtualFiles(allfiles);
                PQCNotifyCPP::get().setVirtualFolders(allfolders);
            }

            PQCNotifyCPP::get().setFilePath(allfiles.length() ? allfiles[0] : allfolders[0]);

        }
    }

}

bool PQCSingleInstance::notify(QObject *obj, QEvent *e) {

    // this only lets the events pass that relate to the currently active window
    if(!qobject_cast<QWindow *>(obj))
        return QApplication::notify(obj, e);

    const QEvent::Type type = e->type();

    switch(type) {

        case QEvent::KeyPress: {
            QKeyEvent *ev = reinterpret_cast<QKeyEvent*>(e);
            Q_EMIT PQCNotifyCPP::get().keyPress(ev->key(), ev->modifiers());
            break;
        }
        case QEvent::KeyRelease: {
            QKeyEvent *ev = reinterpret_cast<QKeyEvent*>(e);
            Q_EMIT PQCNotifyCPP::get().keyRelease(ev->key(), ev->modifiers());
            break;
        }
        case QEvent::Leave: {
            Q_EMIT PQCNotifyCPP::get().mouseWindowExit();
            break;
        }
        case QEvent::Enter: {
            Q_EMIT PQCNotifyCPP::get().mouseWindowEnter();
            break;
        }
        default: {
            break;
        }

    }

    return QApplication::notify(obj, e);

}



PQCSingleInstance::~PQCSingleInstance() {
    if(socket != nullptr)
        delete socket;
    if(server != nullptr) {
        server->close();
        delete server;
    }
}

bool PQCSingleInstance::importConfig() {

    // no import is to be done
    const QString importFN = PQCConfigFiles::get().CONFIG_DIR() % "/to_be_imported.pqt";
    if(!QFile::exists(importFN))
        return true;

#ifdef PQMLIBARCHIVE
    qWarning() << "Performing requested import before startup";

    // All the config files to be imported
    QHash<QString,QString> allfiles;
    // the old settings file CAN be used by the new versions (but not the other way round)
    allfiles["CFG_SETTINGS_DB"] = PQCConfigFiles::get().USERSETTINGS_DB();
    allfiles["CFG_USERSETTINGS_DB"] = PQCConfigFiles::get().USERSETTINGS_DB();
    allfiles["CFG_CONTEXTMENU_DB"] = PQCConfigFiles::get().CONTEXTMENU_DB();
    allfiles["CFG_SHORTCUTS_DB"] = PQCConfigFiles::get().SHORTCUTS_DB();
    allfiles["CFG_IMAGEPLUGINS"] = "";

    // Create new archive handler
    struct archive *a = archive_read_new();

    // Read config file
    archive_read_support_format_all(a);
    archive_read_support_filter_all(a);

// Read file - if something went wrong, output error message and stop here
#ifdef Q_OS_WIN
    if(archive_read_open_filename_w(a, reinterpret_cast<const wchar_t*>(importFN.utf16()), 10240) != ARCHIVE_OK) {
#else
    QByteArray tmpPath = QFile::encodeName(importFN);
    if(archive_read_open_filename(a, tmpPath.constData(), 10240) != ARCHIVE_OK) {
#endif
        qWarning() << "ERROR: archive_read_open_filename() failed:" << archive_error_string(a);
        archive_read_close(a);
        archive_read_free(a);
        return false;
    }

    // Loop over entries in archive
    struct archive_entry *entry;
    while(archive_read_next_header(a, &entry) == ARCHIVE_OK) {

        // Read the current file entry
        // We use the '_w' variant here, as otherwise on Windows this call causes a segfault when a file in an archive contains non-latin characters
        // Also, if the archives is malformed or there is an encoding issue then it is possible that this may return a nullptr
        // and PhotoQt might crash if not handled properly -> check before converting to QString
        const wchar_t *wpath = archive_entry_pathname_w(entry);
        if(!wpath) continue;
        QString filenameinside = QString::fromWCharArray(wpath);

        if(allfiles.contains(filenameinside)) {

            // Find out the size of the data
            int64_t size = archive_entry_size(entry);

            if(size <= 0) {
                qWarning() << "Invalid size of file in archive:" << size;
                archive_read_close(a);
                archive_read_free(a);
                return false;
            }

            // Create a buffer of that size to hold the image data
            QByteArray data;
            data.resize(size);

            // And finally read the file into the buffer in chunks
            char* ptr = data.data();
            qint64 total = 0;
            while (total < size) {
                la_ssize_t chunk = archive_read_data(a, ptr + total, size - total);
                if(chunk < 0) {
                    qWarning() << "Invalid chunk read:" << archive_error_string(a);
                    archive_read_close(a);
                    archive_read_free(a);
                    return false;
                }

                if (chunk == 0) {
                    break;
                }

                total += chunk;
            }

            if(total != size) {
                qWarning() << QString("Failed to read file data, read size (%1) doesn't match expected size (%2).").arg(total).arg(size);
                archive_read_close(a);
                archive_read_free(a);
                return false;
            }

            if(filenameinside == "CFG_IMAGEPLUGINS") {

                // write to temporary file
                const QString ipArchive = PQCConfigFiles::get().CONFIG_DIR() % "/to_be_imported_imageplugins.pqt";
                QFile file(ipArchive);
                // overwrite old content
                if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
                    qWarning() << "ERROR: Unable to write temporary imageplugins archive... Skipping file!";
                    continue;
                }
                QDataStream out(&file);   // we will serialize the data into the file
                out.writeRawData(data, size);
                file.close();
                file.flush();

                if(!PQCHelper::unzipDirectory(ipArchive, PQCConfigFiles::get().CONFIG_DIR() % "/imageplugins/")) {
                    qWarning() << "Failed to import imageplugins config!";
                }

                file.remove();

            } else {

                // The output file...
                QFile file(allfiles[filenameinside]);
                // Overwrite old content
                if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
                    qWarning() << QString("ERROR: Unable to write new config file '%1'... Skipping file!").arg(allfiles[filenameinside]);
                    continue;
                }
                QDataStream out(&file);   // we will serialize the data into the file
                out.writeRawData(data, size);

                file.close();

            }

        }

    }

    // Close archive
    if(archive_read_close(a) != ARCHIVE_OK)
        qWarning() << "ERROR: archive_read_close() failed:" << archive_error_string(a);
    if(archive_read_free(a) != ARCHIVE_OK)
        qWarning() << "ERROR: archive_read_free() failed:" << archive_error_string(a);

#endif

    // no matter what we remove this temporary file to not keep re-attempting importing it every time
    if(!QFile::remove(importFN)) {
        qWarning() << "UNABLE TO REMOVE FILE TO BE IMPORTED:" << importFN;
        qWarning() << "It will likely be imported again at each subsequent start of PhotoQt unless it is deleted.";
    }

    return true;

}
