-- Copyright (C) 2011 Tyson Brown
-- 
-- This file is part QuestHelper2, an AddOn for World of Warcraft.
-- 
-- QuestHelper2 is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- 
-- QuestHelper2 is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with QuestHelper2. If not, see <http://www.gnu.org/licenses/>.

if LibDebug then LibDebug() end
local shared = select(2, ...)

-- The set of taxi nodes the player currently has.
local taxi_nodes = {}

-- The set of taxi nodes the player currently needs to get.
local missing_taxi_nodes = {}

-- Maps a taxi node to an objective to go get it.
local taxi_objectives = {}

-- Maps a node to a quest required to have been completed in order for the node to be available.
-- (Not currently used; but will be in the future.
local node_quests = {
  [322] = 12924, -- "Dun Niffelem" requires "Forging an Alliance"
  [333] = 12899, -- "The Shadow Vault" requires "The Shadow Vault"
  [309] = 12523, -- "Nesingwary Base Camp" requires "Have a Part, Give a Part"
  [605] = 26005, -- "Voldrin's Hold" requires "A Breath of Fresh Air"
  [610] = 26006, -- "Stygian Bounty" requires "A Breath of Fresh Air"
  [616] = 25531, -- "Gates of Southann" requires "Twilight Riot"
  [645] = 26965, -- "Forsaken High Command" requires "The Warchief Cometh"
  [646] = 27290, -- "Forsaken Forward Command" requires "To Forsaken Forward Command"
  [662] = 28598, -- "High Bank" requires "Aiming High"
  [658] = 27564, -- "Vermillion Redoubt" requires "In Defense of the Redoubt"
}

local taxi_root_objective = shared.createObjective("taxi/base")
taxi_root_objective.root = true
taxi_root_objective.text = MINIMAP_TRACKING_FLIGHTMASTER
taxi_root_objective.obtainable = false
taxi_root_objective.enabled = false

local graph = shared.createGraph()
local graph_nodes = {}

-- One plus the highest used taxi node.
local taxi_node_max = 0

-- We'll start with all the taxi nodes missing, and create a graph node for each of them.
for taxi_id in pairs(shared.static.flight_nodes) do
  missing_taxi_nodes[taxi_id] = true
  taxi_node_max = math.max(taxi_node_max, taxi_id+1)
  graph_nodes[taxi_id] = graph.createNode()
end

-- links[taxi_id_a + taxi_id_b*taxi_node_max] holds a map link object between those two nodes.
-- This assumes taxi_id_a is less than taxi_id_b; if it's greater, they're swapped and the reverse_time
-- is used instead of forward_time.
local links = {}

local function updateFlightPaths()
  -- Note that this depends on the assumption that taxi_nodes are only ever added, and that the travel times between nodes never decreases.
  for map_id, data in pairs(shared.static.flight_time_data) do
    for src_taxi_id, dest_map in pairs(data) do
      -- Only allow the source node if the player is known to have the node; the node doesn't depend on a quest, or the player completed the quest the node depends on.
      if taxi_nodes[src_taxi_id] or node_quests[src_taxi_id] == nil or shared.getQuestProgress(node_quests[src_taxi_id], nil) == 1 then
        for dest_taxi_id, travel_time in pairs(dest_map) do
          if taxi_nodes[dest_taxi_id] then
            -- We can go ahead and add links we previously added, as this does
            -- nothing if the travel_time isn't less than what it previously was.
            graph.addPermanantLink(graph_nodes[src_taxi_id], graph_nodes[dest_taxi_id], travel_time)
          end
        end
      end
    end
  end
  
  for src_taxi_node in pairs(shared.static.flight_nodes) do
    -- We add a 5 second overhead to all paths, so that the path finding code doesn't try to
    -- tell you to take a flight path, land, and then immediately take another flight path.
    graph.clearStartNodes()
    graph.addStart(graph_nodes[src_taxi_node], 5)
    
    for dest_taxi_node in pairs(shared.static.flight_nodes) do
      if src_taxi_node ~= dest_taxi_node then
        local travel_time = graph.nodeCost(graph_nodes[dest_taxi_node])
        
        if travel_time then
          local id_a, id_b, time_key = src_taxi_node, dest_taxi_node, "forward_time"
          
          if id_a > id_b then
            id_a, id_b, time_key = id_b, id_a, "reverse_time"
          end
          
          local id = id_a + id_b*taxi_node_max
          
          local link = links[id]
          
          if not link then
            link = shared.createMapLink()
            link:setOrigin(unpack(shared.static.flight_nodes[id_a]))
            link:setDestination(unpack(shared.static.flight_nodes[id_b]))
            links[id] = link
          end
          
          link[time_key] = travel_time
        end
      end
    end
  end
end

local function addNode(taxi_id)
  if missing_taxi_nodes[taxi_id] then
    missing_taxi_nodes[taxi_id] = nil
    taxi_nodes[taxi_id] = true
    
    -- If there was an objective to get this node, make it complete.
    local objective = taxi_objectives[taxi_id]
    if objective then
      objective.complete = true
      taxi_root_objective:removeDependencies(objective)
      taxi_objectives[taxi_id] = nil
      
      -- If there are no taxi objectives remaining, then disable the root taxi objective.
      if not next(taxi_objectives) then
        taxi_root_objective.complete = true
      end
    end
    
    shared.callOnce(updateFlightPaths)
  end
end

shared.onEvent("TAXIMAP_OPENED", function ()
  local map_id = shared.areaVirtualMap(shared.whereAmI())
  
  if map_id then
    for i=1,NumTaxiNodes() do
      local node_type = TaxiNodeGetType(i)
      if node_type == "REACHABLE" or node_type == "CURRENT" then
        local x, y = TaxiNodePosition(i)
        local region = shared.static.flight_maps[map_id]
        
        -- Convert the position on the taxi map frame to a position on the actual game map.
        x, y = (region.x2-region.x1)*x+region.x1, (region.y2-region.y1)*y+region.y1
        
        -- Try to find the taxi node nearest to this location.
        local best_taxi_id, best_d2
        
        for taxi_id in pairs(shared.static.flight_time_data[map_id]) do
          local nx, ny = shared.mapVirtualPosition(unpack(shared.static.flight_nodes[taxi_id]))
          local d2 = (x-nx)^2 + (y-ny)^2
          
          if not best_taxi_id or d2 < best_d2 then
            best_taxi_id, best_d2 = taxi_id, d2
          end
        end
        
        -- These distances should be nearly perfect, but for 
        -- whatever reason the transformations for outland are
        -- off by a few yards.
        if best_taxi_id and best_d2^.5 < 5 then
          addNode(best_taxi_id)
        else
          print("Failed to associate '"..TaxiNodeName(i).."' with a taxi node id. ", best_taxi_id and best_taxi_id.."@"..(best_d2^.5) or "(none)")
        end
      end
    end
  end
end)

shared.onLoad(function ()
  if _G.QuestHelper2_FlightPoints then
    for i, taxi_id in ipairs(_G.QuestHelper2_FlightPoints) do
      addNode(taxi_id)
    end
    
    _G.QuestHelper2_FlightPoints = nil
  end
  
  local player_level = UnitLevel("player")
  local create_objectives = true
  local filter_by_level = true
  
  local function updateObjectives()
    -- Remove any complete taxi objectives.
    for taxi_id, objective in pairs(taxi_objectives) do
      if not missing_taxi_nodes[taxi_id] then
        objective.complete = true
        taxi_root_objective:removeDependencies(objective)
        taxi_objectives[taxi_id] = nil
      end
    end
    
    -- Add any missing taxi objectives.
    for taxi_id in pairs(missing_taxi_nodes) do
      if not taxi_objectives[taxi_id] then
        local filtered = (node_quests[taxi_id] and shared.getQuestProgress(node_quests[taxi_id], nil) == 0) or
                         (filter_by_level and player_level < shared.areaRecommendedLevel(unpack(shared.static.flight_nodes[taxi_id])))
        
        if not filtered then
          local objective = shared.createObjective("taxi/"..taxi_id)
          objective.text = "#"..taxi_id -- TODO: Better names for these nodes.
          objective.icon = "Interface\\AddOns\\QuestHelper2\\Icons\\FlightMaster.tga"
          objective:setLocation(unpack(shared.static.flight_nodes[taxi_id]))
          taxi_root_objective:addDependencies(objective)
          objective.enabled = true
          
          taxi_objectives[taxi_id] = objective
        end
      end 
    end
    
    taxi_root_objective.complete = not next(taxi_objectives) and true or false
  end
  
  shared.onSetting("flight_create_objectives", function (enabled)
    create_objectives = enabled
    if enabled then
      taxi_root_objective.enabled = true
      
      updateObjectives()
    else
      -- Remove all created the objectives.
      for taxi_id, objective in pairs(taxi_objectives) do
        objective.enabled = false
        taxi_root_objective:removeDependencies(objective)
        taxi_objectives[taxi_id] = nil
      end
      
      taxi_root_objective.enabled = false
    end
  end)
  
  shared.onSetting("flight_filter_by_level", function (enabled)
    filter_by_level = enabled
    
    if create_objectives then
      updateObjectives()
    end
  end)
  
  shared.onQuestRemove(function (quest_id, reason)
    -- Do an update when a quest is completed; because some taxi nodes depend on quests being completed to be available.
    if reason ~= "abandon" and create_objectives then
      updateObjectives()
    end
  end)
  
  shared.onEvent("PLAYER_LEVEL_UP", function (level)
    player_level = level
    if create_objectives and filter_by_level then updateObjectives() end
  end)
end)

shared.onSave(function ()
  local taxi_list = {}
  
  for taxi_id in pairs(taxi_nodes) do
    table.insert(taxi_list, taxi_id)
  end
  
  table.sort(taxi_list)
  
  _G.QuestHelper2_FlightPoints = taxi_list
end)
