-- This file uses models and textures taken from TomTom. The 3D arrow model was created by Guillotine (curse.guillotine@gmail.com) and 2D minimap textures by Cladhaire.

---@class DBM
local DBM = DBM

local L = DBM_CORE_L

--------------
--  Locals  --
--------------
---@class DBMArrow
local arrowFrame = {}
local frame, runAwayArrow, targetType, targetPlayer, targetX, targetY, targetMapId, hideTime, hideDistance

DBM.Arrow = arrowFrame

--------------------------------------------------------
--  Cache frequently used global variables in locals  --
--------------------------------------------------------
local pi, pi2 = math.pi, math.pi * 2
local floor, sin, cos, atan2, sqrt, min = math.floor, math.sin, math.cos, math.atan2, math.sqrt, math.min
local UnitPosition, GetTime = UnitPosition, GetTime

--------------------
--  Create Frame  --
--------------------
---@class DBMArrowFrame: Button
frame = CreateFrame("Button", "DBMArrow", UIParent)
frame:Hide()
frame:SetFrameStrata("HIGH")
frame:SetSize(56, 42)
frame:SetMovable(true)
frame:EnableMouse(false)
frame:RegisterForDrag("LeftButton", "RightButton")
frame:SetScript("OnDragStart", function(self)
	self:StartMoving()
end)
frame:SetScript("OnDragStop", function(self)
	self:StopMovingOrSizing()
	local point, _, _, x, y = self:GetPoint(1)
	DBM.Options.ArrowPoint = point
	DBM.Options.ArrowPosX = x
	DBM.Options.ArrowPosY = y
end)

---@class DBMArrowTextFrame: Frame
local textframe = CreateFrame("Frame", nil, frame)
frame.distance = textframe:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
frame.title = textframe:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
frame.title:SetPoint("TOP", frame, "BOTTOM")
frame.distance:SetPoint("TOP", frame.title, "BOTTOM")
textframe:Hide()

local arrow = frame:CreateTexture(nil, "OVERLAY")
arrow:SetTexture("Interface\\AddOns\\DBM-Core\\textures\\arrows\\Arrow.blp")
arrow:SetAllPoints(frame)

---------------------
--  Map Utilities  --
---------------------
local calculateDistance
do
	function calculateDistance(x1, y1, x2, y2)
		local dX = x1 - x2
		local dY = y1 - y2
		return sqrt(dX * dX + dY * dY)
	end
end

-- GetPlayerFacing seems to return values between -pi and pi instead of 0 - 2pi sometimes since 3.3.3
local GetPlayerFacing = function()
	local result = GetPlayerFacing() or 0
	if result < 0 then
		result = result + pi2
	end
	return result
end

------------------------
--  Update the arrow  --
------------------------
local updateArrow
do
	local currentCell
	local formatText = "%dy"
	function updateArrow(direction, distance)
		local cell = floor(direction / pi2 * 108 + 0.5) % 108
		if cell ~= currentCell then
			currentCell = cell
			local column = cell % 9
			local row = floor(cell / 9)
			local xStart = (column * 56) / 512
			local yStart = (row * 42) / 512
			local xEnd = ((column + 1) * 56) / 512
			local yEnd = ((row + 1) * 42) / 512
			arrow:SetTexCoord(xStart, xEnd, yStart, yEnd)
		end
		if distance then
			if runAwayArrow then
				local perc = distance / hideDistance
				arrow:SetVertexColor(1 - perc, perc, 0)
				if distance >= hideDistance then
					frame:Hide()
				end
			else
				local perc = min(distance, 100) / 100
				arrow:SetVertexColor(1, 1 - perc, 0)
				if distance <= hideDistance then
					frame:Hide()
				else
					frame.distance:SetText(formatText:format(distance))
				end
			end
		else
			if runAwayArrow then
				arrow:SetVertexColor(1, 0.3, 0)
			else
				arrow:SetVertexColor(1, 1, 0)
			end
		end
	end
end

------------------------
--  OnUpdate Handler  --
------------------------
do
	local rotateState = 0

	frame:SetScript("OnUpdate", function(self, elapsed)
		if hideTime and GetTime() > hideTime then
			frame:Hide()
		end
		arrow:Show()
		-- the static arrow type is special because it doesn't depend on the player's orientation or position
		if targetType == "static" then
			return updateArrow(targetX) -- targetX contains the static angle to show
		end

		local x, y, _, mapId = UnitPosition("player")
		--New bug in 8.2.5, unit position returns nil for position in areas there aren't restrictions for first few frames in that new area
		--this just has the arrow skip some onupdates during that
		if not x or not y then
			if IsInInstance() then--Somehow x and y returned on entering an instance, before restrictions kicked in?
				frame:Hide()--Hide, if in an instance, disable arrow entirely
			end
			return--Not in instance, but x and y nil, just skip updates until x and y start returning
		end
		if targetType == "player" then
			targetX, targetY, _, targetMapId = UnitPosition(targetPlayer)
			if not targetX or not targetY or mapId ~= targetMapId then
				self:Hide() -- hide the arrow if the target doesn't exist. TODO: just hide the texture and add a timeout
			end
		elseif targetType == "rotate" then
			rotateState = rotateState + elapsed
			targetX = x + cos(rotateState)
			targetY = y + sin(rotateState)
		end
		if not targetX or not targetY then
			return
		end
		local angle = atan2(targetY - y, x - targetX)
		if angle <= 0 then -- -pi < angle < pi but we need/want a value between 0 and 2 pi
			if runAwayArrow then
				angle = -angle -- 0 < angle < pi
			else
				angle = pi - angle -- pi < angle < 2pi
			end
		else
			if runAwayArrow then
				angle = pi2 - angle -- pi < angle < 2pi
			else
				angle = pi - angle  -- 0 < angle < pi
			end
		end
		updateArrow(angle - GetPlayerFacing(), calculateDistance(x, y, targetX, targetY))
	end)
end


----------------------
--  Public Methods  --
----------------------

--/run DBM.Arrow:ShowRunTo(50, 50, 1, nil, true, true, "Waypoint", custom local mapID)
local function show(runAway, x, y, distance, time, legacy, _, title, customAreaID)
	if DBM:HasMapRestrictions() then return end
	local player
	if type(x) == "string" then
		player, hideDistance, hideTime = x, y, hideDistance
	end
	frame:Show()
	textframe:Show()
	if title then
		frame.title:Show()
		frame.title:SetText(title)
	else
		frame.title:Hide()
	end
	if runAway then
		frame.distance:Hide()
	else
		frame.distance:Show()
	end
	runAwayArrow = runAway
	hideDistance = distance or runAway and 100 or 3
	if time then
		hideTime = time + GetTime()
	else
		hideTime = nil
	end
	if player then
		targetType = "player"
		targetPlayer = player
	else
		targetType = "fixed"
		if legacy and x >= 0 and x <= 100 and y >= 0 and y <= 100 then
			local _, temptable = C_Map.GetWorldPosFromMapPos(tonumber(customAreaID) or C_Map.GetBestMapForUnit("player") or 0, CreateVector2D(x / 100, y / 100))
			x, y = temptable.x, temptable.y
		end
		targetX, targetY = x, y
	end
end

function arrowFrame:ShowRunTo(...)
	return show(false, ...)
end

function arrowFrame:ShowRunAway(...)
	return show(true, ...)
end

-- shows a static arrow
function arrowFrame:ShowStatic(angle, time)
	--Static arrows do not need restrictions, and are still permitted even in 7.1
	runAwayArrow = false
	hideDistance = 0
	targetType = "static"
	targetX = angle * pi2 / 360
	if time then
		hideTime = time + GetTime()
	else
		hideTime = nil
	end
	frame:Show()
	textframe:Hide()--just in case they call static while a non static was already showing
end

function arrowFrame:IsShown()
	return frame and frame:IsShown()
end

function arrowFrame:Hide()
	textframe:Hide()
	frame:Hide()
end

local function endMove()
	frame:EnableMouse(false)
	arrowFrame:Hide()
end

function arrowFrame:Move()
	targetType = "rotate"
	runAwayArrow = false
	hideDistance = 0
	frame:EnableMouse(true)
	frame:Show()
	DBT:CreateBar(25, L.ARROW_MOVABLE, 237538)
	DBM:Unschedule(endMove)
	DBM:Schedule(25, endMove)
end

function arrowFrame:LoadPosition()
	frame:SetPoint(DBM.Options.ArrowPoint, DBM.Options.ArrowPosX, DBM.Options.ArrowPosY)
end

do
	SLASH_DEADLYBOSSMODSDWAY1 = "/dway"--/way not used because DBM would load before TomTom and can't check
	SlashCmdList["DEADLYBOSSMODSDWAY"] = function(msg)
		DBM:UpdateMapRestrictions()
		if DBM:HasMapRestrictions() then
			DBM:AddMsg(L.NO_ARROW)
			return
		end
		msg = msg:sub(1):trim()
		local x, y = strsplit(" ", msg) -- Try splitting by space
		local xNum, yNum = tonumber(x or ""), tonumber(y or "")
		if not xNum or not yNum then
			x, y = strsplit(",", msg) -- And then by comma
			xNum, yNum = tonumber(x or ""), tonumber(y or "")
		end
		if xNum and yNum then
			DBM.Arrow:ShowRunTo(xNum, yNum, 1, nil, true)
			return
		end
		if DBM.Arrow:IsShown() then
			DBM.Arrow:Hide()
		else
			DBM:AddMsg(L.ARROW_WAY_USAGE)
		end
	end
end
