-- Kwraz's Flightpath Tracker Addon
--
-- Displays known flightpaths on the world map, etc.
--
-- All contents property of Kwraz of Icecrown. You are hereby given permission
-- to make changes to this addon provided proper credit is given if you
-- disseminate your changes publicly.
--
-- Comments and questions can be addressed to kwraz@kjware.net (or via in game
-- mail to Kwraz if you play Horde on Icecrown)
--

local VERSION = "1.16";

--  Date    Rev   Comments
-- ------   ----  -----------------------------------
-- 06/22/06 1.16  Updated for UI version 1.11
--
-- 10/12/05 1.15  Fixed error that occurred in 1.8 when reporting bound key
--                Updated TOC to 1.8
--
-- 9/13/05  1.14  Fixed WorldMapButton error in 1.7. UI version set to 1.7
--
-- 6/27/05  1.13  OnPlayerEnteringWorld logic only done once
--                Flight timer now takes UI delays into account
--                On screen destination width increased
--
-- 4/18/05  1.12  Removed debug statement causing user errors
--
-- 4/18/05  1.11  Fixed bug with /fp load introduced in 1.10
--
-- 4/18/05  1.10  SetMapToCurrentZone fixup
--                Bound key now toggles dialog on/off
--                Alt-z to hide UI now also hides time remaining counter
--                Cleaned up appearance of zone map connection tooltip
--                Changed regular expressions to correction handle foreign characters
--                Version number displayed on the dialog
--                Other faction's paths were showing on Booty Bay flightmaster. Fixed.
--                Changing zones with the dialog up caused the drop down to display
--                zones instead of locations. Fixed.
--
-- 4/14/05  1.09  Fixed incompatibility with VisibleFlightMap
--                Zone map icons changed to the same as the flight master's map
--                Undiscovered flight path locations show as grey on zone map
--                Dialog drop down box scaled down to fit within dialog
--
-- 4/13/05  1.08  Added bindings.xml to support hotkey assignment via wow menu
--                Flight master icons will no longer display on the zone map if greyed
--                Added flight times and costs to zone map tooltips
--                Added hideremaining and showremaining commands
--                Fixed problem with incorrect locations being stored for flight masters (hopefully)
--
-- 4/12/05  1.07  Cleaned up shared variable names to respect conventions. Added confirmation
--                dialog to /fp erase. Added MyAddons support. Flight time now shown when talking
--                to flight master. Flight time remaining countdown timer added. Flight duration
--                tracked separately for each direction. Added /fp load command. Fixed drop down
--                problem when more than 32 locations.
--
-- 4/8/05   1.06  Added /fp erase, /fp showgrey, /fp hidegray. Additional changes
--                made to location matching logic to try and avoid duplicate path
--                creation for Stormwind, Ironforge, and Moonglade.
--
-- 4/7/05   1.05  Fixed string error the first time FlightPath is installed.
--
-- 4/7/05   1.04  Whether or not to gray out a connection is now tracked by character,
--                since different characters will have different routes available to them.
--                Escape key now closes dialog.
--
-- 4/7/05   1.03  Greyed out unavailable routes in map tooltip as well.
--
-- 4/7/05   1.02  Preloaded flights you cannot take are now coded in a different color.
--
-- 4/6/05   1.01  Fixed a problem with the drop down list running off screen with a large number
--                of entries
-- 4/6/05   1.0   Initial release
--
---------------------------------------------------------------------

-- Info to show on the WoW Key Bindings menu

BINDING_HEADER_FPHEADER = "Kwraz's Flightpath Tracker";
BINDING_NAME_FPDIALOG = "Query dialog";

-- Constants

local STATUS_COLOR                  = "|c0033CCFF";
local CONNECTION_COLOR              = "|c0033FF66";
local MONEY_COLOR                   = "|c00FFCC33";
local DEBUG_COLOR                   = "|c0000FF00";
local GREY                          = "|c00909090";
local BRIGHTGREY                    = "|c00D0D0D0";
local WHITE                         = "|c00FFFFFF";
local MAX_CONNECTIONS               = 16;
local MAX_POI_BUTTONS               = 48;
local CONNECTION_START_ID           = 1101;
local FP_LEARNED_FROM_FLIGHTMASTER  = 1;
local FP_LEARNED_FROM_PRELOAD       = 2;
local NOCOORDS                      = "0,0";
local MAXDROPDOWN                   = 21;
local MORECOLOR                     = "|c00FF9900";
local MOREUP                        = MORECOLOR.."--- More ^ ---";
local MOREDOWN                      = MORECOLOR.."--- More v ---";

-- Shared variables

local StartRecording          = false;
local Recording               = false;
local TaxiOrigin              = "";
local TaxiDestination         = "";
local TaxiDirection           = 0;
local CurrentFlight           = {};
local StartTime;
local Connections             = {};
local LastTime                = time();
local PopulatePass            = 0;
local TaximapOpen             = false;
local TimeRemaining           = 0;
local DropDownStartIndex      = 1;

-- Hooked functions

local Original_WorldMapButton_OnUpdate;
local Original_TaxiNodeOnButtonEnter;
local Original_TakeTaxiNode;

---------------------------------------------------------------------

function FP_Help()
   FPCHAT(" ");
   FPCHAT(BINDING_HEADER_FPHEADER.." commands:");
   FPCHAT("/fp"                              ..FPSPACE(50)..WHITE.." - Displays the FlightPath dialog");
   FPCHAT("/fp enable | disable"             ..FPSPACE(24)..WHITE.." - Enables or disables the FlightPath addon");
   FPCHAT("/fp showgrey | hidegrey"          ..FPSPACE(17)..WHITE.." - Show greyed out flight paths or not");
   FPCHAT("/fp showremaining | hideremaining"..FPSPACE(0) ..WHITE.." - Show or hide in flight time remaining");
   FPCHAT("/fp erase"                        ..FPSPACE(40)..WHITE.." - Erases all remembered flight paths");
   FPCHAT("/fp load misc | horde | alliance" ..FPSPACE(7) ..WHITE.." - Load a supplied flight path list");
   FPCHAT("/fp status"                       ..FPSPACE(40)..WHITE.." - Displays various flight path statistics");
   FPCHAT(" ");
end

---------------------------------------------------------------------

-- Handle command line arguments

function FP_SlashHandler(msg)

   local _,_,command,options = string.find(msg,"([%w%p]+)%s*(.*)$");

   if       (command == nil)                             then  FP_FlightPathDialog();
   elseif   (string.lower(command) == 'enable')          then  FP_Enable();
   elseif   (string.lower(command) == 'disable')         then  FP_Disable();
   elseif   (string.lower(command) == 'status')          then  FP_Status();
   elseif   (string.lower(command) == 'erase')           then  FP_Erase();
   elseif   (string.lower(command) == 'showgrey')        then  FP_SetShowGrey();
   elseif   (string.lower(command) == 'hidegrey')        then  FP_SetHideGrey();
   elseif   (string.lower(command) == 'showremaining')   then  FP_SetShowRemaining();
   elseif   (string.lower(command) == 'hideremaining')   then  FP_SetHideRemaining();
   elseif   (string.lower(command) == 'load')            then  FP_Load(options);
   elseif   (string.lower(command) == 'check')           then  FP_Check();       -- Undocumented: Validate current flight master
   elseif   (string.lower(command) == 'debug')           then  FP_Debug();       -- Undocumented: Enable debug output
   elseif   (string.lower(command) == 'test')            then  FP_Test(options); -- Undocumented: Invoke testbed
   else                                                        FP_Help();
   end

end

---------------------------------------------------------------------

-- Tell WoW what events we are interested in being notified of

function FP_EventFrame_OnLoad(frameName)

   -- Register for any event we want to be notified of

   this:RegisterEvent("VARIABLES_LOADED");
   this:RegisterEvent("PLAYER_ENTERING_WORLD");
   this:RegisterEvent("TAXIMAP_OPENED");
   this:RegisterEvent("TAXIMAP_CLOSED");
   this:RegisterEvent("WORLD_MAP_UPDATE");

end

---------------------------------------------------------------------

-- Map events to the appropriate internal handler

function FP_EventFrame_OnEvent()

   if       (event == "VARIABLES_LOADED")       then FP_VariablesLoaded();
   elseif   (event == "PLAYER_ENTERING_WORLD")  then FP_PlayerEnteringWorld();
   elseif   (event == "TAXIMAP_OPENED")         then FP_TaxiMapOpened();
   elseif   (event == "TAXIMAP_CLOSED")         then FP_TaxiMapClosed();
   elseif   (event == "WORLD_MAP_UPDATE")       then FP_WorldMapUpdate();

   else FPDEBUG("Unhandled Event "..WHITE..event); end;
end

---------------------------------------------------------------------

function FP_VariablesLoaded()

   -- Update the global WoW command handler table so it knows about us

   SlashCmdList["FLIGHTPATH"] = FP_SlashHandler;
   SLASH_FLIGHTPATH1 = "/flightpath";
   SLASH_FLIGHTPATH2 = "/fp";

   if (FlightPath_Config == nil) then FP_InitializeConfig(); end

   -- Let them know we're up and running

   FPCHAT(BINDING_HEADER_FPHEADER.." version "..VERSION.." loaded. Type "..WHITE.."/fp ?"..STATUS_COLOR.." for more info.");
   local sbool = "disabled.";
   if(FlightPath_Config.Enabled) then sbool = "enabled."; end;
   FPCHAT("Flightpath hooks are "..sbool);

   -- MyAddons support

   if(myAddOnsFrame) then
      myAddOnsList.FlightPath = { name = "FlightPath", description = BINDING_HEADER_FPHEADER, version = VERSION, category = MYADDONS_CATEGORY_MAP, frame = "FP_UIFrame"};
   end

end

---------------------------------------------------------------------

function FP_InitializeConfig()
   FlightPath_Config = {};
   FlightPath_Config.Version = VERSION;
   FlightPath_Config.Enabled = true;
   FlightPath_Config.HideRemaining = false;
   FlightPath_Config.FlightPaths = {};
end

---------------------------------------------------------------------

function FP_PlayerEnteringWorld()
   FP_DoVersionFixups(); -- Perform any version specific fixups on the users database
   FlightPath_Config.Version = VERSION;
   FP_EnableHooks();
   FP_ReportBindingKey();
   this:UnregisterEvent("PLAYER_ENTERING_WORLD"); -- So boat/zep zoning doesn't call this again
end

---------------------------------------------------------------------

function FP_Enable()
   FlightPath_Config.Enabled = true;
   FP_EnableHooks();
   FPCHAT("FlightPath hooks have been enabled.");
end

---------------------------------------------------------------------

function FP_Disable()
   FlightPath_Config.Enabled = false;
   Recording = false; -- Just in case we are in flight
   FP_DisableHooks();
   FPCHAT("FlightPath hooks have been disabled.");
end

---------------------------------------------------------------------

function FP_SetShowRemaining()
   FlightPath_Config.HideRemaining = false;
   FPCHAT("The in flight time remaining display has been enabled.");
end

---------------------------------------------------------------------

function FP_SetHideRemaining()
   FlightPath_Config.HideRemaining = true;
   FPCHAT("The in flight time remaining display has been disabled.");
end

---------------------------------------------------------------------

-- User typed /fp erase. Clear out the old database and start from scratch.
-- But first make sure they really really want to

function FP_Erase()
   StaticPopupDialogs["FP_CONFIRM_ERASE"] = {
      text = "\nThis will erase ALL of your recorded flight paths.\n\nAre you sure?",
      button1 = TEXT(ACCEPT),
      button2 = TEXT(CANCEL),
      OnAccept = function()
      FP_EraseConfirmed();
      end,
      timeout = 0,
   };
   StaticPopup_Show("FP_CONFIRM_ERASE")
end

function FP_EraseConfirmed()
   FP_InitializeConfig();
   FPCHAT("All FlightPath routes have been cleared.");
end

---------------------------------------------------------------------

function FP_SetShowGrey()
   if(FlightPath_Config.HideGrey ~= nil) then
      for i in FlightPath_Config.HideGrey do
         if(FlightPath_Config.HideGrey[i] == FP_GetNameServer()) then
            table.remove(FlightPath_Config.HideGrey,i);
            break;
         end
      end
   end
   FPCHAT("Greyed out flight paths will now be shown for "..UnitName("player"));
end

---------------------------------------------------------------------

function FP_SetHideGrey()
   if(FlightPath_Config.HideGrey == nil) then FlightPath_Config.HideGrey = {}; end;

   local found = false;
   for i in FlightPath_Config.HideGrey do
      if(FlightPath_Config.HideGrey[i] == FP_GetNameServer()) then
         found = true;
         break;
      end
   end
   if(not found) then
      table.insert(FlightPath_Config.HideGrey,FP_GetNameServer());
   end
   FPCHAT("Greyed out flight paths will now be hidden for "..UnitName("player"));
end

---------------------------------------------------------------------

function FP_HideGrey()
   if(FlightPath_Config.HideGrey == nil) then return false; end;

   for i in FlightPath_Config.HideGrey do
      if(FlightPath_Config.HideGrey[i] == FP_GetNameServer()) then
         return true;
      end
   end
   return false;
end

---------------------------------------------------------------------

function FP_EnableHooks()

   -- Hook the world map button so we know when map zones change via pulldown menu
   if(WorldMapButton_OnUpdate ~= FP_WorldMapButton_OnUpdate) then
      Original_WorldMapButton_OnUpdate = WorldMapButton_OnUpdate;
      WorldMapButton_OnUpdate = FP_WorldMapButton_OnUpdate;
   end

   -- Hook the TaxiMap tooltip function so we can add duration info
   if(TaxiNodeOnButtonEnter ~= FP_TaxiNodeOnButtonEnter) then
      Original_TaxiNodeOnButtonEnter = TaxiNodeOnButtonEnter
      TaxiNodeOnButtonEnter = FP_TaxiNodeOnButtonEnter;
   end

   -- Hook the TakeTaxiNode function so we know where we are going
   if(TakeTaxiNode ~= FP_TakeTaxiNode) then
      Original_TakeTaxiNode = TakeTaxiNode;
      TakeTaxiNode = FP_TakeTaxiNode;
   end

end

---------------------------------------------------------------------

function FP_DisableHooks()

   -- Unhook the world map button
   if(Original_WorldMapButton_OnUpdate and (WorldMapButton_OnUpdate == FP_WorldMapButton_OnUpdate)) then
      WorldMapButton_OnUpdate = Original_WorldMapButton_OnUpdate;
   end

   -- Unhook the TaxiMap tooltip
   if(Original_TaxiNodeOnButtonEnter and (TaxiNodeOnButtonEnter == FP_TaxiNodeOnButtonEnter)) then
      TaxiNodeOnButtonEnter = Original_TaxiNodeOnButtonEnter;
   end

   -- Unhook the TakeTaxiNode function
   if(Original_TakeTaxiNode and (TakeTaxiNode == FP_TakeTaxiNode)) then
      TakeTaxiNode = Original_TakeTaxiNode;
   end

end

---------------------------------------------------------------------

function FP_DoVersionFixups()

   -- Check what version the user was running previously and do any needed upgrades

   if(FlightPath_Config.Version == nil) then return; end;

   local dbversion;
   _,_,dbversion = string.find(FlightPath_Config.Version,"(%d*.%d+)"); -- Beta had format ".98 beta" so only grab the number part
   dbversion = tonumber(dbversion);

   -- If this is a user that participated in the beta test, clear out the old (incompatible) database

   if(dbversion < 1.0) then
      FlightPath_Config.FlightPaths = {};
      FPCHAT("\nThanks for beta testing FlightPath!");
      FPCHAT("All of your flight path information has been reset.");
      FPCHAT("If you don't want to have to relearn all of your flight paths use the '/fp load' command.");

   -- Versions lower than 1.04 did not have the KnownBy field, and some had the deprecated Greyed flag.

   elseif(dbversion < 1.04) then
      FPCHAT("Converting FlightPath data from version "..dbversion.." to version "..VERSION);
      for i in FlightPath_Config.FlightPaths do

         local greyed = false;
         local thisPathKnownBy = FP_GetNameServer();

         if(FlightPath_Config.FlightPaths[i].Greyed ~= nil) then
            greyed = FlightPath_Config.FlightPaths[i].Greyed;
            if(not greyed) then
               -- This item was forced to be not greyed in prior versions. We accomplish
               -- the same thing in 1.04 by setting the KnownBy to ALL
               thisPathKnownBy = "ALL";
            end
         end
         if(not greyed) then
            if(not FP_UserKnowsPath(FlightPath_Config.FlightPaths[i])) then
               if(FlightPath_Config.FlightPaths[i].KnownBy == nil) then
                  FlightPath_Config.FlightPaths[i].KnownBy = {}
               end
               table.insert(FlightPath_Config.FlightPaths[i].KnownBy,thisPathKnownBy);
            end
         end
         FlightPath_Config.FlightPaths[i].Greyed = nil; -- Remove deprecated field
      end

   -- Version 1.07 changed the duration from a single value to a value for each direction

   elseif(dbversion < 1.07) then

      FPCHAT("Converting FlightPath data from version "..dbversion.." to version "..VERSION);
      for i in FlightPath_Config.FlightPaths do
         if(FlightPath_Config.FlightPaths[i].Duration ~= nil) then
            local duration = FlightPath_Config.FlightPaths[i].Duration;
            FlightPath_Config.FlightPaths[i].Duration = { duration, duration }; -- start with both directions the same
         end
      end

   end

   FlightPath_Config.Version = VERSION;

end

---------------------------------------------------------------------

function FP_EventFrame_OnUpdate()

   -- This function is called frequently so be cpu and memory friendly

   if (FlightPath_Config.Enabled) then

      FP_CheckInFlightStatus();

      local ctime = time();
      if(ctime ~= LastTime) then

         -- Do periodic processing (once per second or so)

         FP_ShowInFlightCountdown(ctime-LastTime);
         LastTime = ctime;

      end

   end
end

---------------------------------------------------------------------

function FP_CheckInFlightStatus()

-- Check the flight recording state.

   if (Recording and not UnitOnTaxi("player")) then
      -- End of the road, stop recording
      FP_FlightPathEnd();
      Recording = false;
      StartRecording = false;
   end

   if (StartRecording) then
      if (UnitOnTaxi("player")) then
         FPDEBUG("Beginning flight from "..TaxiOrigin.." to "..TaxiDestination);
         StartRecording = false;
         Recording = true;
      end
   end

end

---------------------------------------------------------------------

function FP_ShowInFlightCountdown(secondsElapsed)

   if(TimeRemaining > 0) then
      TimeRemaining = TimeRemaining - secondsElapsed;  -- Decrease in flight time remaining
   end

   if((not FlightPath_Config.HideRemaining) and (TimeRemaining > 0)) then
      if(UnitOnTaxi("player")) then
         FP_CountdownTimerCity:SetText(TaxiDestination);
         FP_CountdownTimerCity:Show();
         local text = "Arriving in ";
         text = text..FP_FormatTime(TimeRemaining);
         FP_CountdownTimerRemaining:SetText(text);
         FP_CountdownTimerRemaining:Show();
      end
   else
      FP_CountdownTimerCity:Hide();
      FP_CountdownTimerRemaining:Hide();
   end

end

---------------------------------------------------------------------

function FP_GetNameServer()
   local name, server;
   name = UnitName("player");
   server = GetCVar("realmName");
   return name..","..server
end

---------------------------------------------------------------------

function FP_TaxiMapOpened()

   if(FlightPath_Config.Enabled) then

      TaximapOpen = true;

      -- Loop through the displayed buttons on the taxi map and
      -- add any routes that we haven't yet recorded

      local numButtons = NumTaxiNodes();

      -- Find out what flight masters like to call this location

      for i = 1, numButtons, 1 do
         if(TaxiNodeGetType(i) == "CURRENT") then
            TaxiOrigin = TaxiNodeName(i);
            break;
         end
      end

      -- Now check for new flight paths between here and all the ones shown on the taxi map

      local x, y = FP_GetPlayerCoords();
      local continent,zone = FP_GetRealContinentZone();
      FPDEBUG("Setting continent,zone,coords for current flight master to:",continent,zone,x,y);

      for i = 1, numButtons, 1 do
         if(TaxiNodeGetType(i) == "REACHABLE") then
            local index, path;
            index,path = FP_FindPath(TaxiOrigin, TaxiNodeName(i));
            if(index == 0) then
               path = {};
               path.Endpoints = {TaxiOrigin,TaxiNodeName(i)};
               path.Continent = {continent,0};
               path.Zone = {zone,0};
               path.Faction = UnitFactionGroup("player");
               path.Coords = {x..","..y,NOCOORDS};
            else
               if(path.Endpoints[1] == TaxiOrigin) then
                  path.Coords[1] = x..","..y;
                  path.Continent[1] = continent;
                  path.Zone[1] = zone;
               else
                  path.Coords[2] = x..","..y;
                  path.Continent[2] = continent;
                  path.Zone[2] = zone;
               end
            end
            path.Cost = TaxiNodeCost(i);
            FP_AddNewFlightPath(path,FP_LEARNED_FROM_FLIGHTMASTER);
         end
      end

   end
end

---------------------------------------------------------------------

function FP_TaxiMapClosed()
   if(FlightPath_Config.Enabled) then
      TaximapOpen = false;
   end
end

---------------------------------------------------------------------

function FP_TakeTaxiNode(nodeID)

   TaxiDestination = TaxiNodeName(nodeID);
   StartTime = time();
   TimeRemaining = 0;
   local index;

   index,CurrentFlight = FP_FindPath(TaxiOrigin, TaxiDestination);

   if(index == 0) then
      FPDEBUG("Internal error! FlightPath could not locate the flight path from "..TaxiOrigin.." to "..TaxiDestination);
   else

      -- Durations are stored associated with the from endpoint. TaxiDirection tells whether we are
      -- traveling from the first endpoint or the second endpoint of the connection pair.

      if(TaxiOrigin == CurrentFlight.Endpoints[1]) then TaxiDirection = 1; else TaxiDirection = 2; end

      if(CurrentFlight.Duration == nil) then
         CurrentFlight.Duration = {"",""};
      else
         local _,_,minutes,seconds = string.find(CurrentFlight.Duration[TaxiDirection],"(%d+):(%d+)");
         if(minutes and seconds) then
            TimeRemaining = (minutes*60) + seconds;
         end
      end
   end

   StartRecording =true; -- Tell the OnUpdate handler to start checking flight status

   Original_TakeTaxiNode(nodeID); -- Call the original handler now
end

---------------------------------------------------------------------

function FP_FlightPathEnd()

   -- We've landed. Record the duration of the flight and the coordinates of the
   -- flightmaster here if not already known.

   local x, y = FP_GetPlayerCoords();
   local destIndex = 2;
   if(TaxiDirection == 2) then destIndex = 1; end;

   TimeRemaining = 0;
   CurrentFlight.Duration[TaxiDirection] = FP_Elapsed(StartTime,time());
   CurrentFlight.Coords[destIndex] = x..","..y;
   CurrentFlight.Continent[destIndex],CurrentFlight.Zone[destIndex] = FP_GetRealContinentZone();

   FP_AddNewFlightPath(CurrentFlight,FP_LEARNED_FROM_FLIGHTMASTER);
end

---------------------------------------------------------------------

function FP_AddNewFlightPath(new,source)

   -- We have a potential new flight path. If we already know the path then update any
   -- relevant information, otherwise add the path to our data base.

   local index,foundPath = FP_FindPath(new.Endpoints[1],new.Endpoints[2]);
   local isNewPath = (foundPath == nil);

   -- Update the coords, continent, and zone of any connection point that matches one in the new path.
   -- (The need for this will go away once I properly normalize the data tables)

   if(source == FP_LEARNED_FROM_FLIGHTMASTER) then
      for i in FlightPath_Config.FlightPaths do
         for j = 1, 2, 1 do
            for k = 1, 2, 1 do
               if (new.Endpoints[j] == FlightPath_Config.FlightPaths[i].Endpoints[k]) then
                  if(new.Coords[j] ~= NOCOORDS) then
                     if(new.Coords[j] ~= FlightPath_Config.FlightPaths[i].Coords[k]) then
                        FPDEBUG("Replacing coords on flightpath "..i.." ("..FlightPath_Config.FlightPaths[i].Endpoints[k]..")\nFrom "..FlightPath_Config.FlightPaths[i].Coords[k].." to "..new.Coords[j]);
                        FlightPath_Config.FlightPaths[i].Coords[k] = new.Coords[j];
                     end
                  end
                  if(new.Continent[j] ~= 0) then FlightPath_Config.FlightPaths[i].Continent[k] = new.Continent[j]; end
                  if(new.Zone[j] ~= 0) then FlightPath_Config.FlightPaths[i].Zone[k] = new.Zone[j]; end
               end
            end
         end
      end
   end

   -- Either add it to our flight path database or update the cost and duration

   if(isNewPath) then
      table.insert(FlightPath_Config.FlightPaths,new);
      index = table.getn(FlightPath_Config.FlightPaths);
      if (source == FP_LEARNED_FROM_FLIGHTMASTER) then
         FPCHAT("FlightPath learned a new route between "..new.Endpoints[1].." and "..new.Endpoints[2]);
      end
   else

      -- I'm going to go on the assumption that information coming in from a flightmaster is more current
      -- than the potentially stale preloaded flight path information. If the user types a /fp load command
      -- after having learned their own flight paths I want to ensure that the preloaded data does not overwrite
      -- the learned data. For this reason costs and times are only updated in the database when the path is
      -- one we obtained from a flight master. (On an initial /fp load the paths will be unknown so the the
      -- times and costs from the preloaded paths will be the initial values).

      if(source == FP_LEARNED_FROM_FLIGHTMASTER) then

         -- Cost
         if(new.Cost) then FlightPath_Config.FlightPaths[index].Cost = new.Cost; end;

         -- Duration
         if(new.Duration and (table.getn(new.Duration) >= 1)) then

            if(FlightPath_Config.FlightPaths[index].Duration == nil) then FlightPath_Config.FlightPaths[index].Duration = {"",""}; end

            if(new.Endpoints[1] == FlightPath_Config.FlightPaths[index].Endpoints[1]) then
               FlightPath_Config.FlightPaths[index].Duration[1] = new.Duration[1];
               if(FlightPath_Config.FlightPaths[index].Duration[2] == "") then FlightPath_Config.FlightPaths[index].Duration[2] = new.Duration[2]; end
            else
               FlightPath_Config.FlightPaths[index].Duration[2] = new.Duration[1];
               if(FlightPath_Config.FlightPaths[index].Duration[1] == "") then FlightPath_Config.FlightPaths[index].Duration[1] = new.Duration[2]; end
            end

            -- If one direction still has no duration, assume it's the same as the other leg

            if(FlightPath_Config.FlightPaths[index].Duration[1] == "") then
               FlightPath_Config.FlightPaths[index].Duration[1] = FlightPath_Config.FlightPaths[index].Duration[2];
            end
            if(FlightPath_Config.FlightPaths[index].Duration[2] == "") then
               FlightPath_Config.FlightPaths[index].Duration[2] = FlightPath_Config.FlightPaths[index].Duration[1];
            end
         end;

      end
   end

   -- Record the fact that the user knows this flight so it no longer displays as grey or shows up if they have hidegrey set

   if(source == FP_LEARNED_FROM_FLIGHTMASTER) then
      if(not FP_UserKnowsPath(FlightPath_Config.FlightPaths[index])) then
         if(FlightPath_Config.FlightPaths[index].KnownBy == nil) then FlightPath_Config.FlightPaths[index].KnownBy = {}; end
         table.insert(FlightPath_Config.FlightPaths[index].KnownBy,FP_GetNameServer());
      end
   end

end

---------------------------------------------------------------------

function FP_Elapsed(start,finish)
   return FP_FormatTime(finish-start);
end

---------------------------------------------------------------------

function FP_FormatTime(duration)
   local minutes = floor(duration / 60);
   local seconds = duration - (minutes * 60);
   local tens = floor(seconds/10);
   local single = seconds - (tens * 10);
   return minutes..":"..tens..single;
end

---------------------------------------------------------------------

function FP_GetPlayerCoords()

   FP_ForceMapToCurrentZone(); -- voodoo here to try and address WoW bug

   local px, py = GetPlayerMapPosition("player");

   -- normalize coords to ##.## format

   px = floor(px * 10000);
   py = floor(py * 10000);
   px = px / 100;
   py = py / 100;
   return px,py
end

---------------------------------------------------------------------

function FP_ForceMapToCurrentZone()
   if(GetCurrentMapZone() ~= 0) then
      SetMapToCurrentZone();  -- Normally this is what will happen
   else
      local continent,zone = FP_GetRealContinentZone();
      SetMapZoom(continent,zone);
   end
end

---------------------------------------------------------------------

function FP_GetRealContinentZone()

   if(GetCurrentMapZone() ~= 0) then
      return GetCurrentMapContinent(),GetCurrentMapZone();
   end

   -- We're bugged. Fortunately GetRealZoneText() is accurate so we can use that
   -- to figure out our zone. (Remember, continent and zone numbers equate to their
   -- index number in the world map pulldowns). This idea was first implemented by
   -- Legorol in his ZoneFix mod.

   local continent,zone,name
   local zoneText = GetRealZoneText();
   for continent in ipairs{GetMapContinents()} do
      for zone,name in ipairs{GetMapZones(continent)} do
         if(name == zoneText) then
            FPDEBUG("SetMapToCurrentZone bug detected!! You should zone or logout before talking to any flight masters. Returning fixed up continent,zone:",continent,zone);
            return continent,zone;
         end
      end
   end
   return 0,0; -- Should never happen
end

---------------------------------------------------------------------

function FP_GetLocale()

   local zone = GetRealZoneText();
   local city = GetMinimapZoneText();

   if (zone == city) then
      return zone;
   else
      return city..", "..zone;
   end

end

---------------------------------------------------------------------

function FP_Load(option)

   if(option == nil or option == "") then
      FPCHAT("Usage: "..WHITE.."/fp load name"..STATUS_COLOR.." where name is one of 'misc', 'horde', or 'alliance'.");
      return;
   end

   local toLoad = getglobal("FP_"..option.."Paths");
   if(toLoad == nil) then
      FPCHAT("Could not find a list called "..option.."Paths in the KnownPaths.lua file.\nType '/fp ?' for help.");
      return;
   end

   for i in toLoad do
      FP_AddNewFlightPath(toLoad[i],FP_LEARNED_FROM_PRELOAD);
   end

   FPCHAT("Scanned "..table.getn(toLoad).." "..option.." paths for new connections.");

end

---------------------------------------------------------------------

function FP_IsSameLocation(location1, location2)

   -- So why are we going through all these gyrations to see if one location is
   -- the same as another? Because WoW uses different location names in the taxi
   -- map than they do for the city and zone names proper. For example. the
   -- flight master in Orgrimmar is located in the "Valley of Strength, Orgrimmar"
   -- if you use GetZoneText() or other API calls. However the name of the
   -- taxi node for that flight master is "Orgrimmar, Durotar". There are many
   -- other locations in Azeroth that have similar discrepancies. (Ironforge,
   -- Moonglade, ...)
   --
   -- In order to bring up the correct dialog page for the location the player
   -- is currently, we need to correlate these two different versions of a place name.

   -- See if they just match

   if(location1 == location2) then return true; end

   local c1, z1, c2, z2;
   c1,z1 = FP_ParseLocation(location1);
   c2,z2 = FP_ParseLocation(location2);

   -- See if city names match (since city names are unique)

   if((c1 ~= "") and (c1 == c2)) then return true; end

   -- See if the city and zones are swapped (e.g. Orgrimmar)

   if((c1 ~= "") and (c1 == z2)) then return true; end
   if((c2 ~= "") and (c2 == z1)) then return true; end

   -- See if parts of city match (e.g. "The Crossroads", "Crossroads")

   local b, e, match;
   if(c1 ~= "" and c2 ~= "") then
      b,e,match = string.find(c1,c2,1,true);
      if(b) then return true; end;

      b,e,match = string.find(c2,c1,1,true);
      if(b) then return true; end;
   end

   -- See if parts of zone match city (e.g. "Stormwind,Elwynn", "Trade District,Stormwind City")

   if(c1 ~= "" and z2 ~= "") then
      b,e,match = string.find(c1,z2,1,true);
      if(b) then return true; end;
      b,e,match = string.find(z2,c1,1,true);
      if(b) then return true; end;
   end
   if(c2 ~= "" and z1 ~= "") then
      b,e,match = string.find(c2,z1,1,true);
      if(b) then return true; end;
      b,e,match = string.find(z1,c2,1,true);
      if(b) then return true; end;
   end

   -- Give up and assume the locations are not the same

   return false;
end

---------------------------------------------------------------------

function FP_FindCoords(location)

   -- Loop through all known flight paths and see if we can find the
   -- coordinate position of the specified flight master.
   -- This will be uneccessary when I properly normalize the data tables

   local coords = NOCOORDS;

   for i in FlightPath_Config.FlightPaths do

      if(FP_IsSameLocation(location,FlightPath_Config.FlightPaths[i].Endpoints[1])) then
         if(FlightPath_Config.FlightPaths[i].Coords[1] ~= NOCOORDS) then
            coords = FlightPath_Config.FlightPaths[i].Coords[1];
            break;
         end
      end

      if(FP_IsSameLocation(location,FlightPath_Config.FlightPaths[i].Endpoints[2])) then
         if(FlightPath_Config.FlightPaths[i].Coords[2] ~= NOCOORDS) then
            coords = FlightPath_Config.FlightPaths[i].Coords[2];
            break;
         end
      end

   end

   return coords;
end

---------------------------------------------------------------------

function FP_GetRecordedFlightpaths()

   -- Return a list of known locations for display in the dialog drop down list

   local flightpaths = {}; -- reverse zone and city so list is sorted by zone
   local lookup = {}

   for i in FlightPath_Config.FlightPaths do
      if (FlightPath_Config.FlightPaths[i].Faction == UnitFactionGroup("player")) then
         if(FP_UserKnowsPath(FlightPath_Config.FlightPaths[i]) or (not FP_HideGrey())) then
            for j = 1, 2, 1 do
               local tcity, tzone = FP_ParseLocation(FlightPath_Config.FlightPaths[i].Endpoints[j]);
               local zoneFirst = tzone;
               if(tcity ~= "") then zoneFirst = zoneFirst.." - "..tcity; end
               if (lookup[zoneFirst] == nil) then
                  lookup[zoneFirst] = true;
                  table.insert(flightpaths,zoneFirst);
               end
            end
         end
      end
   end

   table.sort(flightpaths);
   return flightpaths;
end

---------------------------------------------------------------------

function FP_ParseCoord(coord)
   local _, _, x, y = string.find(coord,"(%d*.%d*),(%d*.%d*)");
   return tonumber(x), tonumber(y);
end

---------------------------------------------------------------------

function FP_ParseLocation(locale)

   -- locale can be:
   -- zone              we are in an unnamed area of a zone
   -- city, zone        normal parse
   -- zone - city       dash means swap zone city order (is dropdown entry)

   if(not locale) then return "",""; end

   local b, e, city, zone, dash;

   b,e,dash = string.find(locale," - ", 1, true); -- it's in dropdown format if it has a dash
   if(b) then
      zone = FP_Trim(string.sub(locale,1,b-1));
      city = FP_Trim(string.sub(locale,e+1,string.len(locale)));
      return city,zone;
   end

   b,e,city = string.find(locale, "([%P%'%` ]+),"); -- This should be good for foreign character sets as well
   if(not b) then
      city = "";
      zone = FP_Trim(locale);
   else
      b,e,zone = string.find(locale, "[%P%'%` ]+,%s*([%P%'%` ]+)");
      if(not b) then
         -- For degenerate cases like Moonglade that don't return a zone use the city name as the zone
         zone = FP_Trim(city);
      end;
   end

   return city,zone;
end

---------------------------------------------------------------------

function FP_Trim(text)
   -- Remove trailing spaces from a string
   if(text == nil) then return; end;
   local _,_,trimmed = string.find(text,"(.-)%s*$");
   return trimmed;
end

---------------------------------------------------------------------
--
--              User Interface logic
--
---------------------------------------------------------------------

function FP_FlightPathDialog()

   if (FP_UIFrame:IsVisible() ) then
      FP_UIFrame:Hide();   -- So bound key toggles dialog on/off
   else
      DropDownStartIndex=1;
      FP_DisplayDialogConnections();
      FP_UIFrame:Show();
   end

end

---------------------------------------------------------------------

function FP_WorldMapUpdate()

   -- Changing zones overwrites DropDownList1 which the dialog uses.
   -- If we change zones with the dialog open, repopulate the list

   if (FP_UIFrame:IsVisible() ) then
      FP_UIZoneDropDown_Initialize();
   end

end

---------------------------------------------------------------------

function FP_UIZoneDropDown_Initialize()

   -- Called from the frames OnShow

   FP_UIZoneDropDownClear();

   local index=DropDownStartIndex;
   local info = {};
   local buttonCount = 0;

   local flightpaths = FP_GetRecordedFlightpaths();
   local knownCount = table.getn(flightpaths);

   for i = 1, MAXDROPDOWN, 1 do

      local buttonText = "";

      if((i == 1) and (DropDownStartIndex > 1)) then
         buttonText = MOREUP;

      elseif((i == MAXDROPDOWN) and (index < (knownCount-1))) then
         buttonText = MOREDOWN;

      else
         if(index > knownCount) then break;end;
         buttonText = flightpaths[index];
         index = index + 1;
      end

      info={};
      info.text = buttonText;
      info.func = FP_UIZoneDropDown_OnClick;
      info.keepShownOnClick = true;
      info.notCheckable = true; -- WoW is bugged and will still show check but at least save width
      UIDropDownMenu_AddButton(info,1);
   end

   DropDownList1:ClearAllPoints();
   DropDownList1:SetPoint("CENTER","FP_UIFrame","CENTER",0,0);

end

---------------------------------------------------------------------

function FP_UIZoneDropDownClear()
   local text = UIDropDownMenu_GetText(FP_UIZoneDropDown);
   UIDropDownMenu_ClearAll(FP_UIZoneDropDown);
   UIDropDownMenu_SetText(text,FP_UIZoneDropDown);
   DropDownList1.numButtons = 0;
end

---------------------------------------------------------------------

function FP_UIZoneDropDown_OnClick()

   if(this:GetText() == MOREDOWN) then
      FPDEBUG("Down arrow detected");
      local flightpaths = FP_GetRecordedFlightpaths();
      local knownCount = table.getn(flightpaths);
      DropDownStartIndex = knownCount - MAXDROPDOWN+1;
      FP_UIZoneDropDown_Initialize();
      return;

   elseif(this:GetText() == MOREUP) then
      FPDEBUG("Up arrow detected");
      DropDownStartIndex = 1;
      FP_UIZoneDropDown_Initialize();
      return;

   else
      FP_DisplayDialogConnections(this:GetText());
      DropDownList1:Hide();
   end

end

---------------------------------------------------------------------

function FP_FormatMoney(money)

   if(money == nil) then return ""; end;

   amount = tonumber(money);
   if(amount == nil) then
      FPDEBUG("Failed to convert money string to number: ",money);
      return ""
   end

   local gold, silver, copper, text;

   gold = floor(amount / (100*100));
   silver = mod(floor(amount / 100),  100);
   copper = mod(floor(amount + .5), 100);

   text = "";
   if(gold > 0) then text = gold.."g"; end
   if(silver > 0) then
      if(text ~= "") then text = text.." "; end;
      text = text..silver.."s";
   end
   if(copper > 0) then
      if(text ~= "") then text = text.." "; end;
      text = text..copper.."c";
   end
   return text;
end
---------------------------------------------------------------------

function FP_DisplayDialogConnections(location)

   -- Display the appropriate connections text on the FlightPath dialog box

   local text;
   local routeFound = false;
   local databaseIsEmpty = true;
   local tlocation, b, e, i, j, duration;

   -- Used to make a second attempt based on zone number if we fail to populate based on location text

   PopulatePass = PopulatePass+1;
   local _,currentMapZone = FP_GetRealContinentZone();
   local repopulateLoc = "";

   if(not location) then
      location = FP_GetLocale(); -- On initial call determine where we are
   end
   tlocation = location;

   Connections = {}; -- clear shared table
   local connectionCost = {};
   local connectionColor = {};

   for i in FlightPath_Config.FlightPaths do
      if (FlightPath_Config.FlightPaths[i].Faction == UnitFactionGroup("player")) then
         for j = 1, 2, 1 do

            if(FlightPath_Config.FlightPaths[i].Zone[j] == currentMapZone) then
               repopulateLoc = FlightPath_Config.FlightPaths[i].Endpoints[j]; -- In case we need to make 2nd pass cause 1st came up empty
            end

            if(FP_IsSameLocation(location,FlightPath_Config.FlightPaths[i].Endpoints[j])) then
               tlocation = FlightPath_Config.FlightPaths[i].Endpoints[j];
               local which = 1;
               if(j == 1) then which = 2; end;

               -- If this is a preloaded flight can't yet fly, grey it out

               local color = CONNECTION_COLOR;
               local mcolor = MONEY_COLOR;
               if(not FP_UserKnowsPath(FlightPath_Config.FlightPaths[i])) then
                  color = GREY;
                  mcolor = GREY;
               end

               -- Only show greyed paths if user hasn't disabled displaying them

               if((color ~= GREY) or (not FP_HideGrey())) then

                  connectionColor[FlightPath_Config.FlightPaths[i].Endpoints[which]] = color;

                  -- Add this endpoint to our available connections

                  table.insert(Connections,FlightPath_Config.FlightPaths[i].Endpoints[which]);

                  -- If we know the cost, display it

                  text = ""
                  if(FlightPath_Config.FlightPaths[i].Cost) then
                     text =  mcolor..FP_FormatMoney(FlightPath_Config.FlightPaths[i].Cost);
                  end

                  -- If we know the duration, display it

                  duration = FP_GetDuration(FlightPath_Config.FlightPaths[i], FlightPath_Config.FlightPaths[i].Endpoints[j]);
                  if(duration == nil or duration == "") then duration = "        "; end; -- Because column is right justified
                  text = text..color.."  "..duration;
                  connectionCost[FlightPath_Config.FlightPaths[i].Endpoints[which]] = text;

                  routeFound = true;
               end
            end
            databaseIsEmpty = false;
         end
      end
   end
   table.sort(Connections);

   FP_ClearDialogScreen();

   if (routeFound) then

      local text;

      FP_UIStaticConnections:SetText(WHITE.."Connections");
      FP_UIStaticZone:SetText(WHITE.."Zone");
      for i in Connections do
         if(i > MAX_CONNECTIONS) then
            FPCHAT("Maximum number of connections ("..MAX_CONNECTIONS..") exceeded for zone "..zone..".");
            break;
         end
         text = getglobal("FP_Connection"..i);
         text:SetText(connectionColor[Connections[i]]..Connections[i]);
         text = getglobal("FP_ConnectionCost"..i);
         text:SetText(connectionCost[Connections[i]]);
      end

      local tcity, tzone = FP_ParseLocation(tlocation);
      tlocation = tzone;
      if(tcity ~= "") then
         tlocation = tlocation.." - "..tcity;
      end
      UIDropDownMenu_SetText(tlocation,FP_UIZoneDropDown);
      FP_UIErrorText:SetText("");
      PopulatePass = 0;
   else
      UIDropDownMenu_SetText("",FP_UIZoneDropDown);
      if (databaseIsEmpty) then
         -- If we don't know any flight paths at all display a reassuring message to the user
         text = WHITE..
            "FlightPath has not yet learned any flight paths for the "..UnitFactionGroup("player")..".\n\n"..
            "As you travel around Azeroth, FlightPath will learn the flight paths available to you.\n\n"..
            "Soon it will know connections for each flight master, how much each flight costs, and how long it takes to get from one point to another.\n\n"..
            "If you would rather not wait and would like to load any of the optional flight paths available, use the "..STATUS_COLOR.."/fp load"..WHITE.." command.";
         FP_UIErrorText:SetText(text);
         return;
      else
         if((PopulatePass == 1) and (repopulateLoc ~= "")) then
            -- Didn't match anything first time through
            FP_DisplayDialogConnections(repopulateLoc);
            PopulatePass = 0;
         end
      end
   end

end

---------------------------------------------------------------------

function FP_ClearDialogScreen()

   local button;

   FP_UIErrorText:SetText("");
   FP_UIStaticConnections:SetText("");

   for i = 1, MAX_CONNECTIONS, 1 do

      button = getglobal("FP_Connection"..i);
      button:SetText("");

      button = getglobal("FP_ConnectionCost"..i);
      button:SetText("");

   end

   FP_DialogHeading:SetText(BRIGHTGREY..BINDING_HEADER_FPHEADER);
   FP_DialogVersion:SetText(GREY.."Version "..VERSION);
end

---------------------------------------------------------------------

function FP_UIFrame_OnShow()
--   UIDropDownMenu_Initialize(FP_UIZoneDropDown, FP_UIZoneDropDown_Initialize);
end

---------------------------------------------------------------------

function FP_Clicked(leftOrRight, buttonID)

   -- Process left mouse button click

   local index = 0;
   local tzone, tcity;

   if (tonumber(buttonID) >= CONNECTION_START_ID) then
      index = buttonID - CONNECTION_START_ID + 1;
      if(index <= table.getn(Connections)) then

         if ( leftOrRight == "LeftButton" ) then

            FP_DisplayDialogConnections(Connections[index]);

         else
            local continent, zone = GetMapNumbers(Connections[index]);
            if((continent ~= 0) and (zone ~= 0)) then
               -- Display appropriate map on right click
               ToggleWorldMap();
               SetMapZoom(continent,zone);
            end
         end
      end

   else
      FPCHAT("FlightPath received a click from unknown button "..buttonID.."");
   end

   return;
end

---------------------------------------------------------------------

function FP_UserKnowsPath(path)

   if(path.KnownBy == nil) then return false; end;

   for i in path.KnownBy do
      if(path.KnownBy[i] == "ALL") then return true; end;
      if(path.KnownBy[i] == FP_GetNameServer()) then return true; end;
   end

   return false;
end

---------------------------------------------------------------------

function GetMapNumbers(location)
   local continent, zone;
   for i in FlightPath_Config.FlightPaths do
      for j = 1, 2, 1 do
         if( FP_IsSameLocation(location,FlightPath_Config.FlightPaths[i].Endpoints[j])) then
            continent = FlightPath_Config.FlightPaths[i].Continent[j];
            zone = FlightPath_Config.FlightPaths[i].Zone[j];
            if(continent ~= 0 and zone ~= 0) then
               return continent, zone;
            end
         end
      end
   end
   return 0,0;
end

---------------------------------------------------------------------

function FP_Status()

   if (FlightPath_Config.Enabled) then
      FPCHAT(BINDING_HEADER_FPHEADER.." version "..VERSION.." is enabled.");
   else
      FPCHAT(BINDING_HEADER_FPHEADER.." version "..VERSION.." is disabled.");
   end

   FP_ReportBindingKey();

   if(FlightPath_Config.HideRemaining) then FPCHAT("The in flight time remaining counter is disabled."); end;

   local x, y = FP_GetPlayerCoords();
   FPCHAT("You are now at location "..x..","..y.." in "..FP_GetLocale());
   if (UnitOnTaxi("player")) then
      FPCHAT("You are currently flying from "..TaxiOrigin.." to "..TaxiDestination);
   end

   local flightpaths = FP_GetRecordedFlightpaths();
   if (flightpaths) then
      faction = UnitFactionGroup("player")
      local factionRoutes = 0
      for i in FlightPath_Config.FlightPaths do
         if (FlightPath_Config.FlightPaths[i].Faction == faction) then
            factionRoutes = factionRoutes + 1;
         end
      end
      FPCHAT("FlightPath knows of "..factionRoutes.." "..faction.." routes between "..table.getn(flightpaths).." locations.");
   end
end

---------------------------------------------------------------------

function FP_ReportBindingKey()
   if(GetBindingKey("FPDIALOG")) then
      FPCHAT("The FlightPath dialog is bound to key "..GetBindingKey("FPDIALOG"));
   end
end

---------------------------------------------------------------------
--
--              World Map logic
--
---------------------------------------------------------------------

function FP_Round(value)
   return math.floor(value*10000.0)/10000.0;
end

---------------------------------------------------------------------

function FP_GetPOITooltipText(location)

   local leftSide = {};
   local rightSide = {}
   local text;
   local routeFound = false;
   local city, zone, tcity, tzone, i;

   if(not location) then
      city = "";
      zone = WorldMapZoneDropDownText:GetText();
   else
      city, zone = FP_ParseLocation(location);
   end

   leftSide[1] = location;             rightSide[1] = " ";
   leftSide[2] = " ";                  rightSide[3] = " ";
   leftSide[3] = WHITE.."Connections:";rightSide[4] = " ";
   leftSide[4] = " ";                  rightSide[5] = " ";
   local lineNumber = 5;

   local poiConnections = {};
   local poiCosts = {}
   local unique = {};
   local connectionColors = {};
   local connection,cost,duration, pline;

   for i in FlightPath_Config.FlightPaths do
      if (FlightPath_Config.FlightPaths[i].Faction == UnitFactionGroup("player")) then
         local color = STATUS_COLOR;
         if(not FP_UserKnowsPath(FlightPath_Config.FlightPaths[i])) then color = GREY; end

         if((color ~= GREY) or (not FP_HideGrey())) then
            tcity, tzone = FP_ParseLocation(FlightPath_Config.FlightPaths[i].Endpoints[1]);
            if ((((city == "") or (city == tcity)) and (zone == tzone))) then
               if(unique[FlightPath_Config.FlightPaths[i].Endpoints[2]] == nil) then
                  unique[FlightPath_Config.FlightPaths[i].Endpoints[2]] = true;
                  connection, cost = FP_BuildPOITipLine(FlightPath_Config.FlightPaths[i],2,location, color == GREY)
                  table.insert(poiConnections,connection);
                  poiCosts[connection] = cost;
                  connectionColors[connection] = color;
                  routeFound = true;
               end
            end
            tcity, tzone = FP_ParseLocation(FlightPath_Config.FlightPaths[i].Endpoints[2]);
            if ((((city == "") or (city == tcity)) and (zone == tzone))) then
               if(unique[FlightPath_Config.FlightPaths[i].Endpoints[1]] == nil) then
                  unique[FlightPath_Config.FlightPaths[i].Endpoints[1]] = true;
                  connection, cost = FP_BuildPOITipLine(FlightPath_Config.FlightPaths[i],1,location, color == GREY)
                  table.insert(poiConnections,connection);
                  poiCosts[connection] = cost;
                  connectionColors[connection] = color;
                  routeFound = true;
               end
            end
         end
      end
   end
   table.sort(poiConnections);

   if (routeFound) then
      for i in poiConnections do
         leftSide[lineNumber] = connectionColors[poiConnections[i]]..poiConnections[i];
         rightSide[lineNumber] = poiCosts[poiConnections[i]];
         lineNumber = lineNumber + 1;
      end
   else
      leftSide[lineNumber] = STATUS_COLOR.."No connections found.";
      rightSide[lineNumber] = " ";
   end

   return leftSide,rightSide;
end

---------------------------------------------------------------------

function FP_BuildPOITipLine(path,index,location,isGrey)
   local text = "";
   local color;
   local duration = FP_GetDuration(path,location);
   if(path.Cost ~= nil and path.Cost ~= "") then
      color = MONEY_COLOR;
      if(isGrey) then color = GREY; end;
      text = color..FP_FormatMoney(path.Cost);
   end

   if(duration ~= nil and duration ~="") then
      color = WHITE;
      if(isGrey) then color = GREY; end;
      text = text.."  "..color..duration;
   else
      text = text.."          ";
   end
   return path.Endpoints[index],text;
end

---------------------------------------------------------------------

function FP_WorldMapButton_OnUpdate(elapsed)

   Original_WorldMapButton_OnUpdate(elapsed); -- Call the original owner of the hook so we don't break other mods

   if(FlightPath_Config.Enabled) then

      local path;
      local buttonCount = 0;

      -- Hide all the POI buttons

      for i = 1, MAX_POI_BUTTONS, 1 do
         POI = getglobal("FP_POI"..i);
         if(POI) then
            POI:ClearAllPoints();
            POI.Location = nil;
            POI:Hide();
         end
      end

      -- What map are we looking at?

      -- Returns 0=world, 1=kalimdor or a zone in kalimdor, 2=ek or a zone in ek
      local continent = GetCurrentMapContinent();
      if(continent ==0) then return; end; -- We don't support the world map yet

      -- Returns zone index or 0 if showing the entire continent
      local zone = GetCurrentMapZone();
      if(zone == 0) then return; end; -- We don't support the continent map yet

      -- Create a table of locations to display in this zone

      local displayable = {};

      for i in FlightPath_Config.FlightPaths do
         if((FlightPath_Config.FlightPaths[i].Faction == UnitFactionGroup("player")) and (FP_UserKnowsPath(FlightPath_Config.FlightPaths[i]) or not FP_HideGrey()) ) then
            for j = 1, 2, 1 do
               if(FlightPath_Config.FlightPaths[i].Continent[j] == continent) then
                  if(FlightPath_Config.FlightPaths[i].Zone[j] == zone) then
                     dindex = FlightPath_Config.FlightPaths[i].Endpoints[j];
                     if(displayable[dindex] == nil) then
                        displayable[dindex] = {};
                        displayable[dindex].Coords = FlightPath_Config.FlightPaths[i].Coords[j];
                     end
                     if(FP_UserKnowsPath(FlightPath_Config.FlightPaths[i])) then
                        displayable[dindex].Texture = "Interface\\TaxiFrame\\UI-Taxi-Icon-Yellow";
                     else
                        if(displayable[dindex].Texture == nil) then
                           displayable[dindex].Texture = "Interface\\TaxiFrame\\UI-Taxi-Icon-Gray";
                        end
                     end
                  end
               end
            end
         end
      end

      -- Create POI's for found flight masters

      for j in displayable do
         buttonCount = buttonCount + 1;
         local POI = getglobal("FP_POI"..buttonCount);
         local POITexture = getglobal("FP_POI"..buttonCount.."Icon");
         local x, y = FP_ParseCoord(displayable[j].Coords);
         POITexture:SetTexture(displayable[j].Texture);
         POI:ClearAllPoints();
         POI:SetPoint("CENTER", "WorldMapDetailFrame", "TOPLEFT", x/100*WorldMapButton:GetWidth(), -y/100*WorldMapButton:GetHeight());
         POI:Show();

         -- Add a location field to the POI so we can query it when creating a tooltip

         POI.Location = j;

      end

   end
end

---------------------------------------------------------------------

function FP_POIOnEnter()

   -- Called when the mouse hovers over the flight path POI button on the zone map

   local px, py = this:GetCenter();
   local wx, wy = WorldMapButton:GetCenter();
   local align = "ANCHOR_LEFT";
   if(px <= wx) then align = "ANCHOR_RIGHT"; end

   WorldMapFrameAreaLabel:SetText(this.Location);
   WorldMapTooltip:SetOwner(this, align);
   local leftSide,rightSide = FP_GetPOITooltipText(this.Location);
   for i in leftSide do
      if(rightSide[i] ~= nil) then
         WorldMapTooltip:AddDoubleLine(leftSide[i],rightSide[i]);
      else
         WorldMapTooltip:AddLine(leftSide[i]);
      end
   end
   WorldMapTooltip:Show();
end

---------------------------------------------------------------------

function FP_POIOnLeave()
   WorldMapTooltip:Hide();
   WorldMapTooltip:SetText("");
end

---------------------------------------------------------------------

function FP_TaxiNodeOnButtonEnter(button)

   local showDuration = false;
   local buttonLocation = TaxiNodeName(this:GetID());

   if(buttonLocation ~= "INVALID") then
      if(not FP_IsSameLocation(TaxiOrigin,buttonLocation)) then
         local index,path = FP_FindPath(TaxiOrigin,buttonLocation);
         if(index ~= 0) then
            local duration = FP_GetDuration(path,TaxiOrigin);
            if((duration ~= nil) and (duration ~= "")) then
               ShoppingTooltip2:SetOwner(GameTooltip, "ANCHOR_BOTTOMRIGHT");
               ShoppingTooltip2:ClearAllPoints();
               ShoppingTooltip2:SetPoint("TOPLEFT", "GameTooltip", "TOPRIGHT", 0, -10);
               ShoppingTooltip2:AddLine("Flight time: "..duration, "", 0.5, 1.0, 0.5);
               showDuration = true;
            end
         end
      end
   end

   Original_TaxiNodeOnButtonEnter(button); -- call original handler

   if(showDuration) then
      ShoppingTooltip2:Show();
   end

end

---------------------------------------------------------------------

function FP_GetDuration(path,location)

   if(path.Duration == nil) then
      return nil;

   end

   if(location == path.Endpoints[1]) then
      return path.Duration[1];   -- Duration is always stored as 'time from' the corresponding endpoint
   else
      return path.Duration[2];
   end
end

---------------------------------------------------------------------

function FP_FindPath(location1, location2)
   for i in FlightPath_Config.FlightPaths do
      local endpoint1 = FlightPath_Config.FlightPaths[i].Endpoints[1];
      local endpoint2 = FlightPath_Config.FlightPaths[i].Endpoints[2];
      if((FP_IsSameLocation(location1,endpoint1) and FP_IsSameLocation(location2,endpoint2)) or
         (FP_IsSameLocation(location1,endpoint2) and FP_IsSameLocation(location2,endpoint1))) then
            return i,FlightPath_Config.FlightPaths[i];
      end
   end
   return 0,nil;
end

---------------------------------------------------------------------

function FP_Debug()
   FPDebugShow = true;
   FPDEBUG("Debug output enabled.");
end

---------------------------------------------------------------------

-- Debug routine to help users debug flightmaster/zone name mismatches.

function FP_Check()

   if(not TaximapOpen) then
      FPCHAT("Please open up the flight master's connections map then type /fp check again.");
      return;
   end

   -- Find out what flight masters like to call this location

   local where;
   local numButtons = NumTaxiNodes();
   for i = 1, numButtons, 1 do
      if(TaxiNodeGetType(i) == "CURRENT") then
         where = TaxiNodeName(i);
         break;
      end
   end

   if(FP_IsSameLocation(where, FP_GetLocale())) then
      FPCHAT("FlightPath was able to match the flight master's name for this location ("..where..") to the map location ("..FP_GetLocale().."). No problems detected.");
   else
      FPCHAT("FlightPath was unable to match the flight master's name for this location ("..where..") to the map location ("..FP_GetLocale().."). Please report this to Kwraz!!!");
   end
end

---------------------------------------------------------------------

-- Display functions

function FPCHAT(text)
   DEFAULT_CHAT_FRAME:AddMessage(STATUS_COLOR..text);
end


FPDebugShow = false;

function FPDEBUG(...)
   if(FPDebugShow) then
      local text = "";
      for i = 1, arg.n, 1 do
         if(i>2) then text = text..", ";end;
         local value="";
         local vtype = type(arg[i]);
         if    (vtype == "nil")        then text = text.."(nil)";
         elseif(vtype == "number")     then text = text..tostring(arg[i]);
         elseif(vtype == "string")     then text = text..arg[i];
         elseif(vtype == "boolean")    then if(arg[i]) then text = text.."true"; else text = text.."false"; end
         elseif(vtype == "table"    or
                vtype == "function" or
                vtype == "thread"   or
                vtype == "userdata")   then text = text.."("..vtype..")";
         else                               text = text.."(unknown)";end
      end
      DEFAULT_CHAT_FRAME:AddMessage(DEBUG_COLOR.."FPDBG: "..text);
   end
end

function FPSPACE(count)
   return string.rep(" ",count);
end

---------------------------------------------------------------------
-- Development only
---------------------------------------------------------------------

function FP_Test(text)
   local txt = "";
   if(text ~= nil) then txt=text;end;
   FPDEBUG("In FP_Test("..txt..")...");

   -- Begin test code

   local testForeign = "Grom'Gul, Mitteilung Mu\195\159sein";
   local tcity, tzone = FP_ParseLocation(testForeign);
   FPDEBUG(testForeign.." parsed to city '"..tcity.."' zone='"..tzone.."'");

   -- End test code

   FPDEBUG("...Exited FP_Test");
end

