local name, ns = ...
local Addon = LibStub("AceAddon-3.0"):NewAddon(name, "AceEvent-3.0", "AceTimer-3.0", "AceHook-3.0", "AceConsole-3.0")

local L = LibStub("AceLocale-3.0"):GetLocale(name)

local debug = ns.debug
_G[name] = Addon

hooksecurefunc("GameTooltip_SetDefaultAnchor", function(tooltip, parent)
	tooltip:SetOwner(parent, "ANCHOR_CURSOR")
	tooltip:ClearAllPoints()
end)

local ScriptEnv
--[[
-- Text-provider-code
--]]
local function compileFunction(code)
	local func
	local lua_string = 'return function(unit) if not unit then return \"\" end; '..code..' end'
	local create_func, err = loadstring(lua_string)
	if create_func then
		func = create_func()
		setfenv(func, ScriptEnv)
	else
		geterrorhandler()(err)
	end
	if not func then func = function() return "" end end
	return func
end

local function getOrCreateText(self, code)
	local val = rawget(self, code)
	if not val then
		val = compileFunction(code)
		self[code] = val
	end
	return val
end

local funcs = {
	[""] = function() return "" end,
}
setmetatable(funcs, { __index = getOrCreateText, })

function Addon:RemoveText(code)
	funcs[code] = nil
end

local function getText(code, unit)
	local func = funcs[code]
	return string.format(func(unit))
end

function Addon:GetText(code, unit)
	local success, result = pcall(getText, code, unit)
	if success then
		return result
	else
		self:PrintError(code, unit)
		geterrorhandler()(result)
	end
end

local function printRed(str)
	print(("|cffff0000%s|r"):format(str))
end

local function printYellow(str)
	print(("|cffffff00%s|r"):format(str))
end

function Addon:PrintError(code, unit)
	printRed("An error occured in a LuaText!")
	printRed("Code:")
	printYellow(code)
	local args = {funcs[code](unit)}
	if next(args) then
		printRed("Args:")
 	end
	for i,v in ipairs(args) do
		printYellow(("[%d] %s"):format(i,v))
	end
end

local function getMouseoverUnit()
	local _, tooltipUnit = GameTooltip:GetUnit()
	if not tooltipUnit or not UnitExists(tooltipUnit) or UnitIsUnit(tooltipUnit, "mouseover") then
		return "mouseover"
	else
		return tooltipUnit
	end
end

local mouseoverUnit = getMouseoverUnit()

local leftGameTooltipStrings, rightGameTooltipStrings = {}, {}
setmetatable(leftGameTooltipStrings, {__index=function(self, key)
	for i = #self, key do
		if i > GameTooltip:NumLines() then
			GameTooltip:AddDoubleLine(' ', ' ')
		end
		leftGameTooltipStrings[i] = _G["GameTooltipTextLeft" .. i]
		rightGameTooltipStrings[i] = _G["GameTooltipTextRight" .. i]
	end
	return self[key]
end})
setmetatable(rightGameTooltipStrings, getmetatable(leftGameTooltipStrings))

local _G = _G
local IsShiftKeyDown = _G.IsShiftKeyDown
local ChatFontNormal = _G.ChatFontNormal
local CreateFrame = _G.CreateFrame
local UIParent = _G.UIParent
local GameTooltip = _G.GameTooltip
local ipairs = _G.ipairs
local GetNumFactions = _G.GetNumFactions
local GetFactionInfo = _G.GetFactionInfo
local UnitPlayerControlled = _G.UnitPlayerControlled
local UnitIsPlayer = _G.UnitIsPlayer
local UnitFactionGroup = _G.UnitFactionGroup
local UnitIsConnected = _G.UnitIsConnected
local UnitIsVisible = _G.UnitIsVisible
local UnitIsPVP = _G.UnitIsPVP
local UnitExists = _G.UnitExists

Addon.providerChoices = {}
Addon.providers = {}
Addon.providerOpts = {}

--[[
-- for the locale-script:
-- L["DogTag"]
-- L["Dogtag-code"]
--]]

function Addon:RegisterProvider(name, localName, funcs, opts)
	Addon.providerChoices[name] = localName
	Addon.providers[name] = funcs
	Addon.providerOpts[name] = opts
end

Addon:RegisterProvider("none", L["None"], { getValue = function() return end, }, {})
Addon:RegisterProvider("plain", L["Plain Text"], { getValue = function(value) return value end, }, {
		value = {
			type = 'input',
			multiline = true,
			width = "full",
			name = L["Text"],
			desc = L["Text to display"],
			get = function(info)
					return info.arg.value
				end,
			set = function(info, value)
					info.arg.value = value
				end,
		},
})

local codeFuncs = {
	getValue = function(code, unit) return Addon:GetText(code, unit) end,
}
Addon:RegisterProvider("code", L["Code"], codeFuncs, {
		value = {
			type = 'input',
			multiline = true,
			width = "full",
			name = L["Code"],
			desc = L["function-code to retrieve the code from"],
			get = function(info)
					return info.arg.value
				end,
			set = function(info, value)
					info.arg.value = value
				end,
		},
})

local dbDefaults = {
	profile = {
		left = {
			["*"] = {
				type = "none",
			},
			[1] = {
				type = "template",
				value = "NameWithTitle",
			},
			[2] = {
				type = "template",
				value = "GuildWithRank",
			},
			[3] = {
				type = "template",
				value = "Faction",
			},
			[4] = {
				type = "plain",
				value = TARGET,
			},
			[5] = {
				type = "template",
				value = "LevelClassRace",
			},
			[6] = {
				type = "plain",
				value = STATUS,
			},
			[7] = {
				type = "plain",
				value = ZONE,
			},
			[8] = {
				type = "template",
				value = "TalentsWithIcon",
			},
		},
		right = {
			["*"] = {
				type = "none",
			},
			[1] = {
				type = "none",
			},
			[2] = {
				type = "none",
			},
			[3] = {
				type = "none",
			},
			[4] = {
				type = "template",
				value = "Target",
			},
			[5] = {
				type = "none",
			},
			[6] = {
				type = "template",
				value = "Status",
			},
			[7] = {
				type = "template",
				value = "Zone",
			},
			[8] = {
				type = "template",
				value = "TalentDetail",
			},
		},
		failedCalls = {},
		version = 0,
	}
}

local dbUpdates = {
	[1] = function(dbp)
			if dbp.lefts then
				for k,v in pairs(dbp.lefts) do
					dbp.left[k].type = "code"
					dbp.left[k].value = v
				end
			end
			if dbp.rights then
				for k,v in pairs(dbp.rights) do
					dbp.right[k].type = "code"
					dbp.right[k].value = v
				end
			end
		end,
}

local function updateDB(dbp)
	local version = dbp.version
	for i = version+1,#dbUpdates do
		Addon:Print("Updating DB: ", i - 1, " -> ", i)
		dbUpdates[i](dbp)
		dbp.version = i
	end
end

function Addon:OnInitialize()
	ScriptEnv = self.ScriptEnv
	self.db = LibStub("AceDB-3.0"):New("ToolTipLuaTextDB", dbDefaults, "Default")
	updateDB(self.db.profile)
	self.db.RegisterCallback(self, "OnNewProfile", "OnNewProfile")
	self.db.RegisterCallback(self, "OnProfileReset", "OnNewProfile")
	self.db.RegisterCallback(self, "OnProfileChanged", "ChangeProfile")
	failedCalls = self.db.profile.failedCalls
	self:InitUnitWatcher()
end

function Addon:OnNewProfile()
	self.db.profile.version = #dbUpdates,
	self:ChangeProfile()
end

function Addon:ChangeProfile()
	updateDB(self.db.profile)
end

function Addon:OnEnable()
	if CowTip then
		CowTip:GetModule("Text"):ToggleActive(false)
	end
--	ScriptEnv = Addon.ScriptEnv

	self:RegisterEvent("UPDATE_FACTION")

	self:RawHook(GameTooltip, "GetUnit", "GameTooltip_GetUnit", true)
	self:RawHookScript(GameTooltip, "OnHide", "OnTooltipHide")
	self:RawHookScript(GameTooltip, "OnTooltipSetUnit", "OnTooltipSetUnit")
	self:HookScript(GameTooltip, "OnShow")
	self:UPDATE_FACTION()
	self:CreateOptions()
	-- gui
end

function Addon:OnShow(tip)
	if not tip:GetUnit() then
		GameTooltipStatusBar:Hide()
	end
end

function Addon:OnDisable()
	wipe(funcs)
end

-- patterns
local LEVEL_start = "^" .. LEVEL
local PVP = _G.PVP
local factionList = {}

local function scanTT(unit)
	local npcguild, faction, zone
	local num = 2
	if not unit then
		debug("no unit")
		wipe(ScriptEnv.orig)
		return num
	end
	-- scan for faction
	local left_2 = leftGameTooltipStrings[2]:GetText()
	if not left_2 then
		num = num -1
	elseif not left_2:find(LEVEL_start) then
		num = num + 1
		if not UnitIsPlayer(unit) then
			debug("npcguild found: " .. left_2)
			npcguild = left_2
		end
	end

	if not UnitPlayerControlled(unit) and not UnitIsPlayer(unit) then
		-- npc, faction line
		faction = leftGameTooltipStrings[num+1]:GetText()
		if faction == PVP then
			faction = nil
		end
		if faction and faction:match("%%") then
			faction = nil
		end
		if faction and (factionList[faction] or UnitFactionGroup(unit) == faction) then
			num = num + 1
		else
			faction = nil
		end
	end

	if not UnitIsVisible(unit) and UnitIsConnected(unit) then
		num = num + 1
		zone = leftGameTooltipStrings[num]:GetText()
	elseif not UnitIsConnected(unit) or not UnitIsVisible(unit) or UnitIsPVP(unit) then
		num = num + 1
	end

	wipe(ScriptEnv.orig)
	ScriptEnv.orig.faction = faction
	ScriptEnv.orig.zone = zone
	ScriptEnv.orig.npcguild = npcguild
	return num
end

function Addon:UPDATE_FACTION()
	for i = 1, GetNumFactions() do
		local name = GetFactionInfo(i)
		factionList[name] = true
	end
end

--[[
How normal tooltips look:
{Name}
{[Guild] : ~~~}
Level {Num} {[(Classification)]}
{NPC? [Faction] : ~~~}
{Offline? Offline : !IsVisible? [Zone] : IsPVP? [PvP] : ~~~}
]]

local lastNumRegistered = 0
local linesToAdd = {}
local linesToAddR = {}
local linesToAddG = {}
local linesToAddB = {}
local linesToAddRight = {}
local linesToAddRightR = {}
local linesToAddRightG = {}
local linesToAddRightB = {}

local lastUnitName, lastUnit

function Addon:RefixLines(lastLine)
	lastNumRegistered = 0
	local LinesAdded = 0

	for i = lastLine, GameTooltip:NumLines() do
		local left = leftGameTooltipStrings[i]
		local j = i - lastLine + 1
		linesToAdd[j] = left:GetText()
		local r, g, b = left:GetTextColor()
		linesToAddR[j] = r
		linesToAddG[j] = g
		linesToAddB[j] = b
		local right = rightGameTooltipStrings[i]
		if right:IsShown() then
			linesToAddRight[j] = right:GetText()
			local r, g, b = right:GetTextColor()
			linesToAddRightR[j] = r
			linesToAddRightG[j] = g
			linesToAddRightB[j] = b
		end
		LinesAdded = LinesAdded + 1
	end
	local showHPbar = GameTooltipStatusBar:IsShown()
	GameTooltip:ClearLines()
	local num = self:AddCustomLines()
	if showHPbar then
		GameTooltipStatusBar:Show()
	end
	if lastNumRegistered < num then
		lastNumRegistered = num
	end

	for i = 1, LinesAdded do
		local left = linesToAdd[i]
		local right = linesToAddRight[i]
		if right then
			GameTooltip:AddDoubleLine(left, right, linesToAddR[i], linesToAddG[i], linesToAddB[i], linesToAddRightR[i], linesToAddRightG[i], linesToAddRightB[i])
		else
			GameTooltip:AddLine(left, linesToAddR[i], linesToAddG[i], linesToAddB[i], true)
		end
		linesToAdd[i] = nil
		linesToAddR[i] = nil
		linesToAddG[i] = nil
		linesToAddB[i] = nil
		linesToAddRight[i] = nil
		linesToAddRightR[i] = nil
		linesToAddRightG[i] = nil
		linesToAddRightB[i] = nil
	end
end

local watchedFSFuncs = {}
local watchedFSValues = {}

local function watchFS(fs, func, value)
	watchedFSFuncs[fs] = func
	watchedFSValues[fs] = value
end

local function trimToNil(text)
	if not text then return end
	text = text:trim()
	if text == "" then return end
	return text
end

local function getProviderForEntry(entry)
	return Addon.providers[entry.type] or Addon.providers.plain
end

local function getEntry(side, i)
	return Addon.db.profile[side][i]
end

local function getProvider(side, i)
	local entry = getEntry(side, i)
	return getProviderForEntry(entry)
end

function Addon:AddCustomLines(unit)
	local num = 0
	local db = self.db.profile
	local unit = getMouseoverUnit()
	for i = 1, 30 do
		local left, right = getEntry("left", i), getEntry("right", i)
		local lval, rval = left.value, right.value
		local leftp, rightp = getProviderForEntry(left), getProviderForEntry(right)
		local left_ev, right_ev
		if lval then
			left_ev = trimToNil(leftp.getValue(left.value, unit))
		end
		if rval then
			right_ev = trimToNil(rightp.getValue(right.value, unit))
		end

		if left_ev or right_ev then
			if rval then
				--if right_ev or (left and left_ev and left:find("%[")) then
				if right_ev or db.alwaysShowLefts then
					num = num + 1
					GameTooltip:AddDoubleLine(left_ev or ' ', right_ev, 1, 1, 1, 1, 1, 1)
					local leftFS = leftGameTooltipStrings[num]
					local rightFS = rightGameTooltipStrings[num]
					watchFS(leftFS, leftp.getValue, left.value)
					watchFS(rightFS, rightp.getValue, right.value)
				end
			elseif lval then
				if left_ev then
					num = num + 1
					GameTooltip:AddLine(left_ev, 1, 1, 1, true)
					local leftFS = leftGameTooltipStrings[num]
					watchFS(leftFS, leftp.getValue, left.value)
				end
			end
		end
	end
	return num
end

function Addon:GameTooltip_GetUnit(this)
	if not lastUnitName then
		return self.hooks[GameTooltip].GetUnit(GameTooltip)
	end
	return lastUnitName, lastUnit
end

local delayedSetUnitTimer

local function delayedSetUnit()
	local unit = getMouseoverUnit()
	debug("delayed set unit ", unit)
	if unit and UnitExists(unit) then
		Addon:OnTooltipSetUnit2()
		GameTooltip:Show()
	end
	delayedSetUnitTimer = nil
end

function Addon:OnTooltipSetUnit(this, ...)
	self.hooks[this].OnTooltipSetUnit(this, ...)
	local unit = getMouseoverUnit()
	if unit and UnitExists(unit) then
		debug("set unit ", unit, " immediately")
		self:OnTooltipSetUnit2()
		self:CancelTimer(delayedSetUnitTimer, true)
	elseif not delayedSetUnitTimer then
		delayedSetUnitTimer = self:ScheduleTimer(delayedSetUnit, 0.1)
	end
end

local function fixColor()
	GameTooltipTextLeft1:SetTextColor(1, 1, 1)
end

local updateTextsTimer
function Addon:OnTooltipSetUnit2()
	lastUnitName, lastUnit = self.hooks[GameTooltip].GetUnit(GameTooltip)
	local unit = getMouseoverUnit()
	local num = scanTT(unit)
	debug(num)
	self:RefixLines(num+1)

	if not updateTextsTimer then
		updateTextsTimer = self:ScheduleRepeatingTimer("UpdateTexts", 1, unit)
	end
	self:ScheduleTimer(fixColor, 0.1)
end

function Addon:UpdateTexts(unit)
	if not unit or not UnitExists(unit) then
		return
	end
	fixColor()
	for fs, value in pairs(watchedFSValues) do
		local text = watchedFSFuncs[fs](value, unit)
		fs:SetText(text)
	end
end

function Addon:OnTooltipHide(this, ...)
	self.hooks[this].OnHide(this, ...)
	lastNumRegistered = 0
	lastUnitName, lastUnit = nil, nil

	self:CancelTimer(updateTextsTimer, true)
	updateTextsTimer = nil

	wipe(watchedFSFuncs)
	wipe(watchedFSValues)
end
