import { app, BrowserWindow, ipcMain, dialog, shell, Menu, safeStorage } from "electron";
import path from "path";
import { fileURLToPath } from "url";
import Store from "electron-store";
import fs from "fs/promises";
import { createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import NexusModule from "@nexusmods/nexus-api";
import { gql, GraphQLClient } from "graphql-request";
import { path7za } from "7zip-bin";
import node7z from "node-7z";
const { extractFull } = node7z;
import packageJson from "./package.json" with { type: "json" };
import semverGt from "semver/functions/gt.js";
import { randomUUID } from "crypto";
import { spawn } from "child_process";
import vdf from "vdf-parser";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const gotTheLock = app.requestSingleInstanceLock();
const isDev = !app.isPackaged;
const VERSION = packageJson.version;
const NAME = packageJson.productName;
const userAgent = `${NAME}/${VERSION}`;

const store = new Store();
const bepinexStore = new Store({ name: "bepinex-version" });
const installedModsStore = new Store({ name: "installed-mods-list" });
const NexusAPIStore = new Store({ name: "nexus-api", encryptionKey: packageJson["AES-key-nexus-api"], fileExtension: "encrypted", clearInvalidConfig: true });

const userSavePath = app.getPath("userData");
const tempPath = app.getPath("temp");
const modSavePath = path.join(userSavePath, "mods");
const dataPath = path.join(userSavePath, "config.json");
let sevenZipPath = path7za;

const Nexus = NexusModule.default;
let nexus;
let installedCachedModList;
let installedTotalModsCount;
let onlineCachedModList;
let onlineTotalModsCount;
let thunderstoreCachedModList;
let thunderstoreTotalModsCount;
let allThunderstoreCachedModList;
let allThunderstoreCachedModListNeedRefresh = true;

const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll", "libdoorstop.so", "run_bepinex.sh"];
let bepinexVersion = bepinexStore.get("bepinex-version");
let bepinexBackupVersion;

let mainWindow;
let nexusWindow;
let htmlFile;

//////////////////////////////////////////////////////
////////////////////// STARTUP ///////////////////////

if (!gotTheLock) {
    app.quit();
} else {
    app.on("second-instance", (event, argv) => {
        const nxmUrl = argv.find((arg) => arg.startsWith("nxm://"));
        if (nxmUrl) {
            handleNxmUrl(nxmUrl);
        }
    });
}

async function createWindow() {
    mainWindow = new BrowserWindow({
        width: 1280,
        height: 720,
        webPreferences: {
            preload: path.join(__dirname, "preload.js"),
        },
        backgroundColor: "#000",
        show: false,
    });

    if (await fileExists(dataPath)) {
        htmlFile = "index.html";
    } else {
        htmlFile = "welcome.html";
    }

    mainWindow.loadFile(path.join("renderer", htmlFile));

    mainWindow.webContents.once("did-finish-load", () => {
        mainWindow.show();
        if (!isDev) {
            verifyUpdate();
        }
    });
}

app.whenReady().then(async () => {
    if (isDev) {
        app.setAsDefaultProtocolClient("nxm", process.execPath, [path.resolve(process.argv[1])]);
    } else {
        app.setAsDefaultProtocolClient("nxm");
        sevenZipPath = path7za.replace(path.join("app.asar", "node_modules"), "");
        Menu.setApplicationMenu(null);
    }

    if (gotTheLock) {
        createNexus(loadNexusApi());
        await checkInstalledMods();
        await checkForCoreAndPatcherMods();
        createWindow();
    }

    app.on("activate", async () => {
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
});

app.on("window-all-closed", () => {
    if (process.platform !== "darwin") {
        app.quit();
    }
});

app.on("open-url", (event, url) => {
    event.preventDefault();
    handleNxmUrl(url);
});

async function verifyUpdate() {
    const GITHUB_URL = "https://api.github.com/repos/Gabi-Zar/Silk-Fly-Launcher/releases";

    const res = await fetch(GITHUB_URL, {
        headers: {
            "User-Agent": userAgent,
            Accept: "application/vnd.github+json",
        },
    });

    if (!res.ok) {
        if (res.status == 403) {
            mainWindow.webContents.send("showToast", "Github has blocked the application. Please try again later.", "error");
        }
        throw new Error(`GitHub API error: ${res.status}`);
    }

    const releases = await res.json();
    const prerelease = releases.find((r) => r.prerelease);
    const release = releases.find((r) => !r.prerelease && !r.draft);

    let prereleaseVersion;
    let releaseVersion;
    let latestVersion;

    if (prerelease) {
        prereleaseVersion = prerelease.tag_name.replace(/^v/, "");
        latestVersion = prereleaseVersion;
    }
    if (release) {
        releaseVersion = release.tag_name.replace(/^v/, "");
        latestVersion = releaseVersion;
    }

    if (prereleaseVersion && releaseVersion) {
        latestVersion = semverGt(prereleaseVersion, releaseVersion) ? prereleaseVersion : releaseVersion;
    }
    if (latestVersion != VERSION) {
        mainWindow.webContents.send(
            "showBanner",
            `Update v${latestVersion} is available on <a href="" class="link" onclick="electronAPI.openExternalLink('https://github.com/Gabi-Zar/Silk-Fly-Launcher/releases/tag/v${latestVersion}')">GitHub</a>! Your current version is ${VERSION}.`,
        );
    }
}

//////////////////////////////////////////////////////
///////////////// SAVING AND LOADING /////////////////
ipcMain.handle("save-path", async (event, path) => {
    await saveSilksongPath(path);
});

async function saveSilksongPath(path) {
    store.set("silksong-path", path);
    await checkInstalledMods();
    await checkForCoreAndPatcherMods();
}

function loadSilksongPath() {
    const silksongPath = store.get("silksong-path");
    if (silksongPath == undefined) {
        return "";
    }
    return silksongPath;
}

ipcMain.handle("load-path", () => {
    return loadSilksongPath();
});

function saveBepinexVersion(version) {
    bepinexVersion = version;
    if (bepinexVersion == undefined) {
        bepinexStore.delete("bepinex-version");
        return;
    }
    bepinexStore.set("bepinex-version", version);
}

ipcMain.handle("load-bepinex-version", () => {
    return loadBepinexVersion();
});

function loadBepinexVersion() {
    bepinexVersion = bepinexStore.get("bepinex-version");
    return bepinexVersion;
}

function saveBepinexBackupVersion(version) {
    bepinexBackupVersion = version;
    if (bepinexBackupVersion == undefined) {
        bepinexStore.delete("bepinex-backup-version");
        return;
    }
    bepinexStore.set("bepinex-backup-version", version);
}

ipcMain.handle("load-bepinex-backup-version", () => {
    bepinexBackupVersion = bepinexStore.get("bepinex-backup-version");
    return bepinexBackupVersion;
});

ipcMain.handle("save-nexus-api", async (event, api) => {
    if (api) {
        const encryptedAPI = safeStorage.encryptString(api);
        NexusAPIStore.set("nexus-api", encryptedAPI.toString("base64"));
    } else {
        NexusAPIStore.delete("nexus-api");
    }
    await createNexus(api);
});

function loadNexusApi() {
    const encryptedAPI = NexusAPIStore.get("nexus-api");
    if (encryptedAPI) {
        return safeStorage.decryptString(Buffer.from(encryptedAPI, "base64"));
    }
}

ipcMain.handle("load-nexus-api", () => {
    if (loadNexusApi()) {
        return true;
    }
    return false;
});

ipcMain.handle("save-theme", (event, theme, lacePinState) => {
    store.set("theme.theme", theme);
    store.set("theme.lacePinState", lacePinState);
});

ipcMain.handle("load-theme", () => {
    const theme = [store.get("theme.theme"), store.get("theme.lacePinState")];
    if (theme[0] == undefined) {
        return ["Silksong", false];
    }
    return theme;
});

async function saveModInfo(modId, suppr = false, optionalModInfo = {}) {
    if (suppr == true) {
        installedModsStore.delete(String(modId));
        return;
    }

    let modInfo;
    if (onlineCachedModList) {
        modInfo = onlineCachedModList.find((mod) => mod.modId == modId);
    }
    if (!modInfo) {
        if (thunderstoreCachedModList) {
            modInfo = thunderstoreCachedModList.find((mod) => mod.modId == modId);
        }
    }
    if (!modInfo) {
        modInfo = optionalModInfo;
    }
    modInfo.activated = true;

    const modFiles = await fs.readdir(path.join(modSavePath, modId));
    modInfo.fileSize = 0;
    for (const file of modFiles) {
        const fileStats = await fs.stat(path.join(modSavePath, modId, file));
        modInfo.fileSize += fileStats.size;
    }

    installedModsStore.set(String(modId), modInfo);
}

ipcMain.handle("save-linux-steam", (event, state) => {
    store.set("linux.steam", state);
});

function loadLinuxSteam() {
    return store.get("linux.steam");
}

ipcMain.handle("load-linux-steam", () => {
    return loadLinuxSteam();
});

//////////////////////////////////////////////////////
/////////////////// DATA HANDLING ////////////////////

async function fileExists(filePath) {
    try {
        await fs.access(filePath);
        return true;
    } catch {
        return false;
    }
}

ipcMain.handle("delete-data", async () => {
    if (await fileExists(dataPath)) {
        await fs.unlink(dataPath);
    }
});

ipcMain.handle("export-data", async () => {
    if (!(await fileExists(dataPath))) {
        return;
    }

    const { canceled, filePath } = await dialog.showSaveDialog({
        title: "Export Data",
        defaultPath: "config.json",
        filters: [{ name: "JSON", extensions: ["json"] }],
    });

    if (canceled || !filePath) return;

    await fs.copyFile(dataPath, filePath);
});

ipcMain.handle("import-data", async () => {
    const { canceled, filePaths } = await dialog.showOpenDialog({
        title: "Import Data",
        properties: ["openFile"],
        filters: [{ name: "JSON", extensions: ["json"] }],
    });

    if (canceled || !filePaths) return false;

    if (await fileExists(dataPath)) {
        await fs.unlink(dataPath);
    }
    await fs.copyFile(filePaths[0], dataPath, fs.constants.COPYFILE_EXCL);
    return true;
});

//////////////////////////////////////////////////////
////////////////////// BEPINEX ///////////////////////

async function installBepinex() {
    const silksongPath = loadSilksongPath();
    const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");

    if (!(await fileExists(silksongPath))) {
        mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
        return;
    }

    if (await fileExists(bepinexBackupPath)) {
        mainWindow.webContents.send("showToast", "Installing Bepinex from Backup");

        await fs.cp(bepinexBackupPath, silksongPath, { recursive: true });
        await fs.rm(bepinexBackupPath, { recursive: true });

        bepinexBackupVersion = bepinexStore.get("bepinex-backup-version");
        saveBepinexVersion(bepinexBackupVersion);
        saveBepinexBackupVersion(undefined);
    } else {
        const GITHUB_URL = "https://api.github.com/repos/bepinex/bepinex/releases/latest";

        const res = await fetch(GITHUB_URL, {
            headers: {
                "User-Agent": userAgent,
                Accept: "application/vnd.github+json",
            },
        });

        if (!res.ok) {
            if (res.status == 403) {
                mainWindow.webContents.send("showToast", "Github has blocked the application. Please try again later.", "error");
            }
            throw new Error(`GitHub API error: ${res.status}`);
        }

        const release = await res.json();

        if (loadBepinexVersion() == release.tag_name) {
            mainWindow.webContents.send("showToast", "No new BepInEx update.");
            return;
        }
        mainWindow.webContents.send("showToast", "Installing Bepinex from Github");

        await uninstallBepinex();

        let asset;
        if (process.platform === "win32") {
            asset = release.assets.find((a) => a.name.endsWith(".zip") && a.name.toLowerCase().includes("win_x64"));
        } else if (process.platform === "linux") {
            asset = release.assets.find((a) => a.name.endsWith(".zip") && a.name.toLowerCase().includes("linux_x64"));
        }

        await downloadAndUnzip(asset.browser_download_url, silksongPath);

        saveBepinexVersion(release.tag_name);
    }

    await checkInstalledMods();
    await checkForCoreAndPatcherMods();
}

ipcMain.handle("install-bepinex", async () => {
    await installBepinex();
});

async function uninstallBepinex() {
    const silksongPath = loadSilksongPath();
    const bepinexFolderPath = path.join(silksongPath, "BepInEx");

    if (!(await fileExists(silksongPath))) {
        mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
        return;
    }

    if (await fileExists(bepinexFolderPath)) {
        await fs.rm(bepinexFolderPath, { recursive: true });
    }

    for (const file of bepinexFiles) {
        const filePath = path.join(silksongPath, file);
        if (await fileExists(filePath)) {
            await fs.unlink(filePath);
        }
    }
    saveBepinexVersion(undefined);
}

ipcMain.handle("uninstall-bepinex", async () => {
    await uninstallBepinex();
});

async function backupBepinex() {
    const silksongPath = loadSilksongPath();
    const bepinexFolderPath = path.join(silksongPath, "BepInEx");
    const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");
    const BepinexPluginsPath = path.join(silksongPath, "BepInEx", "plugins");
    const bepinexCorePath = path.join(silksongPath, "BepInEx", "core", "custom");
    const bepinexPatcherPath = path.join(silksongPath, "BepInEx", "patchers", "custom");

    if (!(await fileExists(silksongPath))) {
        mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
        return;
    }

    if ((await fileExists(bepinexBackupPath)) == false) {
        await fs.mkdir(bepinexBackupPath);
    }

    if (await fileExists(BepinexPluginsPath)) {
        await fs.rm(BepinexPluginsPath, { recursive: true });
    }
    if (await fileExists(bepinexCorePath)) {
        await fs.rm(bepinexCorePath, { recursive: true });
    }
    if (await fileExists(bepinexPatcherPath)) {
        await fs.rm(bepinexPatcherPath, { recursive: true });
    }

    if (await fileExists(bepinexFolderPath)) {
        await fs.cp(bepinexFolderPath, path.join(bepinexBackupPath, "BepInEx"), {
            recursive: true,
        });
    }

    for (const file of bepinexFiles) {
        const filePath = path.join(silksongPath, file);
        if (await fileExists(filePath)) {
            await fs.copyFile(filePath, path.join(bepinexBackupPath, file));
        }
    }

    saveBepinexBackupVersion(bepinexVersion);
    await uninstallBepinex();
}

ipcMain.handle("backup-bepinex", async () => {
    await backupBepinex();
});

ipcMain.handle("delete-bepinex-backup", async () => {
    const silksongPath = loadSilksongPath();
    const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");

    if (!(await fileExists(silksongPath))) {
        mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
        return;
    }

    if (await fileExists(bepinexBackupPath)) {
        await fs.rm(bepinexBackupPath, { recursive: true });
        saveBepinexBackupVersion(undefined);
    }
});

//////////////////////////////////////////////////////
//////////////// NEXUS / THUNDERSTORE ////////////////

async function createNexus(api) {
    if (api == undefined) {
        nexus = undefined;
        return;
    }

    try {
        nexus = await Nexus.create(api, NAME, VERSION, "hollowknightsilksong");
    } catch (error) {
        if (error.mStatusCode == 401) {
            mainWindow.webContents.send("showToast", "Invalid Nexus API key", "error");
        } else if (error.code == "ENOTFOUND") {
            mainWindow.webContents.send("showToast", "Unable to communicate with Nexus servers", "error");
        } else {
            mainWindow.webContents.send("showToast", "Unable to create Nexus API ", "error");
        }
        nexus = undefined;
    }
}

ipcMain.handle("verify-nexus-api", async () => {
    return await verifyNexusAPI();
});

async function verifyNexusAPI() {
    if (nexus == undefined) {
        return false;
    }
    if (await nexus.getValidationResult()) {
        return true;
    }
}

ipcMain.handle("get-mods", async (event, type) => {
    if (type == "mods-installed") {
        if (!installedCachedModList) {
            await searchInstalledMods("");
        }
        return { installedModsInfo: installedCachedModList, installedTotalCount: installedTotalModsCount };
    } else if (type == "mods-online") {
        if (!onlineCachedModList) {
            await searchNexusMods("");
        }
        return { onlineModsInfo: onlineCachedModList, onlineTotalCount: onlineTotalModsCount };
    } else if (type == "mods-thunderstore") {
        if (!thunderstoreCachedModList) {
            await searchThunderstoreMods("");
        }
        return { thunderstoreModsInfo: thunderstoreCachedModList, thunderstoreTotalCount: thunderstoreTotalModsCount };
    }
});

ipcMain.handle("open-download", async (event, link) => {
    if (!(await verifyNexusAPI())) {
        mainWindow.webContents.send("showToast", "Unable to download.", "error");
        return;
    }

    nexusWindow = new BrowserWindow({
        width: 1080,
        height: 720,
        modal: true,
        parent: mainWindow,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
        },
        backgroundColor: "#000000",
    });

    nexusWindow.loadURL(link);
});

function handleNxmUrl(url) {
    if (nexusWindow) {
        nexusWindow.close();
    }

    const parsedUrl = new URL(url);

    const key = parsedUrl.searchParams.get("key");
    const expires = Number(parsedUrl.searchParams.get("expires"));

    const [, , modId, , fileId] = parsedUrl.pathname.split("/");

    startDownload(Number(modId), Number(fileId), key, expires);
}

async function startDownload(modId, fileId, key, expires) {
    modId = String(modId);
    const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");

    if (!(await verifyNexusAPI())) {
        mainWindow.webContents.send("showToast", "Unable to download.", "error");
        return;
    }

    const url = await nexus.getDownloadURLs(modId, fileId, key, expires);
    const download_url = url[0].URI;

    if (!(await fileExists(modSavePath))) {
        await fs.mkdir(modSavePath);
    }

    await downloadAndUnzip(download_url, path.join(modSavePath, modId));
    if (await fileExists(bepinexFolderPath)) {
        await fs.cp(path.join(modSavePath, modId), path.join(bepinexFolderPath, "plugins", modId), { recursive: true });
    }

    saveModInfo(modId);
    mainWindow.webContents.send("showToast", "Mod downloaded successfully.");
    installedCachedModList = undefined;
}

ipcMain.handle("search-nexus-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => {
    await searchNexusMods(keywords, offset, count, sortFilter, sortOrder);
});

async function searchNexusMods(keywords, offset = 0, count = 10, sortFilter = "downloads", sortOrder = "DESC") {
    if (keywords.length == 1) {
        mainWindow.webContents.send("showToast", "Your query must contain at least 2 characters.", "warning");
        return;
    }

    const endpoint = "https://api.nexusmods.com/v2/graphql";
    const client = new GraphQLClient(endpoint, {
        headers: {
            "User-Agent": userAgent,
            "Content-Type": "application/json",
        },
    });

    const query = gql`
        query Mods($filter: ModsFilter, $offset: Int, $count: Int, $sort: [ModsSort!]) {
            mods(filter: $filter, offset: $offset, count: $count, sort: $sort) {
                nodes {
                    author
                    endorsements
                    modId
                    name
                    pictureUrl
                    summary
                    updatedAt
                    createdAt
                    version
                    downloads
                    fileSize
                }
                totalCount
            }
        }
    `;

    const variables = {
        filter: {
            op: "AND",
            gameDomainName: [{ value: "hollowknightsilksong" }],
            name: [{ value: keywords, op: "WILDCARD" }],
        },
        offset: offset,
        count: count,
        sort: [{ [sortFilter]: { direction: sortOrder } }],
    };
    if (!keywords) {
        delete variables.filter.name;
    }

    const data = await client.request(query, variables);
    onlineCachedModList = data.mods.nodes;

    for (let i = 0; i < onlineCachedModList.length; i++) {
        onlineCachedModList[i].source = "nexusmods";
        if (onlineCachedModList[i].modId == 26) {
            onlineCachedModList.splice(i, 1);
        }
    }

    onlineTotalModsCount = data.mods.totalCount;
}

ipcMain.handle("search-thunderstore-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => {
    await searchThunderstoreMods(keywords, offset, count, sortFilter, sortOrder);
});

async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilter = "downloads", sortOrder = "DESC") {
    if (allThunderstoreCachedModListNeedRefresh) {
        const res = await fetch("https://thunderstore.io/c/hollow-knight-silksong/api/v1/package/", {
            headers: {
                "User-Agent": userAgent,
            },
        });
        if (!res.ok) {
            if (res.status == 403) {
                mainWindow.webContents.send("showToast", "Thunderstore has blocked the application. Please try again later.", "error");
            }
            throw new Error(`Thunderstore API error: ${res.status}`);
        }
        const modsInfo = await res.json();

        const modsToRemove = ["f21c391c-0bc5-431d-a233-95323b95e01b", "42f76853-d2a4-4520-949b-13a02fdbbbcb", "34eac80c-5497-470e-b98c-f53421b828c0"];
        const reMappedModsInfo = [];
        for (let i = 0; i < modsInfo.length; i++) {
            modsInfo[i].source = "thunderstore";
            if (modsToRemove.includes(modsInfo[i].uuid4)) {
                modsInfo.splice(i, 1);
                i--;
                continue;
            }
            reMappedModsInfo.push(reMapThunderstoreModsInfo(modsInfo[i]));
        }
        allThunderstoreCachedModList = reMappedModsInfo;
        allThunderstoreCachedModListNeedRefresh = false;
        setTimeout(
            () => {
                allThunderstoreCachedModListNeedRefresh = true;
            },
            10 * 60 * 1000,
        );
    }

    const result = sortAndFilterModsList(allThunderstoreCachedModList, keywords, offset, count, sortFilter, sortOrder);
    thunderstoreCachedModList = result.list;
    thunderstoreTotalModsCount = result.totalCount;
}

function reMapThunderstoreModsInfo(modInfo) {
    let totalDownloads = 0;
    for (const version of modInfo.versions) {
        totalDownloads += version.downloads;
    }

    return {
        author: modInfo.owner,
        endorsements: modInfo.rating_score,
        modId: modInfo.uuid4,
        name: modInfo.name,
        pictureUrl: modInfo.versions[0].icon,
        summary: modInfo.versions[0].description,
        updatedAt: modInfo.date_updated,
        createdAt: modInfo.date_created,
        version: modInfo.versions[0].version_number,
        downloads: totalDownloads,
        fileSize: modInfo.versions[0].file_size,
        source: modInfo.source,
        dependencies: modInfo.versions[0].dependencies,
    };
}

ipcMain.handle("download-thunderstore-mods", async (event, url, modId) => {
    await downloadThunderstoreMods(url, modId);
});

async function downloadThunderstoreMods(url, modId) {
    const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
    if (!(await fileExists(loadSilksongPath()))) {
        mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
        return;
    }

    if (!(await fileExists(modSavePath))) {
        await fs.mkdir(modSavePath);
    }

    await downloadAndUnzip(url, path.join(modSavePath, modId));
    if (await fileExists(bepinexFolderPath)) {
        await fs.cp(path.join(modSavePath, modId), path.join(bepinexFolderPath, "plugins", modId), { recursive: true });
    }

    saveModInfo(modId);
    await downloadThunderstoreModsDependencies(modId);
    await checkForCoreAndPatcherMods();
    mainWindow.webContents.send("showToast", "Mod downloaded successfully.");
    installedCachedModList = undefined;
}

async function downloadThunderstoreModsDependencies(modId) {
    const dependencies = allThunderstoreCachedModList.find((mod) => mod.modId == modId).dependencies;
    for (const dependency of dependencies) {
        const dependencyArray = dependency.split("-");
        const modInfo = allThunderstoreCachedModList.find((mod) => mod.author === dependencyArray[0] && mod.name === dependencyArray[1]);
        if (modInfo) {
            const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
            const url = `https://thunderstore.io/package/download/${dependencyArray[0]}/${dependencyArray[1]}/${dependencyArray[2]}`;
            await downloadAndUnzip(url, path.join(modSavePath, modInfo.modId));
            if (await fileExists(bepinexFolderPath)) {
                await fs.cp(path.join(modSavePath, modInfo.modId), path.join(bepinexFolderPath, "plugins", modInfo.modId), { recursive: true });
            }
            saveModInfo(modInfo.modId, undefined, modInfo);
            await downloadThunderstoreModsDependencies(modInfo.modId);
        }
    }
}

//////////////////////////////////////////////////////
//////////////////////// MODS ////////////////////////

ipcMain.handle("search-installed-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => {
    await searchInstalledMods(keywords, offset, count, sortFilter, sortOrder);
});

async function searchInstalledMods(keywords, offset = 0, count = 10, sortFilter = "name", sortOrder = "ASC") {
    const modsInfo = [];
    for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
        modsInfo.push(modInfo);
    }

    const result = sortAndFilterModsList(modsInfo, keywords, offset, count, sortFilter, sortOrder);
    installedCachedModList = result.list;
    installedTotalModsCount = result.totalCount;
}

async function checkInstalledMods() {
    if (!loadSilksongPath()) {
        return;
    }
    const bepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");

    for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
        modInfo.modId = String(modInfo.modId);
        if (!(await fileExists(path.join(modSavePath, modInfo.modId)))) {
            saveModInfo(key, true);
            if (await fileExists(path.join(bepinexPluginsPath, modInfo.modId))) {
                await fs.rm(path.join(bepinexPluginsPath, modInfo.modId), { recursive: true });
            }
            continue;
        }

        if (modInfo.activated) {
            await fs.cp(path.join(modSavePath, modInfo.modId), path.join(bepinexPluginsPath, modInfo.modId), { recursive: true });
        }
    }
}

ipcMain.handle("uninstall-mod", async (event, modId) => {
    modId = String(modId);
    const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
    const modPath = path.join(BepinexPluginsPath, modId);
    if (await fileExists(path.join(modSavePath, modId))) {
        await fs.rm(path.join(modSavePath, modId), { recursive: true });
    }
    if (await fileExists(modPath)) {
        await fs.rm(modPath, { recursive: true });
    }

    for (let i = 0; i < installedCachedModList.length; i++) {
        if (installedCachedModList[i].modId == modId) {
            installedCachedModList.splice(i, 1);
        }
    }

    saveModInfo(modId, true);
    checkForCoreAndPatcherMods();
});

ipcMain.handle("activate-mod", async (event, modId) => {
    await activateMod(modId);
});

async function activateMod(modId) {
    if (!loadSilksongPath()) {
        return;
    }

    const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");

    if (!installedModsStore.get(`${modId}.activated`)) {
        installedModsStore.set(`${modId}.activated`, true);
    }

    if (bepinexVersion) {
        if (!(await fileExists(path.join(BepinexPluginsPath, String(modId))))) {
            await fs.cp(path.join(modSavePath, String(modId)), path.join(BepinexPluginsPath, String(modId)), { recursive: true });
        }
    }
    checkForCoreAndPatcherMods();
}

ipcMain.handle("deactivate-mod", async (event, modId) => {
    const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");

    if (installedModsStore.get(`${modId}.activated`)) {
        installedModsStore.set(`${modId}.activated`, false);

        if (bepinexVersion) {
            if (await fileExists(path.join(BepinexPluginsPath, String(modId)))) {
                await fs.rm(path.join(BepinexPluginsPath, String(modId)), { recursive: true });
            }
        }
        checkForCoreAndPatcherMods();
    }
});

function sortAndFilterModsList(list, keywords, offset, count, sortFilter, sortOrder) {
    const listFiltered = list.filter((element) => element.name.toLowerCase().includes(keywords.toLowerCase()));
    const sortFactor = sortOrder == "ASC" ? 1 : -1;

    let listSorted;
    if (sortFilter == "name" || sortFilter == "createdAt" || sortFilter == "updatedAt") {
        listSorted = listFiltered.sort((a, b) => sortFactor * a[sortFilter].localeCompare(b[sortFilter]));
    } else if (sortFilter == "downloads" || sortFilter == "endorsements" || sortFilter == "size") {
        if (sortFilter == "size") {
            sortFilter = "fileSize";
        }
        listSorted = listFiltered.sort((a, b) => sortFactor * (a[sortFilter] - b[sortFilter]));
    }

    return { list: listSorted.slice(offset, offset + count), totalCount: listSorted.length };
}

ipcMain.handle("add-offline-mod", async () => {
    const { canceled, filePaths } = await dialog.showOpenDialog({
        title: "Add mod",
        properties: ["openFile"],
        filters: [{ name: "mod", extensions: ["dll", "zip", "7z"] }],
    });

    if (canceled || !filePaths) return false;

    const fileName = path.basename(filePaths[0]);
    const extension = path.extname(fileName).toLowerCase();
    let uuid;
    do {
        uuid = randomUUID();
    } while (installedModsStore.get(uuid));

    if (extension === ".dll") {
        await fs.mkdir(path.join(modSavePath, uuid));
        await fs.copyFile(filePaths[0], path.join(modSavePath, uuid, fileName));
    } else if ([".zip", ".7z"].includes(extension)) {
        await extractArchive(filePaths[0], path.join(modSavePath, uuid));
    } else {
        mainWindow.webContents.send("showToast", `Unsupported file type: ${extension}`, "error");
        return false;
    }

    activateMod(uuid);

    const time = new Date().toLocaleDateString();
    const splitedTime = time.split("/");
    let newTime = "";
    for (let i = 2; i >= 0; i--) {
        newTime = newTime.concat(splitedTime[i]);
        if (i > 0) {
            newTime = newTime.concat("-");
        }
    }

    await saveModInfo(uuid, false, {
        modId: uuid,
        name: fileName.split(".").shift(),
        summary: "Local mod",
        updatedAt: newTime,
        createdAt: newTime,
        source: "local",
    });

    return true;
});

async function checkForCoreAndPatcherMods() {
    const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
    const bepinexPluginsPath = path.join(bepinexFolderPath, "plugins");
    const bepinexCorePath = path.join(bepinexFolderPath, "core", "custom");
    const bepinexPatcherPath = path.join(bepinexFolderPath, "patchers", "custom");

    if (await fileExists(bepinexCorePath)) {
        await fs.rm(bepinexCorePath, { recursive: true });
    }
    if (await fileExists(bepinexPatcherPath)) {
        await fs.rm(bepinexPatcherPath, { recursive: true });
    }
    await fs.mkdir(bepinexCorePath, { recursive: true });
    await fs.mkdir(bepinexPatcherPath, { recursive: true });

    async function scanDir(dirPath, relativePath = "") {
        let entries;
        try {
            entries = await fs.readdir(dirPath, { withFileTypes: true });
        } catch {
            return;
        }

        for (const entry of entries) {
            if (!entry.isDirectory()) continue;

            const currentRelative = path.join(relativePath, entry.name);

            if (entry.name === "core") {
                await copyContents(path.join(dirPath, entry.name), bepinexCorePath);
                if (await fileExists(path.join(bepinexPluginsPath, currentRelative))) {
                    await fs.rm(path.join(bepinexPluginsPath, currentRelative), { recursive: true });
                }
            } else if (entry.name === "patchers") {
                await copyContents(path.join(dirPath, entry.name), bepinexPatcherPath);
                if (await fileExists(path.join(bepinexPluginsPath, currentRelative))) {
                    await fs.rm(path.join(bepinexPluginsPath, currentRelative), { recursive: true });
                }
            } else {
                if (!(await fileExists(path.join(bepinexPluginsPath, currentRelative)))) continue;
                await scanDir(path.join(dirPath, entry.name), currentRelative);
            }
        }
    }

    async function copyContents(srcDir, destDir) {
        let files;
        try {
            files = await fs.readdir(srcDir);
        } catch {
            return;
        }

        for (const file of files) {
            const src = path.join(srcDir, file);
            const dest = path.join(destDir, file);
            await fs.copyFile(src, dest);
        }
    }

    await scanDir(modSavePath);
}

//////////////////////////////////////////////////////
//////////////////// UNCATEGORIZE ////////////////////

ipcMain.handle("auto-detect-game-path", async () => {
    const defaultsSilksongPaths = [":/Program Files (x86)/Steam/steamapps/common/Hollow Knight Silksong", ":/SteamLibrary/steamapps/common/Hollow Knight Silksong"];
    for (const path of defaultsSilksongPaths) {
        for (let i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); i++) {
            const fullPath = `${String.fromCharCode(i)}${path}`;
            if (await fileExists(fullPath)) {
                saveSilksongPath(fullPath);
                return;
            }
        }
    }
});

ipcMain.handle("load-main-page", () => {
    htmlFile = "index.html";
    mainWindow.loadFile(path.join("renderer", htmlFile));
});

ipcMain.handle("get-page", () => {
    return htmlFile;
});

ipcMain.handle("open-link", async (event, link) => {
    await shell.openExternal(link);
});

ipcMain.handle("open-window", async (event, file) => {
    const win = new BrowserWindow({
        width: 600,
        height: 720,
        modal: true,
        parent: mainWindow,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
        },
    });

    win.title = file;
    win.loadFile(file);
});

ipcMain.handle("launch-game", async (event, mode) => {
    let silksongExecutable;
    if (process.platform === "win32") {
        silksongExecutable = "Hollow Knight Silksong.exe";
    } else if (process.platform === "linux") {
        silksongExecutable = "Hollow Knight Silksong";
    }

    const silksongExecutablePath = path.join(loadSilksongPath(), silksongExecutable);
    const silksongScriptPath = path.join(loadSilksongPath(), "run_bepinex.sh");
    const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
    if (!fileExists(silksongExecutablePath)) {
        mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
        return;
    }

    if (mode === "modded") {
        if (await fileExists(bepinexFolderPath)) {
            if (process.platform === "win32") {
                executeGame(silksongExecutablePath);
            }
            if (process.platform === "linux") {
                if (loadLinuxSteam()) {
                    if (!(await getSteamLinuxState())) {
                        mainWindow.webContents.send("showToast", "Preparing Steam");
                        await prepareSteamLinux(true);
                    }
                    const game = spawn("steam", ["-applaunch", "1030300"], {
                        detached: true,
                        stdio: "ignore",
                    });
                    game.unref();
                    return;
                }
                executeGame(silksongScriptPath, [silksongExecutable]);
            }
        } else {
            await installBepinex();
            if (process.platform === "win32") {
                executeGame(silksongExecutablePath);
            }
            if (process.platform === "linux") {
                if (loadLinuxSteam()) {
                    if (!(await getSteamLinuxState())) {
                        mainWindow.webContents.send("showToast", "Preparing Steam");
                        await prepareSteamLinux(true);
                    }
                    const game = spawn("steam", ["-applaunch", "1030300"], {
                        detached: true,
                        stdio: "ignore",
                    });
                    game.unref();
                    return;
                }
                executeGame(silksongScriptPath, [silksongExecutable]);
            }
        }
    }
    if (mode === "vanilla") {
        if (await fileExists(bepinexFolderPath)) {
            await backupBepinex();
            if (process.platform === "linux" && loadLinuxSteam() && (await getSteamLinuxState())) {
                mainWindow.webContents.send("showToast", "Preparing Steam");
                await prepareSteamLinux(false);
            }
            executeGame(silksongExecutablePath);
        } else {
            if (process.platform === "linux" && loadLinuxSteam() && (await getSteamLinuxState())) {
                mainWindow.webContents.send("showToast", "Preparing Steam");
                await prepareSteamLinux(false);
            }
            executeGame(silksongExecutablePath);
        }
    }
});

async function executeGame(path, args = []) {
    await fs.chmod(path, 0o755);
    const game = spawn(path, args, {
        cwd: loadSilksongPath(),
        detached: true,
        stdio: "ignore",
    });
    game.unref();
}

async function downloadAndUnzip(url, toPath) {
    url = new URL(url);
    const fileName = url.pathname.split("/").pop();
    const extension = fileName.split(".").pop().toLowerCase();

    const download = await fetch(url.href);
    if (!download.ok) {
        mainWindow.webContents.send("showToast", "Error during download.", "error");
        return;
    }

    const tempPath = path.join(userSavePath, `tempArchive.${extension}`);
    await pipeline(download.body, createWriteStream(tempPath));
    await extractArchive(tempPath, toPath);
    await fs.unlink(tempPath);
}

async function extractArchive(archivePath, destPath) {
    if (process.platform === "linux") {
        await prepareSevenZipLinux();
    }
    return new Promise((resolve, reject) => {
        const stream = extractFull(archivePath, destPath, {
            $bin: sevenZipPath,
        });

        stream.on("end", resolve);
        stream.on("error", reject);
    });
}

ipcMain.handle("get-version", () => {
    return VERSION;
});

async function prepareSevenZipLinux() {
    const targetPath = path.join(tempPath, "7za");
    if (await fileExists(targetPath)) {
        sevenZipPath = targetPath;
        return;
    }

    await fs.copyFile(sevenZipPath, targetPath);
    await fs.chmod(targetPath, 0o755);

    sevenZipPath = targetPath;
}

async function prepareSteamLinux(isModded) {
    const kill = spawn("killall", ["steam"]);
    kill.unref();
    await waitForSteamToClose();

    const steamUserDataPath = path.join(process.env.HOME, ".local", "share", "Steam", "userdata");
    const usersId = await fs.readdir(steamUserDataPath);

    for (const userId of usersId) {
        const steamConfigPath = path.join(steamUserDataPath, userId, "config", "localconfig.vdf");
        const rawConfig = await fs.readFile(steamConfigPath, { encoding: "utf8" });
        const parsedConfig = vdf.parse(rawConfig);
        if (isModded) {
            parsedConfig.UserLocalConfigStore.Software.Valve.Steam.apps[1030300].LaunchOptions = "./run_bepinex.sh %command%";
        } else {
            parsedConfig.UserLocalConfigStore.Software.Valve.Steam.apps[1030300].LaunchOptions = "";
        }

        const config = vdf.stringify(parsedConfig);
        await fs.writeFile(steamConfigPath, config, { encoding: "utf8" });
    }

    const steam = spawn("steam", [], {
        detached: true,
        stdio: "ignore",
    });
    steam.unref();
}

async function getSteamLinuxState() {
    const steamUserDataPath = path.join(process.env.HOME, ".local", "share", "Steam", "userdata");
    const usersId = await fs.readdir(steamUserDataPath);

    const result = [];
    for (const userId of usersId) {
        const steamConfigPath = path.join(steamUserDataPath, userId, "config", "localconfig.vdf");
        const rawConfig = await fs.readFile(steamConfigPath, { encoding: "utf8" });
        const parsedConfig = vdf.parse(rawConfig);

        result.push(parsedConfig.UserLocalConfigStore.Software.Valve.Steam.apps[1030300].LaunchOptions === "./run_bepinex.sh %command%");
    }
    return result.every(Boolean);
}

function waitForSteamToClose() {
    return new Promise((resolve) => {
        function check() {
            const pgrep = spawn("pgrep", ["-x", "steam"]);

            pgrep.on("close", (code) => {
                if (code == 1) {
                    resolve();
                } else {
                    setTimeout(check, 500);
                }
            });
        }

        check();
    });
}
