// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-3.0-only

#include "controller.h"

#include <Quotient/connection.h>

#include <KLocalizedString>

#include <QGuiApplication>
#include <QTimer>

#include <signal.h>

#include <Quotient/events/roommemberevent.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>

#include "enums/roomsortparameter.h"
#include "general_logging.h"
#include "mediasizehelper.h"
#include "models/actionsmodel.h"
#include "models/messagecontentmodel.h"
#include "models/messagemodel.h"
#include "models/roomlistmodel.h"
#include "models/roomtreemodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "proxycontroller.h"
#include "roommanager.h"

#if !defined(Q_OS_ANDROID)
#include "trayicon.h"
#endif

#ifdef HAVE_KUNIFIEDPUSH
#include <kunifiedpush/connector.h>
#endif

using namespace Quotient;

static std::function<bool(const RoomEvent *)> hiddenEventFilter = [](const RoomEvent *event) -> bool {
    if (event->isStateEvent() && !NeoChatConfig::showStateEvent()) {
        return true;
    }
    if (const auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
        if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::showLeaveJoinEvent()) {
            return true;
        }
        if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showRename()) {
            return true;
        }
        if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showAvatarUpdate()) {
            return true;
        }
    }
    return false;
};

Controller::Controller(QObject *parent)
    : QObject(parent)
    , m_config(KSharedConfig::openStateConfig())
    , m_mediaGroup(KConfigGroup(m_config, u"Media"_s))
{
    Connection::setRoomType<NeoChatRoom>();

    Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
    connect(NeoChatConfig::self(), &NeoChatConfig::PreferUsingEncryptionChanged, this, [] {
        Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
    });

    NeoChatConnection::setGlobalUrlPreviewDefault(NeoChatConfig::showLinkPreview());
    connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this] {
        NeoChatConnection::setGlobalUrlPreviewDefault(NeoChatConfig::showLinkPreview());
        Q_EMIT globalUrlPreviewDefaultChanged();
    });

    NeoChatConnection::setKeywordPushRuleDefault(static_cast<PushRuleAction::Action>(NeoChatConfig::keywordPushRuleDefault()));
    connect(NeoChatConfig::self(), &NeoChatConfig::KeywordPushRuleDefaultChanged, this, [] {
        NeoChatConnection::setKeywordPushRuleDefault(static_cast<PushRuleAction::Action>(NeoChatConfig::keywordPushRuleDefault()));
    });

    ActionsModel::setAllowQuickEdit(NeoChatConfig::allowQuickEdit());
    connect(NeoChatConfig::self(), &NeoChatConfig::AllowQuickEditChanged, this, []() {
        ActionsModel::setAllowQuickEdit(NeoChatConfig::allowQuickEdit());
    });

    MessageModel::setHiddenFilter(hiddenEventFilter);
    RoomListModel::setHiddenFilter(hiddenEventFilter);
    RoomTreeModel::setHiddenFilter(hiddenEventFilter);
    NeoChatRoom::setHiddenFilter(hiddenEventFilter);

    MediaSizeHelper::setMaxSize(NeoChatConfig::mediaMaxWidth(), NeoChatConfig::mediaMaxHeight());
    connect(NeoChatConfig::self(), &NeoChatConfig::MediaMaxWidthChanged, this, []() {
        MediaSizeHelper::setMaxSize(NeoChatConfig::mediaMaxWidth(), NeoChatConfig::mediaMaxHeight());
    });
    connect(NeoChatConfig::self(), &NeoChatConfig::MediaMaxHeightChanged, this, []() {
        MediaSizeHelper::setMaxSize(NeoChatConfig::mediaMaxWidth(), NeoChatConfig::mediaMaxHeight());
    });

    RoomSortParameter::setSortOrder(static_cast<RoomSortOrder::Order>(NeoChatConfig::sortOrder()));
    connect(NeoChatConfig::self(), &NeoChatConfig::SortOrderChanged, this, []() {
        RoomSortParameter::setSortOrder(static_cast<RoomSortOrder::Order>(NeoChatConfig::sortOrder()));
    });

    QList<RoomSortParameter::Parameter> configParamList;
    const auto intList = NeoChatConfig::customSortOrder();
    std::transform(intList.constBegin(), intList.constEnd(), std::back_inserter(configParamList), [](int param) {
        return static_cast<RoomSortParameter::Parameter>(param);
    });
    RoomSortParameter::setCustomSortOrder(configParamList);
    connect(NeoChatConfig::self(), &NeoChatConfig::CustomSortOrderChanged, this, []() {
        QList<RoomSortParameter::Parameter> configParamList;
        const auto intList = NeoChatConfig::customSortOrder();
        std::transform(intList.constBegin(), intList.constEnd(), std::back_inserter(configParamList), [](int param) {
            return static_cast<RoomSortParameter::Parameter>(param);
        });
        RoomSortParameter::setCustomSortOrder(configParamList);
    });

    MessageContentModel::setSetMediaHidden([this](const QString &eventId, bool hidden) {
        auto list = m_mediaGroup.readEntry(u"HiddenEvents"_s, QStringList());
        if (hidden) {
            list.push_back(eventId);
        } else {
            list.removeAll(eventId);
        }
        m_mediaGroup.writeEntry(u"HiddenEvents"_s, list);
    });
    MessageContentModel::setMediaShouldBeHidden([this](const QString &eventId) {
        return NeoChatConfig::hideImages() || m_mediaGroup.readEntry(u"HiddenEvents"_s, QStringList()).contains(eventId);
    });

    ProxyController::instance().setApplicationProxy();

#ifndef Q_OS_ANDROID
    setQuitOnLastWindowClosed();
    connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
#endif

    connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
#ifndef Q_OS_ANDROID
        delete m_trayIcon;
#endif
        NeoChatConfig::self()->save();
    });

#ifndef Q_OS_WINDOWS
    const auto unixExitHandler = [](int) -> void {
        QCoreApplication::quit();
    };

    const int quitSignals[] = {SIGQUIT, SIGINT, SIGTERM, SIGHUP};

    sigset_t blockingMask;
    sigemptyset(&blockingMask);
    for (const auto sig : quitSignals) {
        sigaddset(&blockingMask, sig);
    }

    struct sigaction sa;
    sa.sa_handler = unixExitHandler;
    sa.sa_mask = blockingMask;
    sa.sa_flags = 0;

    for (auto sig : quitSignals) {
        sigaction(sig, &sa, nullptr);
    }
#endif

#ifdef HAVE_KUNIFIEDPUSH
    auto connector = new KUnifiedPush::Connector(QGuiApplication::desktopFileName());
    connect(connector, &KUnifiedPush::Connector::endpointChanged, this, [this](const QString &endpoint) {
        m_endpoint = endpoint;
        if (!m_accountManager) {
            return;
        }
        for (auto &quotientConnection : m_accountManager->accounts()->accounts()) {
            auto connection = dynamic_cast<NeoChatConnection *>(quotientConnection);
            connection->setupPushNotifications(endpoint);
        }
    });
    m_endpoint = connector->endpoint();

    connector->registerClient(
        i18nc("The reason for using push notifications, as in: '[Push notifications are used for] Receiving notifications for new messages'",
              "Receiving push notifications"));

#endif
}

Controller &Controller::instance()
{
    static Controller _instance;
    return _instance;
}

void Controller::setAccountManager(AccountManager *manager)
{
    if (manager == m_accountManager) {
        return;
    }

    if (m_accountManager) {
        m_accountManager->disconnect(this);
    }

    m_accountManager = manager;

    if (!m_accountManager) {
        return;
    }

    connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured);
    connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged);
    connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection);
    connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection);
    connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection);
}

void Controller::initConnection(NeoChatConnection *connection)
{
    if (!connection) {
        return;
    }

    connect(
        connection,
        &NeoChatConnection::syncDone,
        this,
        [this, connection] {
            if (!m_endpoint.isEmpty()) {
                connection->setupPushNotifications(m_endpoint);
            }
        },
        Qt::SingleShotConnection);
    connect(connection, &NeoChatConnection::syncDone, this, [this, connection]() {
        m_notificationsManager.handleNotifications(connection);
    });
    connect(this, &Controller::globalUrlPreviewDefaultChanged, connection, &NeoChatConnection::globalUrlPreviewEnabledChanged);
    connect(connection, &NeoChatConnection::roomAboutToBeLeft, &RoomManager::instance(), &RoomManager::roomLeft);
    Q_EMIT connectionAdded(connection);
}

void Controller::teardownConnection(NeoChatConnection *connection)
{
    if (!connection) {
        return;
    }

    connection->disconnect(this);
    Q_EMIT connectionDropped(connection);
}

void Controller::initActiveConnection(NeoChatConnection *oldConnection, NeoChatConnection *newConnection)
{
    if (oldConnection) {
        oldConnection->disconnect(this);
    }

    if (newConnection) {
        connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
        connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);

        // Refresh and update manually, in case we init too late for the badge count to actually change.
        newConnection->refreshBadgeNotificationCount();
        updateBadgeNotificationCount(newConnection->badgeNotificationCount());
    }
    Q_EMIT activeConnectionChanged(newConnection);
}

bool Controller::supportSystemTray() const
{
#ifdef Q_OS_ANDROID
    return false;
#else
    QStringList unsupportedPlatforms{u"GNOME"_s, u"Pantheon"_s};
    return !unsupportedPlatforms.contains(QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP")));
#endif
}

void Controller::setQuitOnLastWindowClosed()
{
#ifndef Q_OS_ANDROID
    if (supportSystemTray() && NeoChatConfig::self()->systemTray()) {
        m_trayIcon = new TrayIcon(this);
        m_trayIcon->show();
    } else if (m_trayIcon) {
        delete m_trayIcon;
    }
#endif
}

NeoChatConnection *Controller::activeConnection() const
{
    if (!m_accountManager) {
        return nullptr;
    }
    return m_accountManager->activeConnection();
}

void Controller::setActiveConnection(NeoChatConnection *connection)
{
    if (!m_accountManager) {
        return;
    }
    m_accountManager->setActiveConnection(connection);
}

QStringList Controller::accountsLoading() const
{
    if (!m_accountManager) {
        return {};
    }
    return m_accountManager->accountsLoading();
}

void Controller::listenForNotifications()
{
#ifdef HAVE_KUNIFIEDPUSH
    auto connector = new KUnifiedPush::Connector(QGuiApplication::desktopFileName());

    connect(connector, &KUnifiedPush::Connector::messageReceived, [](const QByteArray &data) {
        NotificationsManager::postPushNotification(data);
    });

    // Process events before quitting.
    QTimer::singleShot(std::chrono::seconds(5), qGuiApp, &QCoreApplication::quit);

    connector->registerClient(i18n("Receiving push notifications"));
#endif
}

void Controller::clearInvitationNotification(const QString &roomId)
{
    m_notificationsManager.clearInvitationNotification(roomId);
}

void Controller::updateBadgeNotificationCount(int count)
{
    qGuiApp->setBadgeNumber(count);
}

bool Controller::isFlatpak() const
{
#ifdef NEOCHAT_FLATPAK
    return true;
#else
    return false;
#endif
}

AccountRegistry *Controller::accounts()
{
    return m_accountManager->accounts();
}

QString Controller::loadFileContent(const QString &path)
{
    QUrl url(path);
    QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
    if (!file.open(QFile::ReadOnly)) {
        qCWarning(GENERAL) << "Failed to open file" << path;
        return {};
    }
    return QString::fromLatin1(file.readAll());
}

void Controller::removeConnection(const QString &userId) const
{
    m_accountManager->dropConnection(userId);
}

void Controller::revertToDefaultConfig()
{
    const auto config = NeoChatConfig::self();
    config->setDefaults();
    config->save();
}

int Controller::libquotientMinorVersion()
{
    return Quotient_VERSION_MINOR;
}

QString Controller::translateToSaveDirectory(const QUrl &selectedFile, const QUrl &currentFolder) const
{
    // Turn paths like file:///run/user/1000/doc/9d60121c/love2.png to file:///run/user/1000/doc/9d60121c
    // We want to be able to use the currentFolder property, but this currently isn't possible.
    if (isFlatpak()) {
        return selectedFile.adjusted(QUrl::RemoveFilename).toString();
    }

    return currentFolder.toString();
}

#include "moc_controller.cpp"
