
local GetTime = QuestHelper_GetTime

QuestHelper_File["objective.lua"] = "4.0.1.$svnversion$"
QuestHelper_Loadtime["objective.lua"] = GetTime()


local UserIgnored = {
  name = "user_manual_ignored",
  no_disable = true,
  friendly_reason = QHText("FILTERED_USER"),
  AddException = function(self, node)
    QH_Route_UnignoreNode(node, self) -- there isn't really state with this one
  end
}

function QuestHelper:AddObjectiveOptionsToMenu(obj, menu)
  local submenu = self:CreateMenu()
  
  local pri = (QH_Route_GetClusterPriority(obj.cluster) or 0) + 3
  for i = 1, 5 do
    local name = QHText("PRIORITY"..i)
    local item = self:CreateMenuItem(submenu, name)
    local tex
    
    if pri == i then
      tex = self:CreateIconTexture(item, 10)
    else
      tex = self:CreateIconTexture(item, 12)
      tex:SetVertexColor(1, 1, 1, 0)
    end
    
    item:AddTexture(tex, true)
    item:SetFunction(QH_Route_SetClusterPriority, obj.cluster, i - 3)
  end
  
  self:CreateMenuItem(menu, QHText("PRIORITY")):SetSubmenu(submenu)
  
  --[[if self.sharing then
    submenu = self:CreateMenu()
    local item = self:CreateMenuItem(submenu, QHText("SHARING_ENABLE"))
    local tex = self:CreateIconTexture(item, 10)
    if not obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
    item:AddTexture(tex, true)
    item:SetFunction(obj.Share, obj)
    
    local item = self:CreateMenuItem(submenu, QHText("SHARING_DISABLE"))
    local tex = self:CreateIconTexture(item, 10)
    if obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
    item:AddTexture(tex, true)
    item:SetFunction(obj.Unshare, obj)
    
    self:CreateMenuItem(menu, QHText("SHARING")):SetSubmenu(submenu)
  end]]
  
  --self:CreateMenuItem(menu, "(No options available)")
  
  if not obj.map_suppress_ignore then
    self:CreateMenuItem(menu, QHText("IGNORE")):SetFunction(function () QH_Route_IgnoreCluster(obj.cluster, UserIgnored) end) -- There is probably a nasty race condition here. I'm not entirely happy about it.
  end
  if obj.map_custom_menu then
    obj.map_custom_menu(menu)
  end
  
  if obj.cluster and #obj.cluster > 1 and QH_Route_Ignored_Cluster_Active(obj.cluster) > 1 then
    self:CreateMenuItem(menu, QHText("IGNORE_LOCATION")):SetFunction(QH_Route_IgnoreNode, obj, UserIgnored)
  end
end

do return end

local function ObjectiveCouldBeFirst(self)
  if (self.user_ignore == nil and self.auto_ignore) or self.user_ignore then
    return false
  end
  
  for i, j in pairs(self.after) do
    if i.watched then
      return false
    end
  end
  
  return true
end

local function DefaultObjectiveKnown(self)
  if self.user_ignore == nil then
    if (self.filter_zone and QuestHelper_Pref.filter_zone) or
       (self.filter_done and QuestHelper_Pref.filter_done) or
       (self.filter_level and QuestHelper_Pref.filter_level) or
       (self.filter_blocked and QuestHelper_Pref.filter_blocked) or
       (self.filter_watched and QuestHelper_Pref.filter_watched) then
      return false
    end
  elseif self.user_ignore then
    return false
  end
  
  for i, j in pairs(self.after) do
    if i.watched and not i:Known() then -- Need to know how to do everything before this objective.
      return false
    end
  end
  
  return true
end

local function ObjectiveReason(self, short)
  local reason, rc = nil, 0
  if self.reasons then
    for r, c in pairs(self.reasons) do
      if not reason or c > rc or (c == rc and r > reason) then
        reason, rc = r, c
      end
    end
  end
  
  if not reason then reason = "Do some extremely secret unspecified something." end
  
  if not short and self.pos and self.pos[6] then
    reason = reason .. "\n" .. self.pos[6]
  end
  
  return reason
end

local function Uses(self, obj, text)
  if self == obj then return end -- You cannot use yourself. A purse is not food.
  local uses, used = self.uses, obj.used
  
  if not uses then
    uses = QuestHelper:CreateTable("uses")
    self.uses = uses
  end
  
  if not used then
    used = QuestHelper:CreateTable("used")
    obj.used = used
  end
  
  if not uses[obj] then
    uses[obj] = true
    used[self] = text
    obj:MarkUsed()
  end
end

local function DoMarkUsed(self)
  -- Objectives should call 'self:Uses(objective, text)' to mark objectives they use by don't directly depend on.
  -- This information is used in tooltips.
  -- text is passed to QHFormat with the name of the objective being used.
end

local function MarkUsed(self)
  if not self.marked_used then
    self.marked_used = 1
    self:DoMarkUsed()
  else
    self.marked_used = self.marked_used + 1
  end
end

local function MarkUnused(self)
  --[[ assert(self.marked_used) ]]
  
  if self.marked_used == 1 then
    local uses = self.uses
    
    if uses then
      for obj in pairs(uses) do
        obj.used[self] = nil
        obj:MarkUnused()
      end
      
      QuestHelper:ReleaseTable(uses)
      self.uses = nil
    end
    
    if self.used then
      --[[ assert(not next(self.used)) ]]
      QuestHelper:ReleaseTable(self.used)
      self.used = nil
    end
    
    self.marked_used = nil
  else
    self.marked_used = self.marked_used - 1
  end
end

local function DummyObjectiveKnown(self)
  return (self.o.pos or self.fb.pos) and DefaultObjectiveKnown(self)
end

local function ItemKnown(self)
  if not DefaultObjectiveKnown(self) then return false end
  
  if self.o.vendor then
    for i, npc in ipairs(self.o.vendor) do
      local n = self.qh:GetObjective("monster", npc)
      local faction = n.o.faction or n.fb.faction
      if (not faction or faction == self.qh.faction) and n:Known() then
        return true
      end
    end
  end
  
  if self.fb.vendor then
    for i, npc in ipairs(self.fb.vendor) do
      local n = self.qh:GetObjective("monster", npc)
      local faction = n.o.faction or n.fb.faction
      if (not faction or faction == self.qh.faction) and n:Known() then
        return true
      end
    end
  end
  
  if self.o.pos or self.fb.pos then
    return true
  end
  
  if self.o.drop then for monster in pairs(self.o.drop) do
    if self.qh:GetObjective("monster", monster):Known() then
      return true
    end
  end end
  
  if self.fb.drop then for monster in pairs(self.fb.drop) do
    if self.qh:GetObjective("monster", monster):Known() then
      return true
    end
  end end
  
  if self.o.contained then for item in pairs(self.o.contained) do
    if self.qh:GetObjective("item", item):Known() then
      return true
    end
  end end
  
  if self.fb.contained then for item in pairs(self.fb.contained) do
    if self.qh:GetObjective("item", item):Known() then
      return true
    end
  end end
  
  if self.quest then
    local item=self.quest.o.item
    item = item and item[self.obj]
    
    if item then 
      if item.pos then
        return true
      end
      if item.drop then
        for monster in pairs(item.drop) do
          if self.qh:GetObjective("monster", monster):Known() then
            return true
          end
        end
      end
    end
    
    item=self.quest.fb.item
    item = item and item[self.obj]
    if item then 
      if item.pos then
        return true
      end
      if item.drop then
        for monster in pairs(item.drop) do
          if self.qh:GetObjective("monster", monster):Known() then
            return true
          end
        end
      end
    end
  end
  
  return false
end

local function ObjectiveAppendPositions(self, objective, weight, why, restrict)
  local high = 0
  
  if self.o.pos then for i, p in ipairs(self.o.pos) do
    high = math.max(high, p[4])
  end end
  
  if self.fb.pos then for i, p in ipairs(self.fb.pos) do
    high = math.max(high, p[4])
  end end
  
  high = weight/high
  
  if self.o.pos then for i, p in ipairs(self.o.pos) do
    if not restrict or not self.qh:Disallowed(p[1]) then
      objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
    end
  end end
  
  if self.fb.pos then for i, p in ipairs(self.fb.pos) do
    if not restrict or not self.qh:Disallowed(p[1]) then
      objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
    end
  end end
end


local function ObjectivePrepareRouting(self, anywhere)
  self.setup_count = self.setup_count + 1
  if not self.setup then
    --[[ assert(not self.d) ]]
    --[[ assert(not self.p) ]]
    --[[ assert(not self.nm) ]]
    --[[ assert(not self.nm2) ]]
    --[[ assert(not self.nl) ]]
    
    self.d = QuestHelper:CreateTable("objective.d")
    self.p = QuestHelper:CreateTable("objective.p")
    self.nm = QuestHelper:CreateTable("objective.nm")
    self.nm2 = QuestHelper:CreateTable("objective.nm2")
    self.nl = QuestHelper:CreateTable("objective.nl")
    self.distance_cache = QuestHelper:CreateTable("objective.distance_cache")
    
    if not anywhere then
      self:AppendPositions(self, 1, nil, true)
    
      if not next(self.p) then
        QuestHelper:TextOut(QHFormat("INACCESSIBLE_OBJ", self.obj or "whatever it was you just requested"))
        anywhere = true
      end
    end
    
    if anywhere then
      self:AppendPositions(self, 1, nil, false)
    end
    
    self:FinishAddLoc(args)
  end
end

local function ItemAppendPositions(self, objective, weight, why, restrict)
  why2 = why and why.."\n" or ""
  
  if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
    local n = self.qh:GetObjective("monster", npc)
    local faction = n.o.faction or n.fb.faction
    if (not faction or faction == self.qh.faction) then
      n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
    end
  end end
  
  if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
    local n = self.qh:GetObjective("monster", npc)
    local faction = n.o.faction or n.fb.faction
    if (not faction or faction == self.qh.faction) then
      n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
    end
  end end
  
  if next(objective.p, nil) then
    -- If we have points from vendors, then always use vendors. I don't want it telling you to killing the
    -- towns people just because you had to talk to them anyway, and it saves walking to the store.
    return
  end
  
  if self.o.drop then for monster, count in pairs(self.o.drop) do
    local m = self.qh:GetObjective("monster", monster)
    m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
  end end
  
  if self.fb.drop then for monster, count in pairs(self.fb.drop) do
    local m = self.qh:GetObjective("monster", monster)
    m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
  end end
  
  if self.o.contained then for item, count in pairs(self.o.contained) do
    local i = self.qh:GetObjective("item", item)
    i:AppendPositions(objective, i.o.opened and count/i.o.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
  end end
  
  if self.fb.contained then for item, count in pairs(self.fb.contained) do
    local i = self.qh:GetObjective("item", item)
    i:AppendPositions(objective, i.fb.opened and count/i.fb.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
  end end
  
  if self.o.pos then for i, p in ipairs(self.o.pos) do
    if not restrict or not self.qh:Disallowed(p[1]) then
      objective:AddLoc(p[1], p[2], p[3], p[4], why)
    end
  end end
  
  if self.fb.pos then for i, p in ipairs(self.fb.pos) do
    if not restrict or not self.qh:Disallowed(p[1]) then
      objective:AddLoc(p[1], p[2], p[3], p[4], why)
    end
  end end
  
  if self.quest then
    local item_list=self.quest.o.item
    if item_list then
      local data = item_list[self.obj]
      if data and data.drop then
        for monster, count in pairs(data.drop) do
          local m = self.qh:GetObjective("monster", monster)
          m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
        end
      elseif data and data.pos then
        for i, p in ipairs(data.pos) do
          if not restrict or not self.qh:Disallowed(p[1]) then
            objective:AddLoc(p[1], p[2], p[3], p[4], why)
          end
        end
      end
    end
    
    item_list=self.quest.fb.item
    if item_list then 
      local data = item_list[self.obj]
      if data and data.drop then
        for monster, count in pairs(data.drop) do
          local m = self.qh:GetObjective("monster", monster)
          m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
        end
      elseif data and data.pos then
        for i, p in ipairs(data.pos) do
          if not restrict or not self.qh:Disallowed(p[1]) then
            objective:AddLoc(p[1], p[2], p[3], p[4], why)
          end
        end
      end
    end
  end
end

local function ItemDoMarkUsed(self)
  if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
    local n = self.qh:GetObjective("monster", npc)
    local faction = n.o.faction or n.fb.faction
    if (not faction or faction == self.qh.faction) then
      self:Uses(n, "TOOLTIP_PURCHASE")
    end
  end end
  
  if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
    local n = self.qh:GetObjective("monster", npc)
    local faction = n.o.faction or n.fb.faction
    if (not faction or faction == self.qh.faction) then
      self:Uses(n, "TOOLTIP_PURCHASE")
    end
  end end
  
  if self.o.drop then for monster, count in pairs(self.o.drop) do
    self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
  end end
  
  if self.fb.drop then for monster, count in pairs(self.fb.drop) do
    self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
  end end
  
  if self.o.contained then for item, count in pairs(self.o.contained) do
    self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
  end end
  
  if self.fb.contained then for item, count in pairs(self.fb.contained) do
    self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
  end end
  
  if self.quest then
    local item_list=self.quest.o.item
    if item_list then
      local data = item_list[self.obj]
      if data and data.drop then
        for monster, count in pairs(data.drop) do
          self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
        end
      end
    end
    
    item_list=self.quest.fb.item
    if item_list then 
      local data = item_list[self.obj]
      if data and data.drop then
        for monster, count in pairs(data.drop) do
          self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
        end
      end
    end
  end
end










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













local function AddLoc(self, index, x, y, w, why)
  --[[ assert(not self.setup) ]]
  
  if w > 0 then
    local pair = QuestHelper_ZoneLookup[index]
    QuestHelper: Assert(pair, "Index does not exist... it is probably 26 or 38 and the value really is... '" .. tostring(index) .. "'")
    if not pair then return end -- that zone doesn't exist! We require more vespene gas. Not enough rage!
    local c, z = pair[1], pair[2]
    x, y = self.qh.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
    
    x = x * self.qh.continent_scales_x[c]
    y = y * self.qh.continent_scales_y[c]
    local list = self.qh.zone_nodes[index]
    
    local points = self.p[list]
    if not points then
      points = QuestHelper:CreateTable("objective.p[zone] (objective nodes per-zone)")
      self.p[list] = points
    end
    
    for i, p in pairs(points) do
      local u, v = x-p[3], y-p[4]
      if u*u+v*v < 25 then -- Combine points within a threshold of 5 seconds travel time.
        p[3] = (p[3]*p[5]+x*w)/(p[5]+w)
        p[4] = (p[4]*p[5]+y*w)/(p[5]+w)
        p[5] = p[5]+w
        if w > p[7] then
          p[6], p[7] = why, w
        end
        return
      end
    end
    
    local new = QuestHelper:CreateTable("objective.p[zone] (possible objective node)")
    new[1], new[2], new[3], new[4], new[5], new[6], new[7] = list, nil, x, y, w, why, w
    table.insert(points, new)
  end
end

local function FinishAddLoc(self, args)
  local mx = 0
  
  for z, pl in pairs(self.p) do
    for i, p in ipairs(pl) do
      if p[5] > mx then
        self.location = p
        mx = p[5]
      end
    end
  end
  
  if not self.zones then
    -- Not using CreateTable, because it will not be released when routing is complete.
    self.zones = {}
  else
    -- We could remove the already known zones, but I'm operating under the assumtion that locations will only be added,
    -- not removed, so this isn't necessary.
  end
  
  -- Remove probably useless locations.
  for z, pl in pairs(self.p) do
    local remove_zone = true
    local i = 1
    while i <= #pl do
      if pl[i][5] < mx*0.2 then
        QuestHelper:ReleaseTable(pl[i])
        table.remove(pl, i)
      else
        remove_zone = false
        i = i + 1
      end
    end
    if remove_zone then
      QuestHelper:ReleaseTable(self.p[z])
      self.p[z] = nil
    else
      self.zones[z.i] = true
    end
  end
  
  local node_map = self.nm
  local node_list = self.nl
  
  for list, pl in pairs(self.p) do
    local dist = self.d[list]
    
    --[[ assert(not dist) ]]
    
    if not dist then
      dist = QuestHelper:CreateTable("self.d[list]")
      self.d[list] = dist
    end
    
    for i, point in ipairs(pl) do
      point[5] = mx/point[5] -- Will become 1 for the most desired location, and become larger and larger for less desireable locations.
      
      point[2] = QuestHelper:CreateTable("possible objective node to zone edge cache")
      
      for i, node in ipairs(list) do
        QuestHelper: Assert(type(point[3]) == "number", string.format("p3 %s", tostring(point[3])))
        QuestHelper: Assert(type(point[4]) == "number", string.format("p4 %s", tostring(point[4])))
        QuestHelper: Assert(type(node.x) == "number", string.format("nx %s", tostring(node.x)))
        QuestHelper: Assert(type(node.y) == "number", string.format("ny %s", tostring(node.y)))
        local u, v = point[3]-node.x, point[4]-node.y
        local d = math.sqrt(u*u+v*v)
        
        point[2][i] = d
        
        if dist[i] then
          if d*point[5] < dist[i][1]*dist[i][2] then
            dist[i][1], dist[i][2] = d, point[5]
            node_map[node] = point
          end
        else
          local pair = QuestHelper:CreateTable()
          pair[1], pair[2] = d, point[5]
          dist[i] = pair
          
          if not node_map[node] then
            table.insert(node_list, node)
            node_map[node] = point
          else
            u, v = node_map[node][3]-node.x, node_map[node][4]-node.y
            
            if dist[i][1]*dist[i][2] < math.sqrt(u*u+v*v)*node_map[node][5] then
              node_map[node] = point
            end
          end
        end
      end
    end
  end
  
  -- Disabled because we're having some data sanity issues. This should be solved at buildtime, but I'm leery of mucking with the build system right now, so it isn't. Re-enable later.
  --if not args or not args.failable then
  --  if #node_list == 0 and QuestHelper:IsWrath() then QuestHelper:Error(self.cat.."/"..self.obj..": zero nodes!") end
  --end
  
  --[[ assert(not self.setup) ]]
  self.setup = true
  table.insert(self.qh.prepared_objectives, self)
end

local function GetPosition(self)
  --[[ assert(self.setup) ]]
  
  return self.location
end

local QH_TESTCACHE = nil  -- make this "true" or something if you want to test caching (i.e. recalculate everything, then verify that the cache is valid)

-- Note: Pos is the starting point, the objective is the destination. These are different data formats - "self" can be a set of points.
-- More annotation here, if you're trying to learn the codebase. This function is a more complicated version of QH:ComputeTravelTime, so refer to that for information first before reading this one.
local function ObjectiveTravelTime(self, pos, nocache)
  --[[ assert(self.setup) ]]
  
  -- The caching is pretty obvious.
  local key, cached
  if not nocache then
    --[[ assert(pos ~= QuestHelper.pos) ]]
    if not pos.key then
      pos.key = math.random()..""
    end
    key = pos.key
    cached = self.distance_cache[key]
    if cached then
      if not QH_TESTCACHE then
        return unpack(cached)
      end
    end
  end

  local graph = self.qh.world_graph
  local nl = self.nl
  
  graph:PrepareSearch()
  
  -- This is quite similar to the same "create nodes for all zone links" in ComputeTravelTime except that it's creating nodes for all zone links for a set of possible destinations. I'm not sure if the weighting is backwards. It might be.
  for z, l in pairs(self.d) do
    for i, n in ipairs(z) do
      if n.s == 0 then
        n.e, n.w = unpack(l[i])
        n.s = 3
      elseif n.e * n.w < l[i][1]*l[i][2] then
        n.e, n.w = unpack(l[i])
      end
    end
  end
  
  local d = pos[2]
  for i, n in ipairs(pos[1]) do
    graph:AddStartNode(n, d[i], nl)
  end
  
  local e = graph:DoSearch(nl)
  
  -- d changes datatype here. I hate this codebase. Hell, e probably changes datatype also! yaaaay. what does .nm mean? what does .d mean?
  d = e.g+e.e
  e = self.nm[e]
  
  -- There's something going on with weighting here that I don't understand
  local l = self.p[pos[1]]
  if l then
    local x, y = pos[3], pos[4]
    local score = d*e[5]
    
    for i, n in ipairs(l) do
      local u, v = x-n[3], y-n[4]
      local d2 = math.sqrt(u*u+v*v)
      local s = d2*n[5]
      if s < score then
        d, e, score = d2, n, s
      end
    end
  end
  
  --[[ assert(e) ]]
  if not nocache then
    --[[ assert( not cached or (cached[1] == d and cached[2] == e)) ]]
    if not QH_TESTCACHE or not cached then
      local new = self.qh:CreateTable()
      new[1], new[2] = d, e
      self.distance_cache[key] = new
      self.qh:CacheRegister(self)
    end
  else
    if self.distance_cache and self.distance_cache[key] then
      --[[ assert(self.distance_cache[key][1] == d) ]]
    end
  end

  return d, e
end

-- Note: pos1 is the starting point, pos2 is the ending point, the objective is somewhere between them.
-- Yet more annotation! This one is based off ObjectiveTravelTime. Yes, it's nasty that there are three (edit: four) functions with basically the same goal. Have I mentioned this codebase kind of sucks?
local function ObjectiveTravelTime2(self, pos1, pos2, nocache)
  --[[ assert(self.setup) ]]
  
  -- caching is pretty simple as usual
  local key, cached
  if not nocache then
    --[[ assert(pos1 ~= QuestHelper.pos) ]]
    --[[ assert(pos2 ~= QuestHelper.pos) ]]
    -- We don't want to cache distances involving the player's current position, as that would spam the table
    if not pos1.key then
      pos1.key = math.random()..""
    end
    if not pos2.key then
      pos2.key = math.random()..""
    end
    key = pos1.key..pos2.key
    cached = self.distance_cache[key]
    if cached then
      if not QH_TESTCACHE then
        return unpack(cached)
      end
    end
  end

  local graph = self.qh.world_graph
  local nl = self.nl
  
  -- This is the standard pos1-to-self code that we're used to seeing . . .
  graph:PrepareSearch()
  
  for z, l in pairs(self.d) do
    for i, n in ipairs(z) do
      if n.s == 0 then
        n.e, n.w = unpack(l[i])
        n.s = 3
      elseif n.e * n.w < l[i][1]*l[i][2] then
        n.e, n.w = unpack(l[i])
      end
    end
  end
  
  local d = pos1[2]
  for i, n in ipairs(pos1[1]) do
    graph:AddStartNode(n, d[i], nl)
  end
  
  graph:DoFullSearch(nl)
  
  graph:PrepareSearch()
  
  -- . . . and here's where it gets wonky
  -- Now, we need to figure out how long it takes to get to each node.
  for z, point_list in pairs(self.p) do
    if z == pos1[1] then
      -- Will also consider min distance.
      local x, y = pos1[3], pos1[4]
      
      for i, p in ipairs(point_list) do
        local a, b = p[3]-x, p[4]-y
        local u, v = p[3], p[4]
        local d = math.sqrt(a*a+b*b)
        local w = p[5]
        local score = d*w
        for i, n in ipairs(z) do
          a, b = n.x-u, n.y-v
          local bleh = math.sqrt(a*a+b*b)+n.g
          local s = bleh*w
          if s < score then
            d, score = bleh, d
          end
        end
        p[7] = d
      end
    else
      for i, p in ipairs(point_list) do
        local x, y = p[3], p[4]
        local w = p[5]
        local d
        local score
        
        for i, n in ipairs(z) do
          local a, b = n.x-x, n.y-y
          local d2 = math.sqrt(a*a+b*b)+n.g
          local s = d2*w
          if not score or s < score then
            d, score = d2, s
          end
        end
        p[7] = d
      end
    end
  end
  
  d = pos2[2]
  
  for i, n in ipairs(pos2[1]) do
    n.e = d[i]
    n.s = 3
  end
  
  local el = pos2[1]
  local nm = self.nm2
  
  for z, l in pairs(self.d) do
    for i, n in ipairs(z) do
      local x, y = n.x, n.y
      local bp
      local bg
      local bs
      for i, p in ipairs(self.p[z]) do
        local a, b = x-p[3], y-p[4]
        d = p[7]+math.sqrt(a*a+b*b)
        s = d*p[5]
        if not bs or s < bs then
          bg, bp, bs = d, p, s
        end
      end
      
      nm[n] = bp
      -- Using score instead of distance, because we want nodes we're not really interested in to be less likely to get chosen.
      graph:AddStartNode(n, bs, el)
    end
  end
  
  local e = graph:DoSearch(pos2[1])
  
  d = nm[e.p][7]
  local d2 = e.g+e.e-e.p.g+(e.p.g/nm[e.p][5]-nm[e.p][7])
  
  e = nm[e.p]
  local total = (d+d2)*e[5]
  
  if self.p[el] then
    local x, y = pos2[3], pos2[4]
    for i, p in ipairs(self.p[el]) do
      local a, b = x-p[3], y-p[4]
      local c = math.sqrt(a*a+b*b)
      local t = (p[7]+c)*p[5]
      if t < total then
        total, d, d2, e = t, p[7], c, p
      end
    end
  end
  
  -- grim stabilization hack, since obviously the numbers it generates are only vaguely based in reality. This should be fixed and removed ASAP (you know, once I figure out WTF this thing is doing)
  d = QuestHelper:ComputeTravelTime(pos1, e)
  d2 = QuestHelper:ComputeTravelTime(e, pos2)

  --[[ assert(e) ]]
  if not nocache then
    --[[ assert( not cached or (cached[1] == d and cached[2] == d2 and cached[3] == e)) ]]
    if not QH_TESTCACHE or not cached then
      local new = self.qh:CreateTable("ObjectiveTravelTime2 cache")
      new[1], new[2], new[3] = d, d2, e
      self.distance_cache[key] = new
      self.qh:CacheRegister(self)
    end
  else
    if self.distance_cache and self.distance_cache[key] then
      --[[ assert(self.distance_cache[key][1] == d and self.distance_cache[key][2] == d2) ]]
    end
  end

  --[[if pos1 and pos2 then   -- Debug code so I can maybe actually fix the problems someday
    QuestHelper:TextOut("Beginning dumping here")

    local laxa = QuestHelper:ComputeTravelTime(pos1, e, true)
    if math.abs(laxa-d) >= 0.0001 then
      QuestHelper:TextOut(QuestHelper:StringizeTable(pos1))
      QuestHelper:TextOut(QuestHelper:StringizeRecursive(pos1, 2))
      QuestHelper:TextOut(QuestHelper:StringizeTable(e))
      QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
      QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
      QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
      ----[[ QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxa.." vs "..d) ]]     -- wonky commenting is thanks to the de-assert script, fix later
    --[[end
    local laxb = QuestHelper:ComputeTravelTime(e, pos2, true)
    if math.abs(laxb-d2) >= 0.0001 then
      QuestHelper:TextOut(QuestHelper:StringizeTable(pos2))
      QuestHelper:TextOut(QuestHelper:StringizeTable(e))
      QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
      QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
      QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
      ----[[ QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxb.." vs "..d2) ]]
    --[[end
  end]]

  return d, d2, e
end

local function DoneRouting(self)
  --[[ assert(self.setup_count > 0) ]]
  --[[ assert(self.setup) ]]
  
  if self.setup_count == 1 then
    self.setup_count = 0
    QuestHelper:ReleaseObjectivePathingInfo(self)
    for i, obj in ipairs(self.qh.prepared_objectives) do
      if o == obj then
        table.remove(self.qh.prepared_objectives, i)
        break
      end
    end
  else
    self.setup_count = self.setup_count - 1
  end
end

local function IsObjectiveWatched(self)
  -- Check if an objective is being watched.  Note that this is an external query, not a simple Selector.
  local info

  if self.cat == "quest" then
    info = QuestHelper.quest_log[self]
  else
    info = QuestHelper.quest_log[self.quest]
  end

  if info then
    local index = info.index
    if index then
      if UberQuest then
        -- UberQuest has it's own way of tracking quests.
        local uq_settings = UberQuest_Config[UnitName("player")]
        if uq_settings then
          local list = uq_settings.selected
          if list then
            return list[GetQuestLogTitle(index)]
          end
        end
      else
        return IsQuestWatched(index)
      end
    end
  end

  return false
end


local next_objective_id = 0

local function ObjectiveShare(self)
  self.want_share = true
end

local function ObjectiveUnshare(self)
  self.want_share = false
end

QuestHelper.default_objective_param =
 {
  CouldBeFirst=ObjectiveCouldBeFirst,
  
  Uses=Uses,
  DoMarkUsed=DoMarkUsed,
  MarkUsed=MarkUsed,
  MarkUnused=MarkUnused,
  
  DefaultKnown=DefaultObjectiveKnown,
  Known=DummyObjectiveKnown,
  Reason=ObjectiveReason,
  
  AppendPositions=ObjectiveAppendPositions,
  PrepareRouting=ObjectivePrepareRouting,
  AddLoc=AddLoc,
  FinishAddLoc=FinishAddLoc,
  DoneRouting=DoneRouting,
  
  Position=GetPosition,
  TravelTime=ObjectiveTravelTime,
  TravelTime2=ObjectiveTravelTime2,

  IsWatched=IsObjectiveWatched,
  
  Share=ObjectiveShare, -- Invoke to share this objective with your peers.
  Unshare=ObjectiveUnshare, -- Invoke to stop sharing this objective.
 }

QuestHelper.default_objective_item_param =
 {
  Known = ItemKnown,
  AppendPositions = ItemAppendPositions,
  DoMarkUsed = ItemDoMarkUsed
 }

for key, value in pairs(QuestHelper.default_objective_param) do
  if not QuestHelper.default_objective_item_param[key] then
    QuestHelper.default_objective_item_param[key] = value
  end
end

QuestHelper.default_objective_meta = { __index = QuestHelper.default_objective_param }
QuestHelper.default_objective_item_meta = { __index = QuestHelper.default_objective_item_param }

function QuestHelper:NewObjectiveObject()
  next_objective_id = next_objective_id+1
  return
   setmetatable({
    qh=self,
    id=next_objective_id,
    
    want_share=false, -- True if we want this objective shared.
    is_sharing=false, -- Set to true if we've told other users about this objective.
    
    user_ignore=nil, -- When nil, will use filters. Will ignore, when true, always show (if known).
    
    priority=3, -- A hint as to what priority the quest should have. Should be 1, 2, 3, 4, or 5.
    real_priority=3, -- This will be set to the priority routing actually decided to assign it.
    
    setup_count=0,
    
    icon_id=12,
    icon_bg=14,
    
    match_zone=false,
    match_level=false,
    match_done=false,
    
    before={}, -- List of objectives that this objective must appear before.
    after={}, -- List of objectives that this objective must appear after.
    
    -- Routing related junk.
    
    --[[ Will be created as needed.
    d=nil,
    p=nil,
    nm=nil, -- Maps nodes to their nearest zone/list/x/y position.
    nm2=nil, -- Maps nodes to their nears position, but dynamically set in TravelTime2.
    nl=nil, -- List of all the nodes we need to consider.
    location=nil, -- Will be set to the best position for the node.
    pos=nil, -- Zone node list, distance list, x, y, reason.
    sop=nil ]]
   }, QuestHelper.default_objective_meta)
end

local explicit_support_warning_given = false

function QuestHelper:GetObjective(category, objective)
  local objective_list = self.objective_objects[category]
  
  if not objective_list then
    objective_list = {}
    self.objective_objects[category] = objective_list
  end
  
  local objective_object = objective_list[objective]
  
  if not objective_object then
    if category == "quest" then
      local level, hash, name = string.match(objective, "^(%d+)/(%d*)/(.*)$")
      if not level then
        level, name = string.match(objective, "^(%d+)/(.*)$")
        if not level then
          name = objective
        end
      end
      
      if hash == "" then hash = nil end
      objective_object = self:GetQuest(name, tonumber(level), tonumber(hash))
      objective_list[objective] = objective_object
      return objective_object
    end
    
    objective_object = self:NewObjectiveObject()
    
    objective_object.cat = category
    objective_object.obj = objective
    
    if category == "item" then
      setmetatable(objective_object, QuestHelper.default_objective_item_meta)
      objective_object.icon_id = 2
    elseif category == "monster" then
      objective_object.icon_id = 1
    elseif category == "object" then
      objective_object.icon_id = 3
    elseif category == "event" then
      objective_object.icon_id = 4
    elseif category == "loc" then
      objective_object.icon_id = 6
    elseif category == "reputation" then
      objective_object.icon_id = 5
    elseif category == "player" then
      objective_object.icon_id = 1 -- not ideal, will improve later
    else
      if not explicit_support_warning_given then
        self:TextOut("FIXME: Objective type '"..category.."' for objective '"..objective.."' isn't explicitly supported yet; hopefully the dummy handler will do something sensible.")
        explicit_support_warning_given = true
      end
    end
    
    objective_list[objective] = objective_object
    
    if category == "loc" then
      -- Loc is special, we don't store it, and construct it from the string.
      -- Don't have any error checking here, will assume it's correct.
      local i
      local _, _, c, z, x, y = string.find(objective,"^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
      
      if not y then
        _, _, i, x, y = string.find(objective,"^(%d+),([%d%.]+),([%d%.]+)$")
      else
        i = QuestHelper_IndexLookup[c][z]
      end
      
      objective_object.o = {pos={{tonumber(i),tonumber(x),tonumber(y),1}}}
      objective_object.fb = {}
    else
      objective_list = QuestHelper_Objectives_Local[category]
      if not objective_list then
        objective_list = {}
        QuestHelper_Objectives_Local[category] = objective_list
      end
      objective_object.o = objective_list[objective]
      if not objective_object.o then
        objective_object.o = {}
        objective_list[objective] = objective_object.o
      end
      local l = QuestHelper_StaticData[self.locale]
      if l then
        objective_list = l.objective[category]
        if objective_list then
          objective_object.fb = objective_list[objective]
        end
      end
      if not objective_object.fb then
        objective_object.fb = {}
      end
      
      -- TODO: If we have some other source of information (like LightHeaded) add its data to objective_object.fb
      
    end
  end
  
  return objective_object
end

function QuestHelper:AppendObjectivePosition(objective, i, x, y, w)
  if not i then return end  -- We don't have a player position. We have a pile of poop. Enjoy your poop.
  
  local pos = objective.o.pos
  if not pos then
    if objective.o.drop or objective.o.contained then
      return -- If it's dropped by a monster, don't record the position we got the item at.
    end
    objective.o.pos = self:AppendPosition({}, i, x, y, w)
  else
    self:AppendPosition(pos, i, x, y, w)
  end
end

function QuestHelper:AppendObjectiveDrop(objective, monster, count)
  local drop = objective.o.drop
  if drop then
    drop[monster] = (drop[monster] or 0)+(count or 1)
  else
    objective.o.drop = {[monster] = count or 1}
    objective.o.pos = nil -- If it's dropped by a monster, then forget the position we found it at.
  end
end

function QuestHelper:AppendItemObjectiveDrop(item_object, item_name, monster_name, count)
  local quest = self:ItemIsForQuest(item_object, item_name)
  if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
    self:AppendQuestDrop(quest, item_name, monster_name, count)
  else
    if not item_object.o.drop and not item_object.o.pos then
      self:PurgeQuestItem(item_object, item_name)
    end
    self:AppendObjectiveDrop(item_object, monster_name, count)
  end
end

function QuestHelper:AppendItemObjectivePosition(item_object, item_name, i, x, y)
  local quest = self:ItemIsForQuest(item_object, item_name)
  if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
    self:AppendQuestPosition(quest, item_name, i, x, y)
  else
    if not item_object.o.vendor and not item_object.o.drop and not item_object.o.contained and not item_object.o.pos then
      -- Just learned that this item doesn't depend on a quest to drop, remove any quest references to it.
      self:PurgeQuestItem(item_object, item_name)
    end
    self:AppendObjectivePosition(item_object, i, x, y)
  end
end

function QuestHelper:AppendItemObjectiveContainer(objective, container_name, count)
  local container = objective.o.contained
  if container then
    container[container_name] = (container[container_name] or 0)+(count or 1)
  else
    objective.o.contained = {[container_name] = count or 1}
    objective.o.pos = nil -- Forget the position.
  end
end

function QuestHelper:AddObjectiveWatch(objective, reason)
  if not objective.reasons then
    objective.reasons = {}
  end
  
  if not next(objective.reasons, nil) then
    objective.watched = true
    objective:MarkUsed()
    
    objective.filter_blocked = false
    for obj in pairs(objective.swap_after or objective.after) do
      if obj.watched then
        objective.filter_blocked = true
        break
      end
    end
    
    for obj in pairs(objective.swap_before or objective.before) do
      if obj.watched then
        obj.filter_blocked = true
      end
    end
    
    if self.to_remove[objective] then
      self.to_remove[objective] = nil
    else
      self.to_add[objective] = true
    end
  end
  
  objective.reasons[reason] = (objective.reasons[reason] or 0) + 1
end

function QuestHelper:RemoveObjectiveWatch(objective, reason)
  if objective.reasons[reason] == 1 then
    objective.reasons[reason] = nil
    if not next(objective.reasons, nil) then
      objective:MarkUnused()
      objective.watched = false
      
      for obj in pairs(objective.swap_before or objective.before) do
        if obj.watched then
          obj.filter_blocked = false
          for obj2 in pairs(obj.swap_after or obj.after) do
            if obj2.watched then
              obj.filter_blocked = true
              break
            end
          end
        end
      end
      
      if self.to_add[objective] then
        self.to_add[objective] = nil
      else
        self.to_remove[objective] = true
      end
    end
  else
    objective.reasons[reason] = objective.reasons[reason] - 1
  end
end

function QuestHelper:ObjectiveObjectDependsOn(objective, needs)
  --[[ assert(objective ~= needs) ]] -- If this was true, ObjectiveIsKnown would get in an infinite loop.
  -- TODO: Needs sanity checking, especially now that dependencies can be assigned by remote users.
  
  
  -- We store the new relationships in objective.swap_[before|after],
  -- creating and copying them from objective.[before|after],
  -- the routing coroutine will check for those, swap them, and release the originals
  -- when it gets to a safe place to do so.
  
  if not (objective.swap_after or objective.after)[needs] then
    if objective.peer then
      for u, l in pairs(objective.peer) do
        -- Make sure other users know that the dependencies for this objective changed.
        objective.peer[u] = math.min(l, 1)
      end
    end
    
    if not objective.swap_after then
      objective.swap_after = self:CreateTable("swap_after")
      for key,value in pairs(objective.after) do objective.swap_after[key] = value end
    end
    
    if not needs.swap_before then
      needs.swap_before = self:CreateTable("swap_before")
      for key,value in pairs(needs.before) do needs.swap_before[key] = value end
    end
    
    if needs.watched then
      objective.filter_blocked = true
    end
    
    objective.swap_after[needs] = true
    needs.swap_before[objective] = true
  end
end

function QuestHelper:IgnoreObjective(objective)
  if self.user_objectives[objective] then
    self:RemoveObjectiveWatch(objective, self.user_objectives[objective])
    self.user_objectives[objective] = nil
  else
    objective.user_ignore = true
  end
  
  --self:ForceRouteUpdate()
end

function QuestHelper:SetObjectivePriority(objective, level)
  level = math.min(5, math.max(1, math.floor((tonumber(level) or 3)+0.5)))
  if level ~= objective.priority then
    objective.priority = level
    if objective.peer then
      for u, l in pairs(objective.peer) do
        -- Peers don't know about this new priority.
        objective.peer[u] = math.min(l, 2)
      end
    end
    --self:ForceRouteUpdate()
  end
end

local function CalcObjectivePriority(obj)
  local priority = obj.priority
  
  for o in pairs(obj.before) do
    if o.watched then
      priority = math.min(priority, CalcObjectivePriority(o))
    end
  end
  
  return priority
end

local function ApplyBlockPriority(obj, level)
  for o in pairs(obj.before) do
    if o.watched then
      ApplyBlockPriority(o, level)
    end
  end
  
  if obj.priority < level then QuestHelper:SetObjectivePriority(obj, level) end
end

function QuestHelper:SetObjectivePriorityPrompt(objective, level)
  self:SetObjectivePriority(objective, level)
  if CalcObjectivePriority(objective) ~= level then
    local menu = self:CreateMenu()
    self:CreateMenuTitle(menu, QHText("IGNORED_PRIORITY_TITLE"))
    self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_FIX")):SetFunction(ApplyBlockPriority, objective, level)
    self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_IGNORE")):SetFunction(self.nop)
    menu:ShowAtCursor()
  end
end

function QuestHelper:SetObjectiveProgress(objective, user, have, need)
  if have and need then
    local list = objective.progress
    if not list then
      list = self:CreateTable("objective.progress")
      objective.progress = list
    end
    
    local user_progress = list[user]
    if not user_progress then
      user_progress = self:CreateTable("objective.progress[user]")
      list[user] = user_progress
    end
    
    local pct = 0
    local a, b = tonumber(have), tonumber(need)
    if a and b then
      if b ~= 0 then
        pct = a/b
      elseif a == 0 then
        pct = 1
      end
    elseif a == b then
      pct = 1
    end
    
    user_progress[1], user_progress[2], user_progress[3] = have, need, pct
  else
    if objective.progress then
      if objective.progress[user] then
        self:ReleaseTable(objective.progress[user])
        objective.progress[user] = nil
        
        if not next(objective.progress, nil) then
          self:ReleaseTable(objective.progress)
          objective.progress = nil
        end
      end
    end
  end
end
