-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

-- This is loaded before anything else and simply sets up the addon table

local ADDON_NAME, TSM = ...
local VERSION_RAW = GetAddOnMetadata("TradeSkillMaster", "Version")
local IS_DEV_VERSION = strmatch(VERSION_RAW, "^@tsm%-project%-version@$") and true or false
local private = {
	context = {},
	initOrder = {},
	loadOrder = {},
	frame = nil,
	gotAddonLoaded = false,
	gotPlayerLogin = false,
	gotPlayerLogout = false,
}



-- ============================================================================
-- Module Metatable
-- ============================================================================

local MODULE_METHODS = {
	OnModuleLoad = function(self, func)
		local context = private.context[self]
		assert(context and not context.moduleLoadFunc and not context.moduleLoadTime and type(func) == "function")
		context.moduleLoadFunc = func
	end,
	OnSettingsLoad = function(self, func)
		local context = private.context[self]
		assert(context and not context.settingsLoadFunc and not context.settingsLoadTime and type(func) == "function")
		context.settingsLoadFunc = func
	end,
	OnGameDataLoad = function(self, func)
		local context = private.context[self]
		assert(context and not context.gameDataLoadFunc and not context.gameDataLoadTime and type(func) == "function")
		context.gameDataLoadFunc = func
	end,
	OnModuleUnload = function(self, func)
		local context = private.context[self]
		assert(context and not context.moduleUnloadFunc and not context.moduleUnloadTime and type(func) == "function")
		context.moduleUnloadFunc = func
	end,
}

local MODULE_MT = {
	__index = MODULE_METHODS,
	__newindex = function(self, key, value)
		local context = private.context[self]
		assert(context and not MODULE_METHODS[key] and not context.moduleLoadTime)
		rawset(self, key, value)
	end,
	__metatable = false,
}



-- ============================================================================
-- Addon Object Functions
-- ============================================================================

function TSM.Init(path)
	assert(type(path) == "string")
	if private.context[path] then
		error("Module already exists for path: "..tostring(path))
	end
	local module = setmetatable({}, MODULE_MT)
	private.context[path] = {
		path = path,
		module = module,
		moduleLoadFunc = nil,
		moduleLoadTime = nil,
		settingsLoadFunc = nil,
		settingsLoadTime = nil,
		gameDataLoadFunc = nil,
		gameDataLoadTime = nil,
		moduleUnloadFunc = nil,
		moduleUnloadTime = nil,
	}
	-- store a reference to the context by both the module object and the path
	private.context[module] = private.context[path]
	tinsert(private.initOrder, path)
	return module
end

function TSM.Include(path)
	local context = private.context[path]
	if not context then
		error("Module doesn't exist for path: "..tostring(path))
	end
	private.ProcessModuleLoad(path)
	return context.module
end

function TSM.IsDevVersion()
	return IS_DEV_VERSION
end

function TSM.GetVersion()
	return IS_DEV_VERSION and "Dev" or VERSION_RAW
end

function TSM.ModuleInfoIterator()
	return private.ModuleInfoIterator, nil, 0
end

function TSM.IsWowClassic()
	return WOW_PROJECT_ID == WOW_PROJECT_CLASSIC
end

function TSM.IsShadowlands()
	return select(4, GetBuildInfo()) >= 90000
end

function TSM.DebugLogout()
	private.UnloadAll()
end



-- ============================================================================
-- Private Helper Functions
-- ============================================================================

function private.ModuleInfoIterator(_, index)
	index = index + 1
	local path = private.initOrder[index]
	if not path then
		return
	end
	local context = private.context[path]
	assert(context)
	return index, path, context.moduleLoadTime, context.settingsLoadTime, context.gameDataLoadTime, context.moduleUnloadTime
end

function private.ProcessModuleLoad(path)
	local context = private.context[path]
	assert(context)
	if context.moduleLoadTime then
		-- already loaded
		return
	end
	tinsert(private.loadOrder, path)
	context.moduleLoadTime = 0
	if context.moduleLoadFunc then
		local st = debugprofilestop()
		context.moduleLoadFunc()
		context.moduleLoadTime = debugprofilestop() - st
	end
end

function private.ProcessSettingsLoad(path)
	local context = private.context[path]
	assert(context)
	if context.settingsLoadTime then
		-- already loaded
		return
	end
	context.settingsLoadTime = 0
	if context.settingsLoadFunc then
		local st = debugprofilestop()
		context.settingsLoadFunc()
		context.settingsLoadTime = debugprofilestop() - st
	end
end

function private.ProcessGameDataLoad(path)
	local context = private.context[path]
	assert(context)
	if context.gameDataLoadTime then
		-- already loaded
		return
	end
	context.gameDataLoadTime = 0
	if context.gameDataLoadFunc then
		local st = debugprofilestop()
		context.gameDataLoadFunc()
		context.gameDataLoadTime = debugprofilestop() - st
	end
end

function private.ProcessModuleUnload(path)
	local context = private.context[path]
	assert(context)
	if context.moduleUnloadTime then
		-- already unloaded
		return
	end
	context.moduleUnloadTime = 0
	if context.moduleUnloadFunc then
		local st = debugprofilestop()
		context.moduleUnloadFunc()
		context.moduleUnloadTime = debugprofilestop() - st
	end
end

function private.UnloadAll()
	-- unload in the opposite order we loaded
	for i = #private.loadOrder, 1, -1 do
		private.ProcessModuleUnload(private.loadOrder[i])
	end
end

function private.OnEvent(_, event, arg)
	if event == "ADDON_LOADED" and arg == ADDON_NAME and not private.gotAddonLoaded then
		assert(not private.gotAddonLoaded and not private.gotPlayerLogin and not private.gotPlayerLogout)
		private.gotAddonLoaded = true
		-- load any module which haven't already
		for _, path in ipairs(private.initOrder) do
			private.ProcessModuleLoad(path)
		end
		-- settings are now available
		for _, path in ipairs(private.loadOrder) do
			private.ProcessSettingsLoad(path)
		end
		private.frame:UnregisterEvent("ADDON_LOADED")
	elseif event == "PLAYER_LOGIN" then
		assert(private.gotAddonLoaded and not private.gotPlayerLogin and not private.gotPlayerLogout)
		private.gotPlayerLogin = true
		-- game data is now available
		for _, path in ipairs(private.loadOrder) do
			private.ProcessGameDataLoad(path)
		end
	elseif event == "PLAYER_LOGOUT" then
		assert(private.gotAddonLoaded and not private.gotPlayerLogout)
		private.gotPlayerLogout = true
		if not private.gotPlayerLogin then
			-- this can happen if the player exists the game during the loading screen, in which case we just ignore it
			return
		end
		private.UnloadAll()
	end
end



-- ============================================================================
-- Initialization Code
-- ============================================================================

do
	private.frame = CreateFrame("Frame")
	private.frame:RegisterEvent("ADDON_LOADED")
	private.frame:RegisterEvent("PLAYER_LOGIN")
	private.frame:RegisterEvent("PLAYER_LOGOUT")
	private.frame:SetScript("OnEvent", private.OnEvent)
end
