Quantcast
--[[--------------------------------------------------------------------------
--  TomTom - A navigational assistant for World of Warcraft
--
--  CrazyTaxi: A crazy-taxi style arrow used for waypoint navigation.
--    concept taken from MapNotes2 (Thanks to Mery for the idea, along
--    with the artwork.)
----------------------------------------------------------------------------]]

local sformat = string.format
local L = TomTomLocals
local ldb = LibStub("LibDataBroker-1.1")

local function ColorGradient(perc, ...)
	local num = select("#", ...)
	local hexes = type(select(1, ...)) == "string"

	if perc == 1 then
		return select(num-2, ...), select(num-1, ...), select(num, ...)
	end

	num = num / 3

	local segment, relperc = math.modf(perc*(num-1))
	local r1, g1, b1, r2, g2, b2
	r1, g1, b1 = select((segment*3)+1, ...), select((segment*3)+2, ...), select((segment*3)+3, ...)
	r2, g2, b2 = select((segment*3)+4, ...), select((segment*3)+5, ...), select((segment*3)+6, ...)

	if not r2 or not g2 or not b2 then
		return r1, g1, b1
	else
		return r1 + (r2-r1)*relperc,
		g1 + (g2-g1)*relperc,
		b1 + (b2-b1)*relperc
	end
end

local twopi = math.pi * 2

local wayframe = CreateFrame("Button", "TomTomCrazyArrow", UIParent)
wayframe:SetHeight(42)
wayframe:SetWidth(56)
wayframe:EnableMouse(true)
wayframe:SetMovable(true)
wayframe:SetClampedToScreen(true)
wayframe:Hide()

-- Frame used to control the scaling of the title and friends
local titleframe = CreateFrame("Frame", nil, wayframe)

wayframe.title = titleframe:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
wayframe.status = titleframe:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
wayframe.tta = titleframe:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
wayframe.title:SetPoint("TOP", wayframe, "BOTTOM", 0, 0)
wayframe.status:SetPoint("TOP", wayframe.title, "BOTTOM", 0, 0)
wayframe.tta:SetPoint("TOP", wayframe.status, "BOTTOM", 0, 0)

local function OnDragStart(self, button)
	if not TomTom.db.profile.arrow.lock then
		self:StartMoving()
	end
end

local function OnDragStop(self, button)
	self:StopMovingOrSizing()
	self:SetUserPlaced(false)
	-- point, relativeTo, relativePoint, xOfs, yOfs
	TomTom.profile.arrow.position = { self:GetPoint() }
	TomTom.profile.arrow.position[2] = nil  -- Note we are relative to UIParent
end

local function OnEvent(self, event, ...)
	if (event == "ZONE_CHANGED_NEW_AREA" or event == "ZONE_CHANGED") and TomTom.profile.arrow.enable then
		self:Show()
		return
	end
	if (event == "PLAYER_ENTERING_WORLD") then
        wayframe:ClearAllPoints()
        if TomTom.profile.arrow.position then
            local pos = TomTom.profile.arrow.position
            wayframe:SetPoint(pos[1], UIParent, pos[3], pos[4], pos[5])
        else
            wayframe:SetPoint("CENTER", 0, 0)
        end
    end
end

wayframe:SetScript("OnDragStart", OnDragStart)
wayframe:SetScript("OnDragStop", OnDragStop)
wayframe:RegisterForDrag("LeftButton")
wayframe:RegisterEvent("ZONE_CHANGED_NEW_AREA")
wayframe:RegisterEvent("ZONE_CHANGED")
wayframe:RegisterEvent("PLAYER_ENTERING_WORLD")
wayframe:SetScript("OnEvent", OnEvent)

wayframe.arrow = wayframe:CreateTexture(nil, "OVERLAY")
wayframe.arrow:SetTexture("Interface\\Addons\\TomTom\\Images\\Arrow")
wayframe.arrow:SetAllPoints()

local active_point, arrive_distance, showDownArrow, point_title

function TomTom:SetCrazyArrow(uid, dist, title)
	if active_point and active_point.corpse and self.db.profile.arrow.stickycorpse then
		-- do not change the waypoint arrow from corpse
		return
	end

	active_point = uid
	arrive_distance = dist
	point_title = title

	if self.profile.arrow.enable then
		wayframe.title:SetText(title or L["Unknown waypoint"])
		wayframe:Show()
		if wayframe.crazyFeedFrame then
			wayframe.crazyFeedFrame:Show()
		end
	end
end

function TomTom:IsCrazyArrowEmpty()
    return not active_point
end

local status = wayframe.status
local tta = wayframe.tta
local arrow = wayframe.arrow
local count = 0
local last_distance = 0
local tta_throttle = 0
local speed = 0
local speed_count = 0

local function OnUpdate(self, elapsed)
	if not active_point then
		self:Hide()
		return
	end

	local dist,x,y = TomTom:GetDistanceToWaypoint(active_point)

	-- The only time we cannot calculate the distance is when the waypoint
	-- is on another continent, or we are in an instance
	if not dist then
		if not TomTom:IsValidWaypoint(active_point) then
			active_point = nil
			-- Change the crazy arrow to point at the closest waypoint
			if TomTom.profile.arrow.setclosest then
				TomTom:SetClosestWaypoint()
				return
			end
		end

		self:Hide()
		return
	end

	status:SetText(sformat(L["%d yards"], dist))

	local cell

	-- Showing the arrival arrow?
	if dist <= arrive_distance then
		if not showDownArrow then
			arrow:SetHeight(70)
			arrow:SetWidth(53)
			arrow:SetTexture("Interface\\AddOns\\TomTom\\Images\\Arrow-UP")
			arrow:SetVertexColor(unpack(TomTom.db.profile.arrow.goodcolor))
			showDownArrow = true
		end

		count = count + 1
		if count >= 55 then
			count = 0
		end

		local cell = count
		local column = cell % 9
		local row = floor(cell / 9)

		local xstart = (column * 53) / 512
		local ystart = (row * 70) / 512
		local xend = ((column + 1) * 53) / 512
		local yend = ((row + 1) * 70) / 512
		arrow:SetTexCoord(xstart,xend,ystart,yend)
	else
		if showDownArrow then
			arrow:SetHeight(56)
			arrow:SetWidth(42)
			arrow:SetTexture("Interface\\AddOns\\TomTom\\Images\\Arrow")
			showDownArrow = false
		end

		local angle = TomTom:GetDirectionToWaypoint(active_point)
		local player = GetPlayerFacing()

		angle = angle - player

		local perc = math.abs((math.pi - math.abs(angle)) / math.pi)

		local gr,gg,gb = unpack(TomTom.db.profile.arrow.goodcolor)
		local mr,mg,mb = unpack(TomTom.db.profile.arrow.middlecolor)
		local br,bg,bb = unpack(TomTom.db.profile.arrow.badcolor)
		local r,g,b = ColorGradient(perc, br, bg, bb, mr, mg, mb, gr, gg, gb)

		-- If we're 98% heading in the right direction, then use the exact
		-- color instead of the gradient. This allows us to distinguish 'good'
		-- from 'on target'. Thanks to Gregor_Curse for the suggestion.
		if perc > 0.98 then
			r,g,b = unpack(TomTom.db.profile.arrow.exactcolor)
		end
		arrow:SetVertexColor(r,g,b)

		local cell = floor(angle / twopi * 108 + 0.5) % 108
		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

	-- Calculate the TTA every second  (%01d:%02d)

	tta_throttle = tta_throttle + elapsed

	if tta_throttle >= 1.0 then
		-- Calculate the speed in yards per sec at which we're moving
		local current_speed = (last_distance - dist) / tta_throttle

		if last_distance == 0 then
			current_speed = 0
		end

		if speed_count < 2 then
			speed = (speed + current_speed) / 2
			speed_count = speed_count + 1
		else
			speed_count = 0
			speed = current_speed
		end

		if speed > 0 then
			local eta = math.abs(dist / speed)
			tta:SetFormattedText("%s:%02d", math.floor(eta / 60), math.floor(eta % 60))
		else
			tta:SetText("***")
		end

		last_distance = dist
		tta_throttle = 0
	end
end

function TomTom:ShowHideCrazyArrow()
	if self.profile.arrow.enable then
		if self.profile.arrow.hideDuringPetBattles and C_PetBattles and C_PetBattles.IsInBattle() then
			wayframe:Hide()
			return
		end

		wayframe:Show()

		if self.profile.arrow.noclick then
			wayframe:EnableMouse(false)
		else
			wayframe:EnableMouse(true)
		end

		-- Set the scale and alpha
		wayframe:SetScale(TomTom.db.profile.arrow.scale)
		-- Do not allow the arrow to be invisible
		if TomTom.db.profile.arrow.alpha < 0.1 then
		    TomTom.db.profile.arrow.alpha = 1.0
		end
		-- Set the stratum
		if TomTom.db.profile.arrow.highstrata then
		    wayframe:SetFrameStrata("HIGH")
		else
		    wayframe:SetFrameStrata("MEDIUM")
		end
		wayframe:SetAlpha(TomTom.db.profile.arrow.alpha)
		local width = TomTom.db.profile.arrow.title_width
		local height = TomTom.db.profile.arrow.title_height
		local scale = TomTom.db.profile.arrow.title_scale

		wayframe.title:SetWidth(width)
		wayframe.title:SetHeight(height)
		titleframe:SetScale(scale)
		titleframe:SetAlpha(TomTom.db.profile.arrow.title_alpha)

		if self.profile.arrow.showdistance then
			wayframe.status:Show()
			wayframe.tta:ClearAllPoints()
			wayframe.tta:SetPoint("TOP", wayframe.status, "BOTTOM", 0, 0)
		else
			wayframe.status:Hide()
			wayframe.tta:ClearAllPoints()
			wayframe.tta:SetPoint("TOP", wayframe, "BOTTOM", 0, 0)
		end

		if self.profile.arrow.showtta then
			tta:Show()
		else
			tta:Hide()
		end
	else
		wayframe:Hide()
	end
end

wayframe:SetScript("OnUpdate", OnUpdate)


--[[-------------------------------------------------------------------------
--  Dropdown
-------------------------------------------------------------------------]]--

local dropdown_info = {
	-- Define level one elements here
	[1] = {
		{
			-- Title
			text = L["TomTom Waypoint Arrow"],
			isTitle = 1,
		},
		{
			-- Clear waypoint from crazy arrow
			text = L["Clear waypoint from crazy arrow"],
			func = function()
				local prior = active_point

				active_point = nil
				if TomTom.profile.arrow.setclosest then
					local uid = TomTom:GetClosestWaypoint()
					if uid and uid ~= prior then
						TomTom:SetClosestWaypoint()
						return
					end
				end
			end,
		},
		{
			-- Remove a waypoint
			text = L["Remove waypoint"],
			func = function()
				local uid = active_point
				TomTom:RemoveWaypoint(uid)
			end,
		},
        {
            -- Remove all waypoints from this zone
            text = L["Remove all waypoints from this zone"],
            func = function()
                local uid = active_point
                local data = uid
                local mapId = data[1]
                for key, waypoint in pairs(TomTom.waypoints[mapId]) do
                    TomTom:RemoveWaypoint(waypoint)
                end

            end,
        },
		{
			-- Remove all waypoints
			text = L["Remove all waypoints"],
			func = function()
				if TomTom.db.profile.general.confirmremoveall then
					StaticPopup_Show("TOMTOM_REMOVE_ALL_CONFIRM")
				else
					StaticPopupDialogs["TOMTOM_REMOVE_ALL_CONFIRM"].OnAccept()
					return
				end
			end,
		},
	}
}

local function init_dropdown(self, level)
	-- Make sure level is set to 1, if not supplied
	level = level or 1

	-- Get the current level from the info table
	local info = dropdown_info[level]

	-- If a value has been set, try to find it at the current level
	if level > 1 and UIDROPDOWNMENU_MENU_VALUE then
		if info[UIDROPDOWNMENU_MENU_VALUE] then
			info = info[UIDROPDOWNMENU_MENU_VALUE]
		end
	end

	-- Add the buttons to the menu
	for idx,entry in ipairs(info) do
		if type(entry.checked) == "function" then
			-- Make this button dynamic
			local new = {}
			for k,v in pairs(entry) do new[k] = v end
			new.checked = new.checked()
			entry = new
		end
		UIDropDownMenu_AddButton(entry, level)
	end
end

local function WayFrame_OnClick(self, button)
	if active_point then
		if TomTom.db.profile.arrow.menu then
			UIDropDownMenu_Initialize(TomTom.dropdown, init_dropdown)
			ToggleDropDownMenu(1, nil, TomTom.dropdown, "cursor", 0, 0)
		end
	end
end

wayframe:RegisterForClicks("RightButtonUp")
wayframe:SetScript("OnClick", WayFrame_OnClick)

local function getCoords(column, row)
	local xstart = (column * 56) / 512
	local ystart = (row * 42) / 512
	local xend = ((column + 1) * 56) / 512
	local yend = ((row + 1) * 42) / 512
	return xstart, xend, ystart, yend
end

local texcoords = setmetatable({}, {__index = function(t, k)
	local col,row = k:match("(%d+):(%d+)")
	col,row = tonumber(col), tonumber(row)
	local obj = {getCoords(col, row)}
	rawset(t, k, obj)
	return obj
end})

wayframe:RegisterEvent("ADDON_LOADED")
local function wayframe_OnEvent(self, event, arg1, ...)
	if arg1 == "TomTom" then
		if TomTom.db.profile.feeds.arrow then
			-- Create a data feed for coordinates
			local feed_crazy = ldb:NewDataObject("TomTom_CrazyArrow", {
				type = "data source",
				icon = "Interface\\Addons\\TomTom\\Images\\Arrow",
				staticIcon = "Interface\\Addons\\TomTom\\Images\\StaticArrow",
				text = "Crazy",
				iconR = 0.2,
				iconG = 1.0,
				iconB = 0.2,
				iconCoords = texcoords["1:1"],
				OnTooltipShow = function(tooltip)
					local dist = TomTom:GetDistanceToWaypoint(active_point)
					if dist then
						tooltip:AddLine(point_title or L["Unknown waypoint"])
						tooltip:AddLine(sformat(L["%d yards"], dist), 1, 1, 1)
					end
				end,
				OnClick = WayFrame_OnClick,
			})

			local crazyFeedFrame = CreateFrame("Frame")
			local throttle = TomTom.db.profile.feeds.arrow_throttle
			local counter = 0

			function TomTom:UpdateArrowFeedThrottle()
				throttle = TomTom.db.profile.feeds.arrow_throttle
			end

			wayframe.crazyFeedFrame = crazyFeedFrame
			crazyFeedFrame:SetScript("OnUpdate", function(self, elapsed)
				counter = counter + elapsed
				if counter < throttle then
					return
				end

				counter = 0
				if not active_point then
					self:Hide()
				end
				local angle = TomTom:GetDirectionToWaypoint(active_point)
				local player = GetPlayerFacing()
				if not angle or not player then
					feed_crazy.iconCoords = texcoords["1:1"]
					feed_crazy.iconR = 0.2
					feed_crazy.iconG = 1.0
					feed_crazy.iconB = 0.2
					feed_crazy.text = "No waypoint"
					return
				end

				angle = angle - player

				local perc = math.abs((math.pi - math.abs(angle)) / math.pi)

				local gr,gg,gb = unpack(TomTom.db.profile.arrow.goodcolor)
				local mr,mg,mb = unpack(TomTom.db.profile.arrow.middlecolor)
				local br,bg,bb = unpack(TomTom.db.profile.arrow.badcolor)
				local r,g,b = ColorGradient(perc, br, bg, bb, mr, mg, mb, gr, gg, gb)

				-- If we're 98% heading in the right direction, then use the exact
				-- color instead of the gradient. This allows us to distinguish 'good'
				-- from 'on target'. Thanks to Gregor_Curse for the suggestion.
				if perc > 0.98 then
					r,g,b = unpack(TomTom.db.profile.arrow.exactcolor)
				end

				feed_crazy.iconR = r
				feed_crazy.iconG = g
				feed_crazy.iconB = b

				local cell = floor(angle / twopi * 108 + 0.5) % 108
				local column = cell % 9
				local row = floor(cell / 9)

				local key = column .. ":" .. row
				feed_crazy.iconCoords = texcoords[key]
				feed_crazy.text = point_title or L["Unknown waypoint"]
			end)
		end
	end
end

wayframe:HookScript("OnEvent", wayframe_OnEvent)

--[[-------------------------------------------------------------------------
--  API for manual control of Crazy Arrow
--
--  This allow for an addon to specify their own control of the waypoint
--  arrow without needing to tip-toe around the API.  It may not integrate
--  properly, but in general it should work.
--
--  Example Usage:
--
--  if not TomTom:CrazyArrowIsHijacked() then
--    TomTom:HijackCrazyArrow(function(self, elapsed)
--      -- Random angle
--      local angle = math.random(math.pi * 2)
--      TomTom:SetCrazyArrowDirection(angle)
--      -- Random color
--      local r = math.random(100) / 100
--      local g = math.random(100) / 100
--      local b = math.random(100) / 100
--      TomTom:SetCrazyArrowColor(r, g, b)
--      -- Titles
--      TomTom:SetCrazyArrowTitle("Hijacked arrow", "Hijacked", "You will never arrive")
--    end)
--  end
--
--  -- At some point later
--  TomTom:ReleaseCrazyArrow()
-------------------------------------------------------------------------]]--

-- Set the direction of the crazy arrow without taking the player's facing
-- into consideration.  This can be accomplished by subtracting
-- GetPlayerFacing() from the angle before passing it in.
function TomTom:SetCrazyArrowDirection(angle)
    local cell = floor(angle / twopi * 108 + 0.5) % 108
    local column = cell % 9
    local row = floor(cell / 9)

    local key = column .. ":" .. row
    arrow:SetTexCoord(unpack(texcoords[key]))
end

-- Convenience function to set the color of the crazy arrow
function TomTom:SetCrazyArrowColor(r, g, b, a)
    arrow:SetVertexColor(r, g, b, a)
end

-- Convenience function to set the title/status and time to arrival text
-- of the crazy arrow.
function TomTom:SetCrazyArrowTitle(title, status, tta)
    wayframe.title:SetText(title)
    wayframe.status:SetText(status)
    wayframe.tta:SetText(tta)
end

-- Function to actually hijack the crazy arrow by replacing the OnUpdate script
function TomTom:HijackCrazyArrow(onupdate)
    wayframe:SetScript("OnUpdate", onupdate)
    wayframe.hijacked = true
    wayframe:Show()
end

-- Releases the crazy arrow by restoring the original OnUpdate script
function TomTom:ReleaseCrazyArrow()
    wayframe:SetScript("OnUpdate", OnUpdate)
    wayframe.hijacked = false
end

-- Returns whether or not the crazy arrow is currently hijacked
function TomTom:CrazyArrowIsHijacked()
    return wayframe.hijacked
end

-- Logs Crazy Arrow status
function TomTom:DebugCrazyArrow()
    local msg
    msg = string.format(L["|cffffff78TomTom:|r CrazyArrow %s hijacked"], (wayframe.hijacked and L["is"]) or L["not"])
    ChatFrame1:AddMessage(msg)
    msg = string.format(L["|cffffff78TomTom:|r CrazyArrow %s visible"], (wayframe:IsVisible() and L["is"]) or L["not"])
    ChatFrame1:AddMessage(msg)
    msg = string.format(L["|cffffff78TomTom:|r Waypoint %s valid"], (active_point and TomTom:IsValidWaypoint(active_point) and L["is"]) or L["not"])
    ChatFrame1:AddMessage(msg)

    local dist,x,y = TomTom:GetDistanceToWaypoint(active_point)
    msg = string.format("|cffffff78TomTom:|r Waypoint distance=%s", tostring(dist))
    ChatFrame1:AddMessage(msg)

    if wayframe:IsVisible() then
        local point, relativeTo, relativePoint, xOfs, yOfs = wayframe:GetPoint(1)
        relativeTo = (relativeTo and relativeTo:GetName()) or "UIParent"
        msg = string.format("|cffffff78TomTom:|r CrazyArrow point=%s frame=%s rpoint=%s xo=%.2f yo=%.2f",  point, relativeTo, relativePoint, xOfs, yOfs)
        ChatFrame1:AddMessage(msg)
    end
end