-- 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 quest_db = nil

local quest_data_cache = setmetatable({}, {__mode="v"})
local quest_data = {}

local player_level = 0
local show_level, show_quest_type, colour_name, colour_level

local function icon(name)
  return ("Interface\\AddOns\\QuestHelper2\\Icons\\%s.tga"):format(name)
end

local function interpolateRGB(low, high, val, r1, g1, b1, r2, g2, b2)
  local p = 255*((val-low) / (high-low))
  local ip = 255-p
  return ("|cff%02x%02x%02x"):format(r1*ip + r2*p, g1*ip + g2*p, b1*ip + b2*p)
end

local function formatQuestName(level, tag, group_size, daily, name)
  local red = player_level+5
  local orange = player_level+2.5 -- yes, that's right, a fraction.
  local yellow = player_level
  local green = player_level-3
  local grey = player_level-9
  
  if player_level < 5 then
    grey = 0
  elseif player_level < 40 then
    grey = player_level - math.floor(player_level/10) - 5
  elseif player_level < 60 then
    grey = player_level - math.floor(player_level/5) - 1
  end
  
  if green < grey  then green = grey end
  
  local code_start
  
  if level < grey then
    -- Trivial
    code_start = "|cff666666"
  elseif level < green then
    code_start = interpolateRGB(grey,   green,  level, 0.6,0.6,0.6,   0.0,1.0,0.0)
  elseif level < yellow then
    code_start = interpolateRGB(green,  yellow, level, 0.0,1.0,0.0,   1.0,1.0,0.0)
  elseif level < orange then
    code_start = interpolateRGB(yellow, orange, level, 1.0,1.0,0.0,   1.0,0.5,0.0)
  elseif level <= red then
    code_start = interpolateRGB(orange, red,    level, 1.0,0.5,0.0,   1.0,0.0,0.0)
  else
    -- Suicide
    code_start = "|cffcc0000"
  end
  
  if show_level then
    if show_quest_type then
      if tag == RAID then
        level = level.."R"
      elseif tag == GROUP then
        level = level.."G"..group_size
      elseif tag == LFG_TYPE_DUNGEON then
        level = level.."D"
      elseif daily then
        level = level.."Y"
      end
    end
    
    if colour_level and colour_name then
      return code_start .. "[" .. level .. "] " .. name .. "|r"
    elseif colour_level then
      return "[" .. code_start .. level .. "|r] " .. name
    elseif colour_name then
      return "[" .. level .. "] " .. code_start .. name .. "|r"
    else
      return "[" .. level .. "] " .. name
    end
  elseif colour_name then
    return code_start .. name .. "|r"
  else
    return name
  end
end

local function disableQuestPOIs()
  SetCVar("questPOI", 0)
end

-- It seems that the POI functions refuse to return anything unless the 'questPOI' console variable is enabled.
-- Why? Who knows. I assume it's a simple case of somebody not loving me.
-- This function checks if the variable is enabled, and if it isn't, enables it and arranges for it to be disabled again next frame.
local function temporarlyEnableQuestPOIs()
  if not GetCVarBool("questPOI") then
    SetCVar("questPOI", 1)
    shared.callOnce(disableQuestPOIs)
  end
end

local quest_items = {}

local function getQuestData(quest_id)
  local data = quest_data_cache[quest_id]
  
  if data then
    -- Data is already available. Horray.
    return data
  end
  
  if not quest_db then
    if shared.settings.database_use then
      local db_name = UnitFactionGroup("player") == "Alliance" and
                      "QuestHelper2_AllianceQuestDB" or
                      "QuestHelper2_HordeQuestDB"
      
      local loaded, error_reason = LoadAddOn(db_name)
      
      if loaded then
        quest_db = assert(_G[db_name])
      else
        shared.onDelay(5, _G.print, "QuestHelper2 failed to load "..db_name..": "..tostring(error_reason))
        quest_db = {}
      end
    else
      quest_db = {}
    end
  end
  
  local quest_data = shared.createQuestData()
  
  -- For now; we only do POIs.
  -- TODO: Write me properly.
  -- TODO: Support quests the player doesn't have.
  
  local function callback()
    if WorldMapFrame:IsVisible() then
      ToggleFrame(WorldMapFrame)
    end
    
    local index = GetQuestLogIndexByID(quest_id)
    
    if index then
      if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow() then
        local link = GetQuestLink(index)
        if link then ChatEdit_InsertLink(link) end
      else
        QuestLog_OpenToQuest(index)
      end
    end
  end
  
  local data = {objectives={}}
  
  local old_poi_x, old_poi_y
  
  local enabled = false
  
  local function questDataChanged()
    if enabled then
      if not quest_data.acquire:empty()    then data.acquire:setLocationList(quest_data.acquire) end
      if not quest_data.completion:empty() then data.complete_obj:setLocationList(quest_data.completion)
      elseif not quest_data.turnin:empty() then data.complete_obj:setLocationList(quest_data.turnin) end
      if not quest_data.turnin:empty()     then data.turnin:setLocationList(quest_data.turnin) end
      for i, obj in pairs(quest_data.objective) do
        if data.objectives[i] then
          data.objectives[i]:setLocationList(obj.location)
        else
          print("quest_data has locations for objective #"..i.." of quest #"..quest_id..", but there doesn't seem to be any such objective!")
        end
      end
    end
  end
  
  function data:updatePOI()
    temporarlyEnableQuestPOIs()
    
    shared.pushMap()
    
    local area_id, level = GetQuestWorldMapAreaID(quest_id)
    
    SetMapByID(area_id)
    SetDungeonMapLevel(level)
    
    local x, y = select(2, QuestPOIGetIconInfo(quest_id))
    
    --if not x then
    --  print("No POI found for quest #"..quest_id)
    --end
    
    if old_poi_x ~= x and old_poi_y ~= y then
      if x and y then
        --print("POI found for quest #"..quest_id)
        if quest_data.turnin:empty() then self.turnin:setLocation(area_id, level, x, y, true) end
        if quest_data.completion:empty() then self.complete_obj:setLocation(area_id, level, x, y, true) end
        for i, obj in ipairs(self.objectives) do
          if not quest_data.objective[i] then
            obj:setLocation(area_id, level, x, y, true)
          end
        end
      else
        if quest_data.turnin:empty() then self.turnin:clearLocations() end
        if quest_data.completion:empty() then self.complete_obj:clearLocations() end
        for i, obj in ipairs(self.objectives) do
          if not quest_data.objective[i] then
            obj:clearLocations()
          end
        end
      end
      
      old_poi_x, old_poi_y = x, y
    end
    
    shared.popMap()
  end
  
  function data:failed()
    self.acquire.enabled = true
    self.complete_obj.enabled = false
    self.turnin.enabled = false
    for i,obj in pairs(self.objectives) do obj.enabled = false end
  end
  
  function data:complete()
    self.acquire.enabled = false
    self.complete_obj.enabled = false
    self.turnin.text = L.QUEST_TURNIN:format(GetQuestLogTitle(GetQuestLogIndexByID(quest_id)))
    self.turnin.enabled = true
    self.turnin.obtainable = true
    for i,obj in pairs(self.objectives) do obj.enabled = false end
  end
  
  function data:progress(objective_index, have, need, what, obj_type)
    local objective = assert(self.objectives[objective_index])
    if have then
      objective.text = what
      
      local icon_texture, progress_type = nil, "FRACTION"
      
      if obj_type == "item" then
        icon_texture, progress_type = icon("Item"), "FRACTION"
      elseif obj_type == "object" then
        icon_texture, progress_type = icon("Object"), "FRACTION"
      elseif obj_type == "monster" then
        icon_texture, progress_type = icon("Monster"), "COUNTDOWN"
      elseif obj_type == "player" then
        icon_texture, progress_type = icon("Player"), "COUNTDOWN"
      elseif obj_type == "event" then
        icon_texture, progress_type = icon("Event"), "COUNTDOWN"
      elseif obj_type == "spell" then
        icon_texture, progress_type = icon("Spell"), "COUNTDOWN"
      elseif obj_type == "reputation" then
        icon_texture, progress_type = icon("Reputation"), "REPUTATION"
      end
      
      objective.icon = icon_texture
      objective:setProgress(have, need, progress_type)
    else
      objective.text = "Unknown Objective"
      objective:setProgress(0, 1)
    end
  end
  
  function data:enable()
    enabled = true
    
    local old_quest_index = GetQuestLogSelection()
    
    local quest_index = GetQuestLogIndexByID(quest_id)
    
    SelectQuestLogEntry(quest_index)
    
    local title, level, tag, group, header, collapsed, complete, daily, quest_id = GetQuestLogTitle(quest_index)
    
    if not title then
      -- Temporary work around for quests not in your quest log.
      title = "Quest #"..quest_id
    end
    
    local completion_text = GetQuestLogCompletionText(quest_index)
    
    local quest_description, quest_objectives = GetQuestLogQuestText()
    
    local item_link = GetQuestLogSpecialItemInfo(quest_index)
    quest_items[quest_id] = item_link
    
    if not item_link then
      -- The item might possibly not be in the game's cache yet.
      -- Check again in 5 seconds.
      shared.onDelay(5, function ()
        if GetQuestLogSpecialItemInfo(GetQuestLogIndexByID(quest_id)) then
          data:enable()
        end
      end)
    end
    
    self.acquire = shared.createObjective("quest-"..quest_id.."/acquire")
    self.acquire.text = L.QUEST_ACQUIRE:format(title)
    self.acquire.icon = icon("QuestAcquire")
    self.turnin = shared.createObjective("quest-"..quest_id.."/turnin")
    self.turnin.enabled = true
    self.turnin.obtainable = false
    self.turnin.root = true
    self.turnin.text = formatQuestName(level, tag, group, daily, title)
    self.turnin.icon = icon("QuestTurnin")
    self.turnin.item_id = item_link
    
    if completion_text then
      self.turnin.comment = completion_text
    end
    
    self.turnin:setCallback(callback)
    self.complete_obj = shared.createObjective("quest-"..quest_id.."/complete")
    self.complete_obj.icon = icon("QuestTurnin")
    self.complete_obj:setCallback(callback)
    self.complete_obj.text = completion_text or "Complete: "..title
    self.complete_obj.item_id = item_link
    
    if quest_description then
      self.complete_obj.comment = quest_description
    end
    
    for i = 1,GetNumQuestLeaderBoards(quest_index) do
      local objective = shared.createObjective("quest-"..quest_id.."/"..i)
      self.objectives[i] = objective
      objective:setCallback(callback)
      objective.item_id = item_link
      
      if quest_objectives then
        objective.comment = quest_objectives
      end
      
      self:progress(i, shared.getQuestProgress(quest_id, i))
      objective:addDependencies(self.acquire)
    end
    
    self.turnin:addDependencies(self.complete_obj, unpack(self.objectives))
    
    -- The objectives have been re-created and have lost their locations, so it's
    -- important that we set old_poi{x,y} to nil so that the POI locations will be
    -- reassigned.
    old_poi_x, old_poi_y = nil, nil
    
    questDataChanged()
    self:updatePOI()
    
    if complete == -1 then
      self:failed()
    elseif complete == 1 then
      self:complete()
    else
      local incomplete_objectives = false
      
      for i, obj in ipairs(self.objectives) do
        obj.enabled = true
        
        if not obj.complete then
          incomplete_objectives = true
        end
      end
      
      if not incomplete_objectives then
        -- We have all the objectives done, but the quest isn't complete.
        -- Direct them toward the location where the quest is finished.
        self.complete_obj.enabled = true
      end
    end
    
    SelectQuestLogEntry(old_quest_index)
  end
  
  -- This function only exists so that there will be an objectiveProgress event
  -- when the quest is completed.
  function data:finished()
    self.turnin.complete = true
  end
  
  function data:disable()
    quest_items[quest_id] = nil
    enabled = false
    self.acquire.enabled = false
    self.complete_obj.enabled = false
    self.turnin.enabled = false
    for i,obj in pairs(self.objectives) do obj.enabled = false end
    
    -- Hold on to the quest data for at least 30 seconds, so that the
    -- quest data doesn't need to be decompressed again, in case the quest
    -- is restored.
    shared.delayCollection(data, 30)
  end
  
  shared.getQuestData(quest_id, function (collected_data)
    quest_data:merge(collected_data)
    quest_data:reduceToEssentials()
    questDataChanged()
  end)
  
  if quest_db[quest_id] then
    shared.onUnserialize(quest_db[quest_id], function (static_data)
      quest_data:merge(static_data)
      quest_data:reduceToEssentials()
      questDataChanged()
    end)
  end
  
  -- Keep the data in a cache for future reference.
  quest_data_cache[quest_id] = data
  
  return data
end

-- The QUEST_ITEM_UPDATE event has failed me!
-- Go through all the player's quests and check to see if the
-- quest item for that quest has changed.
shared.onEvent("QUEST_LOG_UPDATE", function ()
  local i = 1
  
  while true do
    local title, _, _, _, header, _, _, _, quest_id = GetQuestLogTitle(i)
    
    if not title then
      break
    end
    
    if not header then
      local item_link = GetQuestLogSpecialItemInfo(i)
      
      if quest_data[quest_id] and quest_items[quest_id] ~= item_link then
        quest_data[quest_id]:enable()
      end
    end
    
    i = i + 1
  end
end)

local filter_tracked, filter_zone, filter_group, filter_level = false, false, false, false
local filter_level_offset = 0

local watched_quest_set = {}
local group_type = "solo" -- "solo", "group", or "raid"
local player_area = nil

-- Returns true if the quest is allowed to be displayed.
local function filterCheck(id)
  if filter_tracked and not watched_quest_set[id] then return false end
  
  local quest_level, quest_group
  
  if filter_zone and player_area ~= GetQuestWorldMapAreaID(id) then return false end
  
  if filter_level or filter_group then
    local _, level, tag = GetQuestLogTitle(GetQuestLogIndexByID(id))
    
    quest_level = level
    
    if tag == nil or tag == PVP then
      quest_group = "solo"
    elseif tag == RAID then
      quest_group = "raid"
    else
      quest_group = "group"
    end
  end
  
  if filter_group then
    if group_type == "raid" and quest_group ~= "raid" then return false end
    if group_type == "solo" and quest_group ~= "solo" then return false end
  end
  
  if filter_level then
    if player_level < quest_level+filter_level_offset then return false end
  end
  
  return true
end

shared.questFilterCheck = filterCheck

local function enableNewQuest(id)
  local data = quest_data[id]
  
  if not data then
    data = getQuestData(id)
    quest_data[id] = data
  end
  
  data:enable()
end

shared.onQuestAdd(function (id)
  -- Quests can start tracked, which evades our hooks for the quest watching functions, so explicitly check here.
  watched_quest_set[id] = IsQuestWatched(GetQuestLogIndexByID(id)) and true or nil
  
  if filterCheck(id) then
    enableNewQuest(id)
  else
    -- On the off chance it's filtered because the quest's zone used by our filter
    -- isn't up-to-date, we'll try checking the filter again in 2 seconds.
    shared.onDelay(2, function ()
      if filterCheck(id) then
        --[===[@debug@
        print("quest #"..id.." ("..(GetQuestLogTitle(GetQuestLogIndexByID(id)) or "UNKNOWN")..") became unfiltered after a delay.")
        --@end-debug@]===]
        enableNewQuest(id)
      end
    end)
  end
end)

shared.onQuestRemove(function (id, reason)
  -- If the quest has been removed, it's obviously not being watched any more.
  watched_quest_set[id] = nil
  
  local data = quest_data[id]
  if data then
    if reason ~= "abandon" then
      data:finished()
    end
    
    -- No promises that the quests is available to verify
    -- whether or not it should be filtered,
    -- so those assertions were removed.
    data:disable()
    quest_data[id] = nil
  end
end)

shared.onQuestFailed(function (id)
  local data = quest_data[id]
  if data then
    data:failed()
    data:updatePOI()
  end
end)

shared.onQuestComplete(function (id)
  local data = quest_data[id]
  if data then
    data:complete()
    data:updatePOI()
  end
end)

shared.onQuestProgress(function (id, objective_index, have, need, what, objective_type)
  local data = quest_data[id]
  if data then
    data:progress(objective_index, have, need, what, objective_type)
    data:updatePOI()
  end
end)

shared.onEvent("QUEST_POI_UPDATE", function ()
  for quest_id, data in pairs(quest_data) do
    data:updatePOI()
  end
end)

local function refilterQuests()
  for id in shared.iterateActiveQuests() do
    local data = quest_data[id]
    if filterCheck(id) then
      if not data then
        data = getQuestData(id)
        quest_data[id] = data
        data:enable()
      end
    else
      if data then
        data:disable()
        quest_data[id] = nil
      end
    end
  end
end

local function regenerateQuestNames()
  for quest_id, data in pairs(quest_data) do
    -- Calling enable is probably overkill; all well.
    data:enable()
  end
end

local function updateQuestNameSettings()
  show_level = shared.settings.quest_show_level
  show_quest_type = shared.settings.quest_show_type
  colour_name = shared.settings.quest_colour_name
  colour_level = shared.settings.quest_colour_level
  shared.callOnce(regenerateQuestNames)
end

shared.onSetting("quest_show_level", updateQuestNameSettings)
shared.onSetting("quest_show_type", updateQuestNameSettings)
shared.onSetting("quest_colour_name", updateQuestNameSettings)
shared.onSetting("quest_colour_level", updateQuestNameSettings)

local function updateFilterSettings()
  filter_tracked = shared.settings.quest_filter_tracked
  filter_zone = shared.settings.quest_filter_zone
  filter_group = shared.settings.quest_filter_group
  filter_level = shared.settings.quest_filter_level
  filter_level_offset = shared.settings.quest_filter_level_offset
  shared.callOnce(refilterQuests)
end

shared.onSetting("quest_filter_tracked", updateFilterSettings)
shared.onSetting("quest_filter_zone", updateFilterSettings)
shared.onSetting("quest_filter_group", updateFilterSettings)
shared.onSetting("quest_filter_level", updateFilterSettings)
shared.onSetting("quest_filter_level_offset", updateFilterSettings)

-- There doesn't seem to be a good way to detect when the list of watched quests change.
local function rescanWatches()
  table.wipe(watched_quest_set)
  
  for i = 1,GetNumQuestWatches() do
    watched_quest_set[select(9, GetQuestLogTitle(GetQuestIndexForWatch(i)))] = true
  end
  
  if filter_tracked then shared.callOnce(refilterQuests) end
end

hooksecurefunc("AddQuestWatch", rescanWatches)
hooksecurefunc("RemoveQuestWatch", rescanWatches)

-- Update the player's level for use by the level filter.
shared.onEvent("PLAYER_LEVEL_UP", function (level)
  player_level = level
  if filter_level then shared.callOnce(refilterQuests) end
  if show_level or colour_name then shared.callOnce(regenerateQuestNames) end
end)

local function updatePlayerArea()
  shared.pushMap()
  
  SetMapToCurrentZone()
  player_area = GetCurrentMapAreaID()
  
  shared.popMap()
  
  if filter_zone then shared.callOnce(refilterQuests) end
end

-- Delay after the zone change, because things don't seem to always get updated immediately.
shared.onEvent("ZONE_CHANGED_NEW_AREA", shared.onDelay, .25, updatePlayerArea)

local function updateGroupType()
  if GetNumRaidMembers() > 0 then
    group_type = "raid"
  elseif GetNumPartyMembers() > 0 then
    group_type = "group"
  else
    group_type = "solo"
  end
  
  if filter_group then shared.callOnce(refilterQuests) end
end

shared.onEvent("PARTY_MEMBERS_CHANGED", updateGroupType)

-- Initiate the values used by the filters.
shared.callOnce(function ()
  player_level = UnitLevel("player")
  
  rescanWatches()
  updateGroupType()
  updatePlayerArea()
  
  shared.callOnce(refilterQuests)
  shared.callOnce(regenerateQuestNames)
end)
