-------------------------------------------------------------------------------
-- BigWigs API
-- @module BigWigs
-- @alias core

local core, plugins, bossPrototype, pluginPrototype
do
	local _, tbl =...
	core = tbl.core
	plugins = tbl.plugins
	bossPrototype = tbl.bossPrototype
	pluginPrototype = tbl.pluginPrototype

	core.name = "BigWigs"
end

local CL = BigWigsAPI:GetLocale("BigWigs: Common")
local loader = BigWigsLoader
core.SendMessage = loader.SendMessage

local mod, bosses = {}, {}
local coreEnabled = false

-- Try to grab unhooked copies of critical loading funcs (hooked by some crappy addons)
local GetBestMapForUnit = loader.GetBestMapForUnit
local GetInstanceInfo = loader.GetInstanceInfo
local UnitGUID = loader.UnitGUID
local UnitIsDeadOrGhost = loader.UnitIsDeadOrGhost

-- Upvalues
local next, type, error, setmetatable = next, type, error, setmetatable

local pName = loader.UnitName("player")

-------------------------------------------------------------------------------
-- Event handling
--

do
	local noEvent = "Module %q tried to register/unregister an event without specifying which event."
	local noFunc = "Module %q tried to register event %q to the function %q which doesn't exist in the module."
	local curEvent = "Module %q tried to register event %q to the function %q but the event is in the middle of dispatching."

	local eventMap = {}
	local bwUtilityFrame = CreateFrame("Frame")
	local currentEvent = nil
	bwUtilityFrame:SetScript("OnEvent", function(_, event, ...)
		currentEvent = event
		for k,v in next, eventMap[event] do
			if type(v) == "function" then
				v(event, ...)
			else
				k[v](k, event, ...)
			end
		end
		currentEvent = nil
	end)

	function core:RegisterEvent(event, func)
		if type(event) ~= "string" then error((noEvent):format(self.moduleName)) end
		local functionType = type(func)
		if (not func and not self[event]) or (functionType == "string" and not self[func]) then error((noFunc):format(self.moduleName or "?", event, func or event)) end
		if not eventMap[event] then eventMap[event] = {} end

		if eventMap[event][self] then -- Event is already registered to this specific module, just change the assigned function
			eventMap[event][self] = func or event
		else -- Event has not been previously registered to this specific module
			if event == currentEvent then
				core:Error(curEvent:format(self.moduleName or "?", event, functionType == "function" and "<local func>" or func or event))
			end
			eventMap[event][self] = func or event
			bwUtilityFrame:RegisterEvent(event)
		end
	end
	function core:UnregisterEvent(event)
		if type(event) ~= "string" then error((noEvent):format(self.moduleName)) end
		if not eventMap[event] then return end
		eventMap[event][self] = nil
		if not next(eventMap[event]) then
			bwUtilityFrame:UnregisterEvent(event)
			eventMap[event] = nil
		end
	end

	local function UnregisterAllEvents(_, module)
		for k,v in next, eventMap do
			for j in next, v do
				if j == module then
					module:UnregisterEvent(k)
				end
			end
		end
	end
	loader.RegisterMessage(mod, "BigWigs_OnBossDisable", UnregisterAllEvents)
	loader.RegisterMessage(mod, "BigWigs_OnBossWipe", UnregisterAllEvents)
	loader.RegisterMessage(mod, "BigWigs_OnPluginDisable", UnregisterAllEvents)
end

-------------------------------------------------------------------------------
-- ENCOUNTER event handler
--

if loader.isRetail then
	function mod:ENCOUNTER_START(_, encounterId)
		for _, module in next, bosses do
			if module:IsEncounterID(encounterId) then
				if not module:IsEnabled() then
					module:Enable()
				end
				module:Engage()
			end
		end
	end
elseif loader.isMists or loader.isCata then
	function mod:ENCOUNTER_START(_, encounterId)
		for _, module in next, bosses do
			if module:IsEncounterID(encounterId) and not module:IsEnabled() then
				module:Enable()
				if UnitGUID("boss1") then -- Only if _START fired after IEEU
					module:Engage()
				end
			end
		end
	end
else
	function mod:ENCOUNTER_START(_, encounterId)
		for _, module in next, bosses do
			if module:IsEncounterID(encounterId) then
				if not module:IsEnabled() then
					module:Enable()
				end
				module:Engage()
			end
		end
	end
end

-------------------------------------------------------------------------------
-- Target monitoring
--

local enablemobs = {}

local function UpdateMouseoverUnit()
	local guid = UnitGUID("mouseover")
	if issecretvalue and issecretvalue(guid) then return end -- XXX 12.0 compat
	if not guid or UnitIsCorpse("mouseover") or UnitIsDead("mouseover") then return end
	local _, _, _, _, _, mobIdString = strsplit("-", guid)
	local mobId = tonumber(mobIdString)
	if mobId then
		local moduleNameOrTable = enablemobs[mobId]
		if type(moduleNameOrTable) == "table" then
			for i = 1, #moduleNameOrTable do
				local moduleName = moduleNameOrTable[i]
				local module = bosses[moduleName]
				if module and not module:IsEnabled() and (not module.VerifyEnable or module:VerifyEnable("mouseover", mobId, GetBestMapForUnit("player"))) then
					module:Enable()
					if not module.worldBoss then
						module:Sync("Enable", module.moduleName, true)
					end
				end
			end
		elseif moduleNameOrTable then
			local module = bosses[moduleNameOrTable]
			if module and not module:IsEnabled() and (not module.VerifyEnable or module:VerifyEnable("mouseover", mobId, GetBestMapForUnit("player"))) then
				module:Enable()
				if not module.worldBoss then
					module:Sync("Enable", module.moduleName, true)
				end
			end
		end
	end
end

local function UnitTargetChanged(_, mobId, unitTarget)
	if not UnitIsDead(unitTarget) then
		local moduleNameOrTable = enablemobs[mobId]
		if type(moduleNameOrTable) == "table" then
			for i = 1, #moduleNameOrTable do
				local moduleName = moduleNameOrTable[i]
				local module = bosses[moduleName]
				if module and not module:IsEnabled() and (not module.VerifyEnable or module:VerifyEnable(unitTarget, mobId, GetBestMapForUnit("player"))) then
					module:Enable()
				end
			end
		elseif moduleNameOrTable then
			local module = bosses[moduleNameOrTable]
			if module and not module:IsEnabled() and (not module.VerifyEnable or module:VerifyEnable(unitTarget, mobId, GetBestMapForUnit("player"))) then
				module:Enable()
			end
		end
	end
end

function core:RegisterEnableMob(moduleName, ...)
	local module = bosses[moduleName]
	if not module then
		core:Error(("RegisterEnableMob failed, no boss module named '%s' found."):format(tostring(moduleName)))
	else
		for i = 1, select("#", ...) do
			local mobId = select(i, ...)
			if type(mobId) ~= "number" or mobId < 1 then
				core:Error(("Module %q tried to register the mobId %q, but it wasn't a valid number."):format(moduleName, tostring(mobId)))
			else
				module.enableMobs[mobId] = true -- Module specific list
				-- Global list
				local entryType = type(enablemobs[mobId])
				if entryType == "nil" then
					enablemobs[mobId] = moduleName
				elseif entryType == "table" then
					enablemobs[mobId][#enablemobs[mobId] + 1] = moduleName
				elseif entryType == "string" then -- Converting from 1 module registered to this mobId, to multiple modules
					local previousModuleEntry = enablemobs[mobId]
					enablemobs[mobId] = {previousModuleEntry, moduleName}
				else
					core:Error(("Unknown type in a enable trigger table at index %d for %q."):format(i, moduleName))
				end
			end
		end
	end
end

-------------------------------------------------------------------------------
-- Communication
--

local function bossComm(_, msg, extra, sender)
	if msg == "Enable" and extra then
		local m = bosses[extra]
		if m and not m:IsEnabled() and sender ~= pName then
			m:Enable()
		end
	end
end

do
	local SendAddonMessage = loader.SendAddonMessage
	local Timer = loader.CTimerAfter
	local issecretvalue = issecretvalue or function() return false end -- XXX 12.0 compat
	function mod:RAID_BOSS_WHISPER(_, msg) -- Purely for Transcriptor to assist in logging purposes.
		if not issecretvalue(msg) and msg ~= "" and IsInGroup() and coreEnabled then
			local result = SendAddonMessage("Transcriptor", msg, IsInGroup(2) and "INSTANCE_CHAT" or "RAID")
			if type(result) == "number" and result > 0 then
				if result == 3 or result == 8 or result == 9 then -- AddonMessageThrottle, ChannelThrottle, GeneralError
					Timer(1, function() self:RAID_BOSS_WHISPER(nil, msg) end)
				elseif result ~= 11 then -- AddOnMessageLockdown
					core:Error("Failed to send TS comm. Error code: ".. result)
				end
			end
		end
	end
end

-------------------------------------------------------------------------------
-- Initialization
--

local initModules = {}
do
	local function InitializeModules()
		local count = #initModules
		if count > 0 then
			for i = 1, count do
				initModules[i]:Initialize()
			end
			initModules = {}
		end
	end

	local addonName = ...
	function mod:ADDON_LOADED(_, name)
		if name ~= addonName then return end

		mod.ADDON_LOADED = InitializeModules
		InitializeModules()
	end
	core.RegisterEvent(mod, "ADDON_LOADED")
end

do
	local function DisableModules()
		for _, module in next, bosses do
			if module:IsEngaged() and (module:GetJournalID() or module:GetAllowWin()) and UnitIsDeadOrGhost("player") then
				module:Wipe()
			end
			module:Disable()
		end
		for i = #plugins, 1, -1 do
			plugins[i]:Disable()
		end
	end
	local function DisableCore(skipDelveEvent)
		if coreEnabled then
			coreEnabled = false

			loader.UnregisterMessage(mod, "BigWigs_BossComm")
			loader.UnregisterMessage(mod, "BigWigs_UNIT_TARGET")
			core.UnregisterEvent(mod, "ENCOUNTER_START")
			core.UnregisterEvent(mod, "RAID_BOSS_WHISPER")
			core.UnregisterEvent(mod, "UPDATE_MOUSEOVER_UNIT")
			core.UnregisterEvent(mod, "PLAYER_LEAVING_WORLD")
			core.UnregisterEvent(mod, "ZONE_CHANGED_NEW_AREA")
			if loader.isRetail and not skipDelveEvent then
				core.UnregisterEvent(mod, "PLAYER_MAP_CHANGED")
			end
			core.UnregisterEvent(mod, "PLAYER_LOGIN")

			core:SendMessage("BigWigs_StopConfigureMode")
			if BigWigsOptions then
				BigWigsOptions:Close()
			end
			DisableModules()
			core:SendMessage("BigWigs_CoreDisabled")
		end
	end
	local function zoneChanged()
		-- Not if you released spirit on a world boss or if the GUI is open
		if not UnitIsDeadOrGhost("player") and (not BigWigsOptions or not BigWigsOptions:IsOpen()) then
			local bars = plugins.Bars
			if not BigWigs3DB.breakTime and (not bars or not bars:HasActiveBars()) then -- Not if break time or bars are showing
				DisableCore() -- Alive in a non-enable zone, disable
			end
		end
	end

	local function EnablePlugins()
		for i = 1, #plugins do
			plugins[i]:Enable()
		end
	end
	local zoneList = loader.zoneTbl
	local function CheckIfLeavingDelve(_, oldId, newId)
		if zoneList[oldId] and not zoneList[newId] then
			DisableCore() -- Leaving a Delve
		elseif zoneList[newId] then
			-- Joining a delve but we were already enabled from something
			DisableCore(true) -- Avoid re-registering PLAYER_MAP_CHANGED whilst it's still dispatching
			core:Enable()
		end
	end
	function core:Enable()
		if not coreEnabled then
			coreEnabled = true

			-- Always make sure to unregister everything that's added here in DisableCore()
			loader.RegisterMessage(mod, "BigWigs_BossComm", bossComm)
			loader.RegisterMessage(mod, "BigWigs_UNIT_TARGET", UnitTargetChanged)
			core.RegisterEvent(mod, "ENCOUNTER_START")
			core.RegisterEvent(mod, "RAID_BOSS_WHISPER")
			core.RegisterEvent(mod, "UPDATE_MOUSEOVER_UNIT", UpdateMouseoverUnit)
			core.RegisterEvent(mod, "PLAYER_LEAVING_WORLD", function() DisableCore() end) -- Simple disable when leaving instances
			local _, instanceType = GetInstanceInfo()
			if instanceType == "none" then -- We don't want to be disabling in instances
				core.RegisterEvent(mod, "ZONE_CHANGED_NEW_AREA", zoneChanged) -- Special checks for disabling after world bosses
			end
			if loader.isRetail then
				core.RegisterEvent(mod, "PLAYER_MAP_CHANGED", CheckIfLeavingDelve)
			end

			if IsLoggedIn() then
				EnablePlugins()
			else
				core.RegisterEvent(mod, "PLAYER_LOGIN", EnablePlugins)
			end

			core:SendMessage("BigWigs_CoreEnabled")
		end
	end
end

function core:IsEnabled()
	return coreEnabled
end

function core:Print(msg)
	print("BigWigs: |cffffff00"..msg.."|r")
end

function core:Error(msg, noPrint)
	if not noPrint then
		core:Print(msg)
	end
	geterrorhandler()("BigWigs: ".. msg)
end

-------------------------------------------------------------------------------
-- API
--

do
	local currentLocale = GetLocale()
	if currentLocale == "enGB" then currentLocale = "enUS" end
	function core:NewBossLocale(moduleName, locale)
		local module = bosses[moduleName]
		if module and currentLocale == locale then
			return module:GetLocale()
		end
	end
end

-------------------------------------------------------------------------------
-- Module handling
--

do
	local bossesPendingInit, pluginsPendingInit = {}, {}

	do
		local bossAlreadyRegistered = "%q already exists as a boss module in BigWigs, but something is trying to register it again."
		local errorJournalIdInvalid = "%q is using the invalid journal id of %q."
		local bossMeta = { __index = bossPrototype, __metatable = false }
		local EJ_GetEncounterInfo = (loader.isCata or loader.isMists) and function(key)
			return EJ_GetEncounterInfo(key) or BigWigsAPI:GetLocale("BigWigs: Encounters")[key]
		end or loader.isRetail and EJ_GetEncounterInfo or function(key)
			return BigWigsAPI:GetLocale("BigWigs: Encounters")[key]
		end
		function core:NewBoss(moduleName, zoneId, journalId)
			if bosses[moduleName] then
				core:Print(bossAlreadyRegistered:format(moduleName))
			else
				local m = setmetatable({
					name = "BigWigs_Bosses_"..moduleName, -- XXX AceAddon/AceDB backwards compat
					moduleName = moduleName,
					enableMobs = {},

					-- Embed callback handler
					RegisterMessage = loader.RegisterMessage,
					UnregisterMessage = loader.UnregisterMessage,
					SendMessage = loader.SendMessage,

					-- Embed event handler
					RegisterEvent = core.RegisterEvent,
					UnregisterEvent = core.UnregisterEvent,
				}, bossMeta)
				bosses[moduleName] = m
				initModules[#initModules+1] = m
				bossesPendingInit[moduleName] = true

				if journalId then
					local name = EJ_GetEncounterInfo(journalId)
					if name or journalId < 0 then
						m.journalId = journalId
						m.displayName = name or moduleName
					else
						m.displayName = moduleName
						core:Print(errorJournalIdInvalid:format(moduleName, journalId))
					end
				else
					m.displayName = moduleName
				end

				if type(zoneId) == "table" or zoneId > 0 then
					m.instanceId = zoneId
				else
					m.mapId = -zoneId
				end
				return m, CL
			end
		end
	end

	do
		local pluginAlreadyRegistered = "%q already exists as a plugin in BigWigs, but something is trying to register it again."
		local L = BigWigsAPI:GetLocale("BigWigs")
		local pluginMeta = { __index = pluginPrototype, __metatable = false }
		function core:NewPlugin(moduleName, globalFuncs)
			if plugins[moduleName] then
				core:Print(pluginAlreadyRegistered:format(moduleName))
			else
				local m = setmetatable({
					name = "BigWigs_Plugins_"..moduleName, -- XXX AceAddon/AceDB backwards compat
					moduleName = moduleName,
					globalFuncs = globalFuncs or {"db"},

					-- Embed callback handler
					RegisterMessage = loader.RegisterMessage,
					UnregisterMessage = loader.UnregisterMessage,
					SendMessage = loader.SendMessage,

					-- Embed event handler
					RegisterEvent = core.RegisterEvent,
					UnregisterEvent = core.UnregisterEvent,
				}, pluginMeta)
				plugins[#plugins+1] = m
				plugins[moduleName] = m
				initModules[#initModules+1] = m
				pluginsPendingInit[moduleName] = true

				return m, L
			end
		end
	end

	do
		local moduleOptions
		do
			local C_EncounterJournal_GetSectionInfo = (loader.isCata or loader.isMists) and function(key)
				return C_EncounterJournal.GetSectionInfo(key) or BigWigsAPI:GetLocale("BigWigs: Encounter Info")[key]
			end or loader.isRetail and C_EncounterJournal.GetSectionInfo or function(key)
				return BigWigsAPI:GetLocale("BigWigs: Encounter Info")[key]
			end
			local C = core.C -- Set from Constants.lua
			local standardFlag = C.BAR + C.CASTBAR + C.ICON + C.SOUND
				+ (loader.db.profile.bossModMessagesDisabled and 0 or C.MESSAGE)
				+ (loader.db.profile.bossModNameplatesDisabled and 0 or C.NAMEPLATE)
				+ (loader.db.profile.bossModVoiceDisabled and 0 or C.VOICE)
				+ C.SAY + C.SAY_COUNTDOWN + C.PROXIMITY
				+ C.FLASH + C.ALTPOWER + C.INFOBOX
			local defaultToggles = setmetatable({
				berserk = C.BAR + C.MESSAGE + C.SOUND,
				proximity = C.PROXIMITY,
				altpower = C.ALTPOWER,
				infobox = C.INFOBOX,
			}, {__index = function()
				return standardFlag
			end})
			function moduleOptions(module)
				if module.GetOptions then
					local toggles, headers, notes = module:GetOptions(CL) -- XXX stop passing CL at some point
					if toggles then module.toggleOptions = toggles end
					if headers then module.optionHeaders = headers end
					if notes then module.notes = notes end
					module.GetOptions = nil
				end

				if module.optionHeaders then
					for k, v in next, module.optionHeaders do
						if type(v) == "string" then
							if CL[v] then
								module.optionHeaders[k] = CL[v]
							end
						elseif type(v) == "number" then
							if v > 0 then
								local n = loader.GetSpellName(v)
								if not n then core:Error(("Invalid spell ID %d in the optionHeaders for module %s."):format(v, module.name)) end
								module.optionHeaders[k] = n or v
							else
								local tbl = C_EncounterJournal_GetSectionInfo(-v)
								if not tbl then core:Error(("Invalid journal ID (-)%d in the optionHeaders for module %s."):format(-v, module.name)) end
								module.optionHeaders[k] = tbl and tbl.title or v
							end
						end
					end
				end

				if module.toggleOptions then
					module.toggleDefaults = {}
					for k, v in next, module.toggleOptions do
						local bitflags = 0
						local disabled = false
						local t = type(v)
						if t == "table" then
							for i = 2, #v do
								local flagName = v[i]
								if flagName == "NAMEPLATE" and loader.db.profile.bossModNameplatesDisabled then
									bitflags = bitflags -- Don't add the NAMEPLATE flag
								elseif C[flagName] then
									bitflags = bitflags + C[flagName]
								elseif flagName == "OFF" then
									disabled = true
									break
								else
									error(("%q tried to register '%q' as a bitflag for toggleoption '%q'"):format(module.moduleName, flagName, v[1]))
								end
							end
							v = v[1]
							t = type(v)
						end
						-- mix in default toggles for keys we know
						-- this allows for mod.toggleOptions = {{1234, "bar", "message"}}
						-- while option keys don't usually specify common features such as bar or message
						for _, b in next, C do
							if bit.band(defaultToggles[v], b) == b and bit.band(bitflags, b) ~= b then
								bitflags = bitflags + b
							end
						end
						if disabled then
							if not module.toggleDisabled then
								module.toggleDisabled = {}
							end
							module.toggleDisabled[v] = bitflags
							bitflags = 0
						end
						if t == "string" then
							local custom = v:match("^custom_(o[nf]f?)_.*")
							if custom then
								module.toggleDefaults[v] = custom == "on" and true or false
							elseif v:find("custom_select", nil, true) then
								module.toggleDefaults[v] = 1
							else
								module.toggleDefaults[v] = bitflags
							end
						elseif t == "number" then
							if v > 0 then
								local n = loader.GetSpellName(v)
								if not n then core:Error(("Invalid spell ID %d in the toggleOptions for module %s."):format(v, module.name)) end
								module.toggleDefaults[v] = bitflags
							else
								local tbl = C_EncounterJournal_GetSectionInfo(-v)
								if not tbl then core:Error(("Invalid journal ID (-)%d in the toggleOptions for module %s."):format(-v, module.name)) end
								module.toggleDefaults[v] = bitflags
							end
						end
					end
					module.db = loader.db:RegisterNamespace(module.name, { profile = module.toggleDefaults })
					local db = module.db.profile
					for k, v in next, db do -- Option validation
						local defaultType = type(module.toggleDefaults[k])
						if defaultType == "nil" then
							db[k] = nil
						elseif type(v) ~= defaultType then
							db[k] = module.toggleDefaults[k]
						end
					end
				end

				module.SetupOptions = nil
			end
		end

		function core:RegisterBossModule(moduleName)
			local module = bosses[moduleName]
			if not module then
				error(("RegisterBossModule failed, no boss module named '%s' found."):format(tostring(moduleName)))
			elseif not bossesPendingInit[moduleName] then
				error(("RegisterBossModule failed, boss module '%s' is already registered."):format(tostring(moduleName)))
			end

			bossesPendingInit[moduleName] = nil
			module.SetupOptions = moduleOptions

			-- Call the module's OnRegister (which is our OnInitialize replacement)
			if type(module.OnRegister) == "function" then
				module:OnRegister()
				module.OnRegister = nil
			end

			core:SendMessage("BigWigs_BossModuleRegistered", module.moduleName, module)
		end
	end

	function core:RegisterPlugin(moduleName)
		local module = plugins[moduleName]
		if not module then
			error(("RegisterPlugin failed, no plugin named '%s' found."):format(tostring(moduleName)))
		elseif not pluginsPendingInit[moduleName] then
			error(("RegisterPlugin failed, plugin '%s' is already registered."):format(tostring(moduleName)))
		end

		pluginsPendingInit[moduleName] = nil
		if type(module.defaultDB) == "table" then
			module.db = loader.db:RegisterNamespace(module.name, { profile = module.defaultDB } )
		end

		-- Call the module's OnRegister (which is our OnInitialize replacement)
		if type(module.OnRegister) == "function" then
			module:OnRegister()
			module.OnRegister = nil
		end
		core:SendMessage("BigWigs_PluginOptionsReady", module.moduleName, module.pluginOptions, module.subPanelOptions)

		if coreEnabled then
			module:Enable() -- Support LoD plugins that load after we're enabled (e.g. zone based)
		end
	end
end

function core:IterateBossModules()
	return next, bosses
end

function core:GetBossModule(moduleName, silent)
	if not silent and not bosses[moduleName] then
		error(("No boss module named '%s' found."):format(moduleName))
	else
		return bosses[moduleName]
	end
end

function core:GetPluginOptions()
	local tbl = {}
	for i = 1, #plugins do
		local module = plugins[i]
		tbl[module.moduleName] = {module.pluginOptions, module.subPanelOptions}
	end
	return tbl
end

function core:GetPlugin(moduleName, silent)
	if not plugins[moduleName] then
		if not silent then
			error(("No plugin named '%s' found."):format(moduleName))
		else
			return
		end
	else
		local moduleTbl = {}
		for i = 1, #plugins[moduleName].globalFuncs do
			local entry = plugins[moduleName].globalFuncs[i]
			moduleTbl[entry] = plugins[moduleName][entry]
		end
		return moduleTbl
	end
end

function core:AddColors(moduleName, options)
	local module = bosses[moduleName]
	if not module then
		-- core:Error(("AddColors: Invalid module %q."):format(moduleName))
		return
	end
	module.colorOptions = options
end

function core:AddSounds(moduleName, options)
	local module = bosses[moduleName]
	if not module then
		-- core:Error(("AddSounds: Invalid module %q."):format(moduleName))
		return
	end
	module.soundOptions = options
end

-------------------------------------------------------------------------------
-- Global
--

BigWigs = setmetatable({}, { __index = core, __newindex = function() end, __metatable = false })
