"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LwjglDownloader = void 0;
/**
 * @author NovaStepStudios
 * @alias StepnickaSantiago
 * @license Apache-2.0
 * @link https://www.apache.org/licenses/LICENSE-2.0
 */
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const https = __importStar(require("https"));
const events_1 = __importDefault(require("events"));
const unzipper_1 = __importDefault(require("unzipper"));
const crypto_1 = __importDefault(require("crypto"));
const p_limit_1 = __importDefault(require("p-limit"));
class LwjglDownloader extends events_1.default {
    #root;
    #concurrency;
    #maxRetries;
    #overwrite;
    constructor(rootDir, concurrency = 6, maxRetries = 8, overwrite = false) {
        super();
        this.#root = rootDir;
        this.#concurrency = Math.max(1, Math.floor(concurrency));
        this.#maxRetries = maxRetries;
        this.#overwrite = overwrite;
    }
    getArchFolder() {
        const arch = os_1.default.arch();
        return arch === "x64" || arch === "arm64" ? "aarch64" : "aarch";
    }
    static getLwjglVersion(mcVersion) {
        if (!mcVersion)
            return null;
        const [majorStr, minorStr] = mcVersion.split(".");
        const major = Number(majorStr) || 0;
        const minor = Number(minorStr) || 0;
        if (major > 1 || (major === 1 && minor >= 13))
            return null;
        if (major === 1 && minor === 12)
            return "3.2.2";
        if (major === 1 && minor === 11)
            return "3.2.1";
        if (major === 1 && minor === 10)
            return "3.1.6";
        if (major === 1 && minor === 9)
            return "3.1.2";
        return "2.9.4";
    }
    async #readLocalJSON(version) {
        const archFolder = this.getArchFolder();
        const folder = path_1.default.join(__dirname, '../', '../', '../', "json", archFolder);
        const jsonFiles = await fs_1.default.promises.readdir(folder);
        const matchFile = jsonFiles.find(f => f.startsWith(version) && f.endsWith(".json"));
        if (!matchFile)
            throw new Error(`No se encontró JSON para LWJGL versión ${version}`);
        const filePath = path_1.default.join(folder, matchFile);
        const content = await fs_1.default.promises.readFile(filePath, "utf-8");
        return JSON.parse(content);
    }
    async start(root, version, concurrency) {
        const versionId = typeof version === "string" ? version : version?.id;
        if (!versionId) {
            this.emit("warn", "Versión inválida, no se puede descargar LWJGL.");
            this.emit("done", { total: 0, downloaded: 0 });
            return;
        }
        const lwjglVer = LwjglDownloader.getLwjglVersion(versionId);
        if (!lwjglVer) {
            this.emit("info", `Versión ${versionId} no requiere LWJGL.`);
            this.emit("done", { total: 0, downloaded: 0 });
            return;
        }
        let lwjglJSON;
        try {
            lwjglJSON = await this.#readLocalJSON(lwjglVer);
        }
        catch (err) {
            this.emit("warn", `No se pudo leer JSON LWJGL: ${err.message}`);
            this.emit("done", { total: 0, downloaded: 0 });
            return;
        }
        const nativesDir = path_1.default.join(root ?? this.#root, "versions", versionId, "natives");
        await fs_1.default.promises.mkdir(nativesDir, { recursive: true });
        const rawConcurrency = concurrency ?? this.#concurrency;
        let safeConcurrency = Number(rawConcurrency);
        if (!Number.isInteger(safeConcurrency) || safeConcurrency < 1) {
            safeConcurrency = 6;
        }
        const limit = (0, p_limit_1.default)(safeConcurrency);
        let downloaded = 0;
        const total = lwjglJSON.libraries.length;
        const tasks = lwjglJSON.libraries.map(lib => limit(async () => {
            try {
                await this.#downloadAndExtract(lib, nativesDir);
                downloaded++;
                this.emit("progress", {
                    current: lib.name,
                    stepPercent: parseFloat(((downloaded / total) * 100).toFixed(2)),
                    totalPercent: parseFloat(((downloaded / total) * 100).toFixed(2)),
                });
            }
            catch (err) {
                this.emit("log", `Error descargando ${lib.name}: ${err.message}`);
            }
        }));
        await Promise.all(tasks);
        this.emit("done", { total, downloaded });
    }
    async #downloadAndExtract(lib, destDir) {
        const url = lib.downloads.artifact.url;
        const jarName = path_1.default.basename(url);
        const jarPath = path_1.default.join(destDir, jarName);
        const needsDownload = this.#overwrite || !(await this.#verifyFile(jarPath, lib.downloads.artifact.sha1));
        if (needsDownload)
            await this.#downloadFileWithRetries(url, jarPath, lib.downloads.artifact.sha1);
        await this.#extractNativeFiles(jarPath, destDir);
    }
    async #verifyFile(filePath, expectedHash) {
        if (!expectedHash || !fs_1.default.existsSync(filePath))
            return false;
        const hash = await this.#calcSHA1(filePath);
        return hash === expectedHash;
    }
    async #calcSHA1(filePath) {
        return new Promise((resolve, reject) => {
            const hash = crypto_1.default.createHash("sha1");
            fs_1.default.createReadStream(filePath)
                .on("data", d => hash.update(d))
                .on("end", () => resolve(hash.digest("hex")))
                .on("error", reject);
        });
    }
    async #downloadFileWithRetries(url, dest, expectedHash, retries = 0) {
        try {
            await this.#downloadFile(url, dest);
            if (expectedHash && !(await this.#verifyFile(dest, expectedHash)))
                throw new Error("SHA1 mismatch");
        }
        catch (err) {
            if (retries < this.#maxRetries) {
                this.emit("log", `Reintentando (${retries + 1}) descarga: ${url}`);
                await new Promise(r => setTimeout(r, 1000 * (retries + 1)));
                return this.#downloadFileWithRetries(url, dest, expectedHash, retries + 1);
            }
            throw err;
        }
    }
    async #downloadFile(url, dest) {
        await fs_1.default.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
        return new Promise((resolve, reject) => {
            const file = fs_1.default.createWriteStream(dest);
            https.get(url, (res) => {
                if (res.statusCode !== 200)
                    return reject(new Error(`HTTP ${res.statusCode} - ${url}`));
                res.on("data", (chunk) => file.write(chunk));
                res.on("end", () => file.end(resolve));
                res.on("error", (err) => reject(err));
            }).on("error", (err) => reject(err));
        });
    }
    async #extractNativeFiles(jarPath, destDir) {
        return new Promise((resolve, reject) => {
            fs_1.default.createReadStream(jarPath)
                .pipe(unzipper_1.default.Parse())
                .on("entry", entry => {
                const ext = path_1.default.extname(entry.path).toLowerCase();
                const validExts = [".dll", ".so", ".dylib", ".jnilib", ".jar"];
                if (validExts.includes(ext)) {
                    const outPath = path_1.default.join(destDir, path_1.default.basename(entry.path));
                    fs_1.default.promises.mkdir(path_1.default.dirname(outPath), { recursive: true })
                        .then(() => {
                        entry.pipe(fs_1.default.createWriteStream(outPath));
                    })
                        .catch(() => entry.autodrain());
                }
                else {
                    entry.autodrain();
                }
            })
                .on("close", resolve)
                .on("error", reject);
        });
    }
}
exports.LwjglDownloader = LwjglDownloader;
