local _, ns = ...
local SlickUI = ns.SlickUI
local Theme = SlickUI.Theme

local FlightTimer = {}
SlickUI.FlightTimer = FlightTimer

-- Routes contributed through the Developer export. Keep these separate from
-- SavedVariables so new installations get estimates without making bundled
-- routes look like newly collected local data.
FlightTimer.BuiltInRoutes = {
	["Amani Foothold, Vaults of Atal'Utek\031Tokka's Landing, The Coiled Isle"] = { seconds = 87.178, samples = 1 },
	["Amani'Zar Village, Zul'Aman\031Shadebasin Watch, Zul'Aman"] = { seconds = 90.172, samples = 1 },
	["Amani'Zar Village, Zul'Aman\031Tokka's Landing, The Coiled Isle"] = { seconds = 205.903, samples = 2 },
	["Camp Stonewash, Zul'Aman\031Amani'Zar Village, Zul'Aman"] = { seconds = 94.723, samples = 1 },
	["Fairbreeze Village, Eversong Woods\031Sanctum of Light, Silvermoon City"] = { seconds = 49.883, samples = 2 },
	["Fuselight, Badlands\031Farstrider Lodge, Loch Modan"] = { seconds = 48.190, samples = 1 },
	["Howling Ridge, Voidstorm\031Masters' Perch, Voidstorm"] = { seconds = 51.444, samples = 1 },
	["Paw'Don Village, Jade Forest\031Pang's Stead, Valley of the Four Winds"] = { seconds = 78.206, samples = 1 },
	["Sanctum of Light, Silvermoon City\031Camp Stonewash, Zul'Aman"] = { seconds = 161.219, samples = 2 },
	["Sanctum of Light, Silvermoon City\031Fairbreeze Village, Eversong Woods"] = { seconds = 50.324, samples = 2 },
	["Sanctum of Light, Silvermoon City\031Tokka's Landing, The Coiled Isle"] = { seconds = 431.287, samples = 4 },
	["Sanctum of Light, Silvermoon City\031Torntusk Overlook, Zul'Aman"] = { seconds = 223.435, samples = 1 },
	["Thrallmar, Hellfire Peninsula\031Hellfire Peninsula, The Dark Portal"] = { seconds = 70.052, samples = 1 },
	["Tokka's Landing, The Coiled Isle\031Amani'Zar Village, Zul'Aman"] = { seconds = 206.122, samples = 1 },
	["Xibala, Zuldazar\031Veiled Grotto, Zuldazar"] = { seconds = 73.807, samples = 1 },
}

local function FormatTime(seconds)
	seconds = math.max(0, math.floor((seconds or 0) + 0.5))
	return string.format("%d:%02d", math.floor(seconds / 60), seconds % 60)
end

local function RouteKey(source, destination)
	return (source or "?") .. "\031" .. (destination or "?")
end

function FlightTimer:GetEstimate(source, destination)
	local key = RouteKey(source, destination)
	local route = SlickUI.DB.flightTimer.routes[key] or self.BuiltInRoutes[key]
	return route and route.seconds
end

function FlightTimer:GetRouteCount()
	local count = 0
	for _, route in pairs(SlickUI.DB.flightTimer.routes or {}) do
		if type(route) == "table" and type(route.seconds) == "number" then count = count + 1 end
	end
	return count
end

function FlightTimer:GetNewRouteCount()
	local count = 0
	for key, route in pairs(SlickUI.DB.flightTimer.routes or {}) do
		if type(key) == "string" and type(route) == "table" and type(route.seconds) == "number" and not self.BuiltInRoutes[key] then
			count = count + 1
		end
	end
	return count
end

function FlightTimer:BuildExportText()
	local entries = {}
	for key, route in pairs(SlickUI.DB.flightTimer.routes or {}) do
		if type(key) == "string" and type(route) == "table" and type(route.seconds) == "number" then
			entries[#entries + 1] = {
				key = key,
				seconds = route.seconds,
				samples = math.max(1, math.floor(tonumber(route.samples) or 1)),
			}
		end
	end
	table.sort(entries, function(left, right) return left.key < right.key end)
	local lines = {
		"-- SlickUI locally learned flight routes",
		"-- Generated " .. date("%Y-%m-%d %H:%M:%S"),
		"return {",
	}
	for _, entry in ipairs(entries) do
		lines[#lines + 1] = string.format("\t[%q] = { seconds = %.3f, samples = %d },", entry.key, entry.seconds, entry.samples)
	end
	lines[#lines + 1] = "}"
	-- Route keys use an internal control-character separator. EditBoxes can stop
	-- rendering when those bytes are present, so serialize every control byte as
	-- a visible Lua decimal escape before handing the text to the export window.
	return (table.concat(lines, "\n"):gsub("[%z\1-\8\11\12\14-\31]", function(character)
		return string.format("\\%03d", string.byte(character))
	end))
end

function FlightTimer:StartFlight(source, destination)
	self.Active = { source = source or "Unknown", destination = destination or "Unknown", started = GetTime() }
	self.Active.estimate = self:GetEstimate(self.Active.source, self.Active.destination)
	self.Frame:SetShown(SlickUI.DB.flightTimer.enabled == true)
	self.Frame:SetScript("OnUpdate", function() FlightTimer:UpdateBar() end)
end

function FlightTimer:FinishFlight()
	if not self.Active then return end
	local elapsed = GetTime() - self.Active.started
	if elapsed > 10 then
		local key = RouteKey(self.Active.source, self.Active.destination)
		local old = SlickUI.DB.flightTimer.routes[key] or self.BuiltInRoutes[key]
		local seconds = old and ((old.seconds * old.samples + elapsed) / (old.samples + 1)) or elapsed
		SlickUI.DB.flightTimer.routes[key] = { seconds = seconds, samples = (old and old.samples or 0) + 1 }
	end
	self.Active = nil; self.Frame:Hide(); self.Frame:SetScript("OnUpdate", nil)
end

function FlightTimer:UpdateBar()
	if not self.Active then return end
	local elapsed = GetTime() - self.Active.started
	local estimate = self.Active.estimate
	self.Frame.Route:SetText(self.Active.source .. "  >  " .. self.Active.destination)
	if estimate and estimate > 0 then
		self.Frame.Bar:SetMinMaxValues(0, estimate); self.Frame.Bar:SetValue(math.min(elapsed, estimate))
		self.Frame.Time:SetText(FormatTime(math.max(0, estimate - elapsed)) .. " remaining")
	else
		self.Frame.Bar:SetMinMaxValues(0, 1); self.Frame.Bar:SetValue(0)
		self.Frame.Time:SetText(FormatTime(elapsed) .. " - learning route")
	end
	if not UnitOnTaxi("player") and elapsed > 2 then self:FinishFlight() end
end

function FlightTimer:CreateFrame()
	local frame = CreateFrame("Frame", "SlickUIFlightTimer", UIParent)
	frame:SetSize(390, 48); frame:SetFrameStrata("MEDIUM"); frame:SetClampedToScreen(true); Theme:CreatePanel(frame)
	local route = Theme:CreateText(frame, 11); route:SetPoint("TOP", 0, -7); route:SetTextColor(unpack(Theme.Colors.goldText)); frame.Route = route
	local bar = CreateFrame("StatusBar", nil, frame); bar:SetPoint("BOTTOMLEFT", 7, 7); bar:SetPoint("BOTTOMRIGHT", -7, 7); bar:SetHeight(14); bar:SetStatusBarTexture(Theme.Texture); bar:SetStatusBarColor(0.15, 0.65, 0.95); frame.Bar = bar
	local time = Theme:CreateText(bar, 9); time:SetPoint("CENTER"); frame.Time = time
	self.Frame = frame; frame:Hide(); SlickUI.Movers:Register(frame, "flightTimer", "Flight Timer")
end

function FlightTimer:AttachMapCheckbox(parent, globalName, storageKey)
	if not parent then return end
	self.MapCheckboxes = self.MapCheckboxes or {}
	local existing = self.MapCheckboxes[storageKey]
	local yOffset = storageKey == "taxiMap" and -64 or -36
	if existing then
		existing:ClearAllPoints()
		existing:SetPoint("TOPLEFT", parent, "TOPLEFT", 92, yOffset)
		existing:SetChecked(SlickUI.DB.flightTimer.showMapTimes)
		return
	end
	local check = CreateFrame("CheckButton", globalName, parent, "UICheckButtonTemplate")
	check:SetPoint("TOPLEFT", parent, "TOPLEFT", 92, yOffset); check:SetSize(26, 26)
	check:SetFrameStrata("DIALOG"); check:SetFrameLevel((parent:GetFrameLevel() or 0) + 100)
	check.Text:SetText("SlickUI flight times"); check.Text:SetTextColor(1, 0.82, 0)
	check:SetChecked(SlickUI.DB.flightTimer.showMapTimes)
	check:SetScript("OnClick", function(self)
		SlickUI.DB.flightTimer.showMapTimes = self:GetChecked() == true
		for _, other in pairs(FlightTimer.MapCheckboxes or {}) do other:SetChecked(self:GetChecked()) end
	end)
	self.MapCheckboxes[storageKey] = check
end

function FlightTimer:AttachMapUI()
	self:AttachMapCheckbox(_G.FlightMapFrame, "SlickUIFlightMapTimesCheckbox", "flightMap")
	self:AttachMapCheckbox(_G.TaxiFrame, "SlickUIClassicTaxiTimesCheckbox", "taxiMap")
	self:HookMapPins()
	self:HookClassicTaxiButtons()
end

function FlightTimer:QueueMapAttach()
	for _, delay in ipairs({ 0, 0.05, 0.2, 0.5, 1 }) do
		C_Timer.After(delay, function() FlightTimer:AttachMapUI() end)
	end
end

function FlightTimer:HookTaxi()
	if self.HookedTaxi or not TakeTaxiNode then return end
	self.HookedTaxi = true
	hooksecurefunc("TakeTaxiNode", function(slot)
		local source, destination
		for i = 1, (NumTaxiNodes and NumTaxiNodes() or 0) do
			local kind = TaxiNodeGetType(i)
			if kind == "CURRENT" then source = TaxiNodeName(i) end
			if i == slot then destination = TaxiNodeName(i) end
		end
		if destination then C_Timer.After(0.5, function() FlightTimer:StartFlight(source, destination) end) end
	end)
end

function FlightTimer:HookMapPins()
	if not self.HookedPins and FlightMap_FlightPointPinMixin then
		self.HookedPins = true
		hooksecurefunc(FlightMap_FlightPointPinMixin, "OnMouseEnter", function(pin)
			FlightTimer:AppendModernFlightTooltip(pin)
		end)
	end

	-- Current flight maps populate concrete pin objects through a data provider.
	-- Hook those live pins too; relying on the mixin alone misses some continents.
	local map = _G.FlightMapFrame
	if not map or not map.dataProviders then return end
	for provider in pairs(map.dataProviders) do
		if provider.AddFlightNode and provider.slotIndexToPin then
			for _, pin in pairs(provider.slotIndexToPin) do
				if pin and not pin.SlickUIFlightTimeHooked then
					pin.SlickUIFlightTimeHooked = true
					pin:HookScript("OnEnter", function(hoveredPin)
						C_Timer.After(0.03, function()
							if hoveredPin.IsMouseOver and hoveredPin:IsMouseOver() then
								FlightTimer:AppendModernFlightTooltip(hoveredPin, true)
							end
						end)
					end)
				end
			end
		end
	end
end

function FlightTimer:AppendModernFlightTooltip(pin, force)
	if not SlickUI.DB.flightTimer.showMapTimes or not pin or not GameTooltip:IsShown() then return end
	local destination = pin.taxiNodeData and pin.taxiNodeData.name
	if type(destination) ~= "string" or destination == "" then return end
	local now = GetTime()
	if not force and self.LastModernTooltipDestination == destination and now - (self.LastModernTooltipAt or 0) < 0.05 then return end
	self.LastModernTooltipDestination, self.LastModernTooltipAt = destination, now
	local source
	for i = 1, (NumTaxiNodes and NumTaxiNodes() or 0) do
		if TaxiNodeGetType(i) == "CURRENT" then source = TaxiNodeName(i); break end
	end
	local estimate = self:GetEstimate(source, destination)
	GameTooltip:AddLine(estimate and ("SlickUI flight time: " .. FormatTime(estimate)) or "SlickUI flight time: not learned", 0.35, 0.8, 1)
	GameTooltip:Show()
end

function FlightTimer:AppendClassicTaxiTooltip(button, force)
	if not SlickUI.DB.flightTimer.showMapTimes or not button or not GameTooltip:IsShown() then return end
	local slot = button.GetID and button:GetID()
	if type(slot) ~= "number" then return end
	local destination = TaxiNodeName and TaxiNodeName(slot)
	if type(destination) ~= "string" or destination == "" then return end
	local now = GetTime()
	if not force and self.LastClassicTooltipSlot == slot and now - (self.LastClassicTooltipAt or 0) < 0.05 then return end
	self.LastClassicTooltipSlot, self.LastClassicTooltipAt = slot, now
	local source
	for index = 1, (NumTaxiNodes and NumTaxiNodes() or 0) do
		if TaxiNodeGetType(index) == "CURRENT" then source = TaxiNodeName(index); break end
	end
	local estimate = self:GetEstimate(source, destination)
	if SlickUI.DiagnosticLog then
		SlickUI:DiagnosticLog("FlightTimer", "legacy-node-hover", string.format("source=%s destination=%s estimate=%s", tostring(source), tostring(destination), tostring(estimate)))
	end
	GameTooltip:AddLine(estimate and ("SlickUI flight time: " .. FormatTime(estimate)) or "SlickUI flight time: not learned", 0.35, 0.8, 1)
	GameTooltip:Show()
end

local function FindLegacyTaxiButtonUnderMouse()
	-- Legacy taxi maps use numbered TaxiButton frames. Their child artwork can
	-- receive the mouse focus instead of the button, so test the buttons first.
	for slot = 1, (NumTaxiNodes and NumTaxiNodes() or 0) do
		local button = _G["TaxiButton" .. slot]
		if button and button.IsShown and button:IsShown() and button.IsMouseOver and button:IsMouseOver() then
			return button
		end
	end

	local focuses = {}
	if type(GetMouseFoci) == "function" then
		focuses = { GetMouseFoci() }
		if #focuses == 1 and type(focuses[1]) == "table" then focuses = focuses[1] end
	elseif type(GetMouseFocus) == "function" then
		focuses[1] = GetMouseFocus()
	end
	for _, initialFocus in ipairs(focuses) do
		local focus = initialFocus
		for _ = 1, 12 do
			if not focus then break end
			local name = focus.GetName and focus:GetName()
			if type(name) == "string" and name:match("^TaxiButton%d+$") then return focus end
			focus = focus.GetParent and focus:GetParent()
		end
	end
end

function FlightTimer:CreateLegacyTaxiHoverWatcher()
	if self.LegacyTaxiHoverWatcher then return end
	local watcher = CreateFrame("Frame")
	watcher:Hide()
	watcher.elapsed = 0
	watcher:SetScript("OnUpdate", function(frame, elapsed)
		frame.elapsed = frame.elapsed + elapsed
		if frame.elapsed < 0.05 then return end
		frame.elapsed = 0
		if not (_G.TaxiFrame and _G.TaxiFrame:IsShown()) then
			frame.lastButton = nil
			frame:Hide()
			return
		end
		local button = FindLegacyTaxiButtonUnderMouse()
		if button == frame.lastButton then return end
		frame.lastButton = button
		if button then
			C_Timer.After(0.05, function()
				if FindLegacyTaxiButtonUnderMouse() == button then FlightTimer:AppendClassicTaxiTooltip(button, true) end
			end)
		end
	end)
	self.LegacyTaxiHoverWatcher = watcher
end

function FlightTimer:StartLegacyTaxiHoverWatcher()
	self:CreateLegacyTaxiHoverWatcher()
	self.LegacyTaxiHoverWatcher.lastButton = nil
	self.LegacyTaxiHoverWatcher.elapsed = 0
	self.LegacyTaxiHoverWatcher:Show()
end

function FlightTimer:HookClassicTaxiButtonFrames()
	local count = NumTaxiNodes and NumTaxiNodes() or 0
	for slot = 1, count do
		local button = _G["TaxiButton" .. slot]
		if button and not button.SlickUIFlightTimeHooked then
			button.SlickUIFlightTimeHooked = true
			button:HookScript("OnEnter", function(hoveredButton)
				FlightTimer:AppendClassicTaxiTooltip(hoveredButton)
			end)
		end
	end
end

function FlightTimer:HookClassicTaxiButtons()
	if not self.HookedClassicTaxi and type(_G.TaxiNodeOnButtonEnter) == "function" then
		self.HookedClassicTaxi = true
		hooksecurefunc("TaxiNodeOnButtonEnter", function(button)
			FlightTimer:AppendClassicTaxiTooltip(button)
		end)
	end
	-- Older continents still use TaxiButton1..N. Hooking those buttons after the
	-- taxi map has populated makes the tooltip attachment independent of addon
	-- load order and of changes to the legacy global hover handler.
	self:HookClassicTaxiButtonFrames()
end

function FlightTimer:SetEnabled(enabled)
	SlickUI.DB.flightTimer.enabled = enabled == true
	if not enabled then self.Frame:Hide() elseif self.Active then self.Frame:Show() end
end

function FlightTimer:Initialize()
	self:CreateFrame(); self:CreateLegacyTaxiHoverWatcher(); self:HookTaxi(); self:QueueMapAttach()
	local events = CreateFrame("Frame")
	for _, event in ipairs({ "PLAYER_ENTERING_WORLD", "TAXIMAP_OPENED", "PLAYER_CONTROL_GAINED", "ADDON_LOADED" }) do events:RegisterEvent(event) end
	events:SetScript("OnEvent", function(_, event, addonName)
		if event == "TAXIMAP_OPENED" then FlightTimer:QueueMapAttach(); FlightTimer:StartLegacyTaxiHoverWatcher()
		elseif event == "ADDON_LOADED" and (addonName == "Blizzard_TaxiUI" or addonName == "Blizzard_FlightMap") then FlightTimer:QueueMapAttach()
		elseif event == "PLAYER_CONTROL_GAINED" and FlightTimer.Active and not UnitOnTaxi("player") then FlightTimer:FinishFlight() end
	end)
	self.EventFrame = events
end

SlickUI:RegisterModule("FlightTimer", FlightTimer)
