--#region AML
local aml_mod_data = {};

local function aml_hook_load_order(ModManager)
	local aml_mod_data = aml_mod_data;
	local string_format = string.format;
	local string_match = string.match;
	local table_contains = table.contains;
	local table_find = table.find;
	local table_insert = table.insert;
	local table_remove = table.remove;

	local log_lines = {};

	local function aml_print(message, ...)
		local message = (... and string_format(message, ...) or message or '[nil]');
		table_insert(log_lines, message);
		__print("[AML] "..message);
	end

	local _audio = {
		"Audio",
	};

	-- Known presets
	local before_presets = {};
	local after_presets = {
		LogMeIn = {
			"dmf",
		},
		DarktideLocalServer = {
			"dmf",
			"LogMeIn",
		},
		psych_ward = {
			"dmf",
			"LogMeIn",
		},
		Audio = {
			"DarktideLocalServer",
		},
		Rock = _audio,
		UnlimitedPower = _audio,
		Clang = _audio,
		EnemyAudioReplacer = _audio,
		HeKnew = _audio,
		LeeroyJenkins = _audio,
	};
	local require_presets = {
		Audio = {
			"DarktideLocalServer",
		},
		Rock = _audio,
		UnlimitedPower = _audio,
		Clang = _audio,
		EnemyAudioReplacer = _audio,
		HeKnew = _audio,
		LeeroyJenkins = _audio,
	};
	local before_rules = {};
	local after_rules = {};
	local require_rules = {};

	aml_print("==================================================================================================");
	aml_print("[WARNING] AML IS MANAGING MOD LIST AND LOAD ORDER: PLEASE REPORT ANY LOADING-RELATED ERRORS TO AML");
	aml_print(" PLEASE UPDATE YOUR MODS BEFORE YOU REPORT ISSUES (a good practice in general)");
	aml_print(" AML IGNORES mod_load_order.txt and loads all valid mod folders in a correct order automatically");
	aml_print(" To disable a mod, disable it in Vortex, prepend an underscore to its name, remove, or delete it");
	aml_print(" Updates and help at: https://www.nexusmods.com/warhammer40kdarktide/mods/246");
	aml_print("==================================================================================================");

	-- Generate mod list
	---@type string[]
	local mod_list = {"dmf"};
	-- Vortex leaves empty folders, find folders with .mod files
	-- Calling dir with a path and with /S didn't seem to work correctly
	local dir = Mods.lua.io.popen([[cd ..\mods & dir /B /S *.mod & cd ..\binaries]]);
	for line in dir:lines() do
		-- Allow users to disable by prefixing folder with "_"
		local mod_folder, mod_file_name = string_match(line, [[\mods\([^_][^\]+)\([^\]-)%.mod]]);
		-- Base is ignored as this patch includes base.mod for vertex install
		if mod_folder and mod_folder ~= "base" and mod_folder ~= "dmf" then
			if mod_folder ~= mod_file_name then
				if string.lower(mod_folder) == string.lower(mod_file_name) then
					aml_print("[INFO] '%s' and '%s.mod' case differ, this could cause issues on Linux", mod_folder, mod_file_name);
					table_insert(mod_list, mod_folder);
				elseif string.sub(mod_file_name, 1, 1) == "_" then
					-- Ignore mod files that don't match the folder name and are prefixed with _
					-- This is to help Vortex install a mod that is a patch for another mod, as it expects mods to have a .mod file
					aml_print("[INFO] Skipping '%s/%s.mod', assuming it is a patch", mod_folder, mod_file_name);
				else
					aml_print("[INFO] Skipping '%s/%s.mod' as the mod file name does not match the mod folder name", mod_folder, mod_file_name);
				end
			else
				table_insert(mod_list, mod_folder);
			end
		end
	end
	dir:close();

	local num_mods = #mod_list;
	-- Preload mod data
	aml_print("Preloading mod metadata..")
	for i = 1, num_mods, 1 do
		local mod_folder_name = mod_list[i];
		local data = Mods.file.exec_with_return(mod_folder_name, mod_folder_name, "mod");
		aml_mod_data[mod_folder_name] = data;
		-- Load rules, or presets if there are no empty overrides
		if data then
			if data.load_before and #data.load_before > 0 then
				before_rules[mod_folder_name] = data.load_before;
			elseif not data.load_before then
				before_rules[mod_folder_name] = before_presets[mod_folder_name];
			end
			if data.load_after and #data.load_after > 0 then
				after_rules[mod_folder_name] = data.load_after;
			elseif not data.load_after then
				after_rules[mod_folder_name] = after_presets[mod_folder_name];
			end
			if data.require and #data.require > 0 then
				require_rules[mod_folder_name] = data.require;
			elseif not data.require then
				require_rules[mod_folder_name] = require_presets[mod_folder_name];
			end
		end
	end

	-- Process load orders
	for order_loop = 1, 10, 1 do
		--[[
			This is a blunt solution and does result in more loops than necessary.
			However, I prefer blunt over broken so until someone suggets a better solution
			this is what I'm going with. I have not observed any practical issues.
		]]
		local order_settled = true;

		for ar_mod, ar_list in pairs(after_rules) do
			local ar_mod_position = table_find(mod_list, ar_mod);
			-- load_after is for both "I need to be loaded after" (Weapon Customization plugins) and "Load me immediately after" (LogMeIn)
			if ar_mod_position then
				for i = num_mods, 1, -1 do
					if table_contains(ar_list, mod_list[i]) then
						table_remove(mod_list, ar_mod_position);
						table_insert(mod_list, i+1, ar_mod);
						order_settled = false;
						-- Reduce jiggling by only moving to the top once
						if mod_list[i] == "dmf" then
							table_remove(after_rules[ar_mod], table_find(ar_list, i));
						end
						break
					end
				end
			end
		end

		for br_mod, br_list in pairs(before_rules) do
			local br_mod_position = table_find(mod_list, br_mod);
			if br_mod_position then
				for i = 1, br_mod_position, 1 do
					if table_contains(br_list, mod_list[i]) then
						table_remove(mod_list, br_mod_position);
						table_insert(mod_list, i, br_mod);
						order_settled = false;
						break
					end
				end
			end
		end

		if order_settled then
			break;
		end
	end

	for handle, require in pairs(require_rules) do
		local skip = false;
		for _, required_mod in ipairs(require) do
			if not table_contains(mod_list, required_mod) then
				aml_print([[Mod "%s" not loaded: Requires "%s"]], handle, required_mod);
				table.insert(ModManager._chat_print_buffer, string_format([[Mod "%s" not loaded: Requires "%s"]], handle, required_mod));
				skip = true;
			end
		end
		if skip then
			table_remove(mod_list, table_find(mod_list, handle));
		end
	end

	aml_print("Final load order:");
	for index, handle in ipairs(mod_list) do
		local version = aml_mod_data[handle].version;
		if version then
			version = string_format(" (%s)", version);
		else
			version = "";
		end
		aml_print([[%d:"%s"%s]], index, handle, version);
	end

	-- Remove DMF as it is added back after hook
	table_remove(mod_list, 1);

	aml_print("Returning control to DML");

	local log_file = Mods.lua.io.open([[..\mods\auto_mod_loader_log.txt]], "w");
	log_file:write(table.concat(log_lines, "\n"));
	log_file:close();

	return mod_list;
end
--#endregion



local ModManager = class("ModManager")

local Keyboard = Keyboard
local BUTTON_INDEX_R = Keyboard.button_index("r")
local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift")
local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl")

local LOG_LEVELS = {
	spew = 4,
	info = 3,
	warning = 2,
	error = 1
}

local string_format = string.format
local function printf(f, ...)
	print(string.format(f, ...))
end

local function deepcopy(orig)
	local orig_type = type(orig)
	local copy
	if orig_type == 'table' then
			copy = {}
			for orig_key, orig_value in next, orig, nil do
					copy[deepcopy(orig_key)] = deepcopy(orig_value)
			end
			setmetatable(copy, deepcopy(getmetatable(orig)))
	else -- number, string, boolean, etc
			copy = orig
	end
	return copy
end

-- Clone the global mod file library to a local table
local _io = deepcopy(Mods.file)

ModManager.init = function (self, boot_gui)
	self._mods = {}
	self._num_mods = nil
	self._state = "not_loaded"
	self._settings = Application.user_setting("mod_manager_settings") or {
		log_level = 1,
		developer_mode = false
	}
	self._chat_print_buffer = {}
	self._reload_data = {}
	self._gui = boot_gui
	self._ui_time = 0
	self._network_callbacks = {}

	Crashify.print_property("realm", "modded")
	
	print("[ModManager] Starting mod manager...")

	self._mod_metadata = {}
	self._state = "scanning"
end

ModManager.developer_mode_enabled = function (self)
	return self._settings.developer_mode
end

ModManager._draw_state_to_gui = function (self, gui, dt)
	local state = self._state
	local t = self._ui_time + dt
	self._ui_time = t
	local status_str = "Loading mods"

	if state == "scanning" then
		status_str = "Scanning for mods"
	elseif state == "loading" then
		local mod = self._mods[self._mod_load_index]
		status_str = string.format("Loading mod %q", mod.name)
	end

	--Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1))
end

ModManager.remove_gui = function (self)
	self._gui = nil
end

ModManager._has_enabled_mods = function (self)
	return true
end

ModManager._check_reload = function (self)
	return Keyboard.pressed(BUTTON_INDEX_R) and
		Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2
end

ModManager.update = function (self, dt)
	local chat_print_buffer = self._chat_print_buffer
	local num_delayed_prints = #chat_print_buffer

	if num_delayed_prints > 0 and Managers.chat then
		for i = 1, num_delayed_prints, 1 do
			Mods.message.echo(chat_print_buffer[i])

			chat_print_buffer[i] = nil
		end
	end

	local old_state = self._state

	if self._settings.developer_mode and self:_check_reload() then
		self._reload_requested = true
	end

	if self._reload_requested and self._state == "done" then
		self:_reload_mods()
	end

	if self._state == "done" then
		if self._num_mods then
			for i = 1, self._num_mods, 1 do
				local mod = self._mods[i]

				if mod and mod.enabled and not mod.callbacks_disabled then
					self:_run_callback(mod, "update", dt)
				end
			end
		end
		
	elseif self._state == "scanning" then
		self:_build_mod_table()
		self._state = self:_load_mod(1)
		self._ui_time = 0
		
	elseif self._state == "loading" then
		local mod = self._mods[self._mod_load_index]
		local mod_data = mod.data

		mod.state = "running"
		local ok, object = pcall(mod_data.run)

		if not ok then
			self:print("error", "%s", object)
		end

		local name = mod.name
		mod.object = object or {}

		self:_run_callback(mod, "init", self._reload_data[mod.id])
		self:print("info", "%s loaded.", name)

		self._state = self:_load_mod(self._mod_load_index + 1)
	end

	local gui = self._gui

	if gui then
		self:_draw_state_to_gui(gui, dt)
	end

	if old_state ~= self._state then
		self:print("info", "%s -> %s", old_state, self._state)
	end
end

ModManager.all_mods_loaded = function (self)
	return self._state == "done"
end

ModManager.destroy = function (self)
	self:unload_all_mods()
end

ModManager._run_callback = function (self, mod, callback_name, ...)
	local object = mod.object
	local cb = object[callback_name]

	if not cb then
		return
	end

	local success, val = pcall(cb, object, ...)

	if success then
		return val
	else
		self:print("error", "%s", val or "[unknown error]")
		self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.",
				callback_name, mod.name, mod.id)

		mod.callbacks_disabled = true
	end
end

ModManager._build_mod_table = function (self)
	fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table")

	--[[ REPLACED BY AML
	-- Get the mods' load order from mod_load_order file
	local mod_load_order = _io.read_content_to_table("mod_load_order", "txt")
	if not mod_load_order then
		print("ERROR executing mod_load_order: " .. tostring(mod_load_order))
		mod_load_order = {}
	end
	]]
	local mod_load_order = aml_hook_load_order(self)
	
	-- Add DMF to the mod load order
	table.insert(mod_load_order, 1, "dmf")
	
	-- Read the .mod files of given mods and, if everything's fine, add mods' entries to the mods list.
	for i = 1, #mod_load_order do
		local mod_name = mod_load_order[i]
		self._mods[i] = {
			state = "not_loaded",
			callbacks_disabled = false,
			id = i,
			name = mod_name,
			enabled = true,
			handle = mod_name,
			loaded_packages = {}
		}
	end
	
	self._num_mods = #self._mods
end

ModManager._load_mod = function (self, index)
	self._ui_time = 0
	local mods = self._mods
	local mod = mods[index]

	while mod and not mod.enabled do
		index = index + 1
		mod = mods[index]
	end

	if not mod then
		table.clear(self._reload_data)
		return "done"
	end

	local id = mod.id
	local mod_name = mod.name

	self:print("info", "loading mod %s", id)
	Crashify.print_property("modded", true)

	--[[ REPLACED BY AML
	local mod_data = _io.exec_with_return(mod_name, mod_name, "mod")
	]]
	local mod_data = aml_mod_data[mod_name]

	if not mod_data then
		self:print("error", "Mod file is invalid or missing. Mod %q with id %d skipped.", mod.name, mod.id)

		mod.enabled = false

		return self:_load_mod(index + 1)
	end
	self:print("spew", "<mod info>\n%s\n</mod info>", mod_data)

	mod.data = mod_data
	mod.name = mod.name or mod_data.NAME or "Mod " .. mod.id
	mod.state = "loading"

	Crashify.print_property(string.format("Mod:%s", mod.name), true)
	print(string.format("[ModManager] Loading mod %s with id %s", mod.name, id))

	self._mod_load_index = index

	return "loading"
end

ModManager.unload_all_mods = function (self)
	if self._state ~= "done" then
		self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state)

		return
	end

	self:print("info", "Unload all mod packages")

	for i = self._num_mods, 1, -1 do
		local mod = self._mods[i]

		if mod and mod.enabled then
			self:unload_mod(i)
		end

		self._mods[i] = nil
	end

	self._num_mods = nil
	self._state = "unloaded"
end

ModManager.unload_mod = function (self, index)
	local mod = self._mods[index]

	if mod then
		self:print("info", "Unloading %q.", mod.name)
		self:_run_callback(mod, "on_unload")

		mod.state = "not_loaded"
	else
		self:print("error", "Mod index %i can't be unloaded, has not been loaded", index)
	end
end

ModManager._reload_mods = function (self)
	for i = 1, self._num_mods, 1 do
		local mod = self._mods[i]

		if mod and mod.state == "running" then
			self:print("info", "reloading %s", mod.name)

			self._reload_data[mod.id] = self:_run_callback(mod, "on_reload")
		else
			self:print("info", "not reloading mod, state: %s", mod.state)
		end
	end

	self:unload_all_mods()
	self._state = "scanning"
	self._reload_requested = false
	
	Mods.message.notify("Mods reloaded.")
end

ModManager.on_game_state_changed = function (self, status, state_name, state_object)
	if self._state == "done" then
		for i = 1, self._num_mods, 1 do
			local mod = self._mods[i]

			if mod and mod.enabled and not mod.callbacks_disabled then
				self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object)
			end
		end
	else
		self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state)
	end
end

ModManager._visit = function (self, mod_list, visited, sorted, mod_data)
	self:print("debug", "Visiting mod %q with id %d", mod_data.name, mod_data.id)

	if visited[mod_data] then
		return mod_data.enabled
	end

	if visited[mod_data] ~= nil then
		self:print("error", "Dependency cycle detected at mod %q with id %d", mod_data.name, mod_data.id)

		return false
	end

	visited[mod_data] = false
	local enabled = mod_data.enabled or false

	for i = 1, mod_data.num_children or 0 do
		local child_id = mod_data.children[i]
		local child_index = table.find_by_key(mod_list, "id", child_id)
		local child_mod_data = mod_list[child_index]

		if not child_mod_data then
			self:print("warning", "Mod with id %d not found", child_id)
		elseif not self:_visit(mod_list, visited, sorted, child_mod_data) and enabled then
			self:print("warning", "Disabled mod %q with id %d due to missing dependency %d.",
					mod_data.name, mod_data.id, child_id)

			enabled = false
		end
	end

	mod_data.enabled = enabled
	visited[mod_data] = true
	sorted[#sorted + 1] = mod_data

	return enabled
end

ModManager.print = function (self, level, str, ...)
	local message = string.format("[ModManager][" .. level .. "] " .. tostring(str), ...)
	local log_level = LOG_LEVELS[level] or 99

	if log_level <= 2 then
		print(message)
	end

	if log_level <= self._settings.log_level then
		self._chat_print_buffer[#self._chat_print_buffer + 1] = message
	end
end

ModManager.network_bind = function (self)
	return
end

ModManager.network_unbind = function (self)
	return
end

ModManager.network_is_occupied = function (self)
	return false
end

ModManager.network_send = function (self)
	return
end

ModManager.rpc_mod_user_data = function (self)
	return
end

ModManager.register_network_event_delegate = function (self)
	return
end

ModManager.unregister_network_event_delegate = function (self)
	return
end

ModManager.network_context_created = function (self)
	return
end

return ModManager
