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

-- Wait at least this many seconds before attempting to clean dirty data.
-- Lower times mean more work potentially means wasted cpu cycles, cleaning
-- things that may just need to be cleaned again later.
-- Higher times mean that that we potential waste more memory keeping all the dirty
-- objects in memory, and that logout will take longer since all the work will
-- be defered then in one huge block rather than spread out over time.
local expire_delay = 150

-- This maps dirty objects to the manager_state object that is responsible for them
local dirty_data = {}

-- This is the list of dirty objects in the order that they are to be cleaned.
local dirty_list = {}

-- And this is when a given piece of data is scheduled to be cleaned.
local dirty_expire_time = {}

local current_serialize_task = nil

-- This function is given to onDelay, to clean up the next piece of dirty data
-- when its time for it to expire.
local function dataCleanerCallback()
  local data = assert(dirty_list[1])
  
  assert(not current_serialize_task)
  
  dirty_expire_time[data] = nil
  current_serialize_task = shared.onSerialize(data, function (encoded_data)
    current_serialize_task = nil
    
    local manager = dirty_data[data]
    dirty_data[data] = nil
    
    local id = manager.lookup[data]
    
    manager.saved_table[id] = encoded_data
    manager.clean[id] = data
    manager.dirty[id] = nil
    
    assert(dirty_list[1] == data)
    table.remove(dirty_list, 1)
    
    -- Invoke this again
    local next_data = dirty_list[1]
    if next_data then shared.onDelay(dirty_expire_time[next_data]-GetTime(), dataCleanerCallback) end
  end)
end

local function addDirtyData(data, manager)
  local id = assert(manager.lookup[data])
  
  if manager.clean[id] then
    assert(dirty_data[data] == nil)
    dirty_data[data] = manager
    manager.clean[id] = nil
    manager.dirty[id] = data
  elseif manager.dirty[id] then
    -- Presumably the data is already dirty.
    assert(dirty_data[data] == manager)
    
    -- Find and remove data from dirty_list; we'll re-add it to the end of the list shortly.
    for i, _data in ipairs(dirty_list) do
      if data == _data then
        table.remove(dirty_list, i)
        
        if i == 1 then
          -- The task you just marked dirty was the one we were supposed to serialize next.
          
          if current_serialize_task then
            -- In fact, we already started!
            shared.cancelSerializeTask(current_serialize_task)
            current_serialize_task = nil
          else
            -- Cancel the current schedule, it's not happening.
            shared.cancelDelayed(dataCleanerCallback)
          end
          
          -- Reschedule for the next piece of dirty data if there is one.
          local next_dirty_data = dirty_list[1]
          if next_dirty_data then shared.onDelay(dirty_expire_time[next_dirty_data]-GetTime(), dataCleanerCallback) end
        end
        
        break
      end
    end
  else
    -- The data is neither clean nor dirty, which obviously means that it has somehow
    -- become quantum entangled with Schrödinger's cat.
    assert(false)
  end
  
  -- We'll try saving the data in 30 seconds if doesn't get dirtied again between now and then.
  dirty_expire_time[data] = GetTime()+expire_delay
  table.insert(dirty_list, data)
  
  if dirty_list[1] == data then
    -- This is the only thing scheduled to be cleaned; which means dataCleanerCallback hasn't yet
    -- been scheduled to run. Let's fix that.
    shared.onDelay(expire_delay, dataCleanerCallback)
  end
end

shared.onSave(function ()
  -- Events/serialize.lua should have registered its onSave callback before us, and 
  -- already finished any tasks that were in progress.
  assert(current_serialize_task == nil)
  
  -- We on the other hand will take this moment to finish serializing all the dirty data that remains.
  for i, data in ipairs(dirty_list) do
    local manager = dirty_data[data]
    local id = manager.lookup[data]
    local serializer = shared.createSerializer(shared.meta)
    
    serializer.encodeObject(data)
    
    manager.saved_table[id] = serializer.flush()
    manager.clean[id] = data
    manager.dirty[id] = nil
  end
  
  table.wipe(dirty_data)
  table.wipe(dirty_list)
  table.wipe(dirty_expire_time)
end)


-- saved_table is where all the encoded data is saved; fallback is called to create a new object
-- if there is no saved data.

-- This function returns two functions, one to retrieve saved data,
-- and another to mark saved data as dirty so that it will be
-- re-encoded.
function shared.createSavedDataManager(saved_table, fallback)
  -- Dirty/Clean objects go here.
  local lookup, clean, dirty = setmetatable({}, {__mode="k"}), setmetatable({}, {__mode="v"}), {}
  
  local manager_state = {
    saved_table = saved_table,
    lookup = lookup,
    clean = clean,
    dirty = dirty
  }
  
  -- If multiple callbacks are waiting on a bit of data to be decoded,
  -- they all get added to this map of ids to lists of callacks.
  local pending_callbacks = {}
  
  -- This function invokes cb with the object saved under id.
  return function (id, cb, ...)
    assert(id)
    
    cb = shared.callback(cb, ...)
    
    local data = clean[id] or dirty[id]
    
    if data then
      -- We have the data already.
      cb(data)
      shared.delayCollection(data)
    else
      local pending_list = pending_callbacks[id]
      
      if pending_list then
        -- The requested data is currently in the process of being decoded;
        -- add the callback to the list of callbacks to be invoked when it's finished.
        table.insert(pending_list, cb)
      else
        local encoded_data = saved_table[id]
        if encoded_data then
          -- The requested data is currently encoded; will start decoding it.
          
          -- Remember the callback.
          pending_list = {cb}
          pending_callbacks[id] = pending_list
          
          shared.onUnserialize(encoded_data, function (data)
            pending_callbacks[id] = nil
            clean[id] = data
            lookup[data] = id
            shared.invokeCallbackList(pending_list, data)
            shared.delayCollection(data)
          end)
        else
          -- We don't have this object; create a new one.
          data = assert(fallback(id))
          clean[id] = data
          lookup[data] = id
          cb(data)
          shared.delayCollection(data)
        end
      end
    end
  end, function (data)
    addDirtyData(data, manager_state)
  end
end
