local E, L, V, P, G = unpack(ElvUI)
local ENP = E:NewModule("Enhanced_NamePlates", "AceHook-3.0", "AceEvent-3.0")
local NP = E:GetModule("NamePlates")
local M = E:GetModule("Misc")
local CH = E:GetModule("Chat")

local _G = _G
local ipairs, next, pairs = ipairs, next, pairs
local sub, gsub, floor = string.sub, string.gsub, math.floor
local match, gmatch, format, lower = string.match, string.gmatch, string.format, string.lower
local tinsert, tremove = table.insert, table.remove

local GetGuildInfo = GetGuildInfo
local IsInGuild = IsInGuild
local IsInInstance = IsInInstance
local IsResting = IsResting
local UnitClass = UnitClass
local UnitInParty = UnitInParty
local UnitInRaid = UnitInRaid
local UnitIsPlayer = UnitIsPlayer
local UnitName = UnitName
local UnitPlayerControlled = UnitPlayerControlled
local UnitReaction = UnitReaction
local UNKNOWN = UNKNOWN

local classMap = {}
local guildMap = {}
local npcTitleMap = {}

local function UpdateNameplateByName(name)
	for frame in pairs(NP.VisiblePlates) do
		if frame.UnitName == name then
			NP.OnShow(frame:GetParent(), nil, true)
		end
	end
end

function ENP:UPDATE_MOUSEOVER_UNIT()
	if UnitIsPlayer("mouseover") and UnitReaction("mouseover", "player") ~= 2 then
		local name, realm = UnitName("mouseover")
		if realm or not name or name == UNKNOWN then return end

		if E.db.enhanced.nameplates.classCache then
			local _, class = UnitClass("mouseover")
			class = classMap[class]

			if EnhancedDB.UnitClass[name] ~= class then
				EnhancedDB.UnitClass[name] = class
			end
		end

		if E.db.enhanced.nameplates.titleCache then
			local guildName = GetGuildInfo("mouseover")
			if not guildName then
				if EnhancedDB.UnitTitle[name] then
					EnhancedDB.UnitTitle[name] = nil
					UpdateNameplateByName(name)
				end
				return
			end

			if not guildMap[guildName] then
				tinsert(EnhancedDB.GuildList, guildName)
				guildMap[guildName] = #EnhancedDB.GuildList
			end

			if EnhancedDB.UnitTitle[name] ~= guildMap[guildName] then
				EnhancedDB.UnitTitle[name] = guildMap[guildName]
				UpdateNameplateByName(name)
			end
		end
	else
		self.scanner:ClearLines()
		self.scanner:SetUnit("mouseover")

		local name = _G["Enhanced_ScanningTooltipTextLeft1"]:GetText()
		if not name then return end
		local description = _G["Enhanced_ScanningTooltipTextLeft2"]:GetText()
		if not description then return end

		if match(description, UNIT_LEVEL_TEMPLATE) then return end

		name = gsub(gsub((name), "|c........", "" ), "|r", "")
		if name ~= UnitName("mouseover") then return end
		if UnitPlayerControlled("mouseover") then return end

		if not npcTitleMap[description] then
			tinsert(EnhancedDB.NPCList, description)
			npcTitleMap[description] = #EnhancedDB.NPCList
		end

		if EnhancedDB.UnitTitle[name] ~= npcTitleMap[description] then
			EnhancedDB.UnitTitle[name] = npcTitleMap[description]
			UpdateNameplateByName(name)
		end
	end
end

-- Class Cache
local grenColorToClass = {}
for class, color in pairs(RAID_CLASS_COLORS) do
	grenColorToClass[color.g] = class
end

local function UnitClassHook(self, frame, unitType)
	if unitType == "FRIENDLY_PLAYER" then
		local unitName = frame.UnitName
		local unit = self[unitType][unitName]
		if unit then
			local _, class = UnitClass(unit)
			if class then
				return class
			end
		elseif EnhancedDB.UnitClass[unitName] then
			return CLASS_SORT_ORDER[EnhancedDB.UnitClass[unitName]]
		else
			return NP:GetUnitClassByGUID(frame)
		end
	elseif unitType == "ENEMY_PLAYER" then
		local _, g = frame.oldHealthBar:GetStatusBarColor()
		return grenColorToClass[floor(g*100 + 0.5) / 100]
	end
end

function ENP:ClassCache()
	if E.db.enhanced.nameplates.classCache then
		if not self:IsHooked(NP, "UnitClass") then
			self:RawHook(NP, "UnitClass", UnitClassHook)
		end
	else
		if self:IsHooked(NP, "UnitClass") then
			self:Unhook(NP, "UnitClass")
		end
	end
end

-- Title Cache
local separatorMap = {
	[" "] = "%s",
	["<"] = "<%s>",
	["("] = "(%s)",
	["["] = "[%s]",
	["{"] = "{%s}"
}

local function Update_NameHook(self, frame)
	if not E.db.enhanced.nameplates.titleCache then return end

	if frame.Health:IsShown() then
		if frame.Title then
			frame.Title:SetText()
			frame.Title:Hide()
		end
		return
	end

	local guildName = EnhancedDB.GuildList[EnhancedDB.UnitTitle[frame.UnitName]]
	if frame.UnitType == "FRIENDLY_PLAYER" and guildName then
		local db = E.db.enhanced.nameplates.guild

		local shown
		if IsResting() then
			shown = db.visibility.city
		else
			local _, instanceType = IsInInstance()
			if instanceType == "pvp" then
				shown = db.visibility.pvp
			elseif instanceType == "arena" then
				shown = db.visibility.arena
			elseif instanceType == "party" then
				shown = db.visibility.party
			elseif instanceType == "raid" then
				shown = db.visibility.raid
			else
				shown = true
			end
		end

		if shown then
			if not frame.Title then
				frame.Title = frame:CreateFontString(nil, "OVERLAY")
				frame.Title:SetWordWrap(false)
			end

			frame.Title:SetFont(E.LSM:Fetch("font", db.font), db.fontSize, db.fontOutline)

			local color
			if UnitInRaid(frame.UnitName) then
				color = db.colors.raid
			elseif UnitInParty(frame.UnitName) then
				color = db.colors.party
			elseif IsInGuild and GetGuildInfo("player") == guildName then
				color = db.colors.guild
			else
				color = db.colors.none
			end

			frame.Title:SetTextColor(color.r, color.g, color.b)
			frame.Title:SetPoint("TOP", frame.Name, "BOTTOM")
			frame.Title:SetFormattedText(separatorMap[db.separator], guildName)
			frame.Title:Show()
		elseif frame.Title then
			frame.Title:Hide()
		end
	elseif (frame.UnitType == "FRIENDLY_NPC" or frame.UnitType == "ENEMY_NPC") and EnhancedDB.NPCList[EnhancedDB.UnitTitle[frame.UnitName]] then
		if not frame.Title then
			frame.Title = frame:CreateFontString(nil, "OVERLAY")
			frame.Title:SetWordWrap(false)
		end

		local db = E.db.enhanced.nameplates.npc
		frame.Title:SetFont(E.LSM:Fetch("font", db.font), db.fontSize, db.fontOutline)

		if E.db.enhanced.nameplates.npc.reactionColor then
			local db = self.db.colors
			if frame.UnitReaction == 5 then -- friendly
				r, g, b = db.reactions.good.r, db.reactions.good.g, db.reactions.good.b
			elseif frame.UnitReaction == 1 or frame.UnitReaction == 2 then -- hostile
				r, g, b = db.reactions.bad.r, db.reactions.bad.g, db.reactions.bad.b
			elseif frame.UnitReaction == 4  then -- neutral
				r, g, b = db.reactions.neutral.r, db.reactions.neutral.g, db.reactions.neutral.b
			else
				r, g, b = 1, 1, 1
			end
			frame.Title:SetTextColor(r, g, b)
		else
			frame.Title:SetTextColor(db.color.r, db.color.g, db.color.b)
		end

		frame.Title:SetPoint("TOP", frame.Name, "BOTTOM")
		frame.Title:SetFormattedText(separatorMap[db.separator], EnhancedDB.NPCList[EnhancedDB.UnitTitle[frame.UnitName]])
		frame.Title:Show()
	elseif frame.Title then
		frame.Title:SetText("")
	end
end

function ENP:TitleCache()
	if E.db.enhanced.nameplates.titleCache then
		if not self:IsHooked(NP, "Update_Name") then
			self:Hook(NP, "Update_Name", Update_NameHook)
		end
	else
		if self:IsHooked(NP, "Update_Name") then
			self:Unhook(NP, "Update_Name")
		end
	end
end

-- Chat Bubbles
local events = {
	"CHAT_MSG_GUILD",
	"CHAT_MSG_OFFICER",
	"CHAT_MSG_PARTY",
	"CHAT_MSG_PARTY_LEADER",
	"CHAT_MSG_RAID",
	"CHAT_MSG_RAID_LEADER",
	"CHAT_MSG_RAID_WARNING",
	"CHAT_MSG_BATTLEGROUND",
	"CHAT_MSG_BATTLEGROUND_LEADER",
	"CHAT_MSG_CHANNEL",
	"CHAT_MSG_SAY",
	"CHAT_MSG_YELL",
	"CHAT_MSG_MONSTER_SAY",
	"CHAT_MSG_MONSTER_YELL"
}

local bubbleList = {}
local delayFrame = CreateFrame("Frame")
delayFrame:Hide()
delayFrame:SetScript("OnUpdate", function(self, elapsed)
	local i, frame = 1

	while bubbleList[i] do
		frame = bubbleList[i]
		frame.delay = frame.delay - elapsed

		if frame.delay <= 0 then
			frame.delay = 0
			E:UIFrameFadeOut(frame, .2, frame:GetAlpha(), 0)
			tremove(bubbleList, i)
		else
			i = i + 1
		end
	end

	if #bubbleList == 0 then
		self:Hide()
	end
end)

local function SetBubbleDelay(frame, delay)
	local found

	if #bubbleList > 0 then
		for _, v in ipairs(bubbleList) do
			if v == frame then
				v.delay = delay
				found = true
				break
			end
		end
	end

	if not found then
		frame.delay = delay
		tinsert(bubbleList, frame)
		delayFrame:Show()
	end
end

local inactiveBubbles = {}

local function ReleaseBubble(frame)
	inactiveBubbles[#inactiveBubbles + 1] = frame
	frame.parent.bubbleFrame = nil
	frame:Hide()
end

local function FadeClosure(frame)
	if frame.fadeInfo.mode == "OUT" then
		ReleaseBubble(frame)
	end
end

local function CreateBubble()
	local frame = CreateFrame("Frame")
	frame:SetFrameStrata("BACKGROUND")
	frame:Hide()
	frame.text = frame:CreateFontString()
	frame.text:SetJustifyH("CENTER")
	frame.text:SetJustifyV("MIDDLE")
	frame.text:SetWordWrap(true)
	frame.text:SetNonSpaceWrap(true)
	frame:SetPoint("TOPLEFT", frame.text, -15, 15)
	frame:SetPoint("BOTTOMRIGHT", frame.text, 15, -15)
	M:SkinBubble(frame)

	frame.delay = 0
	frame.FadeObject = {
		finishedFuncKeep = true,
		finishedArg1 = frame,
		finishedFunc = FadeClosure
	}

	return frame
end

local function AcquireBubble()
	local numInactiveObjects = #inactiveBubbles
	if numInactiveObjects > 0 then
		local frame = inactiveBubbles[numInactiveObjects]
		inactiveBubbles[numInactiveObjects] = nil
		return frame
	end

	return CreateBubble()
end

function ENP:AddBubbleMessage(frame, msg, author, guid)
	if E.private.general.chatBubbleName then
		M:AddChatBubbleName(frame, guid, author)
	else
		frame.Name:SetText()
	end

	frame.text:SetText(msg)

	if frame.text:GetStringWidth() > 300 then
		frame.text:SetWidth(300)
	end

	if E.private.chat.enable and E.private.general.classColorMentionsSpeech then
		local classColorTable, lowerCaseWord, isFirstWord, rebuiltString, tempWord, wordMatch, classMatch
		if msg and match(msg, "%s-%S+%s*") then
			for word in gmatch(msg, "%s-%S+%s*") do
				tempWord = gsub(word, "^[%s%p]-([^%s%p]+)([%-]?[^%s%p]-)[%s%p]*$","%1%2")
				lowerCaseWord = lower(tempWord)

				classMatch = CH.ClassNames[lowerCaseWord]
				wordMatch = classMatch and lowerCaseWord

				if wordMatch and not E.global.chat.classColorMentionExcludedNames[wordMatch] then
					classColorTable = CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS[classMatch] or RAID_CLASS_COLORS[classMatch]
					word = gsub(word, gsub(tempWord, "%-","%%-"), format("\124cff%.2x%.2x%.2x%s\124r", classColorTable.r*255, classColorTable.g*255, classColorTable.b*255, tempWord))
				end

				if not isFirstWord then
					rebuiltString = word
					isFirstWord = true
				else
					rebuiltString = format("%s%s", rebuiltString, word)
				end
			end

			if rebuiltString ~= nil then
				frame.text:SetText(rebuiltString)
			end
		end
	end
end

function ENP:FindNameplateByChatMsg(event, msg, author, _, _, _, _, _, channelID, _, _, _, guid)
	if author == UnitName("player") or not author then return end
	if not next(NP.VisiblePlates) then return end

	local chatType = sub(event, 10)
	if sub(chatType, 1, 7) == "CHANNEL" then
		chatType = "CHANNEL"..channelID
	end

	local info = ChatTypeInfo[chatType]
	if not info then return end

	for frame in pairs(NP.VisiblePlates) do
		if frame.UnitName == author then
			local bubbleFrame
			if not frame.bubbleFrame then
				bubbleFrame = AcquireBubble()
				frame.bubbleFrame = bubbleFrame
				bubbleFrame.text:ClearAllPoints()
				bubbleFrame.text:SetPoint("BOTTOM", frame, "TOP", 0, 20)
				bubbleFrame:Show()
				E:UIFrameFadeIn(bubbleFrame, .2, 0, 1)
			else
				bubbleFrame = frame.bubbleFrame
			end

			bubbleFrame.parent = frame

			bubbleFrame.text:SetSize(0, 0)
			bubbleFrame.text:SetTextColor(info.r, info.g, info.b)
			bubbleFrame.author = author

			if E.private.general.chatBubbles == "backdrop" then
				if E.PixelMode then
					bubbleFrame:SetBackdropBorderColor(info.r, info.g, info.b)
				else
					local r, g, b = info.r, info.g, info.b
					bubbleFrame.bordertop:SetTexture(r, g, b)
					bubbleFrame.borderbottom:SetTexture(r, g, b)
					bubbleFrame.borderleft:SetTexture(r, g, b)
					bubbleFrame.borderright:SetTexture(r, g, b)
				end
			end

			ENP:AddBubbleMessage(bubbleFrame, msg, author, guid)

			if bubbleFrame.delay == 0 then
				E:UIFrameFadeRemoveFrame(bubbleFrame)
				E:UIFrameFadeIn(bubbleFrame, .2, bubbleFrame:GetAlpha(), 1)
			end

			local _, delayMult = gsub(msg, "%s+", "")
			SetBubbleDelay(bubbleFrame, 2 + (0.5 * delayMult))
		end
	end
end

local function OnShowHook(frame, ...)
	ENP.hooks[NP].OnShow(frame, ...)

	if frame.UnitFrame.bubbleFrame then
		frame.UnitFrame.bubbleFrame = nil
	end

	if #bubbleList > 0 then
		for _, bubbleFrame in ipairs(bubbleList) do
			if frame.UnitFrame.UnitName == bubbleFrame.author then
				frame.UnitFrame.bubbleFrame = bubbleFrame
				bubbleFrame.parent = frame.UnitFrame

				bubbleFrame.text:ClearAllPoints()
				bubbleFrame.text:SetPoint("BOTTOM", frame.UnitFrame, "TOP", 0, 20)
				bubbleFrame:Show()
				break
			end
		end
	end
end

local function OnHideHook(frame)
	if frame.UnitFrame.bubbleFrame then
		frame.UnitFrame.bubbleFrame:Hide()
	end
	if frame.UnitFrame.Title then
		frame.UnitFrame.Title:SetText()
		frame.UnitFrame.Title:Hide()
	end
end

function ENP:ChatBubbles()
	if E.db.enhanced.nameplates.chatBubbles then
		for _, event in ipairs(events) do
			ENP:RegisterEvent(event, "FindNameplateByChatMsg")
		end
	else
		for _, event in ipairs(events) do
			ENP:UnregisterEvent(event)
		end
	end
end

function ENP:UpdateAllSettings()
	self:ClassCache()
	self:ChatBubbles()
	self:TitleCache()

	if E.db.enhanced.nameplates.titleCache or E.db.enhanced.nameplates.classCache then
		if not self.scanner then
			self.scanner = CreateFrame("GameTooltip", "Enhanced_ScanningTooltip", nil, "GameTooltipTemplate")
			self.scanner:SetOwner(WorldFrame, "ANCHOR_NONE")
		end

		self:RegisterEvent("UPDATE_MOUSEOVER_UNIT")

	elseif not E.db.enhanced.nameplates.titleCache and not E.db.enhanced.nameplates.classCache then
		self:UnregisterEvent("UPDATE_MOUSEOVER_UNIT")
	end

	if E.db.enhanced.nameplates.chatBubbles or E.db.enhanced.nameplates.titleCache then
		if not ENP:IsHooked(NP, "OnHide") then
			ENP:Hook(NP, "OnHide", OnHideHook)
		end
	elseif not E.db.enhanced.nameplates.chatBubbles and not E.db.enhanced.nameplates.titleCache then
		if ENP:IsHooked(NP, "OnHide") then
			ENP:Unhook(NP, "OnHide")
		end
	end
	if E.db.enhanced.nameplates.chatBubbles then
		if not ENP:IsHooked(NP, "OnShow") then
			ENP:RawHook(NP, "OnShow", OnShowHook, true)
		end
	else
		if ENP:IsHooked(NP, "OnShow") then
			ENP:Unhook(NP, "OnShow")
		end
	end
end

function ENP:Initialize()
	EnhancedDB.UnitClass = EnhancedDB.UnitClass or {}
	EnhancedDB.UnitTitle = EnhancedDB.UnitTitle or {}

	if EnhancedDB.GuildList then
		for i, guildName in ipairs(EnhancedDB.GuildList) do
			guildMap[guildName] = i
		end
	else
		EnhancedDB.GuildList = {}
	end

	if EnhancedDB.NPCList then
		for i, guildName in ipairs(EnhancedDB.NPCList) do
			npcTitleMap[guildName] = i
		end
	else
		EnhancedDB.NPCList = {}
	end

	for i, class in ipairs(CLASS_SORT_ORDER) do
		classMap[class] = i
	end

	ENP:UpdateAllSettings()
end

local function InitializeCallback()
	ENP:Initialize()
end

E:RegisterModule(ENP:GetName(), InitializeCallback)
