-- 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, ...)

-- State information on each of the quests that we know about.
local quest_state = {}

-- Returns an iterator for getting the quest ids of quests the player currently has in their quest log.
function shared.iterateActiveQuests()
  local key = next(quest_state)
  return function()
    local result = key
    if key then key = next(quest_state, key) end
    return result
  end
end

-- This is a set of quests that have been completed.
local quest_complete_cache = {}

shared.onEvent("QUEST_QUERY_COMPLETE", function ()
  for quest_id in pairs(GetQuestsCompleted()) do
    quest_complete_cache[quest_id] = true
  end
end)

shared.onLoad(QueryQuestsCompleted)
-- Populate quest_complete_cache with quests.

local on_quest_add = {}
local on_quest_remove = {}
local on_quest_failed = {}
local on_quest_complete = {}
local on_quest_progress = {}

-- Want to know when something interesting quest related happens?
-- We've got functions for all your quest querying needs.
-- Except all the ones we don't; you have to look on the black
-- market for those ones. It's not something we talk about in
-- polite company.
function shared.onQuestRemove(cb, ...) table.insert(on_quest_remove, shared.callback(cb, ...)) end
function shared.onQuestProgress(cb, ...) table.insert(on_quest_progress, shared.callback(cb, ...)) end

function shared.onQuestAdd(cb, ...)
  cb = shared.callback(cb, ...)
  table.insert(on_quest_add, cb)
  -- Also announce all the currently known quests.
  for quest_id in pairs(quest_state) do cb(quest_id, "existing") end
end

function shared.onQuestComplete(cb, ...)
  cb = shared.callback(cb, ...)
  table.insert(on_quest_complete, cb)
  -- Also announce any currently complete quests.
  for quest_id, state in pairs(quest_state) do if state[0] == 1 then cb(quest_id) end end
end

function shared.onQuestFailed(cb, ...)
  cb = shared.callback(cb, ...)
  table.insert(on_quest_failed, cb)
  -- Also announce any currently failed quests.
  for quest_id, state in pairs(quest_state) do if state[0] == -1 then cb(quest_id) end end
end

local quest_npc_id, quest_object_id, quest_npc_is_self = nil, nil, false
shared.onEvent("QUEST_DETAIL", function ()
  if UnitName("npc") ~= nil then
    quest_npc_id, quest_object_id, quest_npc_is_self = shared.npcID("npc"), shared.objectID("npc"), UnitIsUnit("npc", "player") and true or false
  else
    quest_npc_id, quest_object_id, quest_npc_is_self = nil, nil, false
  end
end)

-- One click quests are quests that you acquire and turn in with the single click of a button.
local quest_oneclick = false
shared.onEvent("QUEST_COMPLETE", function ()
  local quest_id = GetQuestID()
  local index = GetQuestLogIndexByID(quest_id)
  
  quest_oneclick = false
  
  if not index or index == 0 then
    quest_oneclick = true
    -- print("You're completing a quest you don't have!")
  end
end)

local quest_item_id = nil
hooksecurefunc("UseContainerItem", function (bag_id, slot)
  local quest_id, started = select(2, GetContainerItemQuestInfo(bag_id, slot))
  if quest_id and not started then
    quest_item_id = GetContainerItemID(bag_id, slot)
  else
    quest_item_id = nil
  end
end)

hooksecurefunc("DeclineQuest", function ()
  quest_npc_id, quest_object_id, quest_item_id, quest_npc_is_self = nil, nil, nil, false
end)

local reputation_lookup = {
  [FACTION_STANDING_LABEL1] = 0,
  [FACTION_STANDING_LABEL1_FEMALE] = 0,
  [FACTION_STANDING_LABEL2] = 1,
  [FACTION_STANDING_LABEL2_FEMALE] = 1,
  [FACTION_STANDING_LABEL3] = 2,
  [FACTION_STANDING_LABEL3_FEMALE] = 2,
  [FACTION_STANDING_LABEL4] = 3,
  [FACTION_STANDING_LABEL4_FEMALE] = 3,
  [FACTION_STANDING_LABEL5] = 4,
  [FACTION_STANDING_LABEL5_FEMALE] = 4,
  [FACTION_STANDING_LABEL6] = 5,
  [FACTION_STANDING_LABEL6_FEMALE] = 5,
  [FACTION_STANDING_LABEL7] = 6,
  [FACTION_STANDING_LABEL7_FEMALE] = 6,
  [FACTION_STANDING_LABEL8] = 7,
  [FACTION_STANDING_LABEL8_FEMALE] = 7
}

local function leaderboardProgress(quest_index, i)
  local desc, objective_type, complete = GetQuestLogLeaderBoard(i, quest_index)
  
  -- Well, that wasn't a great start; GetQuestLogLeaderBoard didn't return anything.
  if not desc or not desc:find("[^%s]") then return end
  
  local what, have, need
  
  if objective_type == "item" then
    what, have, need = shared.unformat(QUEST_ITEMS_NEEDED, desc)
    
    if not what then
      -- This is for items that get consumed during the course of the quest,
      -- and aren't actually required to turn the quest in.
      what, have, need = shared.unformat(QUEST_INTERMEDIATE_ITEMS_NEEDED, desc)
    end
  elseif objective_type == "object" then
    what, have, need = shared.unformat(QUEST_OBJECTS_FOUND, desc)
  elseif objective_type == "monster" then
    what, have, need = shared.unformat(QUEST_MONSTERS_KILLED, desc)
    
    if not what then
      -- Some monsters objectives make no sense at all.
      -- Seen some that look like "%s called: %d/%d", which isn't in GlobalStrings.lua,
      -- and thus I can't check for, and some that look like "%s: %d/%d"
      -- In any case, I'm going to lie and say this is an event type objective,
      -- lest I reconstruct the description into "X called slain: 1/6"
      objective_type, what, have, need = "event", shared.unformat(QUEST_OBJECTS_FOUND, desc)
    end
  elseif objective_type == "player" then
    what, have, need = assert(PLAYER), shared.unformat(QUEST_PLAYERS_KILLED, desc)
  elseif objective_type == "reputation" then
    what, have, need = shared.unformat(QUEST_FACTION_NEEDED, desc)
    if what then
      -- Convert the reputation strings to numbers.
      have, need = assert(reputation_lookup[have], have), assert(reputation_lookup[need], need)
    end
  elseif objective_type == "event" or objective_type == "log" then
    -- Maybe I'm just insane, but the log type seems redundant to me.
    objective_type, what, have, need = "event", desc, complete and 1 or 0, 1
  elseif objective_type == "spell" then
    what, have, need = shared.unformat(QUEST_SPELL_NEEDED, desc), complete and 1 or 0, 1
  else
    error("Unknown objective type '"..objective_type.."'; description is: "..desc)
  end
  
  -- Apparently, we failed to unformat the string.
  -- Either this was a bug on our part or the data for the quest
  -- isn't fully loaded yet.
  if not what or not what:find("[^%s]") then
    -- print("Failed to parse objective string: "..desc)
    return
  end
  
  assert(type(have) == "number")
  assert(type(need) == "number")
  
  if have >= need then
    complete = true
  end
  
  if need < 1 then
    need = 1
  end
  
  if complete then
    have = need
  end
  
  return have, need, what, objective_type
end

-- A set of quests that we're missing information for, and need to be rechecked soon.
local missing_quest_info = {}

-- Callback to check the quests for missing information.
local recheckMissingQuestInfo

local function processQuestObjective(quest_id, obj)
  local have, need, what, type = leaderboardProgress(GetQuestLogIndexByID(quest_id), obj)
  
  if have then
    local quest_state = assert(quest_state[quest_id])
    local objective_state = quest_state[obj]
    
    if objective_state then
      local made_progress = have > objective_state.have
      objective_state.have = have
      
      if made_progress then
        shared.invokeCallbackList(on_quest_progress, quest_id , obj, have, objective_state.need, objective_state.what, objective_state.type)
      end
    else
      objective_state = {have=have, need=need, what=what, type=type}
      quest_state[obj] = objective_state
      return true
    end
  else
    if not missing_quest_info[quest_id] then
      if not next(missing_quest_info) then
        shared.onDelay(0.2, recheckMissingQuestInfo)
      end
      
      missing_quest_info[quest_id] = true
    end
  end
  
  return false
end

recheckMissingQuestInfo = function()
  local missing = false
  
  for quest_id in pairs(missing_quest_info) do
    -- Check all the quests that are reported to have missing data.
    local quest_missing = false
    local state = quest_state[quest_id]
    
    if state then
      for i = 1,state.objectives do
        if not state[i] then
          if processQuestObjective(quest_id, i) then
            -- Re-announce this quest so anyone that cares can display the correct information.
            shared.invokeCallbackList(on_quest_add, quest_id, "existing")
          else
            quest_missing = true
          end
        end
      end
    end
    
    if quest_missing then
      missing = true
    else
      -- No longer missing.
      missing_quest_info[quest_id] = nil
    end
  end
  
  if missing then
    -- If there is still missing data, ask to be called again in 1/5th of a second.
    return 0.2
  end
end

local function processQuestIndex(index)
  local title, level, tag, group, header, collapsed, complete, daily, quest_id = GetQuestLogTitle(index)
  
  -- Should only be used on actual quests.
  assert(title and not header)
  
  local state = quest_state[quest_id]
  
  local is_new = false
  
  if not state then
    -- New state; don't anounce any changes about it.
    is_new = true
    state = {}
    quest_state[quest_id] = state
    
    state.objectives = GetNumQuestLeaderBoards(index)
    state.complete = complete
    state.title = title
  end
  
  -- Check state for changes and announce them.
  for i=1,state.objectives do
    if processQuestObjective(quest_id, i) then
      -- If the objective was only now just created, pretend this entire quest is new, so that anyone that cares can
      -- display the correct information.
      is_new = true
    end
  end
  
  if complete ~= state.complete then
    state.complete = complete
    
    if complete == 1 then
      shared.invokeCallbackList(on_quest_complete, quest_id)
    elseif complete == -1 then
      shared.invokeCallbackList(on_quest_failed, quest_id)
    else
      -- Lie and say the quest is new so that it will be announced as existing; presumably the quest was failed and then re-acquired it,
      -- or they tossed an item that the quest required.
      is_new = true
    end
  end
  
  return is_new
end

-- Removed quests don't always immediately disappear from the quest log;
-- so add the quests we removed to this list to prevent the
-- QUEST_LOG_UPDATE event from re-adding them only to remove them
-- again.
local removed_quest_set = {}

shared.onEvent("QUEST_ACCEPTED", function (index)
  local quest_id = select(9, GetQuestLogTitle(index))
  if quest_npc_id or quest_object_id or quest_item_id then
    -- In case the player acquired a quest they just removed.
    removed_quest_set[quest_id] = nil
    
    if processQuestIndex(index) then
      if quest_npc_is_self then
        shared.invokeCallbackList(on_quest_add, quest_id, "autoquest")
      elseif quest_object_id then
        shared.invokeCallbackList(on_quest_add, quest_id, "object", quest_object_id)
      elseif quest_item_id then
        shared.invokeCallbackList(on_quest_add, quest_id, "item", quest_item_id)
      elseif quest_npc_id then
        shared.invokeCallbackList(on_quest_add, quest_id, "npc", quest_npc_id)
      end
    end
  else
    -- We don't seem to know where this quest came from; assume it came from a player.
    -- TODO: actually check if it came from a player and append the player's name to the call.
    shared.invokeCallbackList(on_quest_add, quest_id, "player")
  end
  
  quest_npc_id, quest_object_id, quest_item_id, quest_npc_is_self = nil, nil, nil, false
end)

hooksecurefunc("GetQuestReward", function ()
  local quest_id = GetQuestID()
  
  if UnitExists("npc") and (quest_state[quest_id] or quest_oneclick) then
    local npc_id, object_id = shared.npcID("npc"), shared.objectID("npc")
    
    assert(npc_id or object_id or UnitIsUnit("npc", "player"))
    
    quest_complete_cache[quest_id] = true
    removed_quest_set[quest_id] = true
    quest_state[quest_id] = nil
    
    if quest_oneclick then
      if object_id then
        shared.invokeCallbackList(on_quest_add, quest_id, "object", object_id)
      elseif npc_id then
        shared.invokeCallbackList(on_quest_add, quest_id, "npc", npc_id)
      end
      
      -- I don't think a one-click autoquest quest makes sense. That would be dumb. Not checking it.
    end
    
    if object_id then
      shared.invokeCallbackList(on_quest_remove, quest_id, "object", object_id)
    elseif npc_id then
      shared.invokeCallbackList(on_quest_remove, quest_id, "npc", npc_id)
    else
      shared.invokeCallbackList(on_quest_remove, quest_id, "autoquest")
    end
  end
end)

local abandoned_quest_id = nil

hooksecurefunc("SetAbandonQuest", function ()
  local abandoned_name, i = GetAbandonQuestName(), 1
  abandoned_quest_id = nil
  
  while true do
    local title, _, _, _, header, _, _, _, quest_id = GetQuestLogTitle(i)
    
    if not title then
      break
    end
    
    if not header and title == abandoned_name then
      assert(quest_id and quest_id ~= 0)
      abandoned_quest_id = quest_id
      break
    end
    
    i = i + 1
  end
end)

hooksecurefunc("AbandonQuest", function ()
  if abandoned_quest_id and quest_state[abandoned_quest_id] then
    removed_quest_set[abandoned_quest_id] = true
    quest_state[abandoned_quest_id] = nil
    shared.invokeCallbackList(on_quest_remove, abandoned_quest_id, "abandon")
  end
  
  abandoned_quest_id = nil
end)

function shared.getQuestProgress(quest_id, objective)
  local state = quest_state[quest_id]
  
  if state then
    if not objective then
      return complete == 1 and 1 or 0, 1, state.title, "quest"
    else
      assert(objective > 0 and objective <= state.objectives)
      local objective_state = state[objective]
      
      if objective_state then
        return objective_state.have, objective_state.need, objective_state.what, objective_state.type
      end
    end
  else
    -- TODO: It should be possible to get quest names and objectives for quests
    -- we don't actually have by extracting the information from a tooltip,
    -- i.e., GameTooltip:SetHyperlink("quest:xyz")
    
    if not objective then
      -- Asking for the progress of a quest the player doesn't have.
      return quest_complete_cache[quest_id] and 1 or 0, 1, "QUEST#"..quest_id, "quest"
    end
  end
end

local checked = {}
local function checkPlayerQuests()
  table.wipe(checked)
  
  -- Can't use 1,GetNumQuestLogEntries(), that only iterates through the quests that
  -- aren't under folded headers.
  local i=1
  while true do
    local title, _, _, _, header, _, _, _, quest_id = GetQuestLogTitle(i)
    
    if title then
      if not header and not removed_quest_set[quest_id] then
        assert(quest_id and quest_id ~= 0)
        checked[quest_id] = true
        
        if processQuestIndex(i) then
          shared.invokeCallbackList(on_quest_add, quest_id, "existing")
        end
      end
      
      i = i + 1
    else
      -- No more quests.
      break
    end
  end
  
  -- We don't want to prevent quests from being added premanantly, as some are repeatable,
  -- so clear the set now that this check is done.
  table.wipe(removed_quest_set)
  
  for id in pairs(quest_state) do
    if not checked[id] then
      local quest_index = GetQuestLogIndexByID(id)
      if quest_index and quest_index > 0 then
        -- I don't believe this code will be invoked anymore; remove it in the future if
        -- nobody complains about this failing.
        assert(false)
        if processQuestIndex(quest_index) then
          -- We of course already had the quest, this probably means the player threw away a quest item,
          -- or restarted a failed quest.
          shared.invokeCallbackList(on_quest_add, id, "existing")
        end
      else
        -- We may never know what foul fate befell this quest, but we're going to pretend it was abandoned.
        quest_state[id] = nil
        shared.invokeCallbackList(on_quest_remove, id, "abandon")
      end
    end
  end
end

-- UNIT_QUEST_LOG_CHANGED absolutely refused to be invoked when completing
-- 26855/A Pilot's Revenge, and I did it over repeatedly.
-- So, I'll fall back to usg QUEST_LOG_UPDATE, even if it fires
-- more often than necessary.
shared.onEvent("QUEST_LOG_UPDATE", shared.callOnce, checkPlayerQuests)
shared.callOnce(checkPlayerQuests)

-- TESTING
--@debug@
--[[
shared.onQuestProgress(print, "quest-progress")
shared.onQuestAdd(print, "quest-add")
shared.onQuestRemove(print, "quest-remove")
shared.onQuestComplete(print, "quest-complete")
shared.onQuestFailed(print, "quest-failed")
]]
-- Feel free to invoke this from a macro for debugging.
function shared.dumpQuests()
  for quest_id, state in pairs(quest_state) do
    _G.print(quest_id.."/"..GetQuestLogTitle(GetQuestLogIndexByID(quest_id))..((state.complete == -1 and " failed") or (state.complete == 1 and " complete") or " incomplete"))
    for i=1,state.objectives do
      local data = state[i]
      if data then
        _G.print(i..": "..(data.have or "?").."/"..(data.need or "?").." "..(data.what or "?").." ("..(data.type or "?")..")")
      else
        _G.print(i..": ???")
      end
    end
  end
end
--@no-debug@
