-- 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, ...)
local L = shared.L

local progressed_objectives = {}

local objective_constants = {}
local objective_getters = {}
local objective_setters = {}
local objective_meta = {}

function objective_meta.__index(self, key)
  assert(type(key) == "string")
  local const = objective_constants[key]
  if const then return const end
  local getter = objective_getters[key]
  if getter then return getter(self, key) end
  if objective_setters[key] then
    error("'"..key.."' is write-only.")
  else
    error("'"..key.."' isn't a valid objective key.")
  end
end

function objective_meta.__newindex(self, key, value)
  assert(type(key) == "string")
  local setter = objective_setters[key]
  if setter then return setter(self, value, key) end
  if objective_constants[key] or objective_getters[key] then
    error("'"..key.."' is read-only.")
  else
    error("'"..key.."' isn't a valid objective key.")
  end
end

-- Stores the internal data for the objectives.
-- We use weak keys and values so that objectives can be garbage collected.
local internal = setmetatable({}, {__mode = "k"})
local unique_objectives = setmetatable({}, {__mode = "v"})

local selected_objective = nil

function objective_meta.__call(self, ...)
  local cb = internal[self].callback
  if cb then cb(self, ...) end
end

-- These functions are defined later in this file.
local announceObjectiveDisabled,
      announceObjectiveEnabled,
      announceObjectiveModified,
      updateObjectiveImplicitStates

function shared.lookupObjective(unique_id)
  assert(type(unique_id) == "string")
  
  return unique_objectives[unique_id]
end

local function resetObjective(objective)
  local int = assert(internal[objective])
  
  -- We're about to reset the objective to its default state, which is disabled.
  local was_previously_enabled = int.enabled_implicit
  
  -- Reset everything that should be reset.
  int.text = int.unique_id or "(OBJECTIVE)"
  int.comment = ""
  -- priority is not reset, in case the user modified it.
  int.locations:clear()
  int.locations_need_prune = false
  int.progress = 0
  int.progress_max = 1
  int.progress_type = "FRACTION"
  int.enabled = false
  int.obtainable = true
  int.root = false
  int.enabled_implicit = false
  int.complete_implicit = false
  int.obtainable_implicit = false
  int.root_implicit = false
  int.icon = nil
  int.item_id = nil
  int.spell_id = nil
  int.macro_text = nil
  int.callback = nil
  int.route_location = nil
  
  -- Remove all the dependencies.
  for dep in pairs(int.dependencies) do
    int.dependencies[dep] = nil
    internal[dep].reverse_dependencies[objective] = nil
    updateObjectiveImplicitStates(dep)
  end
  
  for rdep in pairs(int.reverse_dependencies) do
    int.reverse_dependencies[rdep] = nil
    internal[rdep].dependencies[objective] = nil
    updateObjectiveImplicitStates(rdep)
  end
  
  if was_previously_enabled then
    announceObjectiveDisabled(objective)
  end
end

function shared.createObjective(unique_id, no_reset_if_existing)
  assert(unique_id == nil or type(unique_id) == "string")
  
  local objective = unique_id and unique_objectives[unique_id]
  
  if objective then
    -- Horray, this poor lost objective has been adopted! (If it wasn't already.)
    internal[objective].owned = true
    
    -- Reset the objective to default values, since presumably
    -- the caller plans to set it up, and expects it to
    -- be pristine.
    if no_reset_if_existing then
      if internal[objective].enabled_implicit then
        announceObjectiveModified(objective, false)
      end
      
      return objective, true
    end
    
    resetObjective(objective)
    
    return objective, false
  end
  
  local stack = (debugstack(2, 1, 0) or "")
  
  local addon = stack:match("AddOns[\\/](.-)[\\/]") or stack:match("[\\/](.-)[\\/]") or "<<UNKNOWN>>"
  
  objective = setmetatable({}, objective_meta)
  
  internal[objective] = {
    objective = objective,
    unique_id = unique_id,
    creator = addon,
    creation_time = time(),
    owned = true,
    text = unique_id or "(OBJECTIVE)",
    comment = "",
    -- the default priority is 'nil', meaning 'assign a sensible priority automatically.'
    priority_implicit = 3,
    locations = shared.createLocationList(),
    locations_need_prune = false,
    dependencies = {},
    reverse_dependencies = {},
    progress = 0,
    progress_max = 1,
    progress_type = "FRACTION",
    enabled = false,
    obtainable = true,
    root = false,
    enabled_implicit = false,
    complete_implicit = false,
    obtainable_implicit = false,
    root_implicit = false,
    icon = nil,
    item_id = nil,
    spell_id = nil,
    macro_text = nil,
    callback = nil,
    route_location = nil
  }
  
  if unique_id then
    unique_objectives[unique_id] = objective
  end
  
  return objective, false
end

local function genericGet(self, key)
  return internal[self][key]
end

local function genericGetImplicit(self, key)
  return internal[self][key.."_implicit"]
end

objective_getters.creator = genericGet
objective_getters.owned = genericGet
objective_getters.text = genericGet
objective_getters.comment = genericGet
objective_getters.enabled = genericGetImplicit
objective_getters.complete = genericGetImplicit
objective_getters.obtainable = genericGetImplicit
objective_getters.root = genericGetImplicit
objective_getters.priority = genericGetImplicit
objective_getters.unique_id = genericGet
objective_getters.item_id = genericGet
objective_getters.spell_id = genericGet
objective_getters.macro_text = genericGet
objective_getters.callback = genericGet

function objective_getters.selected(self)
  return self == selected_objective
end

function objective_getters.has_locations(self)
  return not internal[self].locations:empty()
end

function objective_getters.icon(self)
  local icon = internal[self].icon or "INV_misc_note_04"
  
  if not icon:find("%.[%a]+$") then
    -- If no extension, assume .blp.
    icon = icon .. ".blp"
  end
  
  if not icon:find("[\\/]") then
    -- If no path delimeters, assume the WoW's icon directory.
    icon = "Interface\\Icons\\" .. icon
  end
  
  return icon
end

function objective_constants:setTextureToIcon(texture)
  local icon = self.icon
  
  if icon:find("%.[bB][lL][pP]$") then
    SetPortraitToTexture(texture, (icon:match("^(.*)....$")))
  else
    texture:SetTexture(icon)
  end
  
  if self.obtainable then
    if not texture:SetDesaturated(false) then
      texture:SetVertexColor(1,1,1)
    end
  else
    if not texture:SetDesaturated(true) then
      texture:SetVertexColor(.5,.5,.5)
    end
  end
end

function objective_getters.priority_text(self)
  return L["PRIORITY"..internal[self].priority_implicit]
end

function objective_getters.creation_time(self)
  local now, when = time(), internal[self].creation_time
  local delta = now - when
  local text
  
  if delta < 0 then
    -- Using World of Warcraft to time travel is not
    -- recommended and may void your warrently.
    -- We accept no responability for any temporal
    -- paradoxes and their unpleasant consequences, including
    -- but not limited to your ceasing to exist, created
    -- while using this product.
    text = L.FUTURE_DATE_ERROR
  else
    local seconds = delta%60
    local minutes = math.floor(delta/60)%60
    local hours = math.floor(delta/3600)
    local when_day = date("%A", when)
    local today = date("%A", now)
    local yesterday = date("%A", now-86400)
    
    if hours >= 120 then
      -- 5-∞ days.
      text = L.TIME_DELTA_DATE:format(date(L.DATE_FORMAT, when), date(L.TIME_FORMAT, when))
    elseif hours >= 8 then
      -- 8-120 hours.
      if when_day == today then
        text = L.TIME_DELTA_TODAY:format(date(L.TIME_FORMAT, when))
      elseif when_day == yesterday then
        text = L.TIME_DELTA_YESTERDAY:format(date(L.TIME_FORMAT, when))
      else
        text = L.TIME_DELTA_WEEKDAY:format(when_day, date(L.TIME_FORMAT, when))
      end
    elseif hours >= 1 then
      -- 1-8 hours.
      text = L.TIME_DELTA_HOURS:format(hours, minutes)
    elseif minutes >= 1 then
      -- 1-60 minutes.
      text = L.TIME_DELTA_MINUTES:format(minutes, seconds)
    else
      -- 0-60 seconds.
      text = L.TIME_DELTA_SECONDS:format(seconds)
    end
  end
  
  return text
end

function objective_getters.formatted_progress(self)
  local internal = internal[self]
  
  if self.complete then
    return ("|cff00ff00%s|r"):format(L.COMPLETE)
  elseif internal.progress_max == 1 or not internal.obtainable_implicit then
    -- Don't display progress for objectives that can't currently be completed,
    -- or when theres only one thing to do.
    return ""
  else
    local pct, r, g, b = self.progress
    if pct < 0.5 then r, g, b = 255, pct*510, 0
    else r, g, b = 510-pct*510, 255, 0 end
    
    if internal.progress_type == "FRACTION" then
      return ("|cff%02x%02x%02x%d/%d|r"):format(r, g, b, internal.progress, internal.progress_max)
    elseif internal.progress_type == "COUNTDOWN" then
      return ("|cff%02x%02x%02x%d|r"):format(r, g, b, internal.progress_max-internal.progress)
    elseif internal.progress_type == "REPUTATION" then
      return ("|cff%02x%02x%02x%s|r"):format(r, g, b, _G["FACTION_STANDING_LABEL"..(internal.progress_max+1)] or ("REP"..internal.progress_max))
    elseif internal.progress_type == "MONEY" then
      local required = internal.progress_max - internal.progress
      
      -- Because I'm a complete moron; I shall even obey these global variables,
      -- although why anyone would change them is anyones guess.
      local COPPER_PER_SILVER=math.max(1, COPPER_PER_SILVER)
      local SILVER_PER_GOLD=math.max(1, SILVER_PER_GOLD)
      
      local COPPER_PER_GOLD=SILVER_PER_GOLD*COPPER_PER_SILVER
      
      -- In any sane world, we'll be padding zeros up to 2 digits.
      local COPPER_ZEROS=math.ceil(math.log(COPPER_PER_SILVER)/math.log(10))
      local SILVER_ZEROS=math.ceil(math.log(SILVER_PER_GOLD)/math.log(10))
      
      local gold, silver, copper = math.floor(required/COPPER_PER_GOLD),
                                   math.floor(required/COPPER_PER_SILVER%SILVER_PER_GOLD),
                                   math.floor(required%COPPER_PER_SILVER)
      
      local gold_str, silver_str, copper_str = "", "", ""
      
      if gold > 0 then
        gold_str = ("|cffffff00%d|TInterface\\MoneyFrame\\UI-GoldIcon:0|t"):format(gold)
        if silver > 0 then silver_str = ("|cffd0d0d0%0"..SILVER_ZEROS.."d|r|TInterface\\MoneyFrame\\UI-SilverIcon:0|t"):format(silver) end
        if copper > 0 then copper_str = ("|cffc2621d%0"..COPPER_ZEROS.."d|r|TInterface\\MoneyFrame\\UI-CopperIcon:0|t"):format(copper) end
      elseif silver > 0 then
        silver_str = ("|cffd0d0d0%d|TInterface\\MoneyFrame\\UI-SilverIcon:0|t"):format(silver)
        if copper > 0 then copper_str = ("|cffc2621d%0"..COPPER_ZEROS.."d|r|TInterface\\MoneyFrame\\UI-CopperIcon:0|t"):format(copper) end
      else
        copper_str = ("|cffc2621d%d|r|TInterface\\MoneyFrame\\UI-CopperIcon:0|t"):format(copper)
      end
      
      return ("%s%s%s"):format(gold_str, silver_str, copper_str)
    else
      return "(TODO: PROGRESS_TYPE "..progress_type..")"
    end
  end
end

function objective_getters.formatted_text(self)
  local progress = self.formatted_progress
  
  if progress == "" then
    return self.text
  else
    return ("%s: %s"):format(self.text, progress)
  end
end

function objective_getters.progress(self)
  local internal = internal[self]
  local progress, progress_max = internal.progress, internal.progress_max
  
  if progress == progress_max then
    return 1
  elseif progress_max == 0 then
    return 0
  else
    local pct = progress/progress_max
    if pct < 0 then return 0
    elseif pct > 1 then return 1
    else return pct end
  end
end

local objective_enabled_callbacks = {}
local objective_disabled_callbacks = {}
local objective_modified_callbacks = {}
local objective_progress_callbacks = {}

local progressed_objectives = {}

local function announceProgressedObjectives()
  for objective in pairs(progressed_objectives) do
    progressed_objectives[objective] = nil
    shared.invokeCallbackList(objective_progress_callbacks, objective)
  end
end

local function objectiveProgress(objective)
  progressed_objectives[objective] = true
  shared.callOnce(announceProgressedObjectives)
end

-- We queue up modified changes and announce them next frame,
-- that way you can change a bunch of properties for an objective.
-- and only have the onObjectiveModified callbacks invoked a single time.
local modified_objectives = {}

local function announceModifiedObjectivesCallback()
  for objective, affects_routing in pairs(modified_objectives) do
    modified_objectives[objective] = nil
    
    shared.invokeCallbackList(objective_modified_callbacks, objective, affects_routing)
  end
end

announceObjectiveModified = function (objective, affects_routing)
  if internal[objective].enabled_implicit then
    if not next(modified_objectives) then
      -- There are no modified objectives yet; so
      -- announceModifiedObjectivesCallback hasn't been arranged to
      -- be called yet.
      shared.callOnce(announceModifiedObjectivesCallback)
    end
    
    if affects_routing then
      modified_objectives[objective] = true
    else
      -- Yes, the 'or false' makes sense; it will prevent
      -- modified_objectives[objective] from remaining 'nil' if that was its original value.
      modified_objectives[objective] = modified_objectives[objective] or false
    end
  end
end

announceObjectiveEnabled = function (objective)
  -- Tell anyone that cares that this objective is now enabled.
  shared.invokeCallbackList(objective_enabled_callbacks, objective)
end

announceObjectiveDisabled = function (objective)
  -- We don't announce modifications for disabled objectives.
  modified_objectives[objective] = nil
  
  -- Tell anyone that cares that this objective is disabled.
  shared.invokeCallbackList(objective_disabled_callbacks, objective)
end

function shared.onObjectiveEnabled(cb, ...)
  cb = shared.callback(cb, ...)
  
  table.insert(objective_enabled_callbacks, cb)
  
  -- Announce to the callback immediately any objectives that are currently enabled.
  for objective, internal_data in pairs(internal) do
    if internal_data.enabled_implicit then
      cb(objective)
    end
  end
end

function shared.onObjectiveDisabled(cb, ...) table.insert(objective_disabled_callbacks, shared.callback(cb, ...)) end
function shared.onObjectiveModified(cb, ...) table.insert(objective_modified_callbacks, shared.callback(cb, ...)) end
function shared.onObjectiveProgress(cb, ...) table.insert(objective_progress_callbacks, shared.callback(cb, ...)) end

-- force_modified should be nil to not force announcing the objective as modified,
-- setting it to false causes it to be announced as modified, but that the modification doesn't affect routing.
updateObjectiveImplicitStates = function(objective, force_modified)
  local internal = internal[objective]
  
  local enabled = internal.enabled
  local complete = internal.progress == internal.progress_max
  local priority = internal.priority
  local obtainable = internal.obtainable
  local root = internal.root
  
  -- enabled : disabled become invisible to the world. They aren't displayed in the tracker, in routes, or appear as dependencies of other objectives.
  -- obtainable: obtainable objectives are objectives that the player is capable of completing, and appear in the quest tracker.
  --             unobtainable objectives may also appear in the quest tracker, but only if they have obtainable dependencies.
  -- complete: complete objectives are explicitly disabled, to prevent them from appearing in routes and on the tracker.
  -- root: root objectives act as parents to their dependencies. They also mark the stopping point where unobtainable objectives will be added to the tracker.
  --       root objectives are displayed differently from ordinary objectives in the tracker. An objective is explicitly a root objective if no
  --       enabled objective depends on it.
  
  if selected_objective == objective then
    -- If the objective is selected, then its priority in implicitly 0, so that it will be completed before anything else.
    priority = 0
  end
  
  local has_enabled_rdeps = false
  local has_incomplete_rdeps = false
  
  for dep in pairs(internal.reverse_dependencies) do
    if dep.enabled then
      has_enabled_rdeps = true
      
      -- This objective's priority needs to be at least as high as anything that depends on it.
      if not priority or dep.priority < priority then priority = dep.priority end
    end
    
    has_enabled_rdeps = has_enabled_rdeps or dep.enabled
    has_incomplete_rdeps = has_incomplete_rdeps or not dep.complete
  end
  
  if not has_enabled_rdeps then
    -- As nothing depends on this objective, it is implicitly a root objective.
    root = true
  end
  
  if not priority then
    -- If we failed to inherit a priority from any of our parents, then use the default.
    -- Root objectives get 3, child objectives get 5.
    -- Since objectives need to have a priority at least as high as their parent,
    -- the value of 5 will get overridden to whatever their parent is.
    priority = root and 3 or 5
  end
  
  if has_enabled_rdeps and not has_incomplete_rdeps then
    -- Everything that depends on this objective is complete,
    -- therefore it must also be complete since it doesn't need to be done.
    complete = true
  end
  
  if complete then
    -- Complete objectives are always obtainable; with no effort required!
    obtainable = true
    
    -- They're also disabled.
    enabled = false
  end
  
  -- Okay, now to see if the objectives state differs from what it originally was...
  
  local enable_state_changed = enabled ~= internal.enabled_implicit
  
  local modified = complete ~= internal.complete_implicit or
                   obtainable ~= internal.obtainable_implicit or
                   root ~= internal.root_implicit or
                   priority ~= internal.priority_implicit or
                   enable_state_changed
  
  local affects_routing = modified
  
  if force_modified ~= nil then
    modified, affects_routing = true, affects_routing or force_modified
  end
  
  if modified then
    internal.complete_implicit = complete
    internal.obtainable_implicit = obtainable
    internal.root_implicit = root
    internal.enabled_implicit = enabled
    internal.priority_implicit = priority
  end
  
  if enable_state_changed then
    if enabled then
      announceObjectiveEnabled(objective)
    else
      announceObjectiveDisabled(objective)
    end
  elseif modified then
    announceObjectiveModified(objective, affects_routing)
  end
  
  if modified then
    -- Our change of state may have affected the objectives we depend on or objectives that depend on us.
    for dep in pairs(internal.dependencies) do updateObjectiveImplicitStates(dep, nil) end
    for dep in pairs(internal.reverse_dependencies) do updateObjectiveImplicitStates(dep, nil) end
  end
end

-- force: nil=not modified, false=modified, but doesn't affect route, true=modified and change affects route
local function genericObjectiveStateChanger(objective, state, key)
  state = state and true or false
  local internal = internal[objective]
  if internal[key] ~= state then
    internal[key] = state
    updateObjectiveImplicitStates(objective, false)
  end
end

objective_setters.enabled = genericObjectiveStateChanger
objective_setters.obtainable = genericObjectiveStateChanger
objective_setters.root = genericObjectiveStateChanger

function objective_setters.priority(self, priority)
  if priority ~= internal[self].priority then
    assert(priority == nil or (type(priority) == "number" and priority >= 0 and priority <= 5 and math.floor(priority) == priority))
    internal[self].priority = priority
    updateObjectiveImplicitStates(self, nil)
  end
end

function objective_setters.callback(self, callback)
  if callback ~= internal[self].callback then
    internal[self].callback = callback
    announceObjectiveModified(self, false)
  end
end

function objective_setters.selected(self, selected)
  local old_selected_objective = selected_objective
  
  if selected then
    if self ~= selected_objective then
      local old = selected_objective
      selected_objective = self
      if old then updateObjectiveImplicitStates(old, false) end
      updateObjectiveImplicitStates(self, false)
    end
  else
    if self == selected_objective then
      selected_objective = nil
      updateObjectiveImplicitStates(self, false)
    end
  end
end

function objective_setters.text(self, text)
  assert(type(text) == "string")
  if internal[self].text ~= text then
    internal[self].text = text
    announceObjectiveModified(self, false)
  end
end

function objective_setters.comment(self, comment)
  assert(type(comment) == "string")
  if internal[self].comment ~= comment then
    internal[self].comment = comment
    announceObjectiveModified(self, false)
  end
end

function objective_setters.complete(self, state)
  local internal = internal[self]
  
  if state then
    if internal.progress ~= internal.progress_max then
      internal.progress = internal.progress_max
      if internal.enabled_implicit then objectiveProgress(self) end
      updateObjectiveImplicitStates(self, false)
    end
  else
    if internal.progress == internal.progress_max then
      internal.progress = internal.progress_max - 1
      updateObjectiveImplicitStates(self, false)
    end
  end
end

function objective_setters.icon(self, icon)
  if icon ~= internal[self].icon then
    assert(icon == nil or type(icon) == "string")
    internal[self].icon = icon
    announceObjectiveModified(self, false)
  end
end

function objective_setters.item_id(self, item_id)
  local internal = internal[self]
  local original_item_id = internal.item_id
  
  if item_id ~= nil then
    item_id = tonumber(item_id) or tonumber(item_id:match("|Hitem:(%d+):"))
    assert(item_id)
  end
  
  if item_id ~= original_item_id then
    internal.item_id = item_id
    announceObjectiveModified(self, false)
  end
end

function objective_setters.spell_id(self, spell_id)
  local internal = internal[self]
  local original_spell_id = internal.spell_id
  
  if spell_id ~= nil then
    spell_id = tonumber(spell_id) or tonumber(item_id:match("|Hspell:(%d+)|"))
    assert(spell_id)
  end
  
  if spell_id ~= original_spell_id then
    internal.spell_id = spell_id
    announceObjectiveModified(self, false)
  end
end

function objective_setters.macro_text(self, macro_text)
  assert(macro_text == nil or type(macro_text) == "text")
  
  local internal = internal[self]
  
  if internal.macro_text ~= macro_text then
    internal.macro_text = macro_text
    announceObjectiveModified(self, false)
  end
end

function objective_constants:setProgress(progress, progress_max, progress_type)
  progress_type = progress_type or "FRACTION"
  assert(progress_type == "FRACTION" or progress_type == "COUNTDOWN" or progress_type == "MONEY" or progress_type == "REPUTATION", "Bad progress type: "..progress_type)
  assert(type(progress) == "number" and type(progress_max) == "number")
  local internal = internal[self]
  if internal.progress ~= progress or internal.progress_max ~= progress_max or progress_type ~= internal.progress_type then
    if internal.enabled_implicit and internal.progress_max == progress_max and progress > internal.progress then
      objectiveProgress(self)
    end
    
    internal.progress = progress
    internal.progress_max = progress_max
    internal.progress_type =  progress_type
    updateObjectiveImplicitStates(self, false)
  end
end

function objective_constants:getProgress()
  local internal = internal[self]
  return internal.progress, internal.progress_max, internal.progress_type
end

-- Returns true if obj or any of its dependencies is dep.
local function checkDependency(obj, dep, offender, seen)
  if obj == dep then return offender end
  
  if not seen then seen = {}
  elseif seen[obj] then return nil end
  
  seen[obj] = true
  
  for sub_obj in obj:fwdDependencies() do
    local result = checkDependency(sub_obj, dep, obj, seen)
    if result then return result end
  end
  
  return nil
end

function objective_constants:addDependencies(...)
  local deps, rev_deps = internal[self].dependencies, internal[self].reverse_dependencies
  local changed = false
  
  for i = 1,select("#", ...) do
    local obj = select(i, ...)
    if not deps[obj] and obj ~= self then
      local offender = checkDependency(obj, self)
      
      while offender do
        -- offender, obj or a dependency there of, depends on us.
        -- Break the old dependency to prevent the creation of a circular dependency loop.
        assert(rev_deps[offender])
        rev_deps[offender] = nil
        internal[offender].dependencies[self] = nil
        offender = checkDependency(obj, self)
        updateObjectiveImplicitStates(offender, true)
      end
      
      deps[obj] = true
      internal[obj].reverse_dependencies[self] = true
      changed = true
      updateObjectiveImplicitStates(obj, true)
    end
  end
  
  if changed then
    updateObjectiveImplicitStates(self, true)
  end
end

function objective_constants:removeDependencies(...)
  local deps = internal[self].dependencies
  local changed = false
  
  for i = 1,select("#", ...) do
    local obj = select(i, ...)
    if deps[obj] then
      deps[obj] = nil
      internal[obj].reverse_dependencies[self] = nil
      changed = true
      updateObjectiveImplicitStates(obj, true)
    end
  end
  
  if changed then
    updateObjectiveImplicitStates(self, true)
  end
end

function objective_constants:fwdDependencies()
  local set = internal[self].dependencies
  local cur = next(set)
  return function ()
    -- Skip over any disabled objectives.
    while cur and not cur.enabled do cur = next(set, cur) end
    
    local result = cur
    if cur then cur = next(set, cur) end
    return result
  end
end

function objective_constants:setCallback(cb, ...)
  if cb then
    if internal[self].callback ~= cb then
      internal[self].callback = shared.callback(cb, ...)
      announceObjectiveModified(self, false)
    end
  else
    internal[self].callback = nil
    announceObjectiveModified(self, false)
  end
end

function objective_constants:revDependencies()
  local set = internal[self].reverse_dependencies
  local cur = next(set)
  return function ()
    -- Skip over any disabled objectives.
    while cur and not cur.enabled do cur = next(set, cur) end
    
    local result = cur
    if cur then cur = next(set, cur) end
    return result
  end
end

function objective_constants:addLocation(area_id, level, x, y, entrance_x, entrance_y, w)
  internal[self].locations:addLocation(area_id, level, x, y, entrance_x, entrance_y, w)
  internal[self].locations_need_prune = true
  announceObjectiveModified(self, true)
end

function objective_constants:addLocationList(location_list)
  local locations = internal[self].locations
  
  for area_id, level, x, y, ex, ey, w in location_list:iterate() do
    locations:addLocation(area_id, level, x, y, ex, ey, w)
  end
  
  internal[self].locations_need_prune = true
  announceObjectiveModified(self, true)
end

function objective_constants:clearLocations()
  if not internal[self].locations:empty() then
    internal[self].locations:clear()
    announceObjectiveModified(self, true)
  end
end

function objective_constants:setLocation(area_id, level, x, y, entrance_x, entrance_y)
  -- TODO: check if location differs from original.
  self:clearLocations()
  self:addLocation(area_id, level, x, y, entrance_x, entrance_y)
end

function objective_constants:setLocationList(location_list)
  -- TODO: check if location differs from original.
  self:clearLocations()
  self:addLocationList(location_list)
end

local function pruneInternalLocations(int)
  if int.locations_need_prune then
    int.locations_need_prune = false
    int.locations:pruneLocations(8)
  end
end

function objective_constants:nearestLocation(area_id, level, x, y, entrance_x, entrance_y)
  local int = internal[self]
  
  pruneInternalLocations(int)
  
  return int.locations:nearestLocation(shared.calculateTravelTime, area_id, level, x, y, entrance_x, entrance_y)
end

function objective_constants:iterate()
  local int = internal[self]
  
  pruneInternalLocations(int)
  
  return int.locations:iterate()
end

function objective_constants:setRouteLocation(area_id, level, x, y, ex, ey)
  local internal = internal[self]
  
  if area_id then
    area_id, level, x, y, ex, ey = shared.normalizeLocation(area_id, level, x, y, ex, ey)
    
    local old_route_location = internal.route_location
    
    if not old_route_location or
       old_route_location[1] ~= area_id or
       old_route_location[2] ~= level or
       old_route_location[3] ~= x or
       old_route_location[4] ~= y or
       old_route_location[5] ~= ex or
       old_route_location[6] ~= ey then
      if old_route_location then
        old_route_location[1], old_route_location[2], old_route_location[3],
          old_route_location[4], old_route_location[5], old_route_location[6] =
            area_id, level, x, y, ex, ey
      else
        internal.route_location = { area_id, level, x, y, ex, ey }
      end
      
      announceObjectiveModified(self, false)
    end
  elseif internal.route_location then
    internal.route_location = nil
    announceObjectiveModified(self, false)
  end
end

function objective_constants:getRouteLocation()
  local route_location = internal[self].route_location
  
  if route_location then
    return unpack(route_location, 1, 6)
  else
    -- No route location was set, return the location with the most weight instead.
    return internal[self].locations:nearestLocation()
  end
end

shared.onLoad(function ()
  local saved = _G.QuestHelper2_SavedObjectives or {}
  _G.QuestHelper2_SavedObjectives = nil
  
  -- Re-create objectives for everything we have saved data for, or remove
  -- the saved data if the objective has already been created this session.
  for unique_id, data in pairs(saved) do
    local obj = unique_objectives[unique_id]
    if obj then
      -- Has already been re-created; don't overwrite it with the saved data.
      saved[unique_id] = nil
      
      -- Do however restore the creation time and priority.
      if data.creation_time then
        internal[obj].creation_time = data.creation_time
      end
      
      obj.priority = data.priority
    else
      -- Create the objective right now so we can reference it as a dependency later if necessary.
      obj = shared.createObjective(unique_id)
      
      -- Make the objective unowned.
      internal[obj].owned = nil
      
      -- Also request that its collection be delayed, so that it doesn't get garbage collected
      -- before we get around to making use of it.
      shared.delayCollection(obj, 15)
    end
  end
  
  -- Set the state of the objective to whatever it was in the previous session.
  for unique_id, data in pairs(saved) do
    local objective = assert(unique_objectives[unique_id])
    
    for i, key in ipairs({"text", "comment", "priority", "obtainable", "root", "icon", "item_id", "spell_id", "macro_text"}) do
      if data[key] ~= nil then
        objective[key] = data[key]
      end
    end
    
    internal[objective].owned = false
    internal[objective].creator = data.creator or "UNKNOWN"
    
    if data.creation_time then
      internal[objective].creation_time = data.creation_time
    end
    
    for i, loc in ipairs(data.locations or {}) do
      objective:addLocation(unpack(loc, 1, 7))
    end
    
    for i, dep_unique_id in ipairs(data.dependencies or {}) do
      local dep_objective = unique_objectives[dep_unique_id]
      if dep_objective then
        objective:addDependencies(dep_objective)
      end
    end
    
    if data.progress then
      objective:setProgress(data.progress, data.progress_max, data.progress_type)
    end
    
    if data.route_location then
      objective:setRouteLocation(unpack(data.route_location, 1, 6))
    end
  end
  
  -- Reselect the objective that was selected in the previous session, if it exists.
  if _G.QuestHelper2_SavedSelectedObjective then
    local selected = unique_objectives[_G.QuestHelper2_SavedSelectedObjective]
    
    if selected then
      selected.selected = true
    end
  end
  
  _G.QuestHelper2_SavedSelectedObjective = nil
  
  -- Enable all the objectives we created.
  for unique_id in pairs(saved) do
    assert(unique_objectives[unique_id]).enabled = true
  end
end)

shared.onSave(function ()
  local saved = {}
  _G.QuestHelper2_SavedObjectives = saved
  
  for unique_id, objective in pairs(unique_objectives) do
    if objective.enabled and objective.owned then
      local int = internal[objective]
      
      local deps = {}
      
      for dep in pairs(int.dependencies) do
        if dep.unique_id and dep.enabled and dep.owned then
          table.insert(deps, dep.unique_id)
        end
      end
      
      local locations = {}
      
      for a,l,x,y,ex,ey,w in int.locations:iterate() do
        table.insert(locations, {a,l,x,y,ex,ey,w})
      end
      
      local data = {
        creator = int.creator,
        creation_time = int.creation_time,
        text = int.text,
        comment = int.comment,
        priority = int.priority,
        locations = locations,
        
        dependencies = deps,
        
        progress = int.progress,
        progress_max = int.progress_max,
        progress_type = int.progress_type,
        
        obtainable = int.obtainable,
        root = int.root,
        
        icon = int.icon,
        item_id = int.item_id,
        spell_id = int.spell_id,
        macro_text = int.macro_text,
        
        route_location = int.route_location
      }
      
      saved[unique_id] = data
    end
  end
  
  if selected_objective then
    _G.QuestHelper2_SavedSelectedObjective = selected_objective.unique_id
  else
    _G.QuestHelper2_SavedSelectedObjective = nil
  end
end)
