-- 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 on_achievement_progress = {}
local on_achievement_asset = {}

-- There's actually a perfectly good event for this, can you beleive it?
function shared.onAchievementComplete(cb, ...) shared.onEvent("ACHIEVEMENT_EARNED", cb, ...) end

-- This gets called when the player makes progress toward an achievement.
function shared.onAchievementProgress(cb, ...) table.insert(on_achievement_progress, shared.callback(cb, ...)) end

-- This gets called when we associate an asset to an achievement; currently we only check for completed quests.
function shared.onAchievementAsset(cb, ...) table.insert(on_achievement_asset, shared.callback(cb, ...)) end

local achievements = {}

local function criteriaProgress(achievement_id, criteria_num)
  local _, criteria_type, complete, quantity, req_quantity = GetAchievementCriteriaInfo(achievement_id, criteria_num)
  
  if req_quantity == 0 then
    req_quantity = 1
  end
  
  if complete then
    quantity = req_quantity
  end
  
  return quantity, req_quantity, criteria_type
end

-- QuestHelper2 uses this function, so let's export it.
shared.getAchievementCriteriaProgress = criteriaProgress

local loading = true

shared.onLoad(function ()
  local thread = coroutine.create(function ()
    local count = 0
    for i, category_id in ipairs(GetCategoryList()) do
      for index = 1, GetCategoryNumAchievements(category_id) do
        
        local achievement_id, _, _, complete = GetAchievementInfo(category_id, index)
        if not complete then
          local criteria = {}
          achievements[achievement_id] = criteria
          
          for criteria_num = 1,GetAchievementNumCriteria(achievement_id) do
            criteria[criteria_num] = criteriaProgress(achievement_id, criteria_num)
          end
        end
        
        count = count + 1
        if count == 10 then
          coroutine.yield()
          count = 0
        end
      end
    end
  end)
  
  shared.onDelay(0, function ()
    coroutine.resume(thread)
    if coroutine.status(thread) == "suspended" then
      return 0
    else
      loading = false
    end
  end)
end)

-- Store the quest_id here while it looks like the player is turning in a quest, but hasn't actually done so.
local last_completed_quest_id = nil

shared.onQuestRemove(function (quest_id, reason)
  if reason == "npc" or reason == "object" or reason == "autoquest" then
    last_completed_quest_id = quest_id
  else
    assert(reason == "abandon")
  end
end)

-- 0 = no scan, 1 = scan in progress, 2 = scan in progress and needs to be repeated due to possibly missed changes.
local scan_in_progress = 0
local function scanAchievements()
  if loading then
    -- We have nothing to compare with, as the original scan isn't yet complete.
    return
  end
  
  if InCombatLockdown() then
    -- If in combat, delay the scan until we leave combat.
    -- The game seems to spam the CRITERIA_UPDATE event when you're fighting. I don't know why.
    shared.whenOutOfCombat(shared.callOnce, scanAchievements)
    return
  end
  
  if scan_in_progress ~= 0 then
    -- 2 means to repeat the scan a second time when scan is complete.
    scan_in_progress = 2
    return
  end
  
  scan_in_progress = 1
  
  local achievement_id = nil
  
  shared.onDelay(0, function (...)
    for i = 1,10 do
      achievement_id = next(achievements, achievement_id)
      
      if not achievement_id then
        if scan_in_progress == 2 then
          -- scanAchievements was called while we were scanning; scan again.
          scan_in_progress = 1
          return 0
        else
          -- Done scanning. Yay.
          -- Unset this variable, to reduce the chances of attributing this quest to things it shouldn't be attributed to.
          scan_in_progress = 0
          last_completed_quest_id = nil
          return nil
        end
      end
      
      local criteria = achievements[achievement_id]
      
      for criteria_num, old_value in ipairs(criteria) do
        local new_value, new_req, criteria_type = criteriaProgress(achievement_id, criteria_num)
        
        if new_value ~= old_value then
          criteria[criteria_num] = new_value
          
          if new_value > old_value then
            shared.invokeCallbackList(on_achievement_progress, achievement_id, criteria_num, new_value, new_req, criteria_type)
          end
          
          if criteria_type == 10 or
            criteria_type == 11 or
            criteria_type == 14 then
            -- This achievement criteria involves completing quests; make note of the quest the player just changed.
            
            if last_completed_quest_id then
              shared.invokeCallbackList(on_achievement_asset, achievement_id, criteria_num, last_completed_quest_id, criteria_type)
            end
          end
        end
      end
    end
    
    return 0
  end)
end

shared.onEvent("CRITERIA_UPDATE", shared.callOnce, scanAchievements)
