-- 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 functions = {}

shared.meta.QuestData = { __index = functions, name="QuestData" }

function shared.createQuestData(id)
  return setmetatable({
    turnin = shared.createLocationList(),
    acquire = shared.createLocationList(),
    completion = shared.createLocationList(),
    objective = {}
  }, shared.meta.QuestData)
end

function functions:addTurninLocation(...)
  self.turnin:addLocation(...)
  shared.dirtyQuestData(self)
end

function functions:addStartNPC(npc_id)
  local npcs = self.start_npc
  if not npcs then npcs = {} self.start_npc = npcs end
  npcs[npc_id] = (npcs[npc_id] or 0)+1
  shared.dirtyQuestData(self)
end

function functions:addEndNPC(npc_id)
  local npcs = self.end_npc
  if not npcs then npcs = {} self.end_npc = npcs end
  npcs[npc_id] = (npcs[npc_id] or 0)+1
  shared.dirtyQuestData(self)
end

function functions:addStartObject(object_id)
  local objects = self.start_object
  if not objects then objects = {} self.start_object = objects end
  objects[object_id] = (objects[object_id] or 0)+1
  shared.dirtyQuestData(self)
end

function functions:addEndObject(object_id)
  local objects = self.end_object
  if not objects then objects = {} self.end_object = objects end
  objects[object_id] = (objects[object_id] or 0)+1
  shared.dirtyQuestData(self)
end

function functions:addStartItem(item_id)
  local items = self.start_item
  if not items then items = {} self.start_item = items end
  items[item_id] = (items[item_id] or 0)+1
  shared.dirtyQuestData(self)
end

function functions:addAcquireLocation(...)
  self.acquire:addLocation(...)
  shared.dirtyQuestData(self)
end

function functions:addCompletionLocation(...)
  self.completion:addLocation(...)
  shared.dirtyQuestData(self)
end

-- These are bit flags that are used to specify
-- what class/race has been seen to be
-- allowed to do a quest.
  
local class_bits = {
  DRUID=1,
  HUNTER=2,
  MAGE=4,
  PALADIN=8,
  PRIEST=16,
  ROGUE=32,
  SHAMAN=64,
  WARLOCK=128,
  WARRIOR=256,
  DEATHKNIGHT=512
}

-- Note that this is also used to determine which
-- factions are allowed to do a quest.
local race_bits = {
  HUMAN=1,
  DWARF=2,
  NIGHTELF=4,
  GNOME=8,
  DRAENEI=16,
  WORGEN=32,
  ORC=64,
  SCOURGE=128, UNDEAD=128,
  TAUREN=256,
  TROLL=512,
  BLOODELF=1024,
  GOBLIN=2048
}

local alliance_race_mask, horde_race_mask =
  bit.bor(
    race_bits.HUMAN,
    race_bits.DWARF,
    race_bits.NIGHTELF,
    race_bits.GNOME,
    race_bits.DRAENEI,
    race_bits.WORGEN),
  bit.bor(
    race_bits.ORC,
    race_bits.SCOURGE,
    race_bits.TAUREN,
    race_bits.TROLL,
    race_bits.BLOODELF,
    race_bits.GOBLIN)

function functions:isAllianceQuest()
  return 0 ~= bit.band(self.race or 0, alliance_race_mask)
end

function functions:isHordeQuest()
  return 0 ~= bit.band(self.race or 0, horde_race_mask)
end

local function bitSetter(key, bits)
  return function(self, alias)
    alias = tostring(alias):upper():gsub(" ", "")
    assert(bits[alias], alias)
    local old_bits = self[key] or 0
    local new_bits = bit.bor(old_bits, bits[alias])
    if old_bits ~= new_bits then
      self[key] = new_bits
      shared.dirtyQuestData(self)
    end
  end
end

functions.addClass = bitSetter("class", class_bits)
functions.addRace = bitSetter("race", race_bits)

-- To determine the level required to obtain a quest.
function functions:addLevel(level)
  local old_level = self.level or 255
  local new_level = math.min(old_level, level)
  if old_level ~= new_level then
    self.level = new_level
    shared.dirtyQuestData(self)
  end
end

function functions:addObjectiveLocation(objective, ...)
  local objective_data = self.objective[objective]
  
  if not objective_data then
    objective_data = {
      location = shared.createLocationList()
    }
    self.objective[objective] = objective_data
  end
  
  objective_data.location:addLocation(...)
  
  shared.dirtyQuestData(self)
end

function functions:addAutoQuestAcquire()
  self.autoquest_acquire = (self.autoquest_acquire or 0) + 1
end

function functions:addAutoQuestTurnin()
  self.autoquest_turnin = (self.autoquest_turnin or 0) + 1
end

local function weightMapWeight(map)
  local map_weight = 0
  
  if map then
    for key, key_weight in pairs(map) do
      map_weight = map_weight + key_weight
    end
  end
  
  return map_weight
end

-- Returns an estimate of how many data samples this quest was constructed from,
-- this is basically only used when compiling the database to determine whether or not
-- to merge data from older versions of WoW.
function functions:estimateWeight()
  local start_weight = weightMapWeight(self.start_npc)
                     + weightMapWeight(self.start_object)
                     + weightMapWeight(self.start_item)
                     + (self.autoquest_acquire or 0)
  
  local end_weight = weightMapWeight(self.end_npc)
                   + weightMapWeight(self.end_object)
                   + (self.autoquest_turnin or 0)
  
  return math.min(start_weight, end_weight)
end

local function mergeBits(a, b)
  if a or b then return bit.bor(a or 0, b or 0) end
end

local function mergeWeight(a, b)
  if a or b then return (a or 0) + (b or 0) end
end

local function mergeWeightMap(a, b)
  if a or b then
    a = a or {}
    if b then
      for key, weight in pairs(b) do
        a[key] = mergeWeight(a[key], weight)
      end
    end
    return a
  end
end

local function mergeLocationList(a, b)
  if a or b then
    a = a or shared.createLocationList()
    
    if b then
      for area_id, level, x, y, ex, ey, w in b:iterate() do
        a:addLocation(area_id, level, x, y, ex, ey, w)
      end
    end
    
    return a
  end
end

local function mergeObjective(a, b)
  if a or b then
    a = a or { location = shared.createLocationList() }
    if b then a.location = mergeLocationList(a.location, b.location) end
    return a
  end
end

-- The following functions absolutely must not be used on quests in the collected quest database; it will irreparably damange them!
-- They are only to be used in the creation of the static quest database, or quest objects that came from the static quest database.

-- Merges the values of quest into self.
function functions:merge(quest)
  if self ~= quest then
    self.class = mergeBits(self.class, quest.class)
    self.race = mergeBits(self.race, quest.race)
    
    if quest.level then self.level = math.min(self.level or 255, quest.level) end
    
    for i, weight_key in ipairs({"start_npc", "end_npc", "start_object", "end_object", "start_item"}) do
      self[weight_key] = mergeWeightMap(self[weight_key], quest[weight_key])
    end
    
    self.autoquest_acquire = mergeWeight(self.autoquest_acquire, quest.autoquest_acquire)
    self.autoquest_turnin = mergeWeight(self.autoquest_turnin, quest.autoquest_turnin)
    
    self.acquire = mergeLocationList(self.acquire, quest.acquire)
    self.completion = mergeLocationList(self.completion, quest.completion)
    self.turnin = mergeLocationList(self.turnin, quest.turnin)
    
    for i, objective in pairs(quest.objective) do
      self.objective[i] = mergeObjective(self.objective[i], objective)
    end
  end
end

-- Throws away all but the most likely to be useful locations, and any other data that can't (yet) be used by QuestHelper2/quests.lua
function functions:reduceToEssentials()
  self.class, self.race = nil, nil
  self.level = nil
  self.start_npc, self.end_npc = nil, nil
  self.start_object, self.end_object = nil, nil
  self.autoquest_acquire, self.autoquest_turnin = nil, nil
  
  self.acquire:pruneLocations(8)
  self.completion:pruneLocations(8)
  self.turnin:pruneLocations(8)
  
  for i, objective in pairs(self.objective) do
    if 0 == objective.location:pruneLocations(8) then
      self.objective[i] = nil
    end
  end
end
