/* Copyright 2022 the SumatraPDF project authors (see AUTHORS file).
   License: GPLv3 */

#include "utils/BaseUtil.h"
#include "utils/Dpi.h"
#include "utils/FileUtil.h"
#include "utils/FileWatcher.h"
#include "utils/UITask.h"
#include "utils/ScopedWin.h"
#include "utils/WinUtil.h"
#include "utils/Timer.h"

#include "wingui/UIModels.h"

#include "Settings.h"
#include "Commands.h"
#include "DisplayMode.h"
#include "DocController.h"
#include "EngineBase.h"
#include "EngineAll.h"
#include "SumatraConfig.h"
#include "DisplayModel.h"
#include "FileHistory.h"
#include "GlobalPrefs.h"
#include "ProgressUpdateUI.h"
#include "SumatraPDF.h"
#include "WindowTab.h"
#include "ExternalViewers.h"
#include "Flags.h"
#include "MainWindow.h"
#include "AppSettings.h"
#include "AppTools.h"
#include "Favorites.h"
#include "Toolbar.h"
#include "Translations.h"
#include "Accelerators.h"
#include "Theme.h"
#include "TextToSpeech.h"

#include "utils/Log.h"
#include <Notifications.h>

// workaround for OnMenuExit
// if this flag is set, CloseWindow will not save prefs before closing the window.
bool gDontSaveSettings = false;
HANDLE gInstanceMutex = nullptr;

static bool ApplyReadAloudVoiceFromSettings() {
    if (!gGlobalPrefs) {
        return false;
    }

    const char* voiceId = gGlobalPrefs->readAloudVoiceId;
    if (str::IsEmpty(voiceId)) {
        TtsSetVoiceById("");
        return false;
    }

    if (str::Eq(voiceId, kTtsSmartBilingualVoiceId) || str::Eq(voiceId, kTtsSmartOnlineBilingualVoiceId)) {
        // pseudo voice: actual voices are picked per chunk while speaking
        TtsSetVoiceById("");
        return false;
    }

    if (!TtsSetVoiceById(voiceId)) {
        logf("ApplyReadAloudVoiceFromSettings: voice '%s' not available, using system default\n", voiceId);
        str::ReplaceWithCopy(&gGlobalPrefs->readAloudVoiceId, nullptr);
        TtsSetVoiceById("");
        return true;
    }
    return false;
}

static void ApplyReadAloudSpeakingRateFromSettings() {
    if (!gGlobalPrefs) {
        return;
    }

    // Migrate legacy single rate into per-language rates when both are still default.
    float legacy = gGlobalPrefs->readAloudSpeakingRate;
    float zh = gGlobalPrefs->readAloudSpeakingRateZh;
    float en = gGlobalPrefs->readAloudSpeakingRateEn;
    if (legacy > 0 && legacy != 1.0f && zh == 1.0f && en == 1.0f) {
        gGlobalPrefs->readAloudSpeakingRateZh = legacy;
        gGlobalPrefs->readAloudSpeakingRateEn = legacy;
        zh = legacy;
        en = legacy;
    }
    if (zh <= 0) {
        zh = 1.0f;
        gGlobalPrefs->readAloudSpeakingRateZh = zh;
    }
    if (en <= 0) {
        en = 1.0f;
        gGlobalPrefs->readAloudSpeakingRateEn = en;
    }
    // Start with Chinese rate; speak-chunk logic switches per language.
    TtsSetSpeakingRate(zh);
}

static bool IsValidAiChatProvider(const char* provider) {
    return str::EqI(provider, "doubao") || str::EqI(provider, "deepseek") || str::EqI(provider, "chatgpt");
}

static void MigrateAiChatProvider(const char* settingsRaw) {
    if (!gGlobalPrefs) {
        return;
    }
    if (!IsValidAiChatProvider(gGlobalPrefs->aiChatProvider)) {
        str::ReplaceWithCopy(&gGlobalPrefs->aiChatProvider, "doubao");
    }
    if (!gGlobalPrefs->aiChatUseDeepSeekInsteadOfDoubao) {
        return;
    }
    if (settingsRaw && str::Find(settingsRaw, "AiChatProvider")) {
        gGlobalPrefs->aiChatUseDeepSeekInsteadOfDoubao = false;
        return;
    }
    str::ReplaceWithCopy(&gGlobalPrefs->aiChatProvider, "deepseek");
    gGlobalPrefs->aiChatUseDeepSeekInsteadOfDoubao = false;
}

// SumatraPDF.cpp
extern void RememberDefaultWindowPosition(MainWindow* win);

static WatchedFile* gWatchedSettingsFile = nullptr;

struct UiFontsAtDpi {
    int dpi = 0;
    HFONT appFont = nullptr;
    HFONT menuFont = nullptr;
    HFONT treeFont = nullptr;
    HFONT biggerFont = nullptr;
};

static Vec<UiFontsAtDpi> gFontsAtDpi;

static UiFontsAtDpi* FindFontsAtDpi(int dpi) {
    for (size_t i = 0; i < gFontsAtDpi.size(); i++) {
        if (gFontsAtDpi[i].dpi == dpi) {
            return &gFontsAtDpi[i];
        }
    }
    return nullptr;
}

static HWND GetDefaultUiFontHwnd() {
    HWND fg = GetForegroundWindow();
    if (fg) {
        MainWindow* win = FindMainWindowByHwnd(fg);
        if (win) {
            return win->hwndFrame;
        }
    }
    if (gWindows.size() > 0) {
        return gWindows.at(0)->hwndFrame;
    }
    return nullptr;
}

static int UiFontDpiForHwnd(HWND hwnd) {
    if (hwnd) {
        return DpiGet(hwnd);
    }
    return 96;
}

// TODO: if font sizes change, would need to re-layout the app
static void ResetCachedFonts() {
    InvalidateUiFonts();
}

void InvalidateUiFonts() {
    for (size_t i = 0; i < gFontsAtDpi.size(); i++) {
        if (gFontsAtDpi[i].menuFont) {
            DeleteFont(gFontsAtDpi[i].menuFont);
            gFontsAtDpi[i].menuFont = nullptr;
        }
    }
    gFontsAtDpi.Reset();
    DeleteCreatedFonts();
}

// number of weeks past since 2011-01-01
static int GetWeekCount() {
    SYSTEMTIME date20110101{};
    date20110101.wYear = 2011;
    date20110101.wMonth = 1;
    date20110101.wDay = 1;
    FILETIME origTime, currTime;
    BOOL ok = SystemTimeToFileTime(&date20110101, &origTime);
    ReportIf(!ok);
    GetSystemTimeAsFileTime(&currTime);
    return (currTime.dwHighDateTime - origTime.dwHighDateTime) / 1408;
    // 1408 == (10 * 1000 * 1000 * 60 * 60 * 24 * 7) / (1 << 32)
}

static int cmpFloat(const void* a, const void* b) {
    return *(float*)a < *(float*)b ? -1 : *(float*)a > *(float*)b ? 1 : 0;
}

TempStr GetSettingsFileNameTemp() {
    return str::DupTemp("SumatraPDF-settings.txt");
}

// this could be virtual path when running in app store
TempStr GetSettingsPathTemp() {
    return GetPathInAppDataDirTemp(GetSettingsFileNameTemp());
}

static void setMin(int& i, int minVal) {
    if (i < minVal) {
        i = minVal;
    }
}

static void SetCommandNameAndShortcut(CustomCommand* cmd, const char* name, const char* key) {
    if (!cmd) {
        return;
    }
    cmd->name = str::IsEmptyOrWhiteSpace(name) ? nullptr : str::Dup(name);
    if (str::IsEmptyOrWhiteSpace(key)) {
        return;
    }
    if (!IsValidShortcutString(key)) {
        logf("SetCommandNameAndShortcut: '%s' is not a valid shortcut for '%s'\n", key, cmd->definition);
        MaybeDelayedWarningNotification("'%s' is not a valid shortcut for '%s'", key, cmd->definition);
        return;
    }
    cmd->key = str::Dup(key);
}

/* for every selection handler defined by user in advanced settings, create
    a command that will be inserted into a menu item */
static void CreateSelectionHandlerCommands() {
    if (!HasPermission(Perm::InternetAccess) || !HasPermission(Perm::CopySelection)) {
        // TODO: when we add exe handlers, only filter the URL ones
        return;
    }

    for (auto& sh : *gGlobalPrefs->selectionHandlers) {
        if (!sh || !sh->url || !sh->name) {
            // can happen for bad selection handler definition
            continue;
        }
        if (str::IsEmptyOrWhiteSpace(sh->url) || str::IsEmptyOrWhiteSpace(sh->name)) {
            continue;
        }

        CommandArg* args = NewStringArg(kCmdArgURL, sh->url);
        auto cmd = CreateCustomCommand(sh->url, CmdSelectionHandler, args);
        SetCommandNameAndShortcut(cmd, sh->name, sh->key);
    }
}

static void CreateExternalViewersCommands() {
    for (ExternalViewer* ev : *gGlobalPrefs->externalViewers) {
        if (!ev || str::IsEmptyOrWhiteSpace(ev->commandLine)) {
            continue;
        }
        CommandArg* args = NewStringArg(kCmdArgCommandLine, ev->commandLine);
        if (!str::IsEmptyOrWhiteSpace(ev->filter)) {
            auto arg = NewStringArg(kCmdArgFilter, ev->filter);
            InsertArg(&args, arg);
        }
        if (!str::IsEmptyOrWhiteSpace(ev->toolbarText)) {
            auto arg = NewStringArg(kCmdArgToolbarText, ev->toolbarText);
            InsertArg(&args, arg);
        }
        auto cmd = CreateCustomCommand("", CmdViewWithExternalViewer, args);
        SetCommandNameAndShortcut(cmd, ev->name, ev->key);
    }
}

static void CreateZoomCommands() {
    auto prefs = gGlobalPrefs;
    delete prefs->zoomLevelsCmdIds;
    int n = prefs->zoomLevels->Size();
    if (n <= 0) {
        return;
    }
    Vec<int>* cmdIds = new Vec<int>(n);
    prefs->zoomLevelsCmdIds = cmdIds;
    for (int i = 0; i < n; i++) {
        float zoomLevel = prefs->zoomLevels->At(i);
        CommandArg* arg = NewFloatArg(kCmdArgLevel, zoomLevel);
        auto cmd = CreateCustomCommand("CmdZoomCustom", CmdZoomCustom, arg);
        cmdIds->InsertAt(i, cmd->id);
    }
}

static void CreateCustomShortcuts() {
    for (Shortcut* shortcut : *gGlobalPrefs->shortcuts) {
        auto cmd = CreateCommandFromDefinition(shortcut->cmd);
        if (!cmd) {
            continue;
        }
        // if command already has a key bound (from a previous shortcut entry),
        // create a separate command so both shortcuts work
        if (cmd->key && !str::IsEmptyOrWhiteSpace(shortcut->key)) {
            cmd = CreateCustomCommand(shortcut->cmd, cmd->origId, nullptr);
        }
        shortcut->cmdId = cmd->id;
        SetCommandNameAndShortcut(cmd, shortcut->name, shortcut->key);
    }
}

/* Caller needs to CleanUpSettings() */
bool LoadSettings() {
    ReportIf(gGlobalPrefs);

    auto timeStart = TimeGet();

    GlobalPrefs* gprefs = nullptr;
    TempStr settingsPath = GetSettingsPathTemp();
    AutoFreeStr settingsRaw;
    {
        ByteSlice prefsData = file::ReadFile(settingsPath);
        if (prefsData.size() > 0) {
            settingsRaw.Set(str::Dup(prefsData));
        }

        gGlobalPrefs = NewGlobalPrefs(settingsRaw.Get());
        ReportIf(!gGlobalPrefs);
        gprefs = gGlobalPrefs;
        prefsData.Free();
    }
    MigrateAiChatProvider(settingsRaw.Get());

    if (trans::ValidateLangCode(gprefs->uiLanguage)) {
        SetCurrentLang(gprefs->uiLanguage);
    } else {
        // guess the ui language on first start
        str::ReplaceWithCopy(&gprefs->uiLanguage, trans::DetectUserLang());
        SetCurrentLang(gprefs->uiLanguage);
    }

    gprefs->lastPrefUpdate = file::GetModificationTime(settingsPath);
    gprefs->defaultDisplayModeEnum = DisplayModeFromString(gprefs->defaultDisplayMode, DisplayMode::Automatic);
    gprefs->defaultZoomFloat = ZoomFromString(gprefs->defaultZoom, kZoomActualSize);
    ReportIf(!IsValidZoom(gprefs->defaultZoomFloat));
    if (gprefs->imageUI.defaultZoom) {
        gprefs->imageUI.defaultZoomFloat = ZoomFromString(gprefs->imageUI.defaultZoom, 0);
    }

    int weekDiff = GetWeekCount() - gprefs->openCountWeek;
    gprefs->openCountWeek = GetWeekCount();
    if (weekDiff > 0) {
        // "age" openCount statistics (cut in in half after every week)
        for (FileState* fs : *gprefs->fileStates) {
            fs->openCount >>= weekDiff;
        }
    }

    // make sure that zoom levels are in the order expected by DisplayModel
    gprefs->zoomLevels->Sort(cmpFloat);
    while (gprefs->zoomLevels->size() > 0 && gprefs->zoomLevels->at(0) < kZoomMin) {
        gprefs->zoomLevels->PopAt(0);
    }
    while (gprefs->zoomLevels->size() > 0 && gprefs->zoomLevels->Last() > kZoomMax) {
        gprefs->zoomLevels->Pop();
    }

    // sanitize WindowMargin and PageSpacing values
    // https://github.com/sumatrapdfreader/sumatrapdf/issues/1899
    {
        auto&& m = gprefs->fixedPageUI.windowMargin;
        setMin(m.bottom, 0);
        setMin(m.top, 0);
        setMin(m.left, 0);
        setMin(m.right, 0);
    }
    {
        auto&& m = gprefs->comicBookUI.windowMargin;
        setMin(m.bottom, 0);
        setMin(m.top, 0);
        setMin(m.left, 0);
        setMin(m.right, 0);
    }
    {
        auto&& s = gprefs->fixedPageUI.pageSpacing;
        setMin(s.dx, 0);
        setMin(s.dy, 0);
    }
    {
        auto&& s = gprefs->comicBookUI.pageSpacing;
        setMin(s.dx, 0);
        setMin(s.dy, 0);
    }
    setMin(gprefs->tabWidth, 60);
    setMin(gprefs->sidebarDx, 0);
    setMin(gprefs->tocDy, 0);
    setMin(gprefs->treeFontSize, 0);
    if (gprefs->toolbarSize == 0) {
        gprefs->toolbarSize = 18; // same as kDefaultIconSize in Toolbar.cpp
    }
    setMinMax(gprefs->toolbarSize, 8, 64);
    setMinMax(gprefs->annotations.freeTextOpacity, 0, 100);

    if (seqstrings::StrToIdxIS(gScrollbarModeNames, gprefs->scrollbars) < 0) {
        str::ReplaceWithCopy(&gprefs->scrollbars, "windows");
    }

    if (!gprefs->treeFontName) {
        gprefs->treeFontName = const_cast<char*>("automatic");
    }

    // TODO: verify that all states have a non-nullptr file path?
    gFileHistory.UpdateStatesSource(gprefs->fileStates);
    //    auto fontName = ToWStrTemp(gprefs->fixedPageUI.ebookFontName);
    //    SetDefaultEbookFont(fontName.Get(), gprefs->fixedPageUI.ebookFontSize);

    ResetCachedFonts();

    // re-create commands
    FreeCustomCommands();
    // Note: some are also created in ReCreateSumatraAcceleratorTable()
    CreateZoomCommands();
    CreateThemeCommands();
    CreateExternalViewersCommands();
    CreateSelectionHandlerCommands();
    CreateCustomShortcuts();

    // re-create accelerators
    FreeAcceleratorTables();
    CreateSumatraAcceleratorTable();

    SetCurrentThemeFromSettings();
    for (MainWindow* win : gWindows) {
        ReCreateToolbar(win);
        RelayoutWindow(win);
        win->RedrawAll(true);
    }

    bool readAloudVoiceCleared = ApplyReadAloudVoiceFromSettings();
    ApplyReadAloudSpeakingRateFromSettings();

    if (!file::Exists(settingsPath)) {
        SaveSettings();
    } else if (readAloudVoiceCleared) {
        SaveSettings();
    }

    logf("LoadSettings('%s') took %.2f ms\n", settingsPath, TimeSinceInMs(timeStart));
    return true;
}

static TabState* CloneTabState(const TabState* src) {
    TabState* dst = (TabState*)AllocStruct<TabState>();
    dst->filePath = str::Dup(src->filePath);
    dst->displayMode = str::Dup(src->displayMode);
    dst->pageNo = src->pageNo;
    dst->zoom = str::Dup(src->zoom);
    dst->rotation = src->rotation;
    dst->scrollPos = src->scrollPos;
    dst->showToc = src->showToc;
    dst->tocState = new Vec<int>(*src->tocState);
    return dst;
}

Vec<SessionData*>* gInitialSessionData = nullptr;

static void RememberSessionState() {
    Vec<SessionData*>* sessionState = gGlobalPrefs->sessionData;
    FreeSessionDataVec(sessionState);

    if (!SettingsRememberOpenedFiles()) {
        return;
    }

    for (auto* win : gWindows) {
        SessionData* windowState = NewSessionData();
        for (WindowTab* tab : win->Tabs()) {
            if (!tab->filePath) {
                // home page tab
                continue;
            }
            const char* fp = tab->filePath;
            if (!tab->ctrl) {
                // lazy loading, file not loaded into a tab
                // use the saved state from previous session
                // note: might stil have issues if multiple tabs with same file
                bool didFind = false;
                if (!gInitialSessionData) {
                    continue;
                }
                int nWindows = gInitialSessionData->Size();
                for (int i = 0; i < nWindows && !didFind; i++) {
                    SessionData* psd = gInitialSessionData->At(i);
                    int nTabs = psd->tabStates->Size();
                    for (int j = 0; j < nTabs; j++) {
                        TabState* pts = psd->tabStates->At(j);
                        if (str::Eq(pts->filePath, fp)) {
                            TabState* ts = CloneTabState(pts);
                            windowState->tabStates->Append(ts);
                            didFind = true;
                            break;
                        }
                    }
                }
                if (!didFind) {
                    logf("RememberSessionState: didn't find state for file '%s'\n", fp ? fp : "(none)");
                }
                continue;
            }
            FileState* fs = NewFileState(fp);
            tab->ctrl->GetDisplayState(fs);
            fs->showToc = tab->showToc;
            *fs->tocState = tab->tocState;
            TabState* ts = NewTabState(fs);
            windowState->tabStates->Append(ts);
            DeleteFileState(fs);
        }
        if (windowState->tabStates->Size() == 0) {
            FreeSessionData(windowState);
            continue;
        }
        windowState->tabIndex = win->GetTabIdx(win->CurrentTab()) + 1;
        if (windowState->tabIndex < 0) {
            windowState->tabIndex = 0;
        }
        // TODO: allow recording this state without changing gGlobalPrefs
        RememberDefaultWindowPosition(win);
        windowState->windowState = gGlobalPrefs->windowState;
        windowState->windowPos = gGlobalPrefs->windowPos;
        windowState->sidebarDx = gGlobalPrefs->sidebarDx;
        sessionState->Append(windowState);
    }
}

// called whenever global preferences change or a file is
// added or removed from gFileHistory (in order to keep
// the list of recently opened documents in sync)
bool SaveSettings() {
    if (gDontSaveSettings) {
        // if we are exiting the application by File->Exit,
        // OnMenuExit will have called SaveSettings() already
        // and we skip the call here to avoid saving incomplete session info
        // (because some windows might have been closed already)
        return true;
    }

    // don't save preferences without the proper permission
    if (!HasPermission(Perm::SavePreferences)) {
        return false;
    }
    logf("SaveSettings\n");
    // update display states for all tabs
    // we snapshot the list because SaveSettings() can be called re-entrantly
    // (e.g. from LoadDocumentFinish while other documents are still loading/closing)
    for (MainWindow* win : gWindows) {
        Vec<WindowTab*> tabs = win->Tabs();
        for (WindowTab* tab : tabs) {
            UpdateTabFileDisplayStateForTab(tab);
        }
    }
    RememberSessionState();

    // remove entries which should (no longer) be remembered
    gFileHistory.Purge(!gGlobalPrefs->rememberStatePerDocument);
    // update display mode and zoom fields from internal values
    str::ReplaceWithCopy(&gGlobalPrefs->defaultDisplayMode, DisplayModeToString(gGlobalPrefs->defaultDisplayModeEnum));
    ZoomToString(&gGlobalPrefs->defaultZoom, gGlobalPrefs->defaultZoomFloat, nullptr);
    if (gGlobalPrefs->imageUI.defaultZoomFloat != 0) {
        ZoomToString(&gGlobalPrefs->imageUI.defaultZoom, gGlobalPrefs->imageUI.defaultZoomFloat, nullptr);
    }

    TempStr path = GetSettingsPathTemp();
    ReportIf(!path);
    if (!path) {
        return false;
    }
    ByteSlice prevPrefs = file::ReadFile(path);
    const char* prevPrefsData = (char*)prevPrefs.data();
    ByteSlice prefs = SerializeGlobalPrefs(gGlobalPrefs, prevPrefsData);
    defer {
        str::Free(prevPrefs.data());
        str::Free(prefs.data());
    };
    ReportIf(prefs.empty());
    if (prefs.empty()) {
        return false;
    }

    // only save if anything's changed at all
    if (prevPrefs.size() == prefs.size() && str::Eq(prefs, prevPrefs)) {
        return true;
    }

    WatchedFileSetIgnore(gWatchedSettingsFile, true);
    bool ok = file::WriteFile(path, prefs);
    if (ok) {
        gGlobalPrefs->lastPrefUpdate = file::GetModificationTime(path);
    }
    WatchedFileSetIgnore(gWatchedSettingsFile, false);
    return ok;
}

// refresh the preferences when a different SumatraPDF process saves them
// or if they are edited by the user using a text editor
static void ReloadSettings() {
    TempStr settingsPath = GetSettingsPathTemp();
    if (!file::Exists(settingsPath)) {
        return;
    }

    // make sure that the settings file is readable - else wait
    // a short while to prevent accidental data loss
    // this is triggered when e.g. saving the file with VS Code
    bool ok = false;
    for (int i = 0; !ok && i < 5; i++) {
        Sleep(200);
        ByteSlice prefsData = file::ReadFile(settingsPath);
        if (prefsData.size() > 0) {
            ok = true;
            prefsData.Free();
        } else {
            logf("ReloadSettings: failed to load '%s', i=%d\n", settingsPath, i);
        }
    }
    if (!ok) {
        return;
    }

    FILETIME time = file::GetModificationTime(settingsPath);
    if (FileTimeEq(time, gGlobalPrefs->lastPrefUpdate)) {
        return;
    }

    const char* uiLanguage = str::DupTemp(gGlobalPrefs->uiLanguage);
    bool showToolbar = gGlobalPrefs->showToolbar;

    gFileHistory.UpdateStatesSource(nullptr);
    CleanUpSettings();

    ok = LoadSettings();
    ReportIf(!ok || !gGlobalPrefs);

    // TODO: about window doesn't have to be at position 0
    if (gWindows.size() > 0 && gWindows.at(0)->IsCurrentTabAbout()) {
        MainWindow* win = gWindows.at(0);
        win->DeleteToolTip();
        DeleteVecMembers(win->staticLinks);
        win->RedrawAll(true);
    }

    if (!str::Eq(uiLanguage, gGlobalPrefs->uiLanguage)) {
        SetCurrentLanguageAndRefreshUI(gGlobalPrefs->uiLanguage);
    }

    for (MainWindow* win : gWindows) {
        if (gGlobalPrefs->showToolbar != showToolbar) {
            ShowOrHideToolbar(win);
        }
        UpdateFavoritesTree(win);
        UpdateControlsColors(win);
    }

    UpdateDocumentColors();
    UpdateFixedPageScrollbarsVisibility();
}

void CleanUpSettings() {
    DeleteGlobalPrefs(gGlobalPrefs);
    gGlobalPrefs = nullptr;
}

static void SchedulePrefsReload() {
    auto fn = MkFunc0Void(ReloadSettings);
    uitask::Post(fn, "TaskReloadSettings");
}

void RegisterSettingsForFileChanges() {
    if (!HasPermission(Perm::SavePreferences)) {
        return;
    }

    ReportIf(gWatchedSettingsFile); // only call me once
    TempStr path = GetSettingsPathTemp();
    auto fn = MkFunc0Void(SchedulePrefsReload);
    gWatchedSettingsFile = FileWatcherSubscribe(path, fn, true);
}

void UnregisterSettingsForFileChanges() {
    FileWatcherUnsubscribe(gWatchedSettingsFile);
    gWatchedSettingsFile = nullptr;
}

constexpr int kMinFontSize = 9;

int GetAppFontSize() {
    auto fntSize = gGlobalPrefs->uIFontSize;
    if (fntSize < kMinFontSize) {
        fntSize = GetSizeOfDefaultGuiFontForDpi(UiFontDpiForHwnd(GetDefaultUiFontHwnd()));
    }
    return fntSize;
}

static int GetAppFontSizeForDpi(int dpi) {
    auto fntSize = gGlobalPrefs->uIFontSize;
    if (fntSize < kMinFontSize) {
        fntSize = GetSizeOfDefaultGuiFontForDpi(dpi);
    }
    return fntSize;
}

HFONT GetAppFontForDpi(int dpi) {
    UiFontsAtDpi* fonts = FindFontsAtDpi(dpi);
    if (fonts && fonts->appFont) {
        return fonts->appFont;
    }
    int fntSize = GetAppFontSizeForDpi(dpi);
    HFONT font = GetUserGuiFontForDpi("auto", fntSize, dpi);
    if (!fonts) {
        UiFontsAtDpi entry{};
        entry.dpi = dpi;
        entry.appFont = font;
        gFontsAtDpi.Append(entry);
    } else {
        fonts->appFont = font;
    }
    return font;
}

HFONT GetAppFontForHwnd(HWND hwnd) {
    return GetAppFontForDpi(UiFontDpiForHwnd(hwnd));
}

HFONT GetAppFont() {
    return GetAppFontForHwnd(GetDefaultUiFontHwnd());
}

constexpr int kMinBiggerFontSize = 14;

// if user provided font size, we use that
// otherwise we return 1.4x of default font size but no smaller than 16
// on my laptop on high dpi default font size is 12
HFONT GetAppBiggerFontForDpi(int dpi) {
    UiFontsAtDpi* fonts = FindFontsAtDpi(dpi);
    if (fonts && fonts->biggerFont) {
        return fonts->biggerFont;
    }
    int fntSize = gGlobalPrefs->uIFontSize;
    if (fntSize < kMinFontSize) {
        fntSize = GetSizeOfDefaultGuiFontForDpi(dpi);
        fntSize = (fntSize * 12) / 10;
        if (fntSize < kMinBiggerFontSize) {
            fntSize = kMinBiggerFontSize;
        }
    }
    HFONT font = GetDefaultGuiFontOfSize(fntSize);
    if (!fonts) {
        UiFontsAtDpi entry{};
        entry.dpi = dpi;
        entry.biggerFont = font;
        gFontsAtDpi.Append(entry);
    } else {
        fonts->biggerFont = font;
    }
    return font;
}

HFONT GetAppBiggerFontForHwnd(HWND hwnd) {
    return GetAppBiggerFontForDpi(UiFontDpiForHwnd(hwnd));
}

HFONT GetAppBiggerFont() {
    return GetAppBiggerFontForHwnd(GetDefaultUiFontHwnd());
}

int GetAppMenuFontSizeForDpi(int dpi) {
    if (gGlobalPrefs->uIFontSize >= kMinFontSize) {
        return gGlobalPrefs->uIFontSize;
    }
    NONCLIENTMETRICS ncm{};
    if (GetNonClientMetricsForDpi(dpi, &ncm)) {
        return (int)std::abs(ncm.lfMenuFont.lfHeight);
    }
    return GetSizeOfDefaultGuiFontForDpi(dpi);
}

int GetAppMenuFontSizeForHwnd(HWND hwnd) {
    return GetAppMenuFontSizeForDpi(UiFontDpiForHwnd(hwnd));
}

int GetAppMenuFontSize() {
    return GetAppMenuFontSizeForHwnd(GetDefaultUiFontHwnd());
}

HFONT GetAppMenuFontForDpi(int dpi) {
    UiFontsAtDpi* fonts = FindFontsAtDpi(dpi);
    if (fonts && fonts->menuFont) {
        return fonts->menuFont;
    }
    NONCLIENTMETRICS ncm{};
    if (!GetNonClientMetricsForDpi(dpi, &ncm)) {
        return GetAppFontForDpi(dpi);
    }
    int fntSize = GetAppMenuFontSizeForDpi(dpi);
    ncm.lfMenuFont.lfHeight = -fntSize;
    HFONT font = CreateFontIndirectW(&ncm.lfMenuFont);
    if (!fonts) {
        UiFontsAtDpi entry{};
        entry.dpi = dpi;
        entry.menuFont = font;
        gFontsAtDpi.Append(entry);
    } else {
        fonts->menuFont = font;
    }
    return font;
}

HFONT GetAppMenuFontForHwnd(HWND hwnd) {
    return GetAppMenuFontForDpi(UiFontDpiForHwnd(hwnd));
}

HFONT GetAppTreeFontForDpi(int dpi) {
    UiFontsAtDpi* fonts = FindFontsAtDpi(dpi);
    if (fonts && fonts->treeFont) {
        return fonts->treeFont;
    }
    bool userTreeFontSize = gGlobalPrefs->treeFontSize >= kMinFontSize;
    int fntSize = gGlobalPrefs->treeFontSize;
    if (fntSize < kMinFontSize) {
        fntSize = gGlobalPrefs->uIFontSize;
    }
    if (fntSize < kMinFontSize) {
        fntSize = GetAppMenuFontSizeForDpi(dpi);
        if (!userTreeFontSize) {
            fntSize += 2;
        }
    }
    char* fntNameUser = gGlobalPrefs->treeFontName;
    HFONT font = nullptr;
    if (fntNameUser && !str::EqI(fntNameUser, "automatic") && !str::EqI(fntNameUser, "auto")) {
        font = GetUserGuiFontForDpi(fntNameUser, fntSize, dpi);
    } else {
        NONCLIENTMETRICS ncm{};
        if (!GetNonClientMetricsForDpi(dpi, &ncm)) {
            font = GetUserGuiFont(nullptr, fntSize);
        } else {
            ncm.lfMenuFont.lfHeight = -fntSize;
            font = CreateFontIndirectW(&ncm.lfMenuFont);
        }
    }
    if (!fonts) {
        UiFontsAtDpi entry{};
        entry.dpi = dpi;
        entry.treeFont = font;
        gFontsAtDpi.Append(entry);
    } else {
        fonts->treeFont = font;
    }
    return font;
}

HFONT GetAppTreeFontForHwnd(HWND hwnd) {
    return GetAppTreeFontForDpi(UiFontDpiForHwnd(hwnd));
}

HFONT GetAppTreeFont() {
    return GetAppTreeFontForHwnd(GetDefaultUiFontHwnd());
}

HFONT GetAppSidebarLabelFontForDpi(int dpi) {
    HFONT menuFont = GetAppMenuFontForDpi(dpi);
    LOGFONTW lf{};
    if (!menuFont || GetObjectW(menuFont, sizeof(lf), &lf) != sizeof(lf)) {
        return menuFont;
    }
    lf.lfWeight = FW_BOLD;
    HFONT font = CreateFontIndirectW(&lf);
    return font ? font : menuFont;
}

HFONT GetAppSidebarLabelFontForHwnd(HWND hwnd) {
    return GetAppSidebarLabelFontForDpi(UiFontDpiForHwnd(hwnd));
}

HFONT GetAppMenuFont() {
    return GetAppMenuFontForHwnd(GetDefaultUiFontHwnd());
}

bool IsMenuFontSizeDefault() {
    auto fntSize = gGlobalPrefs->uIFontSize;
    return fntSize < kMinFontSize;
}

bool IsAppFontSizeDefault() {
    auto fntSize = gGlobalPrefs->uIFontSize;
    return fntSize < kMinFontSize;
}
