--[[--------------------------------------------------------------------------
--  TomTom by Cladhaire <cladhaire@gmail.com>
--
--  All Rights Reserved
----------------------------------------------------------------------------]]

-- Simple localization table for messages
local L = TomTomLocals
local ldb = LibStub("LibDataBroker-1.1")
local hbd = LibStub("HereBeDragons-2.0")

local addonName, addon = ...
local TomTom = addon

addon.hbd = hbd
addon.WOW_MAINLINE = (WOW_PROJECT_ID == WOW_PROJECT_MAINLINE)
addon.WOW_CLASSIC = (WOW_PROJECT_ID == WOW_PROJECT_CLASSIC)
addon.WOW_BURNING_CRUSADE_CLASSIC = (WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC)

local buildVersion = select(4, GetBuildInfo())
addon.WAR_WITHIN = (buildVersion >= 110000)

-- Local definitions
local GetCurrentCursorPosition
local WorldMap_OnUpdate
local Block_OnClick,Block_OnUpdate,Block_OnEnter,Block_OnLeave,Block_OnEvent
local Block_OnDragStart,Block_OnDragStop
local callbackTbl
local RoundCoords

local waypoints = {}

function TomTom:Initialize(event, addon)
    self.defaults = {
        profile = {
            general = {
                confirmremoveall = true,
                announce = false,
                corpse_arrow = true,
            },
            block = {
                enable = true,
                accuracy = 2,
                bordercolor = {1, 0.8, 0, 0.8},
                bgcolor = {0, 0, 0, 0.4},
                lock = false,
                height = 30,
                width = 100,
                fontsize = 12,
                throttle = 0.2,
            },
            mapcoords = {
                playerenable = true,
                playeraccuracy = 2,
                playeroffset = 0,
                cursorenable = true,
                cursoraccuracy = 2,
                cursoroffset = 0,
				throttle = 0.1,
            },
            arrow = {
                enable = true,
                goodcolor = {0, 1, 0},
                badcolor = {1, 0, 0},
                middlecolor = {1, 1, 0},
				exactcolor = {0, 1, 0},
                arrival = 15,
                lock = false,
                noclick = false,
                showtta = true,
				showdistance = true,
				stickycorpse = false,
				highstrata = false,
                autoqueue = true,
                menu = true,
                scale = 1.0,
                alpha = 1.0,
                title_width = 0,
                title_height = 0,
                title_scale = 1,
                title_alpha = 1,
                setclosest = true,
				closestusecontinent = false,
                enablePing = false,
                pingChannel = "SFX",
                hideDuringPetBattles = true,
                distanceUnits = "auto",
                theme = "modern",
            },
            minimap = {
                enable = true,
                otherzone = true,
                tooltip = true,
                menu = true,
                default_iconsize = 16,
                default_icon = "Interface\\AddOns\\TomTom\\Images\\GoldGreenDot",
                theme = "modern-green",
            },
            worldmap = {
                enable = true,
                tooltip = true,
                otherzone = true,
                clickcreate = true,
                menu = true,
                create_modifier = "A",
                default_iconsize = 16,
                default_icon = "Interface\\AddOns\\TomTom\\Images\\GoldGreenDot",
                theme = "modern-green",
            },
            comm = {
                enable = true,
                prompt = false,
            },
            persistence = {
                cleardistance = 10,
                savewaypoints = true,
            },
            feeds = {
                coords = false,
                coords_throttle = 0.3,
                coords_accuracy = 2,
                arrow = false,
                arrow_throttle = 0.1,
            },
            poi = {
                enable = true,
                modifier = "A",
                setClosest = false,
                arrival = 0,
            },
        },
    }

    self.waydefaults = {
        global = {
            converted = {
                ["*"] = {},
            },
        },
        profile = {
            ["*"] = {},
        },
    }

    self.db = LibStub("AceDB-3.0"):New("TomTomDB", self.defaults, "Default")
    self.waydb = LibStub("AceDB-3.0"):New("TomTomWaypointsM", self.waydefaults)

    self.db.RegisterCallback(self, "OnProfileChanged", "ReloadOptions")
    self.db.RegisterCallback(self, "OnProfileCopied", "ReloadOptions")
    self.db.RegisterCallback(self, "OnProfileReset", "ReloadOptions")
    self.waydb.RegisterCallback(self, "OnProfileChanged", "ReloadWaypoints")
    self.waydb.RegisterCallback(self, "OnProfileCopied", "ReloadWaypoints")
    self.waydb.RegisterCallback(self, "OnProfileReset", "ReloadWaypoints")

    self.tooltip = CreateFrame("GameTooltip", "TomTomTooltip", nil, "GameTooltipTemplate")
    self.tooltip:SetFrameStrata("DIALOG")

    -- Both the waypoints and waypointprofile tables are going to contain subtables for each
    -- of the mapids that might exist. Under these will be a hash of key/waypoint pairs consisting
    -- of the waypoints for the given map file.
    self.waypoints = waypoints
    self.waypointprofile = self.waydb.profile

    self:RegisterEvent("PLAYER_LEAVING_WORLD")
    self:RegisterEvent("CHAT_MSG_ADDON")
    -- Since we are now using just (map, x, y), register a new protocol number
    C_ChatInfo.RegisterAddonMessagePrefix("TOMTOM4")

    -- Watch for pet battle start/end so we can hide/show the arrow
    if self.WOW_MAINLINE then
        self:RegisterEvent("PET_BATTLE_OPENING_START", "ShowHideCrazyArrow")
        self:RegisterEvent("PET_BATTLE_CLOSE", "ShowHideCrazyArrow")
    end

    self:ReloadOptions()

    if self.db.profile.feeds.coords then
        -- Create a data feed for coordinates
        local feed_coords = ldb:NewDataObject("TomTom_Coords", {
            type = "data source",
            icon = "Interface\\Icons\\INV_Misc_Map_01",
            text = "",
        })

        local coordFeedFrame = CreateFrame("Frame")
        local throttle, counter = self.db.profile.feeds.coords_throttle, 0
        function TomTom:_privateupdatecoordthrottle(x)
            throttle = x
        end

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

            counter = 0
            local m, x, y = TomTom:GetCurrentPlayerPosition()

            if x and y then
                local opt = TomTom.db.profile.feeds
                feed_coords.text = string.format("%s", RoundCoords(x, y, opt.coords_accuracy))
            end
        end)
    end
end

function TomTom:Enable(addon)
    self:ReloadWaypoints()
    self:CreateConfigPanels()

    self:CreateWorldMapClickHandler()

    if self.WOW_MAINLINE then
        self:EnableDisablePOIIntegration()
    end
end

-- Some utility functions that can pack/unpack data from a waypoint

-- Returns a hashable 'key' for a given waypoint consisting of the
-- map, x, y and the waypoints title. This isn't truly
-- unique, but should be close enough to determine duplicates, etc.
function TomTom:GetKey(waypoint)
    local m,x,y = unpack(waypoint)
    return self:GetKeyArgs(m, x, y, waypoint.title)
end

function TomTom:GetKeyArgs(m, x, y, title)
	-- Fudge the x/y values so they avoid precision/printf issues
	local x = x * 10000
	local y = y * 10000

    local key = string.format("%d:%s:%s:%s", m, x*10e4, y*10e4, tostring(title))
	return key
end

-- Thanks to the user who suggested using the C_Map API to fix some of the
-- previous weirdness that was here
function TomTom:GetCurrentPlayerPosition()
    local playerMapID = C_Map and C_Map.GetBestMapForUnit("player") or nil
    local x, y, mapID = hbd:GetPlayerZonePosition()

    -- If HBD and the WoW API agree on the player map, return the position!
    if x and y and x > 0 and y > 0 and mapID and (playerMapID and mapID == playerMapID) then
        return mapID, x, y
    end

    -- If the WoW API and the HBD map results are different, we fall back to the
    -- WoW API if we have it
    if C_Map and playerMapID then
        local pos = C_Map.GetPlayerMapPosition(playerMapID, "player")
        if pos then
            return playerMapID, pos:GetXY()
        end
    end
end

-- Returns the player's current coordinates without flipping the map or
-- causing any other weirdness. This can and will cause the coordinates to be
-- weird if you zoom the map out to your parent, but there is no way to
-- recover this without changing/setting the map zoom. Deal with it =)
function TomTom:GetCurrentCoords()
    local map, x, y = self:GetCurrentPlayerPosition()
    return x, y
end

function TomTom:ReloadOptions()
    -- This handles the reloading of all options
    self.profile = self.db.profile

    self:ShowHideWorldCoords()
    self:ShowHideCoordBlock()
    self:ShowHideCrazyArrow()
end

function TomTom:ClearAllWaypoints()
    for mapId, entries in pairs(waypoints) do
        for key, waypoint in pairs(entries) do
            -- The waypoint IS the UID now
            self:ClearWaypoint(waypoint)
        end
    end
end

function TomTom:ResetWaypointOptions()
	local minimap = self.profile.minimap.enable
	local world = self.profile.worldmap.enable
    local cleardistance = self.profile.persistence.cleardistance
    local arrivaldistance = self.profile.arrow.arrival

	for map, data in pairs(self.waypointprofile) do
		for key, waypoint in pairs(data) do
			waypoint.minimap = minimap
			waypoint.world = world
			waypoint.cleardistance = cleardistance
			waypoint.arrivaldistance = arrivaldistance
		end
	end
end

function TomTom:ReloadWaypoints()
    self:ClearAllWaypoints()

    waypoints = {}
    self.waypoints = waypoints
    self.waypointprofile = self.waydb.profile

    local cm, cx, cy = TomTom:GetCurrentPlayerPosition()

    for mapId,data in pairs(self.waypointprofile) do
        local same = mapId == cm
        local minimap = self.profile.minimap.enable and (self.profile.minimap.otherzone or same)
        local world = self.profile.worldmap.enable and (self.profile.worldmap.otherzone or same)
        for key,waypoint in pairs(data) do
            local m,x,y = unpack(waypoint)
            local title = waypoint.title

			-- Set up default options
			local options = {
				desc = title,
				title = title,
				persistent = waypoint.persistent,
				minimap = minimap,
				world = world,
				callbacks = nil,
				silent = true,
				from = "Anon",
			}

			-- Override options with what is stored in the profile
			for k,v in pairs(waypoint) do
				if type(k) == "string" then
					if k ~= "callbacks" then
						-- we can never import callbacks, so ditch them
						options[k] = v
					end
				end
			end

            self:AddWaypoint(m, x, y, options)
        end
    end
end

function TomTom:UpdateCoordFeedThrottle()
    self:_privateupdatecoordthrottle(self.db.profile.feeds.coords_throttle)
end

function TomTom:ShowHideWorldCoords()
    -- Bail out if we're not supposed to be showing this frame
    if self.profile.mapcoords.playerenable or self.db.profile.mapcoords.cursorenable then
        -- Create the frame if it doesn't exist
        if not TomTomWorldFrame then
            TomTomWorldFrame = CreateFrame("Frame", "TomTomWorldFrame", WorldMapFrame.BorderFrame)
            TomTomWorldFrame:SetFrameStrata("HIGH")
            TomTomWorldFrame:SetFrameLevel(9000)
            TomTomWorldFrame:SetAllPoints(true)
            TomTomWorldFrame.Player = TomTomWorldFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            TomTomWorldFrame.Cursor = TomTomWorldFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            TomTomWorldFrame:SetScript("OnUpdate", WorldMap_OnUpdate)
        end

        TomTomWorldFrame.Player:ClearAllPoints()
        TomTomWorldFrame.Cursor:ClearAllPoints()

        if WorldMapMixin.isMaximized then
            TomTomWorldFrame.Player:SetPoint("TOPLEFT", WorldMapFrame.BorderFrame, "TOPLEFT",
                                             self.db.profile.mapcoords.playeroffset, -6)
            TomTomWorldFrame.Cursor:SetPoint("TOPLEFT",  WorldMapFrame.BorderFrame, "TOPRIGHT",
                                             - self.db.profile.mapcoords.cursoroffset + -170, -6)
        else
            TomTomWorldFrame.Player:SetPoint("TOPLEFT", WorldMapFrame.BorderFrame, "TOPLEFT",
                                                        self.db.profile.mapcoords.playeroffset + 100, -6)
            TomTomWorldFrame.Cursor:SetPoint("TOPLEFT",  WorldMapFrame.BorderFrame, "TOPRIGHT",
                                                         - self.db.profile.mapcoords.cursoroffset + -170, -6)
        end

        TomTomWorldFrame.Player:Hide()
        TomTomWorldFrame.Cursor:Hide()

        if self.profile.mapcoords.playerenable then
            TomTomWorldFrame.Player:Show()
        end

        if self.profile.mapcoords.cursorenable then
            TomTomWorldFrame.Cursor:Show()
        end

        -- Show the frame
        TomTomWorldFrame:Show()
    elseif TomTomWorldFrame then
        TomTomWorldFrame:Hide()
    end
end

function TomTom:DebugCoordBlock()
    local msg
    msg = string.format(L["|cffffff78TomTom:|r CoordBlock %s visible"], (TomTomBlock:IsVisible() and L["is"]) or L["not"])
    ChatFrame1:AddMessage(msg)

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

function TomTom:ShowHideCoordBlock()
    -- Bail out if we're not supposed to be showing this frame
    if self.profile.block.enable then
        -- Create the frame if it doesn't exist
        if not TomTomBlock then
            -- Create the coordinate display, as of WoW 9.0, BackdropTemplate is needed.
            TomTomBlock = CreateFrame("Button", "TomTomBlock", UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
            TomTomBlock:SetWidth(120)
            TomTomBlock:SetHeight(32)
            TomTomBlock:SetToplevel(1)
            TomTomBlock:SetFrameStrata("MEDIUM")
            TomTomBlock:SetMovable(true)
            TomTomBlock:EnableMouse(true)
            TomTomBlock:SetClampedToScreen(true)
            TomTomBlock:RegisterForDrag("LeftButton")
            TomTomBlock:RegisterForClicks("RightButtonUp")

            TomTomBlock.Text = TomTomBlock:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            TomTomBlock.Text:SetJustifyH("CENTER")
            TomTomBlock.Text:SetPoint("CENTER", 0, 0)

            TomTomBlock:SetBackdrop({
                bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                edgeSize = 16,
                insets = {left = 4, right = 4, top = 4, bottom = 4},
            })
            TomTomBlock:SetBackdropColor(0,0,0,0.4)
            TomTomBlock:SetBackdropBorderColor(1,0.8,0,0.8)

            -- Set behavior scripts
            TomTomBlock:SetScript("OnUpdate", Block_OnUpdate)
            TomTomBlock:SetScript("OnClick", Block_OnClick)
            TomTomBlock:SetScript("OnEnter", Block_OnEnter)
            TomTomBlock:SetScript("OnLeave", Block_OnLeave)
            TomTomBlock:SetScript("OnDragStop", Block_OnDragStop)
            TomTomBlock:SetScript("OnDragStart", Block_OnDragStart)
            TomTomBlock:RegisterEvent("PLAYER_ENTERING_WORLD")
            if self.WOW_MAINLINE then
                TomTomBlock:RegisterEvent("PET_BATTLE_OPENING_START")
                TomTomBlock:RegisterEvent("PET_BATTLE_CLOSE")
            end
            TomTomBlock:SetScript("OnEvent", Block_OnEvent)
        end
        if self.profile.arrow.hideDuringPetBattles and C_PetBattles and C_PetBattles.IsInBattle() then
			TomTomBlock:Hide()
			return
		end
        -- Show the frame
        TomTomBlock:Show()

        local opt = self.profile.block

        -- Update the backdrop color, and border color
        TomTomBlock:SetBackdropColor(unpack(opt.bgcolor))
        TomTomBlock:SetBackdropBorderColor(unpack(opt.bordercolor))

        -- Update the height and width
        TomTomBlock:SetHeight(opt.height)
        TomTomBlock:SetWidth(opt.width)

        -- Set the block position
        TomTomBlock:ClearAllPoints()
        if not self.profile.block.position then
            self.profile.block.position = {"CENTER", nil, "CENTER", 0, -100}
        end
        local pos = self.profile.block.position
        TomTomBlock:SetPoint(pos[1], UIParent, pos[3], pos[4], pos[5])


        -- Update the font size
        local font,height = TomTomBlock.Text:GetFont()
        TomTomBlock.Text:SetFont(font, opt.fontsize, select(3, TomTomBlock.Text:GetFont()))

    elseif TomTomBlock then
        TomTomBlock:Hide()
    end
end

-- -- Hook the WorldMap OnClick
-- local world_click_verify = {
--     ["A"] = function() return IsAltKeyDown() end,
--     ["C"] = function() return IsControlKeyDown() end,
--     ["S"] = function() return IsShiftKeyDown() end,
-- }

local verifyFuncs = {
    ["A"] = function(alt, ctrl, shift) return alt and not (ctrl or shift) end,
    ["C"] = function(alt, ctrl, shift) return ctrl and not (alt or shift) end,
    ["S"] = function(alt, ctrl, shift) return shift and not (alt or ctrl) end,
    ["AC"] = function(alt, ctrl, shift) return (alt and ctrl) and not (shift) end,
    ["AS"] = function(alt, ctrl, shift) return (alt and shift) and not (ctrl) end,
    ["CS"] = function(alt, ctrl, shift) return (ctrl and shift) and not (alt) end,
    ["ACS"] = function(alt, ctrl, shift) return alt and ctrl and shift end,
}

-- Thanks to Nevcariel for the help with this one!
function addon:CreateWorldMapClickHandler()
    if not self.worldMapClickHandler then
        self.worldMapClickHandler = CreateFrame("Frame", addonName .. "WorldMapClickHandler", WorldMapFrame.ScrollContainer)
    end

    local handler = self.worldMapClickHandler
    handler:SetAllPoints()

    handler.UpdatePassThrough = function(self)
        local alt, ctrl, shift = IsAltKeyDown(), IsControlKeyDown(), IsShiftKeyDown()

        local modKey = TomTom.db.profile.worldmap.create_modifier
        if modKey and verifyFuncs[modKey] and verifyFuncs[modKey](alt, ctrl, shift) then
            if self.SetPassThroughButtons and not InCombatLockdown() then
                self:SetPassThroughButtons("LeftButton", "MiddleButton", "Button4", "Button5")
            end

            self:Show()
        else
            if self.SetPassThroughButtons and not InCombatLockdown() then
                self:SetPassThroughButtons("LeftButton", "RightButton", "MiddleButton", "Button4", "Button5")
            end

            self:Hide()
        end
    end

    handler:RegisterEvent("MODIFIER_STATE_CHANGED")
    handler:SetScript("OnEvent", handler.UpdatePassThrough)
    handler:SetScript("OnMouseUp", function(frame, button)
        addon:AddWaypointFromMapClick()
    end)

    handler:SetScript("OnUpdate", function(self, elapsed)
            handler:UpdatePassThrough()
    end)

    handler:UpdatePassThrough()
end

function addon:AddWaypointFromMapClick()
    local m = WorldMapFrame.mapID
    local x,y = WorldMapFrame:GetNormalizedCursorPosition()

    if not m or m == 0 then
        return
    end

    TomTom:AddWaypoint(m, x, y, { title = L["TomTom waypoint"], from="TomTom/wm"})
end

-- This is a taint factory, removed in favour of the above
-- -- This is now a registered click handler.
-- -- If we return false, it gets passed on to the next handler.
-- -- We need to return true when we handle the click.
-- local function WorldMap_OnClick (self, ...)
--     local mouseButton, button = ...
--     if mouseButton == "RightButton" then
--         -- Check for all the modifiers that are currently set
--         for mod in TomTom.db.profile.worldmap.create_modifier:gmatch("[ACS]") do
--             if not world_click_verify[mod] or not world_click_verify[mod]() then
--                 return false
--             end
--         end

--         local m = WorldMapFrame.mapID
--         local x,y = WorldMapFrame:GetNormalizedCursorPosition()

--         if not m or m == 0 then
--             return false
--         end

--         local uid = TomTom:AddWaypoint(m, x, y, { title = L["TomTom waypoint"], from="TomTom/wm"})
--         return true
--     else
--         return false
--     end
-- end

-- -- Add WorldMap_OnClick as a Click Handler on the WorldMapFrame Canvas
-- WorldMapFrame:AddCanvasClickHandler(WorldMap_OnClick,10)

local function WaypointCallback(event, arg1, arg2, arg3)
    if event == "OnDistanceArrive" then
        TomTom:ClearWaypoint(arg1)
    elseif event == "OnTooltipShown" then
        local tooltip = arg1
        if arg3 then
            tooltip:SetText(L["TomTom waypoint"])
            tooltip:AddLine(string.format(L["%s yards away"], math.floor(arg2)), 1, 1 ,1)
            tooltip:AddLine(string.format(L["From: %s"], arg1.from or "?"))
            tooltip:Show()
        else
            tooltip.lines[2]:SetFormattedText(L["%s yards away"], math.floor(arg2), 1, 1, 1)
        end
    end
end

--[[-------------------------------------------------------------------
--  Dropdown menu code
-------------------------------------------------------------------]]--

StaticPopupDialogs["TOMTOM_REMOVE_ALL_CONFIRM"] = {
	preferredIndex = STATICPOPUPS_NUMDIALOGS,
    text = L["Are you sure you would like to remove ALL TomTom waypoints?"],
    button1 = L["Yes"],
    button2 = L["No"],
    OnAccept = function()
        TomTom.waydb:ResetProfile()
        TomTom:ReloadWaypoints()
    end,
    timeout = 30,
    whileDead = 1,
    hideOnEscape = 1,
}

local function waypointDropdownGenerator(owner, rootDescription)
	rootDescription:CreateTitle(L["Waypoint Options"]);

    rootDescription:CreateButton(L["Set as waypoint arrow"], function()
        local uid = TomTom.dropdown_uid
        local data = uid
        addon:SetCrazyArrow(uid, addon.profile.arrow.arrival, data.title or L["TomTom waypoint"])
    end)

	-- Waypoint communications
    local sendWaypoint = rootDescription:CreateButton(L["Send waypoint to"])
	sendWaypoint:CreateTitle(L["Waypoint communication"])
	sendWaypoint:CreateButton(L["Send to party"], function()
		addon:SendWaypoint(addon.dropdown_uid, "PARTY")
	end)
	sendWaypoint:CreateButton(L["Send to raid"], function()
		addon:SendWaypoint(addon.dropdown_uid, "RAID")
	end)
	sendWaypoint:CreateButton(L["Send to battleground"], function()
		addon:SendWaypoint(addon.dropdown_uid, "BATTLEGROUND")
	end)
	sendWaypoint:CreateButton(L["Send to guild"], function()
		addon:SendWaypoint(addon.dropdown_uid, "GUILD")
	end)

	rootDescription:CreateButton(L["Clear waypoint from crazy arrow"], function()
		addon:ClearCrazyArrowPoint(false)
	end)

	rootDescription:CreateButton(L["Remove waypoint"], function()
		local uid = addon.dropdown_uid
		addon:RemoveWaypoint(uid)
	end)

	rootDescription:CreateButton(L["Remove all waypoints from this zone"], function()
		local uid = addon.dropdown_uid
		local data = uid
		local mapId = data[1]
		for key, waypoint in pairs(addon.waypoints[mapId]) do
			addon:RemoveWaypoint(waypoint)
		end
	end)

	rootDescription:CreateButton(L["Remove all waypoints"], function()
		if addon.db.profile.general.confirmremoveall then
			StaticPopup_Show("TOMTOM_REMOVE_ALL_CONFIRM")
		else
			StaticPopupDialogs["TOMTOM_REMOVE_ALL_CONFIRM"].OnAccept()
			return
		end
	end)

    local isDropdownWaypointSaved = function()
        local uid = addon.dropdown_uid
        return not not addon:UIDIsSaved(uid)
    end

    local setDropdownWaypointSaved = function()
        local uid = addon.dropdown_uid
        if not uid then
            return false
        end

        local key = addon:GetKey(uid)
        local mapId = uid[1]
        local saved = addon:UIDIsSaved(uid)

        if saved then
            addon.waypointprofile[mapId][key] = nil
        else
            addon.waypointprofile[mapId][key] = uid
        end
    end

    rootDescription:CreateCheckbox(L["Save this waypoint between sessions"], isDropdownWaypointSaved, setDropdownWaypointSaved)
end

function TomTom:UIDIsSaved(uid)
    if type(uid) ~= "table" then error("TomTom:UIDIsSaved(uid) UID is not a table."); end
    local data = uid
    if data then
        local key = TomTom:GetKey(data)
        local mapId = data[1]

        if data then
            return not not TomTom.waypointprofile[mapId][key]
        end
    end
    return false
end

function TomTom:SendWaypoint(uid, channel)
    if type(uid) ~= "table" then error("TomTom:SendWaypoint(uid) UID is not a table."); end
    local data = uid
    local m, x, y = unpack(data)
    local msg = string.format("%d:%f:%f:%s", m, x, y, data.title or "")
    C_ChatInfo.SendAddonMessage("TOMTOM4", msg, channel)
end

function TomTom:CHAT_MSG_ADDON(event, prefix, data, channel, sender)
    if prefix ~= "TOMTOM4" then return end
    if sender == UnitName("player") then return end

    local m,x,y,title = string.split(":", data)
    if not title:match("%S") then
        title = string.format(L["Waypoint from %s"], sender)
    end

    m = tonumber(m)
    x = tonumber(x)
    y = tonumber(y)

    local zoneName = hbd:GetLocalizedMap(m) or L["Unnamed Map"]
    self:AddWaypoint(m, x, y, {title = title, from=("TomTom/"..sender)})
    local msg = string.format(L["|cffffff78TomTom|r: Added '%s' (sent from %s) to zone %s"], title, sender, zoneName)
    ChatFrame1:AddMessage(msg)
end

--[[-------------------------------------------------------------------
--  Define callback functions
-------------------------------------------------------------------]]--
local function _minimap_onclick(event, uid, self, button)
    if TomTom.db.profile.minimap.menu then
        TomTom.dropdown_uid = uid
        MenuUtil.CreateContextMenu(self, waypointDropdownGenerator)
    end
end

local function _world_onclick(event, uid, self, button)
    if TomTom.db.profile.worldmap.menu then
        TomTom.dropdown_uid = uid
        MenuUtil.CreateContextMenu(self, waypointDropdownGenerator)
    end
end

local function _both_tooltip_show(event, tooltip, uid, dist)
    local data = uid

    tooltip:SetText(data.title or L["Unknown waypoint"])
    if dist and tonumber(dist) then
        tooltip:AddLine(string.format(L["%s yards away"], math.floor(dist)), 1, 1, 1)
    else
        tooltip:AddLine(L["Unknown distance"])
    end
    local m,x,y = unpack(data)
    local zoneName = hbd:GetLocalizedMap(m) or L["Unnamed Map"]

    tooltip:AddLine(string.format(L["%s (%.2f, %.2f)"], zoneName, x*100, y*100), 0.7, 0.7, 0.7)
    tooltip:AddLine(string.format(L["From: %s"], data.from or "?"))
    tooltip:Show()
end

local function _minimap_tooltip_show(event, tooltip, uid, dist)
    if not TomTom.db.profile.minimap.tooltip then
        tooltip:Hide()
        return
    end
    return _both_tooltip_show(event, tooltip, uid, dist)
end

local function _world_tooltip_show(event, tooltip, uid, dist)
    if not TomTom.db.profile.worldmap.tooltip then
        tooltip:Hide()
        return
    end
    return _both_tooltip_show(event, tooltip, uid, dist)
end

local function _both_tooltip_update(event, tooltip, uid, dist)
    if dist and tonumber(dist) then
        tooltip.lines[2]:SetFormattedText(L["%s yards away"], math.floor(dist), 1, 1, 1)
    else
        tooltip.lines[2]:SetText(L["Unknown distance"])
    end
end

local function _both_clear_distance(event, uid, range, distance, lastdistance)
	-- Only clear the waypoint if we weren't inside it when it was set
    if lastdistance and not UnitOnTaxi("player") then
        TomTom:RemoveWaypoint(uid)
    end
end

local function _both_ping_arrival(event, uid, range, distance, lastdistance)
    if TomTom.profile.arrow.enablePing then
        PlaySoundFile("Interface\\AddOns\\TomTom\\Media\\ping.mp3", TomTom.profile.arrow.pingChannel)
    end
end

local function _remove(event, uid)
    local data = uid
    local key = TomTom:GetKey(data)
    local mapId = data[1]
    local sv = TomTom.waypointprofile[mapId]

    if sv and sv[key] then
        sv[key] = nil
    end

    -- Remove this entry from the waypoints table
    if waypoints[mapId] then
        waypoints[mapId][key] = nil
    end
end

local function noop() end

function TomTom:RemoveWaypoint(uid)
    if not uid then return end
    if type(uid) ~= "table" then error("TomTom:RemoveWaypoint(uid) UID is not a table."); end
    local data = uid
    self:ClearWaypoint(uid)

    local key = TomTom:GetKey(data)
    local mapId = data[1]
    local sv = TomTom.waypointprofile[mapId]

    if sv and sv[key] then
        sv[key] = nil
    end

    -- Remove this entry from the waypoints table
    if waypoints[mapId] then
        waypoints[mapId][key] = nil
    end
end


function TomTom:AddWaypointToCurrentZone(x, y, desc)
    local m = TomTom:GetCurrentPlayerPosition()
    if not m then
        return
    end

    return self:AddWaypoint(m, x/100, y/100, {title = desc, from="TomTom/AWTCZ"})
end

-- Return a set of default callbacks that can be used by addons to provide
-- more detailed functionality without losing the tooltip and onclick
-- functionality.
--
-- Options that are used in this 'opts' table in this function:
--  * cleardistance - When the player is this far from the waypoint, the
--    waypoint will be removed.
--  * arrivaldistance - When the player is within this radius of the waypoint,
--    the crazy arrow will change to the 'downwards' arrow, indicating that
--    the player has arrived.

function TomTom:DefaultCallbacks(opts)
    opts = opts or {}

	local callbacks = {
		minimap = {
			onclick = _minimap_onclick,
			tooltip_show = _minimap_tooltip_show,
			tooltip_update = _both_tooltip_update,
		},
		world = {
			onclick = _world_onclick,
			tooltip_show = _world_tooltip_show,
			tooltip_update = _both_tooltip_show,
		},
		distance = {
		},
	}

    local cleardistance = self.profile.persistence.cleardistance
    local arrivaldistance = self.profile.arrow.arrival

    -- Allow both of these to be overriden by options
    if opts.cleardistance then
        cleardistance = opts.cleardistance
    end
    if opts.arrivaldistance then
        arrivaldistance = opts.arrivaldistance
    end

    -- User wants ping, they get ping!
    if TomTom.profile.arrow.enablePing then
        if arrivaldistance <= 0 then
            arrivaldistance = 1
        end
        if arrivaldistance < cleardistance then
            arrivaldistance = cleardistance
        end
    end

    if cleardistance == arrivaldistance then
        callbacks.distance[cleardistance] = function(...)
            _both_clear_distance(...);
            _both_ping_arrival(...);
        end
    else
        if cleardistance > 0 then
            callbacks.distance[cleardistance] = _both_clear_distance
        end
        if arrivaldistance > 0 then
            callbacks.distance[arrivaldistance] = _both_ping_arrival
        end
    end

	return callbacks
end

function TomTom:AddWaypoint(m, x, y, opts)
	opts = opts or {}

	-- Default values
    if opts.persistent == nil then opts.persistent = self.profile.persistence.savewaypoints end
    if opts.minimap == nil then opts.minimap = self.profile.minimap.enable end
    if opts.world == nil then opts.world = self.profile.worldmap.enable end
    if opts.crazy == nil then opts.crazy = self.profile.arrow.autoqueue end
	if opts.cleardistance == nil then opts.cleardistance = self.profile.persistence.cleardistance end
	if opts.arrivaldistance == nil then opts.arrivaldistance = self.profile.arrow.arrival end

    if not opts.callbacks then
		opts.callbacks = TomTom:DefaultCallbacks(opts)
	end

    local zoneName = hbd:GetLocalizedMap(m) or L["Unnamed Map"]

    -- Ensure there isn't already a waypoint at this location
    local key = self:GetKey({m, x, y, title = opts.title})
    if waypoints[m] and waypoints[m][key] then
        return waypoints[m][key]
    end

    -- uid is the 'new waypoint' called this for historical reasons
    local uid = {m, x, y, title = opts.title, from=(opts.from or "?")}

    -- Copy over any options, so we have them
    for k,v in pairs(opts) do
        if not uid[k] then
            uid[k] = v
        end
    end

    -- No need to convert x and y because they're already 0-1 instead of 0-100
    self:SetWaypoint(uid, opts.callbacks, opts.minimap, opts.world)
    if opts.crazy then
        self:SetCrazyArrow(uid, opts.arrivaldistance, opts.title)
    end

    waypoints[m] = waypoints[m] or {}
    waypoints[m][key] = uid

    -- If this is a persistent waypoint, then add it to the waypoints table
    if opts.persistent then
        self.waypointprofile[m][key] = uid
    end

    if not opts.silent and self.profile.general.announce then
        local ctxt = RoundCoords(x, y, 2)
        local desc = opts.title and opts.title or ""
        local sep = opts.title and " - " or ""
        local msg = string.format(L["|cffffff78TomTom:|r Added a waypoint (%s%s%s) in %s"], desc, sep, ctxt, zoneName)
        ChatFrame1:AddMessage(msg)
    end

    return uid
end

-- Check to see if a given uid/waypoint is actually set somewhere
function TomTom:IsValidWaypoint(waypoint)
    if type(waypoint) ~= "table" then error("TomTom:IsValidWaypoint(waypoint) UID is not a table."); end
    local m = waypoint[1]
    local key = self:GetKey(waypoint)
    if waypoints[m] and waypoints[m][key] then
        return true
    else
        return false
    end
end

function TomTom:WaypointHasSameMapXYTitle(waypoint, map, x, y, title)
    -- This is a hack that relies on the UID format, do not use this
    -- in your addons, please.
    local pm, px, py = unpack(waypoint)
    if map == pm and x == px and y == py and waypoint.title == title then
        return true
    end

    return false
end

function TomTom:WaypointExists(m, x, y, desc)
    local key = self:GetKeyArgs(m, x, y, desc)
    if waypoints[m] and waypoints[m][key] then
        return true
    else
        return false
    end
end

function TomTom:SetCustomWaypoint(m, x, y, opts)
    opts.persistent = false
    return self:AddWaypoint(m, x, y, opts)
end

do
    -- Original Code courtesy ckknight, modified for BFA by Ludovicus
    function GetCurrentCursorPosition()
        local cx, cy = WorldMapFrame:GetNormalizedCursorPosition()

        if cx < 0 or cx > 1 or cy < 0 or cy > 1 then
            return nil, nil
        end

        return cx, cy
    end

    local coord_fmt = "%%.%df, %%.%df"
    function RoundCoords(x,y,prec)
        local fmt = coord_fmt:format(prec, prec)
        if not x or not y then
            return "---"
        end
        return fmt:format(x*100, y*100)
    end


	local coord_throttle = 0
    function WorldMap_OnUpdate(self, elapsed)
		coord_throttle = coord_throttle + elapsed
		if coord_throttle <= TomTom.profile.mapcoords.throttle then
			return
		end

		coord_throttle = 0
        local x,y = TomTom:GetCurrentCoords()
        local opt = TomTom.db.profile

        if not x or not y then
            self.Player:SetText(L["Player: ---"])
        else
            self.Player:SetFormattedText(L["Player: %s"], RoundCoords(x, y, opt.mapcoords.playeraccuracy))
        end

        local cX, cY = GetCurrentCursorPosition()

        if not cX or not cY then
            self.Cursor:SetText(L["Cursor: ---"])
        else
            self.Cursor:SetFormattedText(L["Cursor: %s"], RoundCoords(cX, cY, opt.mapcoords.cursoraccuracy))
        end
    end
end

do
    local bcounter = 0
    function Block_OnUpdate(self, elapsed)
        bcounter = bcounter + elapsed
        if (not TomTom) or not (TomTom.profile) then return; end
        if bcounter > TomTom.profile.block.throttle then
            bcounter = bcounter - TomTom.profile.block.throttle

            local x,y = TomTom:GetCurrentCoords()

            local opt = TomTom.db.profile
            if not x or not y then
                local blank = ("-"):rep(8)
                self.Text:SetText(blank)
            else
                self.Text:SetFormattedText("%s", RoundCoords(x, y, opt.block.accuracy))
            end
        end
    end

    function Block_OnDragStart(self, button, down)
        if not TomTom.db.profile.block.lock then
            self:StartMoving()
        end
    end

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

    function Block_OnClick(self, button, down)
        local m,x,y = TomTom:GetCurrentPlayerPosition()
        if m and x and y then
            local zoneName = hbd:GetLocalizedMap(m) or L["Unnamed Map"]
            local desc = string.format("%s: %.2f, %.2f", zoneName, x*100, y*100)
            TomTom:AddWaypoint(m, x, y, {
                title = desc,
                from = "TomTom/Block"
            })
        end
    end

    function Block_OnEvent(self, event, ...)
        if (event == "PLAYER_ENTERING_WORLD") or (event == "PET_BATTLE_OPENING_START") or (event == "PET_BATTLE_CLOSE") then
            TomTom:ShowHideCoordBlock()
        end
    end
end

function TomTom:DebugListLocalWaypoints()
    local m,x,y = self:GetCurrentPlayerPosition()
    local ctxt = RoundCoords(x, y, 2)
    local czone = hbd:GetLocalizedMap(m) or L["Unnamed Map"]
    self:Printf(L["You are at (%s) in '%s' (map: %s)"], ctxt, czone , tostring(m))
    if waypoints[m] then
        for key, wp in pairs(waypoints[m]) do
            local ctxt = RoundCoords(wp[2], wp[3], 2)
            local desc = wp.title and wp.title or L["Unknown waypoint"]
            local indent = "   "
            self:Printf(L["%s%s - %s (map: %d)"], indent, desc, ctxt, wp[1])
        end
    else
        local indent = "   "
        self:Printf(L["%sNo waypoints in this zone"], indent)
    end
end

function TomTom:DebugListAllWaypoints()
    local m,x,y = self:GetCurrentPlayerPosition()
    local ctxt = RoundCoords(x, y, 2)
    local czone = hbd:GetLocalizedMap(m) or L["Unnamed Map"]
    self:Printf(L["You are at (%s) in '%s' (map: %s)"], ctxt, czone, tostring(m))
    for m in pairs(waypoints) do
        local c,z,w = TomTom:GetCZWFromMapID(m)
        local zoneName = hbd:GetLocalizedMap(m) or L["Unnamed Map"]
        self:Printf(L["%s: (map: %d, zone: %s, continent: %s, world: %s)"], zoneName, m, tostring(z), tostring(c), tostring(w))
        for key, wp in pairs(waypoints[m]) do
            local ctxt = RoundCoords(wp[2], wp[3], 2)
            local desc = wp.title and wp.title or L["Unknown waypoint"]
            local indent = "   "
            self:Printf(L["%s%s - %s %s %s %s"], indent, desc, ctxt, wp[1], (wp.minimap_icon or "*"), (wp.worldmap_icon or "*"))
        end
    end
end

local function usage()
    ChatFrame1:AddMessage(L["|cffffff78TomTom |r/way /tway /tomtomway /cway /wayb /wayback /ttpase /tomtom |cffffff78Usage:|r"])
    ChatFrame1:AddMessage(L["|cffffff78/tomtom |r - Open the TomTom options panel"])
    ChatFrame1:AddMessage(L["|cffffff78/ttpaste |r - Open a window to paste and execute multiple TomTom commands"])
    ChatFrame1:AddMessage(L["|cffffff78/cway |r - Activate the closest waypoint"])
    ChatFrame1:AddMessage(L["|cffffff78/wayb [desc] |r - Save the current position with optional description"])
    ChatFrame1:AddMessage(L["|cffffff78/way /tway /tomtomway |r - Commands to set a waypoint: one should work."])
    ChatFrame1:AddMessage(L["|cffffff78/way <x> <y> [desc]|r - Adds a waypoint at x,y with descrtiption desc"])
    ChatFrame1:AddMessage(L["|cffffff78/way <zone> <x> <y> [desc]|r - Adds a waypoint at x,y in zone with description desc"])
    ChatFrame1:AddMessage(L["|cffffff78/way reset all|r - Resets all waypoints"])
    ChatFrame1:AddMessage(L["|cffffff78/way reset away|r - Resets all waypoints not in current zone"])
    ChatFrame1:AddMessage(L["|cffffff78/way reset <zone>|r - Resets all waypoints in zone"])
    ChatFrame1:AddMessage(L["|cffffff78/way reset not <zone>|r - Resets all waypoints not in zone"])
    ChatFrame1:AddMessage(L["|cffffff78/way local|r - Lists active waypoints in current zone"])
    ChatFrame1:AddMessage(L["|cffffff78/way list|r - Lists all active waypoints"])
    ChatFrame1:AddMessage(L["|cffffff78/way arrow|r - Prints status of the Crazy Arrow"])
    ChatFrame1:AddMessage(L["|cffffff78/way block|r - Prints status of the Coordinate Block"])
end

TomTom.slashCommandUsage = usage

TomTom.CZWFromMapID = {}
local overrides = {
    [101] = {mapType = Enum.UIMapType.World}, -- Outland
    [125] = {mapType = Enum.UIMapType.Zone}, -- Dalaran
    [126] = {mapType = Enum.UIMapType.Micro},
    [195] = {suffix = "1"}, -- Kaja'mine
    [196] = {suffix = "2"}, -- Kaja'mine
    [197] = {suffix = "3"}, -- Kaja'mine
    [501] = {mapType = Enum.UIMapType.Zone}, -- Dalaran
    [502] = {mapType = Enum.UIMapType.Micro},
    [572] = {mapType = Enum.UIMapType.World}, -- Draenor
    [579] = {suffix = "1"}, -- Lunarfall Excavation
    [580] = {suffix = "2"}, -- Lunarfall Excavation
    [581] = {suffix = "3"}, -- Lunarfall Excavation
    [582] = {mapType = Enum.UIMapType.Zone}, -- Lunarfall
    [585] = {suffix = "1"}, -- Frostwall Mine
    [586] = {suffix = "2"}, -- Frostwall Mine
    [587] = {suffix = "3"}, -- Frostwall Mine
    [590] = {mapType = Enum.UIMapType.Zone}, -- Frostwall
    [625] = {mapType = Enum.UIMapType.Orphan}, -- Dalaran
    [626] = {mapType = Enum.UIMapType.Micro}, -- Dalaran
    [627] = {mapType = Enum.UIMapType.Zone},
    [628] = {mapType = Enum.UIMapType.Micro},
    [629] = {mapType = Enum.UIMapType.Micro},
    [943] = {suffix = FACTION_HORDE}, -- Arathi Highlands
    [1044] = {suffix = FACTION_ALLIANCE},
}

function TomTom:GetCZWFromMapID(m)
    local zone, continent, world, map
    local mapInfo = nil

    if not m then return nil, nil, nil; end

    -- Return the cached CZW
    if TomTom.CZWFromMapID[m] then
        return unpack(TomTom.CZWFromMapID[m])
    end

    map = m -- Save the original map
    repeat
        mapInfo = C_Map.GetMapInfo(m)
        if not mapInfo then
            -- No more parents, return what we have
            TomTom.CZWFromMapID[map] = {continent, zone, world}
            return continent, zone, world
        end
        local mapType = (overrides[m] and overrides[m].mapType) or mapInfo.mapType
        if mapType == Enum.UIMapType.Zone then
            -- Its a zone map
            zone = m
        elseif mapType == Enum.UIMapType.Continent then
            continent = m
        elseif (mapType == Enum.UIMapType.World) or (mapType == Enum.UIMapType.Cosmic) then
            world = m
            continent = continent or m -- Hack for one continent worlds
        end
        m = mapInfo.parentMapID
    until (m == 0)
    TomTom.CZWFromMapID[map] = {continent, zone, world}
    return continent, zone, world
end

function TomTom:GetClosestWaypoint()
    local m,x,y = self:GetCurrentPlayerPosition()
    local c,z,w = TomTom:GetCZWFromMapID(m)

    local closest_waypoint = nil
    local closest_dist = nil

    if not self.profile.arrow.closestusecontinent then
		-- Simple search within this zone
		if waypoints[m] then
			for key, waypoint in pairs(waypoints[m]) do
				local dist = TomTom:GetDistanceToWaypoint(waypoint)
				if (dist and closest_dist == nil) or (dist and dist < closest_dist) then
					closest_dist = dist
					closest_waypoint = waypoint
				end
			end
		end
	else
		-- Search all waypoints on this continent
		for map, waypoints in pairs(waypoints) do
			if c == TomTom:GetCZWFromMapID(map) then
				for key, waypoint in pairs(waypoints) do
					local dist = TomTom:GetDistanceToWaypoint(waypoint)
					if (dist and closest_dist == nil) or (dist and dist < closest_dist) then
						closest_dist = dist
						closest_waypoint = waypoint
					end
				end
			end
		end
	end

    if closest_dist then
        return closest_waypoint
    end
end

function TomTom:SetClosestWaypoint(verbose)
    local uid = self:GetClosestWaypoint()
    if uid then
        local data = uid
        TomTom:SetCrazyArrow(uid, TomTom.profile.arrow.arrival, data.title)
        local m, x, y = unpack(data)
        local zoneName = hbd:GetLocalizedMap(m) or L["Unnamed Map"]
        local ctxt = RoundCoords(x, y, 2)
        local desc = data.title and data.title or ""
        local sep = data.title and " - " or ""
        if self.profile.general.announce then
            local msg = string.format(L["|cffffff78TomTom:|r Selected waypoint (%s%s%s) in %s"], desc, sep, ctxt, zoneName)
            ChatFrame1:AddMessage(msg)
        end
    else
        local msg
        if not self.profile.arrow.closestusecontinent then
           msg = L["|cffffff78TomTom:|r Could not find a closest waypoint in this zone."]
        else
           msg = L["|cffffff78TomTom:|r Could not find a closest waypoint in this continent."]
        end
        if verbose then
            ChatFrame1:AddMessage(msg)
        end
    end
    return uid
end

SLASH_TOMTOM_CLOSEST_WAYPOINT1 = "/cway"
SLASH_TOMTOM_CLOSEST_WAYPOINT2 = "/closestway"
SlashCmdList["TOMTOM_CLOSEST_WAYPOINT"] = function(msg)
    TomTom:SetClosestWaypoint(true)
end

SLASH_TOMTOM_WAYBACK1 = "/wayb"
SLASH_TOMTOM_WAYBACK2 = "/wayback"
SlashCmdList["TOMTOM_WAYBACK"] = function(msg)
    local title = L["Wayback"]
    if msg and msg:match("%S") then
        title = msg
    end

    local backm,backx,backy = TomTom:GetCurrentPlayerPosition()
    TomTom:AddWaypoint(backm,backx, backy, {
        title = title,
        from = "TomTom/wayb"
    })
end

SLASH_TOMTOM_WAY1 = "/way"
SLASH_TOMTOM_WAY2 = "/tway"
SLASH_TOMTOM_WAY3 = "/tomtomway"

TomTom.NameToMapId = {}
local NameToMapId = TomTom.NameToMapId


do
    -- Fetch the names of the zones
    for id in pairs(hbd.mapData) do
        local c,z,w = TomTom:GetCZWFromMapID(id)
        local mapType = (overrides[id] and overrides[id].mapType) or hbd.mapData[id].mapType
        if (mapType == Enum.UIMapType.Zone) or
           (mapType == Enum.UIMapType.Continent) or
           (mapType == Enum.UIMapType.Micro) then
            -- Record only Zone or Continent or Micro maps
            local name = hbd.mapData[id].name
            if (overrides[id] and overrides[id].suffix) then
                name = name .. " " .. overrides[id].suffix
            end
            -- What about some instances?  Do they have coords?  How to test for that case?
            if w then -- It must be in some world to be named and have coords
                if name and NameToMapId[name] then
                    if type(NameToMapId[name]) ~= "table" then
                        -- convert to table
                        NameToMapId[name] = {NameToMapId[name]}
                    end
                    table.insert(NameToMapId[name], id)
                else
                    NameToMapId[name] = id
                end
            end
            -- Record just the raw map # as a possible override.
            NameToMapId["#" .. id] = id
        end
    end
    -- Handle any duplicates
    local newEntries = {}
    for name, mapID in pairs(NameToMapId) do
        if type(mapID) == "table" then
            NameToMapId[name] = nil
            for idx, mapId in pairs(mapID) do
                local parent = hbd.mapData[mapId].parent
                local parentName = (parent and (parent > 0) and hbd.mapData[parent].name)
                if parentName then
                    -- We rely on the implicit acending order of mapID's so the lowest one wins
                    if not newEntries[name .. ":" .. parentName] then
                        newEntries[name .. ":" .. parentName] = mapId
                    else
                        newEntries[name .. ":" .. tostring(mapId)] = mapId
                    end
                end
            end
        end
    end
    -- Add the de-duplicated entries
    for name, mapID in pairs(newEntries) do
        NameToMapId[name] = mapID
    end
end

local wrongseparator = "(%d)" .. (tonumber("1.1") and "," or ".") .. "(%d)"
local rightseparator =   "%1" .. (tonumber("1.1") and "." or ",") .. "%2"

-- Make comparison only using lowercase letters and no spaces
local function lowergsub(s) return s:lower():gsub("[%s]", "") end

SlashCmdList["TOMTOM_WAY"] = function(msg)
    msg = msg:gsub("(%d)[%.,] (%d)", "%1 %2"):gsub(wrongseparator, rightseparator)
    local tokens = {}
    for token in msg:gmatch("%S+") do table.insert(tokens, token) end

    -- Lower the first token
    local ltoken = tokens[1] and tokens[1]:lower()

    if ltoken == "local" then
        TomTom:DebugListLocalWaypoints()
        return
    elseif ltoken == "list" then
        TomTom:DebugListAllWaypoints()
        return
    elseif ltoken == "arrow" then
        TomTom:DebugCrazyArrow()
        return
    elseif ltoken == "block" then
        TomTom:DebugCoordBlock()
        return
    elseif ltoken == "reset" then
        local ltoken2 = tokens[2] and tokens[2]:lower()
        if ltoken2 == "all" then
            if TomTom.db.profile.general.confirmremoveall then
                StaticPopup_Show("TOMTOM_REMOVE_ALL_CONFIRM")
            else
                StaticPopupDialogs["TOMTOM_REMOVE_ALL_CONFIRM"].OnAccept()
                return
            end
        elseif ltoken2 == "away" then
            local m, x, y = TomTom:GetCurrentPlayerPosition()
            for map, map_waypoints in pairs(waypoints) do
                if map ~= m then
                    local numRemoved = 0
                    for key, uid in pairs(waypoints[map]) do
                        TomTom:RemoveWaypoint(uid)
                        numRemoved = numRemoved + 1
                    end
                    local zoneName = hbd:GetLocalizedMap(map) or L["Unnamed Map"]
                    if numRemoved > 0 then
                        ChatFrame1:AddMessage(L["Removed %d waypoints from %s"]:format(numRemoved, zoneName))
                    end
                end
            end
        elseif tokens[2] then
            -- Reset the named zone
            local notHere = false
            local zone = ""
            if lowergsub(tokens[2]) == "not" then
                notHere = true
                zone = table.concat(tokens, " ", 3)
            else
                zone = table.concat(tokens, " ", 2)
            end
            -- Find a fuzzy match for the zone

            local matches = {}
            local lzone = lowergsub(zone)

            for name, mapId in pairs(NameToMapId) do
                local lname = lowergsub(name)
                if lname == lzone then
                    -- We have an exact match
                    matches = {name}
                    break
                elseif lname:match(lzone) then
                    table.insert(matches, name)
                end
            end

            if #matches > 7 then
                local msg = string.format(L["Found %d possible matches for zone %s.  Please be more specific"], #matches, zone)
                ChatFrame1:AddMessage(msg)
                return
            elseif #matches > 1 then
                table.sort(matches)

                ChatFrame1:AddMessage(string.format(L["Found multiple matches for zone '%s'.  Did you mean: %s"], zone, table.concat(matches, ", ")))
                return
            elseif #matches == 0 then
                local msg = string.format(L["Could not find any matches for zone %s."], zone)
                ChatFrame1:AddMessage(msg)
                return
            end

            local zoneName = matches[1]
            local mapId = NameToMapId[zoneName]

            if notHere then
                for map, map_waypoints in pairs(waypoints) do
                    if map ~= mapId then
                        local numRemoved = 0
                        for key, uid in pairs(waypoints[map]) do
                            TomTom:RemoveWaypoint(uid)
                            numRemoved = numRemoved + 1
                        end
                        local zoneName = hbd:GetLocalizedMap(map) or L["Unnamed Map"]
                        if numRemoved > 0 then
                            ChatFrame1:AddMessage(L["Removed %d waypoints from %s"]:format(numRemoved, zoneName))
                        end
                    end
                end
                return
            end

            local numRemoved = 0
            if waypoints[mapId] then
                for key, uid in pairs(waypoints[mapId]) do
                    TomTom:RemoveWaypoint(uid)
                    numRemoved = numRemoved + 1
                end
                ChatFrame1:AddMessage(L["Removed %d waypoints from %s"]:format(numRemoved, zoneName))
            else
                ChatFrame1:AddMessage(L["There were no waypoints to remove in %s"]:format(zoneName))
            end
        end
    elseif tokens[1] and not tonumber(tokens[1]) then
        -- Example: /way Elwynn Forest 34.2 50.7 Party in the forest!
        -- tokens[1] = Elwynn
        -- tokens[2] = Forest
        -- tokens[3] = 34.2
        -- tokens[4] = 50.7
        -- tokens[5] = Party
        -- ...
        --
        -- Find the first numeric token
        local zoneEnd
        for idx = 1, #tokens do
            local token = tokens[idx]
            if tonumber(token) then
                -- We've encountered a number, so the zone name must have
                -- ended at the prior token
                zoneEnd = idx - 1
                break
            end
        end

        if not zoneEnd then
            usage()
            return
        end

        -- This is a waypoint set, with a zone before the coords
        local zone = table.concat(tokens, " ", 1, zoneEnd)
        local x,y,desc = select(zoneEnd + 1, unpack(tokens))

        if desc then desc = table.concat(tokens, " ", zoneEnd + 3) end

        -- Find a fuzzy match for the zone
        local matches = {}
        local lzone = lowergsub(zone)

        for name,mapId in pairs(NameToMapId) do
            local lname = lowergsub(name)
            if lname == lzone then
                -- We have an exact match
                matches = {name}
                break
            elseif lname:match(lzone) then
                table.insert(matches, name)
            end
        end

        if #matches > 7 then
            local msg = string.format(L["Found %d possible matches for zone %s.  Please be more specific"], #matches, zone)
            ChatFrame1:AddMessage(msg)
            return
        elseif #matches > 1 then
            local msg = string.format(L["Found multiple matches for zone '%s'.  Did you mean: %s"], zone, table.concat(matches, ", "))
            ChatFrame1:AddMessage(msg)
            return
        elseif #matches == 0 then
            local msg = string.format(L["Could not find any matches for zone %s."], zone)
            ChatFrame1:AddMessage(msg)
            return
        end

        -- There was only one match, so proceed
        local zoneName = matches[1]
        local mapId = NameToMapId[zoneName]

        x = x and tonumber(x)
        y = y and tonumber(y)

        if not x or not y then
            return usage()
        end

        x = tonumber(x)
        y = tonumber(y)
        TomTom:AddWaypoint(mapId, x/100, y/100, {
            title = desc or L["TomTom waypoint"],
            from = "TomTom/way",
        })
    elseif tonumber(tokens[1]) then
        -- A vanilla set command
        local x,y,desc = unpack(tokens)
        if not x or not tonumber(x) then
            return usage()
        elseif not y or not tonumber(y) then
            return usage()
        end
        if desc then
            desc = table.concat(tokens, " ", 3)
        end
        x = tonumber(x)
        y = tonumber(y)

        local m = TomTom:GetCurrentPlayerPosition()
        if m and x and y then
            TomTom:AddWaypoint(m, x/100, y/100, {
                title = desc or L["TomTom waypoint"],
                from = "TomTom/way",
            })
        end
    else
        return usage()
    end
end

