// SPDX-License-Identifier: GPL-3.0-only
/*
 *  Luna Launcher - Minecraft Launcher
 *  Copyright (C) 2025 AndreaFrederica <andreafrederica@outlook.com>
 *  Prism Launcher - Minecraft Launcher
 *  Copyright (C) 2024 Tayou <git@tayou.org>
 *  Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
 *
 *  This program 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, version 3.
 *
 *  This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "ThemeManager.h"

#include <QApplication>
#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QIcon>
#include <QImageReader>
#include <QSet>
#include <QStyle>
#include <QStyleFactory>
#include "Exception.h"
#include "ui/themes/BrightTheme.h"
#include "ui/themes/CatPack.h"
#include "ui/themes/CustomTheme.h"
#include "ui/themes/DarkTheme.h"
#include "ui/themes/ModernDarkTheme.h"
#include "ui/themes/SystemTheme.h"

#include "Application.h"
#include "BuildConfig.h"
#include "settings/SettingsObject.h"

ThemeManager::ThemeManager()
{
    QIcon::setFallbackThemeName(QIcon::themeName());
    QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path());

    themeDebugLog() << "Determining System Widget Theme...";
    const auto& style = QApplication::style();
    m_defaultStyle = style->objectName();
    themeDebugLog() << "System theme seems to be:" << m_defaultStyle;

    m_defaultPalette = QApplication::palette();

    initializeThemes();
    initializeCatPacks();
}

ThemeManager::~ThemeManager()
{
    stopSettingNewWindowColorsOnMac();
}

/// @brief Adds the Theme to the list of themes
/// @param theme The Theme to add
/// @return Theme ID
QString ThemeManager::addTheme(std::unique_ptr<ITheme> theme)
{
    QString id = theme->id();
    if (m_themes.find(id) == m_themes.end())
        m_themes.emplace(id, std::move(theme));
    else
        themeWarningLog() << "Theme(" << id << ") not added to prevent id duplication";
    return id;
}

/// @brief Gets the Theme from the List via ID
/// @param themeId Theme ID of theme to fetch
/// @return Theme at themeId
ITheme* ThemeManager::getTheme(QString themeId)
{
    return m_themes[themeId].get();
}

QString ThemeManager::addIconTheme(IconTheme theme)
{
    QString id = theme.id();
    if (m_icons.find(id) == m_icons.end())
        m_icons.emplace(id, std::move(theme));
    else
        themeWarningLog() << "IconTheme(" << id << ") not added to prevent id duplication";
    return id;
}

void ThemeManager::initializeThemes()
{
    // Icon themes
    initializeIcons();

    // Initialize widget themes
    initializeWidgets();
}

void ThemeManager::initializeIcons()
{
    // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies!
    // set icon theme search path!
    themeDebugLog() << "<> Initializing Icon Themes";

    for (const QString& id : builtinIcons) {
        IconTheme theme(id, QString(":/icons/%1").arg(id));
        if (!theme.load()) {
            themeWarningLog() << "Couldn't load built-in icon theme" << id;
            continue;
        }

        addIconTheme(std::move(theme));
        themeDebugLog() << "Loaded Built-In Icon Theme" << id;
    }

    if (!m_iconThemeFolder.mkpath("."))
        themeWarningLog() << "Couldn't create icon theme folder";
    themeDebugLog() << "Icon Theme Folder Path:" << m_iconThemeFolder.absolutePath();

    // Collect icon theme folders from both user data directory and launcher install directory
    QList<QDir> iconThemeFolders;
    iconThemeFolders.append(m_iconThemeFolder);

    // Add icon themes from launcher installation directory
    QString appDir = QCoreApplication::applicationDirPath();
    QStringList installIconPaths;
#if defined(Q_OS_WIN32)
    installIconPaths << FS::PathCombine(appDir, "resource", "iconthemes");
#elif defined(Q_OS_MACOS)
    // On macOS, appDir is .app/Contents/MacOS, resources are in Resources
    installIconPaths << FS::PathCombine(appDir, "..", "Resources", "iconthemes");
#else
    installIconPaths << FS::PathCombine(FS::PathCombine(appDir, "..", "share"), BuildConfig.LAUNCHER_NAME, "resources", "iconthemes");
    installIconPaths << FS::PathCombine(FS::PathCombine(appDir, "..", "share"), BuildConfig.LAUNCHER_APP_BINARY_NAME, "resources", "iconthemes");
    installIconPaths << "/usr/share/" + BuildConfig.LAUNCHER_NAME + "/resources/iconthemes";
    installIconPaths << "/usr/share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME + "/resources/iconthemes";
#endif
    for (const auto& installIconPath : installIconPaths) {
        QDir installIconDir(installIconPath);
        if (installIconDir.exists()) {
            iconThemeFolders.append(installIconDir);
        }
    }

    // Load icon themes from all collected folders (user themes take precedence)
    QSet<QString> loadedIconIds;
    for (const QDir& iconFolder : iconThemeFolders) {
        QDirIterator directoryIterator(iconFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot);
        while (directoryIterator.hasNext()) {
            QDir dir(directoryIterator.next());
            QString themeId = dir.dirName();

            // Skip if already loaded from user folder
            if (loadedIconIds.contains(themeId)) {
                continue;
            }

            IconTheme theme(themeId, dir.path());
            if (!theme.load())
                continue;

            addIconTheme(std::move(theme));
            themeDebugLog() << "Loaded Custom Icon Theme from" << dir.path();
            loadedIconIds.insert(themeId);
        }
    }

    // Update icon theme search paths to include both user and install directories
    QStringList searchPaths = QIcon::themeSearchPaths();
    for (const QDir& folder : iconThemeFolders) {
        QString path = folder.absolutePath();
        if (!searchPaths.contains(path)) {
            searchPaths.append(path);
        }
    }
    QIcon::setThemeSearchPaths(searchPaths);

    themeDebugLog() << "<> Icon themes initialized.";
}

void ThemeManager::initializeWidgets()
{
    themeDebugLog() << "<> Initializing Widget Themes";
    themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<SystemTheme>(m_defaultStyle, m_defaultPalette, true));
    auto darkThemeId = addTheme(std::make_unique<DarkTheme>());
    themeDebugLog() << "Loading Built-in Theme:" << darkThemeId;
    themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<BrightTheme>());
    themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<ModernDarkTheme>());

    themeDebugLog() << "<> Initializing System Widget Themes";
    QStringList styles = QStyleFactory::keys();
    for (auto& st : styles) {
#ifdef Q_OS_WINDOWS
        if (QSysInfo::productVersion() != "11" && st == "windows11") {
            continue;
        }
#endif
        themeDebugLog() << "Loading System Theme:" << addTheme(std::make_unique<SystemTheme>(st, m_defaultPalette, false));
    }

    // TODO: need some way to differentiate same name themes in different subdirectories
    //  (maybe smaller grey text next to theme name in dropdown?)

    if (!m_applicationThemeFolder.mkpath("."))
        themeWarningLog() << "Couldn't create theme folder";
    themeDebugLog() << "Theme Folder Path:" << m_applicationThemeFolder.absolutePath();

    // Collect theme folders from both user data directory and launcher install directory
    QList<QDir> themeFolders;
    themeFolders.append(m_applicationThemeFolder);

    // Add themes from launcher installation directory
    QString appDir = QCoreApplication::applicationDirPath();
    QStringList installThemePaths;
#ifdef Q_OS_WIN
    installThemePaths << FS::PathCombine(appDir, "resource", "themes");
#elif defined(Q_OS_MACOS)
    installThemePaths << FS::PathCombine(appDir, "..", "Resources", "themes");
#else
    installThemePaths << FS::PathCombine(FS::PathCombine(appDir, "..", "share"), BuildConfig.LAUNCHER_NAME, "resources", "themes");
    installThemePaths << FS::PathCombine(FS::PathCombine(appDir, "..", "share"), BuildConfig.LAUNCHER_APP_BINARY_NAME, "resources", "themes");
    installThemePaths << "/usr/share/" + BuildConfig.LAUNCHER_NAME + "/resources/themes";
    installThemePaths << "/usr/share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME + "/resources/themes";
    installThemePaths << "/usr/local/share/" + BuildConfig.LAUNCHER_NAME + "/resources/themes";
    installThemePaths << "/usr/local/share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME + "/resources/themes";
#endif
    for (const auto& installThemePath : installThemePaths) {
        QDir installThemeDir(installThemePath);
        if (installThemeDir.exists()) {
            themeFolders.append(installThemeDir);
        }
    }

    // Load themes from all collected folders (user themes take precedence)
    QSet<QString> loadedThemeIds;
    for (const QDir& themeFolder : themeFolders) {
        QDirIterator directoryIterator(themeFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot);
        while (directoryIterator.hasNext()) {
            QDir dir(directoryIterator.next());
            QString themeId = dir.dirName();

            // Skip if already loaded from user folder
            if (loadedThemeIds.contains(themeId)) {
                continue;
            }

            QFileInfo themeJson(dir.absoluteFilePath("theme.json"));
            if (themeJson.exists()) {
                // Load "theme.json" based themes
                themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath();
                addTheme(std::make_unique<CustomTheme>(getTheme(darkThemeId), themeJson, true));
                loadedThemeIds.insert(themeId);
            } else {
                // Load pure QSS Themes
                QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files);
                while (stylesheetFileIterator.hasNext()) {
                    QFile customThemeFile(stylesheetFileIterator.next());
                    QFileInfo customThemeFileInfo(customThemeFile);
                    themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath();
                    addTheme(std::make_unique<CustomTheme>(getTheme(darkThemeId), customThemeFileInfo, false));
                    loadedThemeIds.insert(themeId);
                    break; // Only load first QSS file per folder
                }
            }
        }
    }

    themeDebugLog() << "<> Widget themes initialized.";
}

#ifndef Q_OS_MACOS
void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) {}
void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) {}
void ThemeManager::stopSettingNewWindowColorsOnMac() {}
#endif

QList<IconTheme*> ThemeManager::getValidIconThemes()
{
    QList<IconTheme*> ret;
    ret.reserve(m_icons.size());
    for (auto&& [id, theme] : m_icons) {
        ret.append(&theme);
    }
    return ret;
}

QList<ITheme*> ThemeManager::getValidApplicationThemes()
{
    QList<ITheme*> ret;
    ret.reserve(m_themes.size());
    for (auto&& [id, theme] : m_themes) {
        ret.append(theme.get());
    }
    return ret;
}

QList<CatPack*> ThemeManager::getValidCatPacks()
{
    QList<CatPack*> ret;
    ret.reserve(m_catPacks.size());
    for (auto&& [id, theme] : m_catPacks) {
        ret.append(theme.get());
    }
    return ret;
}

bool ThemeManager::isValidIconTheme(const QString& id)
{
    return !id.isEmpty() && m_icons.find(id) != m_icons.end();
}

bool ThemeManager::isValidApplicationTheme(const QString& id)
{
    return !id.isEmpty() && m_themes.find(id) != m_themes.end();
}

QDir ThemeManager::getIconThemesFolder()
{
    return m_iconThemeFolder;
}

QDir ThemeManager::getApplicationThemesFolder()
{
    return m_applicationThemeFolder;
}

QDir ThemeManager::getCatPacksFolder()
{
    return m_catPacksFolder;
}

void ThemeManager::setIconTheme(const QString& name)
{
    if (m_icons.find(name) == m_icons.end()) {
        themeWarningLog() << "Tried to set invalid icon theme:" << name;
        return;
    }

    QIcon::setThemeName(name);
}

void ThemeManager::setApplicationTheme(const QString& name, bool initial)
{
    auto systemPalette = qApp->palette();
    auto themeIter = m_themes.find(name);
    if (themeIter != m_themes.end()) {
        auto& theme = themeIter->second;
        themeDebugLog() << "applying theme" << theme->name();
        theme->apply(initial);
        setTitlebarColorOfAllWindowsOnMac(qApp->palette().window().color());

        m_logColors = theme->logColorScheme();
    } else {
        themeWarningLog() << "Tried to set invalid theme:" << name;
    }
}

void ThemeManager::applyCurrentlySelectedTheme(bool initial)
{
    auto settings = APPLICATION->settings();
    setIconTheme(settings->get("IconTheme").toString());
    themeDebugLog() << "<> Icon theme set.";
    auto applicationTheme = settings->get("ApplicationTheme").toString();
    if (applicationTheme == "") {
        applicationTheme = m_defaultStyle;
    }
    setApplicationTheme(applicationTheme, initial);
    themeDebugLog() << "<> Application theme set.";
}

QString ThemeManager::getCatPack(QString catName)
{
    auto catIter = m_catPacks.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString());
    if (catIter != m_catPacks.end()) {
        auto& catPack = catIter->second;
        themeDebugLog() << "applying catpack" << catPack->id();
        return catPack->path();
    } else {
        themeWarningLog() << "Tried to get invalid catPack:" << catName;
    }

    return m_catPacks.begin()->second->path();
}

QString ThemeManager::addCatPack(std::unique_ptr<CatPack> catPack)
{
    QString id = catPack->id();
    if (m_catPacks.find(id) == m_catPacks.end())
        m_catPacks.emplace(id, std::move(catPack));
    else
        themeWarningLog() << "CatPack(" << id << ") not added to prevent id duplication";
    return id;
}

void ThemeManager::initializeCatPacks()
{
    QList<std::pair<QString, QString>> defaultCats{ { "kitteh", QObject::tr("Background Cat (from MultiMC)") },
                                                    { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") },
                                                    { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") },
                                                    { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } };
    for (auto [id, name] : defaultCats) {
        addCatPack(std::unique_ptr<CatPack>(new BasicCatPack(id, name)));
    }
    if (!m_catPacksFolder.mkpath("."))
        themeWarningLog() << "Couldn't create catpacks folder";
    themeDebugLog() << "CatPacks Folder Path:" << m_catPacksFolder.absolutePath();

    QStringList supportedImageFormats;
    for (auto format : QImageReader::supportedImageFormats()) {
        supportedImageFormats.append("*." + format);
    }

    // Collect catpack folders from both user data directory and launcher install directory
    QList<QDir> catPackFolders;
    catPackFolders.append(m_catPacksFolder);

    // Add catpacks from launcher installation directory
    QString appDir = QCoreApplication::applicationDirPath();
    QStringList installCatPaths;
#if defined(Q_OS_WIN32)
    installCatPaths << FS::PathCombine(appDir, "resource", "catpacks");
#elif defined(Q_OS_MACOS)
    installCatPaths << FS::PathCombine(appDir, "..", "Resources", "catpacks");
#else
    installCatPaths << FS::PathCombine(FS::PathCombine(appDir, "..", "share"), BuildConfig.LAUNCHER_NAME, "resources", "catpacks");
    installCatPaths << FS::PathCombine(FS::PathCombine(appDir, "..", "share"), BuildConfig.LAUNCHER_APP_BINARY_NAME, "resources", "catpacks");
    installCatPaths << "/usr/share/" + BuildConfig.LAUNCHER_NAME + "/resources/catpacks";
    installCatPaths << "/usr/share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME + "/resources/catpacks";
#endif
    for (const auto& installCatPath : installCatPaths) {
        QDir installCatDir(installCatPath);
        if (installCatDir.exists()) {
            catPackFolders.append(installCatDir);
        }
    }

    auto loadFiles = [this, supportedImageFormats](QDir dir) {
        // Load image files directly
        QDirIterator ImageFileIterator(dir.absoluteFilePath(""), supportedImageFormats, QDir::Files);
        while (ImageFileIterator.hasNext()) {
            QFile customCatFile(ImageFileIterator.next());
            QFileInfo customCatFileInfo(customCatFile);
            themeDebugLog() << "Loading CatPack from:" << customCatFileInfo.absoluteFilePath();
            addCatPack(std::unique_ptr<CatPack>(new FileCatPack(customCatFileInfo)));
        }
    };

    // Load catpacks from all collected folders (user catpacks take precedence)
    QSet<QString> loadedCatIds;
    for (const QDir& catFolder : catPackFolders) {
        loadFiles(catFolder);

        QDirIterator directoryIterator(catFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot);
        while (directoryIterator.hasNext()) {
            QDir dir(directoryIterator.next());
            QString catId = dir.dirName();

            // Skip if already loaded from user folder
            if (loadedCatIds.contains(catId)) {
                continue;
            }

            QFileInfo manifest(dir.absoluteFilePath("catpack.json"));
            if (manifest.isFile()) {
                try {
                    // Load background manifest
                    themeDebugLog() << "Loading background manifest from:" << manifest.absoluteFilePath();
                    addCatPack(std::unique_ptr<CatPack>(new JsonCatPack(manifest)));
                    loadedCatIds.insert(catId);
                } catch (const Exception& e) {
                    themeWarningLog() << "Couldn't load catpack json:" << e.cause();
                }
            } else {
                loadFiles(dir);
                loadedCatIds.insert(catId);
            }
        }
    }
}

void ThemeManager::refresh()
{
    m_themes.clear();
    m_icons.clear();
    m_catPacks.clear();

    initializeThemes();
    initializeCatPacks();
}
