local sUtil = require "SMODS.preflight.sharedUtil"

local NFS = SMODS.NFS

local blacklist
local function readBlacklist(force)
    if blacklist ~= nil and not force then return blacklist end
    local file = SMODS.MODS_DIR .. "/lovely/blacklist.txt"
    if not NFS.getInfo(file) then
        sendDebugMessage("Blacklist file does not exist", "loader")
        blacklist = {}
        return {}
    end
    blacklist = {}
    for line in NFS.lines(file) do
        if line ~= "" and not line:match("^#") then
            blacklist[line] = true
        end
    end
    return blacklist
end

local function addToBlacklist(name)
    local file = SMODS.MODS_DIR .. "/lovely/blacklist.txt"
    if not NFS.getInfo(file) then
        sendDebugMessage("Blacklist file does not exist, creating it...", "loader")
        NFS.write(file, name .. "\n")
        blacklist = nil -- Invalidate blacklist cache
        return
    end
    NFS.append(file, "\n" .. name)
    blacklist = nil -- Invalidate blacklist cache
end

local function removeFromBlacklist(name)
    local file = SMODS.MODS_DIR .. "/lovely/blacklist.txt"
    if not NFS.getInfo(file) then
        sendWarnMessage("Blacklist file does not exist, can't remove???", "loader")
        blacklist = nil -- Invalidate blacklist cache
        return
    end
    local list = {}
    for line in NFS.lines(file) do
        if line ~= name then
            table.insert(list, line)
        end
    end
    NFS.write(file, table.concat(list, "\n"))
    blacklist = nil -- Invalidate blacklist cache
end

function loadMods(modsDirectory)
    SMODS.Mods = {}
    SMODS.Mods[SMODS.id] = SMODS
    SMODS.Mods['Lovely'] = {
        id = 'Lovely',
        can_load = true,
        version = require'lovely'.version,
        meta_mod = true,
    }
    SMODS.Mods['Balatro'] = {
        id = 'Balatro',
        can_load = true,
        version = sUtil.getBalatroVersion(),
        meta_mod = true,
    }
    SMODS.mod_priorities = {}
    SMODS.mod_list = {}
    SMODS.provided_mods = {}
    -- for legacy header support
    local header_components = {
        name          = { pattern = '%-%-%- MOD_NAME: ([^\n]+)\n', required = true },
        id            = { pattern = '%-%-%- MOD_ID: ([^ \n]+)\n', required = true },
        author        = { pattern = '%-%-%- MOD_AUTHOR: %[(.-)%]\n', required = true, parse_array = true },
        description   = { pattern = '%-%-%- MOD_DESCRIPTION: (.-)\n', required = true },
        priority      = { pattern = '%-%-%- PRIORITY: (%-?%d+)\n', handle = function(x) return x and x + 0 or 0 end },
        badge_colour  = { pattern = '%-%-%- BADGE_COLO[U]?R: (%x-)\n', handle = function(x) return sUtil.hex(x or '666666FF') end },
        badge_text_colour   = { pattern = '%-%-%- BADGE_TEXT_COLO[U]?R: (%x-)\n', handle = function(x) return sUtil.hex(x or 'FFFFFF') end },
        display_name  = { pattern = '%-%-%- DISPLAY_NAME: (.-)\n' },
        dependencies  = {
            pattern = {
                '%-%-%- DEPENDENCIES: %[(.-)%]\n',
                '%-%-%- DEPENDS: %[(.-)%]\n',
                '%-%-%- DEPS: %[(.-)%]\n',
            },
            parse_array = true,
            handle = function(x)
                local t = {}
                for _, v in ipairs(x) do
                    table.insert(t, {
                        id = v:match '(.-)[<>]' or v,
                        min_version = v:match '>=([^<>]+)',
                        max_version = v:match '<=([^<>]+)',
                    })
                end
                return t
            end,
        },
        conflicts     = {
            pattern = '%-%-%- CONFLICTS: %[(.-)%]\n',
            parse_array = true,
            handle = function(x)
                local t = {}
                for _, v in ipairs(x) do
                    table.insert(t, {
                        id = v:match '(.-)[<>]',
                        min_version = v:match '>=([^<>]+)',
                        max_version = v:match '<=([^<>]+)',
                    })
                    if t.min_version and not sUtil.V(t[#t].min_version):is_valid() then t[#t].min_version = nil end
                    if t.max_version and not sUtil.V(t[#t].max_version):is_valid() then t[#t].max_version = nil end
                end

                return t
            end
        },
        prefix        = { pattern = '%-%-%- PREFIX: (.-)\n' },
        version       = { pattern = '%-%-%- VERSION: (.-)\n', handle = function(x) return x and sUtil.V(x):is_valid() and x or '0.0.0' end },
        outdated      = { pattern = { 'SMODS%.INIT', 'SMODS%.Deck[:.]new' } },
        dump_loc      = { pattern = { '%-%-%- DUMP_LOCALIZATION\n'}}
    }


    local json_spec = {
        id = { type = 'string', required = true },
        author = { type = 'table', required = true, check = function(mod, t)
            for k, v in pairs(t) do
                if type(k) ~= 'number' or type(v) ~= 'string' then t[k] = nil end
            end
            return t
        end },
        name = { type = 'string', required = true },
        display_name = { type = 'string', check = function(mod, s) mod.display_name = s or mod.name end },
        description = { type = 'string', required = true },
        priority = { type = 'number', default = 0 },
        badge_colour = { type = 'string', check = function(mod, s) local success, hex = pcall(sUtil.hex, s); mod.badge_colour = success and hex or sUtil.hex('666665FF') end },
        badge_text_colour = { type = 'string', check = function(mod, s) local success, hex = pcall(sUtil.hex, s); mod.badge_text_colour = success and hex or sUtil.hex('FFFFFFFF') end},
        prefix = { type = 'string', required = true },
        version = { type = 'string', check = function(mod, x) return x and sUtil.V(x):is_valid() and x or '0.0.0' end },
        dump_loc = { type = 'boolean' },
        dependencies = { type = 'table', check = function(mod, t)
            local ops = {
                ['<<'] = function(a,b) return a<b end,
                -- ['<~'] = function(a,b) return a<b end,
                ['>>'] = function(a,b) return a>b end,
                ['<='] = function(a,b) return a<=b end,
                ['>='] = function(a,b) return a>=b end,
                ['=='] = function(a,b) return a==b end
            }
            for i,v in ipairs(t or {}) do
                local parts = {}
                parts.str = v
                for part in v:gmatch('([^|]+)') do
                    local x = {}
                    x.id = part:match '^([^(%s]+)'
                    local j = 1
                    for version_string in string.gmatch(part, '%((.-)%)') do
                        local operator, version = string.match(version_string, '^(..)(.*)$')
                        local op = ops[operator]
                        local ver = sUtil.V(version)
                        -- if operator == '<<' and not ver.rev then
                        --     ver.beta = -1
                        --     ver.rev = '~'
                        -- end
                        if op and ver:is_valid(true) then
                            x[j] = { op = op, ver = ver }
                            j = j+1
                        end
                    end
                    parts[#parts+1] = x
                end
                t[i] = parts
            end
        end},
        conflicts = { type = 'table', check = function(mod, t)
            local ops = {
                ['<<'] = function(a,b) return a<b end,
                --['<~'] = function(a,b) return a<b end,
                ['>>'] = function(a,b) return a>b end,
                ['<='] = function(a,b) return a<=b end,
                ['>='] = function(a,b) return a>=b end,
                ['=='] = function(a,b) return a==b end
            }
            for i,v in ipairs(t or {}) do
                v = v:gsub('%s', '')
                local x = {}
                x.str = v
                v = v:gsub('%s', '')
                x.id = v:match '^([^(%s]+)'
                local j = 1
                for version_string in string.gmatch(v, '%((.-)%)') do
                    local operator, version = string.match(version_string, '^(..)(.*)$')
                    local op = ops[operator]
                    local ver = sUtil.V(version)
                    -- if operator == '<<' and not ver.rev then
                    --     ver.beta = -1
                    --     ver.rev = '~'
                    -- end
                    if op and ver:is_valid(true) then
                        x[j] = { op = op, ver = ver, str = '('..version_string..')' }
                        j = j+1
                    end
                end
                t[i] = x
            end
        end},
        main_file = { type = 'string', required = true },
        config_file = {type = 'string', default = 'config.lua' },
        __ = { check = function(mod)
            if SMODS.Mods[mod.id] then error('dupe') end
        end},
        provides = { type = 'table', check = function(mod, t)
            t = t or {}
            for _,v in pairs(t) do
                v = v:gsub('%s', '')
                local id = v:match '^([^(%s]+)'
                local ver = v:match '%((.-)%)'
                ver = (ver and sUtil.V(ver):is_valid()) and ver or mod.version
                if id and ver then
                    SMODS.provided_mods[id] = SMODS.provided_mods[id] or {}
                    table.insert(SMODS.provided_mods[id], { version = ver, mod = mod })
                end
            end
        end}

    }


    local used_prefixes = {}
    local lovely_directories = {}

    -- Function to process each directory (including subdirectories) with depth tracking
    local function processDirectory(directory, depth, flags)
        if depth > 3 or directory..'/' == SMODS.path then
            return
        end

        local isDirLovely = false

        for _, filename in ipairs(NFS.getDirectoryItems(directory)) do
            if depth == 1 then print(directory, filename, depth) end
            local file_path = directory .. "/" .. filename

            -- Check if the current file is a directory
            local file_type = NFS.getInfo(file_path).type
            if file_type == 'directory' or file_type == 'symlink' then
                if not (depth == 1 and filename == "lovely") then -- Ignore lovely's directory
                    -- Lovely patches 
                    local flag = flags
                    if depth == 1 then flag = { name = filename, type = "directory" } end
                    if depth == 2 and filename == "lovely" and not isDirLovely then
                        isDirLovely = true
                        table.insert(lovely_directories, flag)
                    end
                    -- If it's a directory and depth is within limit, recursively process it
                    if depth < 2 or (filename:lower() ~= 'localization' and filename:lower() ~= 'assets') then
                        processDirectory(file_path, depth + 1, flag)
                    end
                end
            elseif depth == 1 and filename:lower():match("%.zip$") then
                local isSMODS = (directory .. "/" .. filename .. "/") == SMODS.path
                local loveName = "__SMODS_MOUNTS__/" .. filename
                local res = NFS.mount(file_path, loveName)
                if res then
                    local items = love.filesystem.getDirectoryItems(loveName)
                    if #items == 1 then -- Possible nested zip
                        local item = items[1]
                        if item ~= "lovely" then -- A plain lovely folder is not considered nested
                            local info = love.filesystem.getInfo(loveName .. "/" .. item)
                            if info.type == "directory" then
                                loveName = loveName .. "/" .. item
                            end
                        end
                    end
                    assert(NFS.smodsAddRedirect(file_path .. ".mnt", loveName))
                    if isSMODS then
                        SMODS.path = file_path .. ".mnt/"
                        sendTraceMessage("SMODS installed in a zip. Corrected path in use.", "loader")
                    else
                        processDirectory(file_path .. ".mnt", depth + 1, { name = filename, type = "zip" })
                    end
                else
                    sendErrorMessage("Failed to mount zip '" .. filename .. "'.", "loader")
                    if isSMODS then error "Steamodded was isntalled as a zip but could not mount itself" end
                end
            elseif depth == 2 and filename == "lovely.toml" and not isDirLovely then
                isDirLovely = true
                table.insert(lovely_directories, flags)
            elseif filename:lower():match('%.json') and depth > 1 then
                local json_str = NFS.read(file_path)
                local parsed, mod = pcall(JSON.decode, json_str)
                local valid = true
                local err
                if not parsed then
                    valid = false
                    err = mod
                else
                    assert(flags, "Loading a mod without the proper metadata? Something went wrong in the scan")
                    mod.json = true
                    mod.path = directory .. '/'
                    mod.optional_dependencies = {}
                    mod.blacklist_name = flags.name
                    mod.load_type = flags.type
                    local success, e = pcall(function()
                        -- remove invalid fields and check required ones first
                        for k, v in pairs(json_spec) do
                            if v.type and type(mod[k]) ~= v.type then mod[k] = nil end
                            if v.required and mod[k] == nil then error(k) end
                        end
                        -- perform additional checks and fill in defaults
                        for k, v in pairs(json_spec) do
                            if v.default then mod[k] = mod[k] or v.default end
                            if v.check then v.check(mod, mod[k]) end
                        end
                    end)
                    if not success then
                        valid = false
                        err = e
                    end
                end
                if not valid then
                    sendErrorMessage(('Found invalid metadata JSON file at %s, ignoring: %s'):format(file_path, err), 'Loader')
                else
                    sendInfoMessage('Valid JSON file found', 'Loader')
                    if flags.type == "directory" and NFS.getInfo(SMODS.MODS_DIR.. "/" .. flags.name ..'/.lovelyignore') then
                        mod.disabled = true
                        mod.lovelyIgnored = true
                    end
                    if readBlacklist()[flags.name] then
                        mod.disabled = true
                        mod.blacklisted = true
                    end

                    if mod.prefix and used_prefixes[mod.prefix] then
                        mod.can_load = false
                        mod.load_issues = {
                            prefix_conflict = used_prefixes[mod.prefix],
                            dependencies = {},
                            conflicts = {},
                        }
                        sendWarnMessage(('Duplicate Mod prefix %s used by %s, %s'):format(mod.prefix, mod.id, used_prefixes[mod.prefix]), 'Loader')
                    end
                    if not NFS.getInfo(mod.path..mod.main_file) then
                        mod.can_load = false
                        mod.load_issues = {
                            main_file_not_found = true,
                            dependencies = {},
                            conflicts = {},
                        }
                        sendWarnMessage(('Unable to load Mod %s: cannot find main file'):format(mod.id), 'Loader')
                    end
                    if mod.dump_loc then
                        SMODS.dump_loc = {
                            path = mod.path,
                        }
                    end
                    SMODS.Mods[mod.id] = mod
                    SMODS.mod_priorities[mod.priority] = SMODS.mod_priorities[mod.priority] or {}
                    table.insert(SMODS.mod_priorities[mod.priority], mod)
                end
            elseif filename:lower():match("%.lua$") then -- Check for legacy headers
                if depth == 1 then
                    sendWarnMessage(('Found lone Lua file %s in Mods directory :: Please place the files for each mod in its own subdirectory.'):format(filename), 'Loader')
                    flags = { name = filename, type = "file" }
                end
                local file_content = NFS.read(file_path)

                -- Convert CRLF in LF
                file_content = file_content:gsub("\r\n", "\n")

                -- Check the header lines using string.match
                local headerLine = file_content:match("^(.-)\n")
                if headerLine == "--- STEAMODDED HEADER" then
                    sendTraceMessage('Processing Mod file (Legacy header): ' .. filename, "Loader")
                    local mod = {}
                    local sane = true
                    for k, v in pairs(header_components) do
                        local component = nil
                        if type(v.pattern) == "table" then
                            for _, pattern in ipairs(v.pattern) do
                                component = file_content:match(pattern) or component
                                if component then break end
                            end
                        else
                            component = file_content:match(v.pattern)
                        end
                        if v.required and not component then
                            sane = false
                            sendWarnMessage(string.format('Mod file %s is missing required header component: %s',
                            filename, k), 'Loader')
                            break
                        end
                        if v.parse_array then
                            local list = {}
                            component = component or ''
                            for val in string.gmatch(component, "([^,]+)") do
                                table.insert(list, val:match("^%s*(.-)%s*$")) -- Trim spaces
                            end
                            component = list
                        end
                        if v.handle and type(v.handle) == 'function' then
                            component = v.handle(component)
                        end
                        mod[k] = component
                    end

                    assert(flags, "Loading a mod without the proper metadata? Something went wrong in the scan")
                    mod.blacklist_name = flags.name
                    mod.load_type = flags.type

                    if flags.type == "directory" and NFS.getInfo(SMODS.MODS_DIR.. "/" .. flags.name ..'/.lovelyignore') then
                        mod.disabled = true
                        mod.lovelyIgnored = true
                    end
                    if readBlacklist()[flags.name] then
                        mod.disabled = true
                        mod.blacklisted = true
                    end
                    if SMODS.Mods[mod.id] then
                        sane = false
                        sendWarnMessage("Duplicate Mod ID: " .. mod.id, 'Loader')
                    end

                    if mod.outdated then
                        mod.prefix_config = { key = { mod = false }, atlas = false }
                    else
                        mod.prefix = mod.prefix or (mod.id or ''):lower():sub(1, 4)
                    end
                    if mod.prefix and used_prefixes[mod.prefix] then
                        mod.can_load = false
                        mod.load_issues = { 
                            prefix_conflict = used_prefixes[mod.prefix],
                            dependencies = {},
                            conflicts = {},
                        }
                        sendWarnMessage(('Duplicate Mod prefix %s used by %s, %s'):format(mod.prefix, mod.id, used_prefixes[mod.prefix]), 'Loader')
                    end

                    if sane then
                        sendTraceMessage('Saving Mod Info: ' .. mod.id, 'Loader')
                        mod.path = directory .. '/'
                        mod.main_file = filename
                        mod.display_name = mod.display_name or mod.name
                        if mod.prefix then
                            used_prefixes[mod.prefix] = mod.id
                        end
                        mod.optional_dependencies = {}
                        if mod.dump_loc then
                            SMODS.dump_loc = {
                                path = mod.path,
                            }
                        end
                        SMODS.Mods[mod.id] = mod
                        SMODS.mod_priorities[mod.priority] = SMODS.mod_priorities[mod.priority] or {}
                        table.insert(SMODS.mod_priorities[mod.priority], mod)
                    end
                end
            end
        end
    end


    boot_print_stage('Processing Mod Files')
    -- Start processing with the initial directory at depth 1
    processDirectory(modsDirectory, 1)
    for _, flags in ipairs(lovely_directories) do
        local hasSMOD = false
        for _, mod in pairs(SMODS.Mods) do
            if mod.blacklist_name == flags.name then
                mod.lovely = true
                hasSMOD = true
            end
        end
        if not hasSMOD then
            local name = flags.name
            if flags.type == "zip" then
                name = name:match("^(.*).zip$")
            end
            local path = SMODS.MODS_DIR.. "/" .. flags.name .. "/"
            if flags.name:find('smods') then smods_dupe = true end
            local mod = {
                name = name,
                id = "lovely-compat-" .. flags.name,
                author = {"???"},
                description = "A lovely mod.",
                prefix_config = { key = { mod = false }, atlas = false },
                priority = 0,
                badge_colour = sUtil.hex("666666FF"),
                badge_text_colour = sUtil.hex('FFFFFF'),
                path = path,
                main_file = "",
                display_name = name,
                dependencies = {},
                optional_dependencies = {},
                conflicts = {},
                version = "0.0.0",
                can_load = true,
                lovely = true,
                lovely_only = true,
                meta_mod = true,
                blacklist_name = flags.name,
                load_type = flags.type,
                load_issues = {
                    dependencies = {},
                    conflicts = {},
                }

            }
            if flags.type == "directory" and NFS.getInfo(path ..'.lovelyignore') then
                mod.disabled = true
                mod.can_load = false
                mod.lovelyIgnored = true
            end
            if readBlacklist()[flags.name] then
                mod.disabled = true
                mod.can_load = false
                mod.blacklisted = true
            end
            SMODS.mod_priorities[mod.priority] = SMODS.mod_priorities[mod.priority] or {}
            table.insert(SMODS.mod_priorities[mod.priority], mod)
            SMODS.Mods[mod.id] = mod
        end
    end

    local function check_dependencies(mod, seen)
        if not (mod.can_load == nil) then return mod.can_load end
        seen = seen or {}
        local can_load = true
        if seen[mod.id] then return true end
        seen[mod.id] = true
        local load_issues = {
            dependencies = {},
            conflicts = {},
        }
        if not mod.json then
            for _, v in ipairs(mod.conflicts or {}) do
                -- block load even if the conflict is also blocked
                if
                    SMODS.Mods[v.id] and
                    (not v.max_version or sUtil.V(SMODS.Mods[v.id].version) <= sUtil.V(v.max_version)) and
                    (not v.min_version or sUtil.V(SMODS.Mods[v.id].version) >= sUtil.V(v.min_version))
                    then
                        can_load = false
                        table.insert(load_issues.conflicts, v.id..(v.max_version and '<='..v.max_version or '')..(v.min_version and '>='..v.min_version or ''))
                    end
                end
                for _, v in ipairs(mod.dependencies or {}) do
                    -- recursively check dependencies of dependencies to make sure they are actually fulfilled
                    if
                        not SMODS.Mods[v.id] or
                        not check_dependencies(SMODS.Mods[v.id], seen) or
                        (v.max_version and sUtil.V(SMODS.Mods[v.id].version) > sUtil.V(v.max_version)) or
                        (v.min_version and sUtil.V(SMODS.Mods[v.id].version) < sUtil.V(v.min_version))
                        then
                            can_load = false
                            table.insert(load_issues.dependencies,
                            v.id .. (v.min_version and '>=' .. v.min_version or '') .. (v.max_version and '<=' .. v.max_version or ''))
                            if v.id == 'Steamodded' then
                                load_issues.version_mismatch = ''..(v.min_version and '>='..v.min_version or '')..(v.max_version and '<='..v.max_version or '')
                            end
                        end
                    end
                else
                    for _, x in ipairs(mod.dependencies or {}) do
                        local fulfilled
                        for _, y in ipairs(x) do
                            if fulfilled then break end
                            local id = y.id
                            if SMODS.Mods[id] and check_dependencies(SMODS.Mods[id], seen) then
                                fulfilled = true
                                local dep_ver = sUtil.V(SMODS.Mods[id].version)
                                for _, v in ipairs(y) do
                                    if not v.op(dep_ver, v.ver) then
                                        fulfilled = false
                                    end
                                end
                                if fulfilled then y.fulfilled = true end
                            else
                                for _, provided in ipairs(SMODS.provided_mods[id] or {}) do
                                    if provided.mod ~= mod and check_dependencies(provided.mod, seen) then
                                        fulfilled = true
                                        local dep_ver = sUtil.V(provided.version)
                                        for _, v in ipairs(y) do
                                            if not v.op(dep_ver, v.ver) then
                                                fulfilled = false
                                            end
                                        end
                                        if fulfilled then y.fulfilled = true; y.provided = provided end
                                    end
                                end
                            end
                        end
                        if not fulfilled then
                            can_load = false
                            table.insert(load_issues.dependencies, x.str)
                        end
                    end
                    for _, y in ipairs(mod.conflicts or {}) do
                        local id = y.id
                        local conflict = false
                        if SMODS.Mods[id] and check_dependencies(SMODS.Mods[id], seen) then
                            conflict = true
                            local dep_ver = sUtil.V(SMODS.Mods[id].version)
                            for _, v in ipairs(y) do
                                if not v.op(dep_ver, v.ver) then
                                    conflict = false
                                    break
                                end
                            end
                        else
                            for _, provided in ipairs(SMODS.provided_mods[id] or {}) do
                                if provided.mod ~= mod and check_dependencies(provided.mod, seen) then
                                    conflict = true
                                    local dep_ver = sUtil.V(provided.version)
                                    for _, v in ipairs(y) do
                                        if not v.op(dep_ver, v.ver) then
                                            conflict = false
                                            break
                                        end
                                    end
                                end
                            end
                        end
                        if conflict then
                            can_load = false
                            table.insert(load_issues.conflicts, y.str)
                        end
                    end
                end
                if mod.disabled then
                    can_load = false
                    load_issues.disabled = true
                end
                if not can_load then
                    mod.load_issues = load_issues
                    return false
                end
                for _, x in ipairs(mod.dependencies or {}) do
                    for _, y in ipairs(x) do
                        if y.fulfilled then
                            if y.provided then
                                y.provided.mod.can_load = true
                            else
                                SMODS.Mods[y.id].can_load = true
                            end
                        end
                    end 
                end
                return true
            end

            -- check dependencies first (for object dependencies)
            for _, mod in pairs(SMODS.Mods) do mod.can_load = check_dependencies(mod) end
        end

        local function load_mods()
            boot_print_stage('Loading Mods')
            -- load the mod files
            -- sort by priority
            local keyset = {}
            for k, _ in pairs(SMODS.mod_priorities) do
                keyset[#keyset + 1] = k
            end
            table.sort(keyset)

            for _, priority in ipairs(keyset) do
                table.sort(SMODS.mod_priorities[priority],
                function(mod_a, mod_b)
                    return mod_a.id < mod_b.id
                end)
                for _, mod in ipairs(SMODS.mod_priorities[priority]) do
                    SMODS.mod_list[#SMODS.mod_list + 1] = mod -- keep mod list in prioritized load order
                    if mod.can_load and not mod.lovely_only then
                        SMODS.current_mod = mod
                        if mod.outdated then
                            SMODS.compat_0_9_8.with_compat(function()
                                mod.config = {}
                                assert(load(NFS.read(mod.path..mod.main_file), ('=[SMODS %s "%s"]'):format(mod.id, mod.main_file)))()
                                for k, v in pairs(SMODS.compat_0_9_8.init_queue) do
                                    v()
                                    SMODS.compat_0_9_8.init_queue[k] = nil
                                end
                            end)
                        else
                            SMODS.load_mod_config(mod)
                            assert(load(NFS.read(mod.path..mod.main_file), ('=[SMODS %s "%s"]'):format(mod.id, mod.main_file)))()
                        end
                        SMODS.current_mod = nil
                    elseif not mod.lovely_only then
                        sendTraceMessage(string.format("Mod %s was unable to load: %s%s%s%s", mod.id,
                        mod.load_issues.outdated and
                        'Outdated: Steamodded versions 0.9.8 and below are no longer supported!\n' or '',
                        mod.load_issues.main_file_not_found and "The main file could not be found.\n" or '',
                        next(mod.load_issues.dependencies) and
                        ('Missing Dependencies: ' .. inspect(mod.load_issues.dependencies) .. '\n') or '',
                        next(mod.load_issues.conflicts) and
                        ('Unresolved Conflicts: ' .. inspect(mod.load_issues.conflicts) .. '\n') or ''
                    ), 'Loader')
                end
            end
        end
        SMODS.get_optional_features()
        -- compat after loading mods
        if SMODS.compat_0_9_8.load_done then
            -- Invasive change to Card:generate_UIBox_ability_table()
            local Card_generate_UIBox_ability_table_ref = Card.generate_UIBox_ability_table
            function Card:generate_UIBox_ability_table(...)
                SMODS.compat_0_9_8.generate_UIBox_ability_table_card = self
                local ret = Card_generate_UIBox_ability_table_ref(self, ...)
                SMODS.compat_0_9_8.generate_UIBox_ability_table_card = nil
                return ret
            end
        end
    end

    function SMODS.injectItems()
        -- Set .key for vanilla undiscovered, locked objects
        for k, v in pairs(G) do
            if type(k) == 'string' and (k:sub(-12, -1) == 'undiscovered' or k:sub(-6, -1) == 'locked') then
                v.key = k
            end
        end
        SMODS.injectObjects(SMODS.GameObject)
        if SMODS.dump_loc then
            boot_print_stage('Dumping Localization')
            SMODS.create_loc_dump()
        end
        boot_print_stage('Initializing Localization')
        init_localization()
        SMODS.SAVE_UNLOCKS()
        table.sort(G.P_CENTER_POOLS["Back"], function (a, b) return (a.order - (a.unlocked and 100 or 0)) < (b.order - (b.unlocked and 100 or 0)) end)
        for _, t in ipairs{
            G.P_CENTERS,
            G.P_BLINDS,
            G.P_TAGS,
            G.P_SEALS,
        } do
        for k, v in pairs(t) do
            assert(v._discovered_unlocked_overwritten, ("Internal: discovery/unlocked of object \"%s\" failed to override."):format(v and v.key or "UNKNOWN"))
        end
    end
end

local function initializeModUIFunctions()
    for id, modInfo in pairs(SMODS.mod_list) do
        G.FUNCS["openModUI_" .. modInfo.id] = function(e)
            G.ACTIVE_MOD_UI = modInfo
            if e and e.config and e.config.page then
                SMODS.LAST_SELECTED_MOD_TAB = e.config.page
            end
            G.FUNCS.overlay_menu({
                definition = create_UIBox_mods(e)
            })
        end
    end
end

local function checkForLoadFailure()
    SMODS.mod_button_alert = false
    for k,v in pairs(SMODS.Mods) do
        if v and not v.can_load and not v.disabled then
            SMODS.mod_button_alert = true
            return
        end
    end
end

function initLoader()
    boot_print_stage("Loading APIs")
    loadMods(SMODS.MODS_DIR)
    checkForLoadFailure()
end

SMODS.booted = false

function SMODS.load_file(path, id)
    if not path or path == "" then
        error("No path was provided to load.")
    end
    local mod
    if not id then
        if not SMODS.current_mod then
            error("No ID was provided! Usage without an ID is only available when file is first loaded.")
        end
        mod = SMODS.current_mod
    else
        mod = SMODS.Mods[id]
    end
    if not mod then
        error("Mod not found. Ensure you are passing the correct ID.")
    end
    local file_path = mod.path .. path
    local file_content, err = NFS.read(file_path)
    if not file_content then return  nil, "Error reading file '" .. path .. "' for mod with ID '" .. mod.id .. "': " .. err end
    local chunk, err = load(file_content, "=[SMODS " .. mod.id .. ' "' .. path .. '"]')
    if not chunk then return nil, "Error processing file '" .. path .. "' for mod with ID '" .. mod.id .. "': " .. err end
    return chunk
end

local function doGameHooks()
    local init_item_prototypes_ref = Game.init_item_prototypes
    function Game:init_item_prototypes()
        init_item_prototypes_ref(self)
        if SMODS.booted then
            SMODS.injectItems()
        end
    end

    -- TODO: vanilla hook
end

local function initSteamodded()
    doGameHooks()
    initGlobals()
    loadAPIs()
    load_mods()
    initializeModUIFunctions()
    -- boot_print_stage("Injecting Items")
    SMODS.injectItems()
    convert_save_data()
    SMODS.booted = true


    -- re-inject on reload
end

return {
    initSteamodded = initSteamodded,
    readBlacklist = readBlacklist,
    addToBlacklist = addToBlacklist,
    removeFromBlacklist = removeFromBlacklist,
}
