-- ============================================================
--  AUTOLOADER nocode — Dual-path récursif
--  Fichier : lua/autorun/bladWinRpMenu_loader.lua
-- ============================================================

local NoCode = {}

NoCode.config = {
    -- Dossier(s)-module(s) à charger.
    --   • sans wildcard  -> nom exact d'UN dossier   : "bladw_announcement"
    --   • avec wildcard  -> glob, charge TOUS ceux qui matchent :
    --        "bladw_*"    (préfixe)   /   "*_menu"  (suffixe)   /   "bladw_*_v2"  (milieu)
    --   • groupes "(a+b)"  -> alternatives séparées par + :
    --        "bladw_(ann*+warn)" = bladw_ann* OU bladw_warn   (les alt peuvent avoir des *)
    --   • "none" (ou vide/nil) -> loader désactivé, rien n'est chargé
    root      = "bladw_(ann*+warn)",
    -- Dossiers de bibliothèques : seulement envoyés au client (AddCSLuaFile),
    -- jamais auto-inclus. Ils sont chargés à la demande via include("lib/...").
    libs      = { "lib" },
    safe_load = true,
    verbose   = true,
    exclude   = { "template.lua", "disabled", "_dev" },
}

-- ============================================================
--  Logs
-- ============================================================

local C = {
    ok    = Color(100, 220, 100),
    err   = Color(255,  80,  80),
    info  = Color(120, 180, 255),
    warn  = Color(255, 200,  50),
    dim   = Color(160, 160, 160),
    title = Color(200, 140, 255),
}

local function Log(col, msg)
    MsgC(col, msg .. "\n")
end

-- ============================================================
--  Utilitaires
-- ============================================================

local function StartsWith(str, prefix)
    return str:sub(1, #prefix) == prefix
end

local function IsExcluded(name)
    for _, ex in ipairs(NoCode.config.exclude) do
        if name == ex then return true end
    end
    return false
end

local function SortFiles(files)
    table.sort(files, function(a, b)
        local function rank(n)
            if StartsWith(n, "sh_") then return 1 end
            if StartsWith(n, "sv_") then return 2 end
            if StartsWith(n, "cl_") then return 3 end
            return 4
        end
        return rank(a) < rank(b)
    end)
end

--- Fusionne deux tables en évitant les doublons
local function Merge(t1, t2)
    local seen   = {}
    local result = {}
    for _, v in ipairs(t1 or {}) do
        if not seen[v] then seen[v] = true; table.insert(result, v) end
    end
    for _, v in ipairs(t2 or {}) do
        if not seen[v] then seen[v] = true; table.insert(result, v) end
    end
    return result
end

-- ============================================================
--  Compteurs
-- ============================================================

local stats = { loaded = 0, sent = 0, errors = 0 }

-- ============================================================
--  Chargement d'un fichier unique
-- ============================================================

local function LoadFile(path)
    local filename = path:match("([^/]+)$")
    local realm, shouldLoad, shouldSend

    if StartsWith(filename, "sh_") then
        realm      = "SHARED"
        shouldSend = SERVER
        shouldLoad = true

    elseif StartsWith(filename, "sv_") then
        realm      = "SERVER"
        shouldLoad = SERVER

    elseif StartsWith(filename, "cl_") then
        realm      = "CLIENT"
        shouldSend = SERVER
        shouldLoad = not SERVER

    else
        return
    end

    if shouldSend then
        AddCSLuaFile(path)
        stats.sent = stats.sent + 1
        if NoCode.config.verbose and not shouldLoad then
            Log(C.info, ("  [→] [%s] %s"):format(realm, path))
        end
    end

    if not shouldLoad then return end

    if NoCode.config.safe_load then
        local ok, err = pcall(include, path)
        if ok then
            stats.loaded = stats.loaded + 1
            if NoCode.config.verbose then
                Log(C.ok, ("  [✓] [%s] %s"):format(realm, path))
            end
        else
            stats.errors = stats.errors + 1
            Log(C.err, ("  [✗] [%s] %s"):format(realm, path))
            Log(C.err, ("       ↳ %s"):format(tostring(err)))
        end
    else
        include(path)
        stats.loaded = stats.loaded + 1
        if NoCode.config.verbose then
            Log(C.ok, ("  [✓] [%s] %s"):format(realm, path))
        end
    end
end

-- ============================================================
--  Scan récursif — cherche TOUJOURS dans LUA et GAME en même temps
-- ============================================================

local function LoadFolder(dir, depth)
    depth = depth or 0

    local basename = dir:match("([^/]+)$") or dir
    if IsExcluded(basename) then
        Log(C.warn, ("  [~] Dossier ignoré : %s"):format(dir))
        return
    end

    -- On cherche dans les deux paths et on fusionne les résultats
    local files1, dirs1 = file.Find(dir          .. "/*", "LUA")
    local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")

    local files = Merge(files1, files2)
    local dirs  = Merge(dirs1,  dirs2)

    if #files == 0 and #dirs == 0 then
        if NoCode.config.verbose and depth > 0 then
            Log(C.warn, ("  [!] Dossier vide ou introuvable : %s"):format(dir))
        end
        return
    end

    if NoCode.config.verbose and depth > 0 then
        Log(C.dim, string.rep("    ", depth) .. "📂 " .. basename .. "/")
    end

    SortFiles(files)

    for _, fname in ipairs(files) do
        if fname:EndsWith(".lua") and not IsExcluded(fname) then
            LoadFile(dir .. "/" .. fname)
        end
    end

    -- Récursion sur tous les sous-dossiers sans limite de profondeur
    for _, subdir in ipairs(dirs) do
        if not IsExcluded(subdir) then
            LoadFolder(dir .. "/" .. subdir, depth + 1)
        end
    end
end

-- ============================================================
--  Envoi récursif d'une bibliothèque au client (sans l'inclure)
--  Les fichiers de lib/ ne suivent pas le préfixe sh_/sv_/cl_ ;
--  on les expédie simplement pour que include("lib/...") fonctionne.
-- ============================================================

local function SendFolder(dir)
    if not SERVER then return end

    local files1, dirs1 = file.Find(dir          .. "/*", "LUA")
    local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
    local files = Merge(files1, files2)
    local dirs  = Merge(dirs1,  dirs2)

    for _, fname in ipairs(files) do
        if fname:EndsWith(".lua") and not IsExcluded(fname) then
            local path = dir .. "/" .. fname
            AddCSLuaFile(path)
            stats.sent = stats.sent + 1
            if NoCode.config.verbose then
                Log(C.info, ("  [→] [LIB]    %s"):format(path))
            end
        end
    end

    for _, subdir in ipairs(dirs) do
        if not IsExcluded(subdir) then
            SendFolder(dir .. "/" .. subdir)
        end
    end
end

-- ============================================================
--  Découverte des dossiers-modules
--  (file.Find ne supporte le wildcard que sur le dernier segment,
--   donc on liste la racine lua/ puis on filtre nous-mêmes.)
--    • pattern sans "*" -> match exact (un seul dossier)
--    • pattern avec "*" -> glob converti en motif Lua (préfixe/suffixe/milieu)
-- ============================================================

--- Échappe les caractères magiques d'un motif Lua.
local function EscapePattern(s)
    return (s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
end

--- Transforme un glob simple ("bladw_*") en motif Lua ancré ("^bladw_.*$").
local function GlobToPattern(glob)
    return "^" .. EscapePattern(glob):gsub("%%%*", ".*") .. "$"
end

--- Étend les groupes "(a+b)" en plusieurs motifs plats.
--  "bladw_(ann*+warn)" -> { "bladw_ann*", "bladw_warn" }
--  Gère plusieurs groupes (produit cartésien) via récursion sur le suffixe.
local function ExpandPattern(pattern)
    local open = pattern:find("(", 1, true)
    if not open then return { pattern } end

    local close = pattern:find(")", open + 1, true)
    if not close then return { pattern } end -- parenthèse non fermée -> littéral

    local prefix = pattern:sub(1, open - 1)
    local group  = pattern:sub(open + 1, close - 1)
    local suffix = pattern:sub(close + 1)

    local out = {}
    for _, alt in ipairs(string.Explode("+", group)) do
        for _, exp in ipairs(ExpandPattern(prefix .. alt .. suffix)) do
            out[#out + 1] = exp
        end
    end
    return out
end

--- Construit un testeur (dossier -> bool) pour un motif plat (avec ou sans "*").
local function Matcher(p)
    if p:find("*", 1, true) then
        local lua_pat = GlobToPattern(p)
        return function(d) return d:match(lua_pat) ~= nil end
    end
    return function(d) return d == p end
end

local function FindRootFolders(pattern)
    local roots = {}

    -- On étend les groupes puis on teste chaque motif : un dossier matche si l'un d'eux matche.
    local matchers = {}
    for _, p in ipairs(ExpandPattern(pattern)) do
        matchers[#matchers + 1] = Matcher(p)
    end

    local function matches(d)
        for _, m in ipairs(matchers) do
            if m(d) then return true end
        end
        return false
    end

    local _, dirs1 = file.Find("*",     "LUA")
    local _, dirs2 = file.Find("lua/*", "GAME")

    for _, d in ipairs(Merge(dirs1, dirs2)) do
        if matches(d) and not IsExcluded(d) then
            table.insert(roots, d)
        end
    end

    table.sort(roots)
    return roots
end

-- ============================================================
--  Polices client (Poppins)
--  Créées quel que soit le root, donc hors du flux de chargement.
-- ============================================================

if CLIENT then
    local fonts = {
        { "bladw_text",         "Poppins-SemiBold", 25,   600 },
        { "bladw_text_Medium",  "Poppins-SemiBold", 20,   800 },
        { "bladw_text_Bold",    "Poppins-Bold",     70,   700 },
        { "bladw_text_Regular", "Poppins-Regular",  35,   400 },
        { "bladw_text2",        "Poppins-SemiBold", 21.5, 800 },
    }

    for _, f in ipairs(fonts) do
        surface.CreateFont(f[1], {
            font      = f[2],
            size      = f[3],
            weight    = f[4],
            antialias = true,
        })
    end
end

-- ============================================================
--  Point d'entrée
-- ============================================================

Log(C.title, "")
Log(C.title, "  ╔══════════════════════════════════════╗")
Log(C.title, "  ║         NoCode  —  Autoloader          ║")
Log(C.title, "  ╚══════════════════════════════════════╝")

local rootPattern = NoCode.config.root

-- root "none" (ou vide/nil) -> loader désactivé, on ne charge rien
if not rootPattern or rootPattern == "" or rootPattern:lower() == "none" then
    Log(C.warn, "  Root = none -> autoloader désactivé, rien n'est chargé.")
    Log(C.dim, "")
    return
end

Log(C.info,  "  Dossier racine : lua/" .. rootPattern)
Log(C.dim,   "")

-- 1) Expédie les bibliothèques partagées au client (rndx, etc.)
for _, lib in ipairs(NoCode.config.libs or {}) do
    SendFolder(lib)
end

-- 2) Charge chaque module racine correspondant au préfixe
local roots = FindRootFolders(rootPattern)

if #roots == 0 then
    Log(C.warn, ("  [!] Aucun module trouvé pour le préfixe : %s"):format(rootPattern))
else
    for _, root in ipairs(roots) do
        if NoCode.config.verbose then
            Log(C.title, "  ▶ " .. root)
        end
        LoadFolder(root)
    end
end

Log(C.dim,   "")
Log(C.title, "  ╔══════════════════════════════════════╗")
if SERVER then
    Log(C.ok, ("  ║  ✓  %d chargé(s)  /  %d envoyé(s) au client"):format(stats.loaded, stats.sent))
else
    Log(C.ok, ("  ║  ✓  %d fichier(s) chargé(s) [CLIENT]"):format(stats.loaded))
end
if stats.errors > 0 then
    Log(C.err, ("  ║  ✗  %d erreur(s)"):format(stats.errors))
end
Log(C.title, "  ╚══════════════════════════════════════╝")
Log(C.dim,   "")