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

-- If we make any serious changes to the way data is saved, increase this number.
local save_version = 0

-- Records a call to be made when everything is loaded.
-- The placeholder record functions place everything here
-- to be invoked when everything is loaded.
local function delayCollection(cb, ...)
  table.insert(delayed_collection, {cb=cb, args=select("#", ...), ...})
end

-- Requests that couldn't be fulfulled because we weren't loaded at the time.
local pending_data_requests = {}

-- These are placeholders to record data requests until we're able to fulfull them.
function shared.getQuestData(id, cb, ...) table.insert(pending_data_requests, {"quest", id, shared.callback(cb, ...)}) end
function shared.getAchievementData(id, cb, ...) table.insert(pending_data_requests, {"achievement", id, shared.callback(cb, ...)}) end

-- You shouldn't be marking anything dirty yet, as nothing has been loaded yet.
shared.dirtyQuestData, shared.dirtyAchievementData = error, error

local on_save_data

local function addRealm(list, realm)
  for i,old_realm in ipairs(list) do
    if old_realm == realm then
      -- Know of this realm already
      return
    end
  end
  
  table.insert(list, realm)
  table.sort(list)
end

shared.onLoad(function()
  -- The unique ID is used to identify multiple versions of a single person's
  -- submitted data, so that any duplicate data can be discounted.
  if not _G.QuestHelper2_Collection_UID then
    local start_time = GetTime()
    
    -- Wait until we the first OnUpdate event; these functions ought to return valid values at that time.
    shared.callOnce(function ()
      -- So, we try to add a bit of entropy, using some values that are hopefully fairly unique
      -- to the person playing.
      local seed = UnitName("player")..math.random()..GetRealmName()..time()..UnitGUID("player")
      
      local prev_byte = 0
      
      -- Watch mouse movement and use it as a source of entropy until seed is a decent length.
      shared.onDelay(1, function ()
        local x, y = GetCursorPosition()
        local byte = math.floor((x+y*math.pi)%255+0.5)
        
        if byte ~= prev_byte then
          prev_byte, byte = byte, bit.bxor(byte, prev_byte)
          seed = string.char(byte)..seed
        end
        
        if seed:len() < 92 then return 1 end
      end)
      
      -- We delay the UID generation until logout, then we can use how long they played as a source
      -- of entropy, and also maybe math.random() will be a little less predictable at that time.
      shared.onSave(function()
        -- We need some random bytes from which to generate the UID.
        local bytes = {}
        
        -- Insert some random numbers. In theory, this would be all I'd need, but I don't actually
        -- trust this to actually generate a unique set of values for every person that runs it.
        for i=1,8 do table.insert(bytes, math.random(0,255)) end
        
        -- So, we try to add a bit of entropy, using some values that are hopefully fairly unique
        -- to the person playing.
        seed = GetTime()..seed..(GetTime()-start_time)
        
        -- Here's my really crappy attempt at a hash function.
        for i=1,32 do
          -- Rotate all the bytes and add 1 to the first byte.
          -- This will hopefully take care of the unlikely case where all the bytes involved are zero,
          -- since the math won't allow any other value than zero.
          table.insert(bytes, 1, (table.remove(bytes)+1)%256)
          
          for j=1,seed:len() do
            -- This is sort of like a LCG, except that I add the next byte to the result,
            -- and xor it with a byte from the seed string.
            local b1, b2 = (j-1)%8+1, j%8+1
            bytes[b1] = bit.bxor((bytes[b1]*161+173+bytes[b2])%256, seed:byte(j))
          end
          
          -- And for no other reason other than that it seemed like a good idea at the time,
          -- we reverse the seed string is reversed for every other iteration.
          seed=seed:reverse()
        end
        
        _G.QuestHelper2_Collection_UID = string.format("%02x%02x%02x%02x%02x%02x%02x%02x", unpack(bytes))
      end)
    end)
  end
  
  local save = _G.QuestHelper2_Collection_SavedData or {}
  _G.QuestHelper2_Collection_SavedData = save
  
  local version = save[save_version]
  
  if not version then
    version = {}
    save[save_version] = version
  end
  
  local wow_version = GetBuildInfo()
  
  local data = version[wow_version]
  
  if not data then
    data = {}
    version[wow_version] = data
  end
  
  shared.saved = data
  data.quests, data.achievements, data.buildings, data.realms, data.quest_order = data.quests or {}, data.achievements or {}, data.buildings or {}, data.realms or {}, data.quest_order or {}
  
  addRealm(data.realms, GetRealmName())
  
  shared.getQuestData, shared.dirtyQuestData = shared.createSavedDataManager(data.quests, shared.createQuestData)
  shared.getAchievementData, shared.dirtyAchievementData = shared.createSavedDataManager(data.achievements, shared.createAchievementData)
  
  -- Fulfill any data requests that were made before we were fully loaded.
  for i, request in ipairs(pending_data_requests) do
    local data_type, id, cb = unpack(request)
    if data_type == "quest" then
      shared.getQuestData(id, cb)
    elseif data_type == "achievement" then
      shared.getAchievementtData(id, cb)
    else
      error(data_type)
    end
  end
  
  pending_data_requests = nil
  
  shared.quest_order_set, shared.quest_order = {}, {}
  
  -- This is delayed, as the GUID of the player isn't immediately available.
  shared.onDelay(0, function ()
    local guid = UnitGUID("player")
    if not guid then return 0.1 end
    
    local player_id = tonumber(guid:sub(-7, -1), 16)
    
    local order_data = data.quest_order[player_id]
    
    -- Since the data for all the players is stored in the same place, we keep the data of all the players we aren't
    -- currently dealing with compressed.
    if order_data then
      local session_order = shared.quest_order
      shared.quest_order, shared.quest_order_set = shared.createUnserializer(order_data).decodeTable(order_data), {}
      
      for i, id in ipairs(shared.quest_order) do
        shared.quest_order_set[id] = true
      end
      
      for i, id in ipairs(session_order) do
        if not shared.quest_order_set[id] then
          shared.quest_order_set[id] = true
          table.insert(shared.quest_order, id)
        end
      end
    end
    
    -- We don't need to keep this string in memory, it will be regenerated on logout.
    data.quest_order[player_id] = nil
    
    -- Re-serialize the quest order list on logout.
    shared.onSave(function ()
      local serializer = shared.createSerializer()
      serializer.encodeTable(shared.quest_order)
      data.quest_order[player_id] = serializer.flush()
    end)
  end)
  
  shared.collected_quest_db = data.quests
  shared.collected_achievement_db = data.achievements
end)

-- Some very basic statistics about our database.
-- Returns: #quests, #achievements, size (string), and #versions of wow.
function shared.databaseStatistics()
  local size, versions, quests, achievements = 0, 0, 0, 0
  local version_set, quest_set, achievement_set = {}, {}, {}
  
  for save_version, data in pairs(_G.QuestHelper2_Collection_SavedData or {}) do
    for wow_version, wow_data in pairs(data) do
      if not version_set[wow_version] then
        version_set[wow_version] = true
        versions = versions + 1
      end
      
      for id, raw_data in pairs(wow_data.quests or {}) do
        size = size + 41 + raw_data:len()
        if not quest_set[id] then
          quest_set[id] = true
          quests = quests + 1
        end
      end
      
      for id, raw_data in pairs(wow_data.achievements or {}) do
        size = size + 41 + raw_data:len()
        
        if not achievement_set[id] then
          achievement_set[id] = true
          achievements = achievements + 1
        end
      end
    end
  end
  
  if size >= 1048576 then
    size = ("%.1fMiB"):format(size/1048576)
  else
    size = ("%dKiB"):format(size/1024)
  end
  
  return quests, achievements, size, versions
end

-- This needs to be called in response to a hardware event,
-- since we call ReloadUI which requires it.
function shared.wipeDatabase()
  _G.QuestHelper2_Collection_UID = nil
  _G.QuestHelper2_Collection_SavedData = nil
  ReloadUI()
end
