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

#include "utils/BaseUtil.h"
#include "utils/ThreadUtil.h"
#include "utils/UITask.h"
#include "utils/SquareTreeParser.h"
#include "utils/HttpUtil.h"
#include "utils/WinUtil.h"
#include "utils/FileUtil.h"

#include "wingui/Layout.h"
#include "wingui/UIModels.h"
#include "wingui/WinGui.h"
#include "wingui/WebView.h"

#include "utils/Timer.h"
#include "Settings.h"
#include "GlobalPrefs.h"
#include "AppTools.h"
#include "AppSettings.h"
#include "Version.h"
#include "SumatraConfig.h"
#include "Translations.h"
#include "Annotation.h"
#include "SumatraPDF.h"
#include "Flags.h"
#include "ProgressUpdateUI.h"
#include "Notifications.h"
#include "MainWindow.h"
#include "SumatraDialogs.h"
#include "HomePage.h"
#include "Installer.h"
#include "UpdateCheck.h"
#include "UpdateCheckPolicy.h"

#include "utils/Log.h"

static const char* kNotifUpdateCheckInProgress = "notifUpdateCheckInProgress";

// Sumatra PDF Plus: update info and downloads are hosted on GitHub.
// update-check.txt lives in the repo root; releases are published as GitHub Releases.
// GitHub raw updates immediately when main changes; jsDelivr can lag behind on @main.
// We fetch both and use whichever reports the highest Latest version.

// clang-format off
constexpr const char* kUpdateInfoURL = "https://raw.githubusercontent.com/dengxibo/sumatrapdf-plus/main/update-check.txt";
constexpr const char* kUpdateInfoURL2 = "https://cdn.jsdelivr.net/gh/dengxibo/sumatrapdf-plus@main/update-check.txt";

#ifndef kWebisteDownloadPageURL
#define kWebisteDownloadPageURL "https://github.com/dengxibo/sumatrapdf-plus/releases/latest"
#endif
// clang-format on

// prevent multiple update tasks from happening simultaneously
// (this might e.g. happen if a user checks manually very quickly after startup)
bool gUpdateCheckInProgress = false;

static UINT_PTR gAutomaticUpdateTimerId = 0;
static int gAutomaticUpdateFailureCount = 0;

static bool CanRunAutomaticUpdateChecks() {
    return !gIsDebugBuild && !gIsStoreBuild && HasPermission(Perm::InternetAccess) &&
           HasPermission(Perm::SavePreferences) && gGlobalPrefs->checkForUpdates;
}

static int AutomaticUpdateIntervalSecs() {
    return gIsPreReleaseBuild ? updatecheck::kSecondsInWeek : updatecheck::kSecondsInDay;
}

static void CancelAutomaticUpdateTimer() {
    if (gAutomaticUpdateTimerId != 0) {
        KillTimer(nullptr, gAutomaticUpdateTimerId);
        gAutomaticUpdateTimerId = 0;
    }
}

static void ScheduleAutomaticUpdateCheck(int delaySecs);

static VOID CALLBACK AutomaticUpdateTimerProc(HWND, UINT, UINT_PTR timerId, DWORD) {
    if (timerId != gAutomaticUpdateTimerId) {
        return;
    }
    CancelAutomaticUpdateTimer();
    if (!CanRunAutomaticUpdateChecks()) {
        return;
    }

    MainWindow* win = gWindows.Size() > 0 ? gWindows.At(0) : nullptr;
    if (!win || !StartAsyncUpdateCheck(win, UpdateCheck::Automatic)) {
        // A manual check or window transition can briefly occupy the updater.
        ScheduleAutomaticUpdateCheck(5 * 60);
    }
}

static void ScheduleAutomaticUpdateCheck(int delaySecs) {
    CancelAutomaticUpdateTimer();
    if (!CanRunAutomaticUpdateChecks()) {
        return;
    }
    if (delaySecs < 0) {
        delaySecs = 0;
    }
    UINT delayMs = delaySecs == 0 ? USER_TIMER_MINIMUM : (UINT)delaySecs * 1000;
    gAutomaticUpdateTimerId = SetTimer(nullptr, 0, delayMs, AutomaticUpdateTimerProc);
    ReportIf(gAutomaticUpdateTimerId == 0);
}

static void ScheduleAutomaticUpdateRetry() {
    int delaySecs = updatecheck::GetRetryDelaySecs(gAutomaticUpdateFailureCount);
    gAutomaticUpdateFailureCount++;
    ScheduleAutomaticUpdateCheck(delaySecs);
}

static void ScheduleAutomaticUpdateAfterSuccess() {
    gAutomaticUpdateFailureCount = 0;
    ScheduleAutomaticUpdateCheck(AutomaticUpdateIntervalSecs());
}

void StartAutomaticUpdateChecks() {
    CancelAutomaticUpdateTimer();
    if (!CanRunAutomaticUpdateChecks()) {
        return;
    }
    FILETIME now{};
    GetSystemTimeAsFileTime(&now);
    int delaySecs = updatecheck::GetAutomaticCheckDelaySecs(now, gGlobalPrefs->timeOfLastUpdateCheck,
                                                            AutomaticUpdateIntervalSecs());
    ScheduleAutomaticUpdateCheck(delaySecs);
}

void RefreshAutomaticUpdateChecks() {
    CancelAutomaticUpdateTimer();
    gAutomaticUpdateFailureCount = 0;
    if (CanRunAutomaticUpdateChecks()) {
        ScheduleAutomaticUpdateCheck(0);
    }
}

struct UpdateInfo {
    HWND hwndParent = nullptr;
    const char* latestVer = nullptr;

    const char* installer64 = nullptr;
    const char* portable64 = nullptr;

    const char* installer32 = nullptr;
    const char* portable32 = nullptr;

    const char* installerArm64 = nullptr;
    const char* portableArm64 = nullptr;

    const char* dlURL = nullptr;
    const char* installerPath = nullptr;

    UpdateInfo() = default;
    ~UpdateInfo() {
        str::Free(latestVer);
        str::Free(installer64);
        str::Free(portable64);
        str::Free(installer32);
        str::Free(portable32);
        str::Free(installerArm64);
        str::Free(portableArm64);
        str::Free(dlURL);
        str::Free(installerPath);
    }
};

/*
The format of update information downloaded from the server:

[SumatraPDF]
Latest: 14276
Installer64: https://www.sumatrapdfreader.org/dl/prerel/14276/SumatraPDF-prerel-64-install.exe
Installer32: https://www.sumatrapdfreader.org/dl/prerel/14276/SumatraPDF-prerel-install.exe
PortableExe64: https://www.sumatrapdfreader.org/dl/prerel/14276/SumatraPDF-prerel-64.exe
PortableExe32: https://www.sumatrapdfreader.org/dl/prerel/14276/SumatraPDF-prerel.exe
PortableZip64: https://www.sumatrapdfreader.org/dl/prerel/14276/SumatraPDF-prerel-64.zip
PortableZip32: https://www.sumatrapdfreader.org/dl/prerel/14276/SumatraPDF-prerel.zip

[Promo]
[
    Name = MarkLexis
    URL = https://marklexis.arslexis.io
    Info = Bookmarking web application
]
*/
static UpdateInfo* ParseUpdateInfo(const char* d) {
    // if a user configures os-wide proxy that is not a regular ie proxy
    // (which we pick up) we might get garbage http response
    // check if response looks valid
    if (!str::StartsWith(d, '[' == d[0] ? "[SumatraPDF]" : "SumatraPDF")) {
        return nullptr;
    }

    SquareTreeNode* root = ParseSquareTree(d);
    if (!root) {
        return nullptr;
    }
    AutoDelete delRoot(root);

    {
        auto promoString = SerializeSquareTreeNode(root->GetChild("Promo"));
        SetPromoString(promoString);
        str::Free(promoString);
    }

    SquareTreeNode* node = root->GetChild("SumatraPDF");
    if (!node) {
        return nullptr;
    }

    const char* latestVer = node->GetValue("Latest");
    if (!IsValidProgramVersion(latestVer)) {
        return nullptr;
    }
    auto res = new UpdateInfo();
    res->latestVer = str::Dup(latestVer);

    // those are optional. if missing, we'll just tell the user to go to website to download
    res->installer64 = str::Dup(node->GetValue("Installer64"));
    res->installerArm64 = str::Dup(node->GetValue("InstallerArm64"));
    res->installer32 = str::Dup(node->GetValue("Installer32"));

    res->portable64 = str::Dup(node->GetValue("PortableExe64"));
    res->portableArm64 = str::Dup(node->GetValue("PortableExeArm64"));
    res->portable32 = str::Dup(node->GetValue("PortableExe32"));

    // figure out which executable to download
    const char* dlURL = nullptr;
    bool isDll = IsDllBuild();
    if (IsArmBuild()) {
        dlURL = isDll ? res->installerArm64 : res->portableArm64;
    } else if (IsProcess64()) {
        dlURL = isDll ? res->installer64 : res->portable64;
    } else {
        dlURL = isDll ? res->installer32 : res->portable32;
    }
    res->dlURL = str::Dup(dlURL);
    return res;
}

static bool ShouldCheckForUpdate(UpdateCheck updateCheckType) {
    // when forcing, we download pre-release, which shows greater version than our build
    // so we don't want to download during automatic check, only when user initiated

#if defined(FORCE_AUTO_UPDATE)
    if (updateCheckType == UpdateCheck::Automatic) {
        return false;
    }
#endif

    bool userInitiated = updateCheckType == UpdateCheck::UserInitiated;
    return updatecheck::CanStartUpdateCheck(userInitiated, gUpdateCheckInProgress, gIsDebugBuild, gIsStoreBuild,
                                            HasPermission(Perm::InternetAccess), HasPermission(Perm::SavePreferences),
                                            gGlobalPrefs->checkForUpdates);
}

bool StartInstallerAutoUpgrade(const char* installerPath) {
    StrBuilder cmd;
    if (IsOurExeInstalled()) {
        // no need for sleep because it shows the installer dialog anyway
        if (gIsPreReleaseBuild) {
            cmd.Append(" -fast-install");
        } else {
            cmd.Append(" -install");
        }
    } else {
        // we're asking to over-write over ourselves, so also wait 2 secs to allow
        // our process to exit
        cmd.AppendFmt(R"( -sleep-ms 2000 -exit-when-done -update-self-to "%s")", GetSelfExePathTemp());
    }
    logf("StartInstallerAutoUpgrade: installer cmd: '%s'\n", cmd.Get());
    return CreateProcessHelper(installerPath, cmd.Get());
}

static bool PromptUserForUpdate(UpdateInfo* updateInfo) {
    auto mainInstr = _TRA("New version available");
    auto ver = updateInfo->latestVer;
    auto fmt = _TRA("You have version '%s' and version '%s' is available.\nDo you want to install new version?");
    auto content = str::Format(fmt, CURR_VERSION_STRA, ver);

    constexpr int kBtnIdDontInstall = 100;
    constexpr int kBtnIdInstall = 101;
    auto title = _TRA("SumatraPDF Update");
    TASKDIALOGCONFIG dialogConfig{};
    TASKDIALOG_BUTTON buttons[2];

    buttons[0].nButtonID = kBtnIdDontInstall;
    auto s = _TRA("Don't install");
    buttons[0].pszButtonText = ToWStrTemp(s);
    buttons[1].nButtonID = kBtnIdInstall;
    s = updateInfo->dlURL ? _TRA("Download update") : _TRA("Visit &Website");
    buttons[1].pszButtonText = ToWStrTemp(s);

    DWORD flags =
        TDF_ALLOW_DIALOG_CANCELLATION | TDF_SIZE_TO_CONTENT | TDF_ENABLE_HYPERLINKS | TDF_POSITION_RELATIVE_TO_WINDOW;
    if (trans::IsCurrLangRtl()) {
        flags |= TDF_RTL_LAYOUT;
    }
    dialogConfig.cbSize = sizeof(TASKDIALOGCONFIG);
    dialogConfig.pszWindowTitle = ToWStrTemp(title);
    dialogConfig.pszMainInstruction = ToWStrTemp(mainInstr);
    dialogConfig.pszContent = ToWStrTemp(content);
    dialogConfig.nDefaultButton = kBtnIdInstall;
    dialogConfig.dwFlags = flags;
    dialogConfig.cxWidth = 0;
    dialogConfig.pfCallback = nullptr;
    dialogConfig.dwCommonButtons = 0;
    dialogConfig.cButtons = dimof(buttons);
    dialogConfig.pButtons = &buttons[0];
    dialogConfig.pszMainIcon = TD_INFORMATION_ICON;
    dialogConfig.hwndParent = updateInfo->hwndParent;

    int buttonPressedId = 0;

    auto hr = TaskDialogIndirect(&dialogConfig, &buttonPressedId, nullptr, nullptr);
    ReportIf(hr == E_INVALIDARG);
    return (hr == S_OK) && (buttonPressedId == kBtnIdInstall);
}

struct UpdateProgressData {
    HWND hwndForNotif = nullptr;
    i64 nDownloaded = 0;
    i64 nTotal = -1;
    LARGE_INTEGER lastSpeedTime{};
    i64 lastSpeedDownloaded = 0;
    i64 speedBytesPerSec = 0;
    bool lastSpeedTimeSet = false;
};

static TempStr FormatSizeShortTransLocal(i64 size) {
    const char* sizeUnits[3] = {
        _TRA("GB"),
        _TRA("MB"),
        _TRA("KB"),
    };
    return str::FormatSizeShortTemp(size, sizeUnits);
}

static TempStr FormatDownloadSpeedTemp(i64 bytesPerSec) {
    if (bytesPerSec <= 0) {
        return (TempStr) "";
    }
    TempStr s = FormatSizeShortTransLocal(bytesPerSec);
    return str::FormatTemp("%s/s", s);
}

static TempStr FormatUpdateDownloadProgressTemp(UpdateProgressData* data) {
    const char* label = _TRA("Downloading update...");
    TempStr downloaded = FormatSizeShortTransLocal(data->nDownloaded);
    TempStr speed = FormatDownloadSpeedTemp(data->speedBytesPerSec);
    if (data->nTotal > 0) {
        TempStr total = FormatSizeShortTransLocal(data->nTotal);
        if (speed) {
            return str::FormatTemp("%s %s, %s / %s", label, speed, downloaded, total);
        }
        return str::FormatTemp("%s %s / %s", label, downloaded, total);
    }
    if (speed) {
        return str::FormatTemp("%s %s, %s", label, speed, downloaded);
    }
    return str::FormatTemp("%s %s", label, downloaded);
}

static void ShowUpdateDownloadingNotification(HWND hwndForNotif) {
    const char* msg = _TRA("Downloading update...");
    auto wnd = GetNotificationForGroup(hwndForNotif, kNotifUpdateCheckInProgress);
    if (wnd) {
        NotificationUpdateMessage(wnd, msg, 0, true);
        return;
    }
    NotificationCreateArgs args;
    args.hwndParent = hwndForNotif;
    args.msg = msg;
    args.warning = true;
    args.timeoutMs = 0;
    args.groupId = kNotifUpdateCheckInProgress;
    ShowNotification(args);
}

static void UpdateDownloadProgressNotif(UpdateProgressData* data) {
    TempStr msg = FormatUpdateDownloadProgressTemp(data);
    logf("UpdateDownloadProgressNotif: %s\n", msg);
    auto wnd = GetNotificationForGroup(data->hwndForNotif, kNotifUpdateCheckInProgress);
    if (wnd) {
        NotificationUpdateMessage(wnd, msg, 0, true);
    } else {
        logf("UpdateDownloadProgressNotif: no wnd\n");
    }
    delete data;
}

static void UpdateProgressCb(UpdateProgressData* data, HttpProgress* progress) {
    if (!data->lastSpeedTimeSet) {
        data->lastSpeedTime = TimeGet();
        data->lastSpeedDownloaded = progress->nDownloaded;
        data->lastSpeedTimeSet = true;
    }

    data->nDownloaded = progress->nDownloaded;
    if (progress->nTotal > 0) {
        data->nTotal = progress->nTotal;
    }

    LARGE_INTEGER now = TimeGet();
    double elapsedMs = TimeSinceInMs(data->lastSpeedTime);
    if (elapsedMs >= 250) {
        i64 delta = progress->nDownloaded - data->lastSpeedDownloaded;
        if (delta > 0) {
            data->speedBytesPerSec = (i64)((double)delta * 1000.0 / elapsedMs);
        }
        data->lastSpeedTime = now;
        data->lastSpeedDownloaded = progress->nDownloaded;
    }

    auto fnData = new UpdateProgressData(*data);
    auto fn = MkFunc0<UpdateProgressData>(UpdateDownloadProgressNotif, fnData);
    uitask::Post(fn, nullptr);
}

struct DownloadUpdateAsyncData {
    HWND hwndForNotif = nullptr;
    UpdateInfo* updateInfo = nullptr;
    HttpProgress httpProgress = {};

    DownloadUpdateAsyncData() = default;
    ~DownloadUpdateAsyncData() { delete updateInfo; }
};

static void DownloadUpdateFinish(DownloadUpdateAsyncData* data) {
    auto hwndForNotif = data->hwndForNotif;
    auto updateInfo = data->updateInfo;
    data->updateInfo = nullptr;
    RemoveNotificationsForGroup(hwndForNotif, kNotifUpdateCheckInProgress);
    auto installerPath = updateInfo->installerPath;
    if (!file::Exists(installerPath)) {
        TempStr msg =
            str::FormatTemp(_TRA("Can't connect to the Internet (error %#x)."), ERROR_INTERNET_CONNECTION_ABORTED);
        MessageBoxWarning(updateInfo->hwndParent, msg, _TRA("SumatraPDF Update"));
        delete updateInfo;
        gUpdateCheckInProgress = false;
        delete data;
        return;
    }

    SaveSettings();
    bool started = StartInstallerAutoUpgrade(installerPath);
    if (!started) {
        file::Delete(installerPath);
        MessageBoxWarning(updateInfo->hwndParent, _TRA("Failed to start the update installer."),
                          _TRA("SumatraPDF Update"));
        delete updateInfo;
        gUpdateCheckInProgress = false;
        delete data;
        return;
    }
    delete updateInfo;
    gUpdateCheckInProgress = false;
    delete data;

    // Exit immediately so the updater can overwrite our exe. PostQuitMessage(0)
    // is unreliable when the dialog was shown from a uitask during startup.
    if (gPluginMode) {
        PostQuitMessage(0);
        return;
    }
    ::ExitProcess(0);
}

static void DownloadUpdateAsync(DownloadUpdateAsyncData* data) {
    auto hwndForNotif = data->hwndForNotif;
    auto updateInfo = data->updateInfo;

    TempStr installerPath = GetTempFilePathTemp("sumatra-installer");
    // the installer must be named .exe or it won't be able to self-elevate
    // with "runas"
    installerPath = str::JoinTemp(installerPath, ".exe");
    UpdateProgressData pd;
    pd.hwndForNotif = hwndForNotif;
    auto cb = MkFunc1<UpdateProgressData, HttpProgress*>(UpdateProgressCb, &pd);
    bool ok = HttpGetToFile(updateInfo->dlURL, installerPath, cb);
    logf("ShowAutoUpdateDialog: HttpGetToFile(): ok=%d, downloaded to '%s'\n", (int)ok, installerPath);
    if (ok) {
        updateInfo->installerPath = str::Dup(installerPath);
    } else {
        file::Delete(installerPath);
    }

    // process the rest on ui thread to avoid threading issues
    auto fn = MkFunc0<DownloadUpdateAsyncData>(DownloadUpdateFinish, data);
    uitask::Post(fn, "TaskShowAutoUpdateDialog");
}

static bool ShouldDownloadUpdate(UpdateInfo* updateInfo, UpdateCheck updateCheckType) {
    if (gIsStoreBuild) {
        // I assume store will take care of updates
        return false;
    }
    auto latestVer = updateInfo->latestVer;
    const char* myVer = UPDATE_CHECK_VERA;
    if (gIsDebugBuild) {
        // in debug build we compare against pre-rel version, like "17616"
        // but our version is like "3.6" so it triggers update
        myVer = "50000";
    }
    bool hasUpdate = CompareProgramVersion(latestVer, myVer) > 0;
#if defined(FORCE_AUTO_UPDATE)
    if (updateCheckType == UpdateCheck::UserInitiated) {
        hasUpdate = true;
    }
#endif
    return hasUpdate;
}

static HRESULT CALLBACK TaskDialogHyperlinkCallback(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam,
                                                    LONG_PTR lpRefData) {
    if (msg == TDN_HYPERLINK_CLICKED) {
        WCHAR* url = (WCHAR*)lParam;
        SumatraLaunchBrowser(ToUtf8Temp(url));
    }
    return S_OK;
}

constexpr const char* kExpectedDlHost = "https://github.com/dengxibo/sumatrapdf-plus/";

static void NotifySuspiciousUpdate(HWND hwndParent, const char* dlURL) {
    logf("NotifySuspiciousUpdate: suspicious download url '%s'\n", dlURL);
    ReportIfFast(true);
    auto title = _TRA("SumatraPDF Update");
    auto content = str::FormatTemp(
        "Suspicious update.\n"
        "\n"
        "Download link should come from <a href=\"%s\">%s</a> but is %s.\n"
        "\n"
        "Visit <a href=\"%s\">%s</a> to download the latest version.",
        kExpectedDlHost, kExpectedDlHost, dlURL, kExpectedDlHost, kExpectedDlHost);

    TASKDIALOGCONFIG dialogConfig{};
    DWORD flags =
        TDF_ALLOW_DIALOG_CANCELLATION | TDF_SIZE_TO_CONTENT | TDF_ENABLE_HYPERLINKS | TDF_POSITION_RELATIVE_TO_WINDOW;
    if (trans::IsCurrLangRtl()) {
        flags |= TDF_RTL_LAYOUT;
    }

    constexpr int kBtnIdVisitWebsite = 100;
    TASKDIALOG_BUTTON buttons[1];
    buttons[0].nButtonID = kBtnIdVisitWebsite;
    buttons[0].pszButtonText = ToWStrTemp(_TRA("Visit &Website"));

    dialogConfig.cbSize = sizeof(TASKDIALOGCONFIG);
    dialogConfig.pszWindowTitle = ToWStrTemp(title);
    dialogConfig.pszContent = ToWStrTemp(content);
    dialogConfig.dwFlags = flags;
    dialogConfig.dwCommonButtons = TDCBF_CLOSE_BUTTON;
    dialogConfig.cButtons = dimof(buttons);
    dialogConfig.pButtons = buttons;
    dialogConfig.nDefaultButton = kBtnIdVisitWebsite;
    dialogConfig.pszMainIcon = TD_WARNING_ICON;
    dialogConfig.hwndParent = hwndParent;
    dialogConfig.pfCallback = TaskDialogHyperlinkCallback;
    int buttonPressedId = 0;
    TaskDialogIndirect(&dialogConfig, &buttonPressedId, nullptr, nullptr);
    if (buttonPressedId == kBtnIdVisitWebsite) {
        SumatraLaunchBrowser(kExpectedDlHost);
    }
}

static void ClearUpdateSnooze() {
    str::ReplaceWithCopy(&gGlobalPrefs->versionToSkip, nullptr);
    gGlobalPrefs->timeOfUpdateCheckSnooze = {};
}

static void SnoozeUpdateVersion(const char* version) {
    str::ReplaceWithCopy(&gGlobalPrefs->versionToSkip, version);
    GetSystemTimeAsFileTime(&gGlobalPrefs->timeOfUpdateCheckSnooze);
    SaveSettings();
}

static bool IsUpdateVersionSnoozed(const char* version) {
    FILETIME now{};
    GetSystemTimeAsFileTime(&now);
    return updatecheck::ShouldSnoozeVersion(version, gGlobalPrefs->versionToSkip, gGlobalPrefs->timeOfUpdateCheckSnooze,
                                            now);
}

static DWORD MaybeStartUpdateDownload(HWND hwndParent, HttpRsp* rsp, UpdateCheck updateCheckType,
                                      bool* downloadStarted) {
    *downloadStarted = false;
    // for store builds we do update check but ignore the result
#if 0
    if (gIsStoreBuild) {
        return 0;
    }
#endif

    const char* url = rsp->url.Get();

    if (rsp->error != 0) {
        logf("ShowAutoUpdateDialog: http get of '%s' failed with %d\n", url, (int)rsp->error);
        return rsp->error;
    }
    if (rsp->httpStatusCode != 200) {
        logf("ShowAutoUpdateDialog: http get of '%s' failed with code %d\n", url, (int)rsp->httpStatusCode);
        return ERROR_INTERNET_INVALID_URL;
    }

    bool isValidURL = str::StartsWith(url, kUpdateInfoURL) || str::StartsWith(url, kUpdateInfoURL2);
    if (!isValidURL) {
        logf("ShowAutoUpdateDialog: '%s' is not a valid url\n", url);
        return ERROR_INTERNET_INVALID_URL;
    }
    StrBuilder* data = &rsp->data;
    if (0 == data->size()) {
        logf("ShowAutoUpdateDialog: empty response from url '%s'\n", url);
        return ERROR_INTERNET_CONNECTION_ABORTED;
    }

    UpdateInfo* updateInfo = ParseUpdateInfo(data->Get());
    if (!updateInfo) {
        logf("ShowAutoUpdateDialog: ParseUpdateInfo() failed. URL: '%s'\nAuto update data:\n%s\n", url, data->Get());
        return ERROR_INTERNET_INCORRECT_FORMAT;
    }
    updateInfo->hwndParent = hwndParent;

    MainWindow* win = FindMainWindowByHwnd(hwndParent);
    if (!win) {
        // could be destroyed since we issued update check
        delete updateInfo;
        return 0;
    }
    HWND hwndForNotif = win->hwndCanvas;
    if (!ShouldDownloadUpdate(updateInfo, updateCheckType)) {
        const char* myVer = UPDATE_CHECK_VERA;
        logf("ShowAutoUpdateDialog: myVer >= latestVer ('%s' >= '%s')\n", myVer, updateInfo->latestVer);
        /* if automated => don't notify that there is no new version */
        if (updateCheckType == UpdateCheck::UserInitiated) {
            auto wnd = GetNotificationForGroup(hwndForNotif, kNotifUpdateCheckInProgress);
            if (wnd) {
                NotificationUpdateMessage(wnd, _TRA("You have the latest version."), 5 * 1000, true);
            } else {
                RemoveNotificationsForGroup(hwndForNotif, kNotifUpdateCheckInProgress);
                MessageBoxWarning(hwndParent, _TRA("You have the latest version."), _TRA("SumatraPDF Update"));
            }
        }
        delete updateInfo;
        return 0;
    }

    if (updateCheckType == UpdateCheck::Automatic && IsUpdateVersionSnoozed(updateInfo->latestVer)) {
        logf("ShowAutoUpdateDialog: update '%s' is snoozed\n", updateInfo->latestVer);
        delete updateInfo;
        return 0;
    }

    // An expired snooze, a newly published version, or a legacy VersionToSkip
    // without a snooze timestamp must never suppress the prompt indefinitely.
    if (gGlobalPrefs->versionToSkip) {
        ClearUpdateSnooze();
        SaveSettings();
    }

    if (updateInfo->dlURL && !str::StartsWith(updateInfo->dlURL, kExpectedDlHost)) {
        RemoveNotificationsForGroup(win->hwndCanvas, kNotifUpdateCheckInProgress);
        NotifySuspiciousUpdate(hwndParent, updateInfo->dlURL);
        delete updateInfo;
        return 0;
    }

    RemoveNotificationsForGroup(win->hwndCanvas, kNotifUpdateCheckInProgress);
    if (!PromptUserForUpdate(updateInfo)) {
        SnoozeUpdateVersion(updateInfo->latestVer);
        delete updateInfo;
        return 0;
    }

    if (!updateInfo->dlURL) {
        logf("ShowAutoUpdateDialog: didn't find download url. Auto update data:\n%s\n", data->Get());
        SumatraLaunchBrowser(kWebisteDownloadPageURL);
        delete updateInfo;
        return 0;
    }

    ClearUpdateSnooze();
    SaveSettings();
    logf("ShowAutoUpdateDialog: starting to download '%s'\n", updateInfo->dlURL);
    gUpdateCheckInProgress = true;
    ShowUpdateDownloadingNotification(hwndForNotif);

    auto fnData = new DownloadUpdateAsyncData;
    fnData->hwndForNotif = hwndForNotif;
    fnData->updateInfo = updateInfo;
    auto fn = MkFunc0<DownloadUpdateAsyncData>(DownloadUpdateAsync, fnData);
    RunAsync(fn, "DownloadUpdateAsync");
    *downloadStarted = true;
    return 0;
}

static void BuildUpdateURL(StrBuilder& url, const char* baseURL, UpdateCheck updateCheckType) {
    url = baseURL;
    url.Append("?v=");
    url.Append(UPDATE_CHECK_VERA);
    char* osVerTemp = GetWindowsVerTemp();
    url.Append("&os=");
    url.Append(osVerTemp);
    url.Append("&64bit=");
    url.Append(IsProcess64() ? "yes" : "no");
    url.Append("&arm=");
    url.Append(IsArmBuild() ? "yes" : "no");
    const char* lang = trans::GetCurrentLangCode();
    url.Append("&lang=");
    url.Append(lang);
    char* webView2ver = GetWebView2VersionTemp();
    if (webView2ver) {
        url.Append("&webview=");
        url.Append(webView2ver);
    }
    if (gIsStoreBuild) {
        url.Append("&store");
    }
    url.Append("&simd=");
    url.Append(LatestSupportedSIMD());
    url.Append("&withPromo");
    if (UpdateCheck::UserInitiated == updateCheckType) {
        url.Append("&force");
    }
}

struct UpdateCheckAsyncData {
    MainWindow* win = nullptr;
    UpdateCheck updateCheckType = UpdateCheck::Automatic;
    HttpRsp* rsp = nullptr;
    UpdateCheckAsyncData() = default;
    ~UpdateCheckAsyncData() { delete rsp; }
};

static void UpdateCheckFinish(UpdateCheckAsyncData* data) {
    log("UpdateCheckFinish\n");

    AutoDelete delData(data);

    auto updateCheckType = data->updateCheckType;
    auto rsp = data->rsp;
    MainWindow* win = nullptr;
    if (IsMainWindowValid(data->win)) {
        win = data->win;
    } else {
        if (gWindows.Size() > 0) {
            win = gWindows.At(0);
        }
    }
    if (!win) {
        gUpdateCheckInProgress = false;
        if (updateCheckType == UpdateCheck::Automatic) {
            ScheduleAutomaticUpdateCheck(5 * 60);
        }
        return;
    }
    HWND hwnd = win->hwndFrame;
    if (!rsp) {
        gUpdateCheckInProgress = false;
        if (updateCheckType == UpdateCheck::Automatic) {
            ScheduleAutomaticUpdateRetry();
        }
        if (updateCheckType == UpdateCheck::UserInitiated) {
            RemoveNotificationsForGroup(win->hwndCanvas, kNotifUpdateCheckInProgress);
            TempStr msg =
                str::FormatTemp(_TRA("Can't connect to the Internet (error %#x)."), ERROR_INTERNET_CANNOT_CONNECT);
            MessageBoxWarning(hwnd, msg, _TRA("SumatraPDF Update"));
        }
        return;
    }
    bool downloadStarted = false;
    DWORD err = MaybeStartUpdateDownload(hwnd, rsp, updateCheckType, &downloadStarted);
    if (!downloadStarted) {
        gUpdateCheckInProgress = false;
    }
    if (err == 0) {
        GetSystemTimeAsFileTime(&gGlobalPrefs->timeOfLastUpdateCheck);
        SaveSettings();
        ScheduleAutomaticUpdateAfterSuccess();
    } else {
        if (updateCheckType == UpdateCheck::Automatic) {
            ScheduleAutomaticUpdateRetry();
        } else {
            RemoveNotificationsForGroup(win->hwndCanvas, kNotifUpdateCheckInProgress);
            // notify the user about network error during a manual update check
            TempStr msg = str::FormatTemp(_TRA("Can't connect to the Internet (error %#x)."), err);
            MessageBoxWarning(hwnd, msg, _TRA("SumatraPDF Update"));
        }
    }
}

static HttpRsp* FetchUpdateCheckResponse(UpdateCheck updateCheckType) {
    const char* urls[2] = {kUpdateInfoURL, kUpdateInfoURL2};
    HttpRsp* bestRsp = nullptr;
    UpdateInfo* bestInfo = nullptr;

    for (const char* baseURL : urls) {
        StrBuilder url;
        BuildUpdateURL(url, baseURL, updateCheckType);
        char* uri = url.Get();
        HttpRsp* rsp = new HttpRsp;
        rsp->url.SetCopy(uri);
        if (!HttpGet(uri, rsp) || rsp->httpStatusCode != 200 || rsp->data.size() == 0) {
            delete rsp;
            continue;
        }
        UpdateInfo* info = ParseUpdateInfo(rsp->data.Get());
        if (!info) {
            logf("FetchUpdateCheckResponse: ParseUpdateInfo failed for '%s'\n", baseURL);
            delete rsp;
            continue;
        }
        if (!bestInfo || CompareProgramVersion(info->latestVer, bestInfo->latestVer) > 0) {
            delete bestInfo;
            delete bestRsp;
            bestInfo = info;
            bestRsp = rsp;
        } else {
            delete info;
            delete rsp;
        }
    }
    delete bestInfo;
    return bestRsp;
}

static void UpdateCheckAsync(UpdateCheckAsyncData* data) {
    auto updateCheckType = data->updateCheckType;
    data->rsp = FetchUpdateCheckResponse(updateCheckType);
    auto fn = MkFunc0<UpdateCheckAsyncData>(UpdateCheckFinish, data);
    uitask::Post(fn, "TaskUpdateCheckFinish");
}

// start auto-update check by downloading auto-update information from url
// on a background thread and processing the retrieved data on ui thread
// if autoCheck is true, this is a check *not* triggered by explicit action
// of the user and therefore will show less UI
bool StartAsyncUpdateCheck(MainWindow* win, UpdateCheck updateCheckType) {
    if (!ShouldCheckForUpdate(updateCheckType)) {
        if (UpdateCheck::UserInitiated == updateCheckType) {
            MessageBoxWarning(win->hwndFrame, _TRA("An update check is already in progress."),
                              _TRA("SumatraPDF Update"));
        }
        return false;
    }

    if (UpdateCheck::UserInitiated == updateCheckType) {
        NotificationCreateArgs args;
        args.hwndParent = win->hwndCanvas;
        args.msg = _TRA("Checking for update...");
        args.warning = true;
        args.timeoutMs = 0;
        args.groupId = kNotifUpdateCheckInProgress;
        ShowNotification(args);
    }
    gUpdateCheckInProgress = true;

    // data freed in UpdateCheckFinish()
    auto data = new UpdateCheckAsyncData();
    data->win = win;
    data->updateCheckType = updateCheckType;
    auto fn = MkFunc0<UpdateCheckAsyncData>(UpdateCheckAsync, data);
    RunAsync(fn, "UpdateCheckAsync");
    return true;
}

// the assumption is that this is a portable version downloaded to temp directory
// we should copy ourselves over the existing file, launch ourselves and
// tell our new copy to delete ourselves
void UpdateSelfTo(const char* path) {
    ReportIf(!path);
    if (!file::Exists(path)) {
        logf("UpdateSelfTo: failed because destination doesn't exist\n");
        return;
    }

    auto sleepMs = gCli->sleepMs;
    logf("UpdateSelfTo: '%s', sleep for %d ms\n", path, sleepMs);
    // sleeping for a bit to make sure that the program that launched us
    // had time to exit so that we can overwrite it
    ::Sleep(gCli->sleepMs);

    TempStr srcPath = GetSelfExePathTemp();
    bool ok = false;
    for (int i = 0; i < 20; i++) {
        ok = file::Copy(path, srcPath, false);
        if (ok) {
            break;
        }
        ::Sleep(250);
    }
    // TODO: maybe retry if copy fails under the theory that the file
    // might be temporarily locked
    if (!ok) {
        logf("UpdateSelfTo: failed to copy self to file\n");
        return;
    }
    logf("UpdateSelfTo: copied self to file\n");

    TempStr args = str::FormatTemp(R"(-sleep-ms 500 -delete-file "%s")", srcPath);
    CreateProcessHelper(path, args);
}
