-- Copyright (C) 2010 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, ...)

-- This file provides functions for managing the pather objects that QuestHelper2
-- is using for pathfinding, and keeping them up-to-date when the player's
-- travel speed changes, or map links are enabled or disabled for the maps the
-- pather objects operate on.

-- It should be noted that the pather objects operates on virtual maps, so for example the pathing data for
-- Azuremyst Isle is located in the Kalimdor map even though it's technically in the Outland map.

-- An array of callbacks to invoke when you're character's travel speed on a map changes.
local on_map_speed_changed = {}

-- Your characters speed in various circumstances.
local run_speed = 7
local water_speed = 14/3
local mount_speed = nil
local air_speed = nil

-- A set of maps in which mounting is legal.
local mount_legal_maps = {
  [0] = true, -- Eastern Kingdoms
  [1] = true, -- Kalimdor
  [530] = true, -- Outland
  [571] = true, -- Northrend
  
  -- Instances
  [329] = true, -- Stratholme
  [309] = true, -- Zul'gurub
  
  -- ... and probably others, but meh.
}

-- A set of maps in which flying is legal.
local flight_legal_maps = {
  -- You're always considered legally allowed to fly in Outland; the trick keeping you from
  -- flying in outland is that until you have the training, your flight speed will be nil.
  [530] = true
}

-- Returns run speed, water speed, mount speed, and air_speed.
-- Speeds will be nil if you can't travel via that method.
function shared.getMapTravelSpeed(map_id)
  -- TODO: force mount_speed to be nil if you can't mount in a specific map.
  return run_speed,
         water_speed,
         mount_legal_maps[map_id] and mount_speed or nil,
         flight_legal_maps[map_id] and air_speed or nil
end

-- I don't offer area level resolution for travel speed, even though I should,
-- so this is only a convenience function.
function shared.getAreaTravelSpeed(area_id, level)
  return shared.MapTravelSpeed(shared.areaVirtualMap(area_id, level))
end

-- We temporarly store the IDs of maps affected by speed changes here.
local affected_maps = {}

local function recheckSpells()
  local old_mount_speed, old_air_speed = mount_speed, air_speed
  
  if GetSpellInfo(33388) then
    -- Apprentice Riding
    mount_speed = 11.2
    air_speed = nil
  end
  
  if GetSpellInfo(33391) then
    -- Journeyman Riding
    mount_speed = 14
    air_speed = nil
  end
  
  if GetSpellInfo(34090) then
    -- Expert Riding
    mount_speed = 14
    air_speed = 17.5
  end
  
  if GetSpellInfo(34091) then
    -- Artisan Riding
    mount_speed = 14
    air_speed = 26.6
  end
  
  if GetSpellInfo(90265) then
    -- Master Riding
    mount_speed = 14
    air_speed = 28.7
  end
  
  if GetSpellInfo(54197) and not flight_legal_maps[571] then
    -- Cold Weather Flying
    flight_legal_maps[571] = true
    affected_maps[571] = true
  end
  
  if GetSpellInfo(90267) and not flight_legal_maps[0] then
    -- Flight Master's License
    flight_legal_maps[0] = true -- Eastern Kingdoms
    flight_legal_maps[1] = true -- Kalimdor
    affected_maps[0] = true
    affected_maps[1] = true
  end
  
  -- If mount speed has changed, mark all maps where it's legal to mount as affected.
  if mount_speed ~= old_mount_speed then
    for map_id in pairs(mount_legal_maps) do
      affected_maps[map_id] = true
    end
  end
  
  -- If flight speed has changed, mark all maps where it's legal to fly as affected.
  if air_speed ~= old_air_speed then
    for map_id in pairs(flight_legal_maps) do
      affected_maps[map_id] = true
    end
  end
  
  -- Announce the affected maps.
  for map_id in pairs(affected_maps) do
    shared.invokeCallbackList(on_map_speed_changed, map_id, shared.getMapTravelSpeed(map_id))
  end
  
  table.wipe(affected_maps)
end

-- I don't know how to get speed information about mounts in a Locale independent way,
-- so I'm just going to assume that if you have the training, that you also have the mount.
shared.onEvent("LEARNED_SPELL_IN_TAB", recheckSpells)
shared.onLoad(recheckSpells)

-- The objects responsible for map specific pathing.
-- There are multiple kinds of pathing objects, but that all expose the same interface.
local map_pathers = {}

-- A list of callbacks to invoked when the pather object for a map is changed or replaced.
local on_map_pather_modified = {}

function shared.onMapPatherModified(cb, ...)
  cb = shared.callback(cb, ...)
  table.insert(on_map_pather_modified, cb)
  
  -- Announce any already existing map pathers to the callback.
  for map_id, pather in pairs(map_pathers) do
    cb(map_id, pather)
  end
end

-- Returns the pather objective being used on some map, or nil if there is no map assigned.
function shared.getMapPather(map_id)
  return map_pathers[map_id]
end

-- Request to be notified when the player's speed on a map changes.
function shared.onMapSpeedChanged(cb, ...)
  cb = shared.callback(cb, ...)
  table.insert(on_map_speed_changed, cb)
  
  -- Announce the speed of any existing maps.
  for map_id in pairs(map_pathers) do
    cb(map_id, shared.getMapTravelSpeed(map_id))
  end
end

-- Update speed assumptions of the various pathing objects when 
-- player's travel speed changes.
shared.onMapSpeedChanged(function (map_id, ...)
  local pather = map_pathers[map_id]
  
  if pather and pather:updateTravelSpeedAssumptions(...) then
    -- Pather modified; announce this.
    shared.invokeCallbackList(on_map_pather_modified, map_id, pather)
  end
end)

-- Maps enabled map_link objects to the maps they belong to.
local links = {}

shared.onMapLinkEnabled(function (link)
  local src_map  = shared.areaVirtualMap(link:getOrigin())
  local dest_map = shared.areaVirtualMap(link:getDestination())
  
  -- If the link takes place entirely on the same map, we'll
  -- use it with our pather objects.
  if src_map == dest_map then
    -- We shouldn't already have this link already; if it was altered, it should have been disabled first.
    assert(links[link] == nil)
    
    links[link] = src_map
    
    local pather = map_pathers[src_map]
    
    -- Add the link to the pather for this map if there is one, and announce it as modified if necessary.
    if pather and pather:addLink(link) then
      shared.invokeCallbackList(on_map_pather_modified, src_map, pather)
    end
  end
end)

shared.onMapLinkDisabled(function (link)
  local map_id = links[link]
  
  if map_id then
    -- We were keeping track of this link object..
    
    -- ... but no longer.
    links[link] = nil
    
    local pather = map_pathers[map_id]
    
    -- Remove the link from the pather for this map if there is one, and announce it as modified if necessary.
    if pather and pather:removeLink(link) then
      shared.invokeCallbackList(on_map_pather_modified, map_id, pather)
    end
  end
end)

-- A function to assign a pather object to a particular map.
function shared.assignPatherToMap(map_id, pather)
  local old_pather = map_pathers[map_id]
  
  if pather ~= old_pather then
    if pather then
      -- Update the pather object being assigned with everything we know about how the world works.
      
      pather:clearLinks()
      
      for link, link_map_id in pairs(links) do
        if map_id == link_map_id then
          pather:addLink(link)
        end
      end
      
      pather:updateTravelSpeedAssumptions(shared.getMapTravelSpeed(map_id))
    end
    
    if not old_pather then
      -- We didn't previously know about this map, announce its travel speed to anyone that cares.
      shared.invokeCallbackList(on_map_speed_changed, map_id, shared.getMapTravelSpeed(map_id))
    end
    
    -- Remember the pather now being used for this map.
    map_pathers[map_id] = pather
    
    -- Announce that the pather object was replaced.
    shared.invokeCallbackList(on_map_pather_modified, map_id, pather)
  end
end
