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

-- Here we provide some convenience functions compressing/decompressing in the background.

local compress_icon = CreateFrame("Frame", "QuestHelper2_CompressionIcon", UIParent)
compress_icon:Hide()
compress_icon:SetWidth(16)
compress_icon:SetHeight(16)

local compress_icon_bg = compress_icon:CreateTexture(nil, "BACKGROUND")
local compress_icon_rot = compress_icon:CreateTexture(nil, "ARTWORK")

compress_icon_bg:SetAllPoints()

-- Functions to call when serialization is started or stopped.
-- I wanted these mostly to disable to routing code when
local on_serialize_start = {}
local on_serialize_stop = {}

function shared.onSerializeStart(cb, ...)
  table.insert(on_serialize_start, shared.callback(cb, ...))
  if compress_icon:IsShown() then cb(...) end
end

function shared.onSerializeStop(cb, ...)
  table.insert(on_serialize_stop, shared.callback(cb, ...))
  if not compress_icon:IsShown() then cb(...) end
end

compress_icon:SetScript("OnShow", function ()
  shared.invokeCallbackList(on_serialize_start)
end)

compress_icon:SetScript("OnHide", function ()
  shared.invokeCallbackList(on_serialize_stop)
end)

local function iconCompress()
  compress_icon:Show()
  compress_icon_bg:SetTexture("Interface\\AddOns\\QuestHelper2_Collection\\Events\\CompressIconStatic.tga")
  compress_icon_rot:SetTexture("Interface\\AddOns\\QuestHelper2_Collection\\Events\\CompressIconRotate.tga")
end

local function iconDecompress()
  compress_icon:Show()
  compress_icon_bg:SetTexture("Interface\\AddOns\\QuestHelper2_Collection\\Events\\DecompressIconStatic.tga")
  compress_icon_rot:SetTexture("Interface\\AddOns\\QuestHelper2_Collection\\Events\\DecompressIconRotate.tga")
end

local function iconHide()
  compress_icon:Hide()
end

-- Texture:SetRotation scales the texture down so that the entire thing can be seen.
-- We in fact don't want the texture scaled, so this is to compensate.
-- Values are (sqrt(x^x*2)-x)/2, where x is the frames size, i.e., 16.
compress_icon_rot:SetPoint("BOTTOMLEFT", -3.31370849898476, -3.31370849898476)
compress_icon_rot:SetPoint("TOPRIGHT", 3.31370849898476, 3.31370849898476)
local compress_icon_angle = 0

compress_icon:SetScript("OnUpdate", function (_, delta)
  -- Updates the compress_icon animation; this function should only be called
  -- while the frame is visible.
  
  -- I honestly tried using animation groups for this; but
  -- they're crap.
  
  compress_icon_angle = (compress_icon_angle + delta*6)%6.283185307179586
  compress_icon_rot:SetRotation(-compress_icon_angle, .5, .5)
end)

-- QuestHelper2 will attach the icon to the tracker, but until then, or if it's
-- not loaded, give the icon a reasonable position.
compress_icon:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -5, -160)

-- This is a list of coroutines that need to be invoked each frame for compression.
local compression_threads = {}

-- This maps each thread to a function to call when we begin them.
-- Only used to change the colour of the compression icon.
local thread_startfunc = {}

local create, resume, yield, status = coroutine.create, coroutine.resume, coroutine.yield, coroutine.status

-- If we're saving, we set this to true. All compression/decompression is done immediately.
-- Yes, we finish decompression jobs as well, as some of their callbacks are to
-- modify the uncompressed data and then recompress it.
local saving = false

shared.onSave(function ()
  -- Finish all compression/decompression right now.
  saving = true
  
  for i, thread in ipairs(compression_threads) do
    repeat resume(thread) until status(thread) ~= "suspended"
  end
  
  table.wipe(compression_threads)
  table.wipe(thread_startfunc)
  
  iconHide()
end)

local function doCompression()
  local thread = compression_threads[1]
  local ok, err_msg = resume(thread)
  
  if status(thread) ~= "suspended" then
    -- Thread is presumed dead.
    table.remove(compression_threads, 1)
    
    if err_msg then
      print("Error in codec thread: "..err_msg)
    end
    
    thread = compression_threads[1]
    
    if thread then
      thread_startfunc[thread]()
      thread_startfunc[thread] = nil
    else
      -- Clean up all the garbage the compressors/decompressors made.
      -- They've very messy like that.
      iconHide()
      collectgarbage()
      
      -- There is no more to do; stop calling this function.
      return nil
    end
  end
  
  -- Still more to do!
  return 0
end

local function addThread(thread, startfunc)
  if compression_threads[1] == nil then
    compression_threads[1] = thread
    shared.onDelay(0, doCompression)
    startfunc()
  else
    table.insert(compression_threads, thread)
    thread_startfunc[thread] = startfunc
  end
  
  return thread
end

-- 
function shared.cancelSerializeTask(task)
  for i = 1,#compression_threads do
    if compression_threads[i] == task then
      table.remove(compression_threads,i)
      
      if i == 1 then
        -- We killed the current thread; start the next one.
        local next_thread = compression_threads[1]
        
        if next_thread then
          thread_startfunc[next_thread]()
          thread_startfunc[next_thread] = nil
        else
          -- Okay, we killed the only thread.
          shared.cancelDelayed(doCompression)
          iconHide()
        end
      else
        -- That task isn't currently running, and won't ever run, so
        -- remove its start function.
        thread_startfunc[task] = nil
      end
      
      return
    end
  end
  
  -- task wasn't found, all wells.
end

-- Serializes object in the background, and invokes cb with the encoded data when done.
function shared.onSerialize(object, cb, ...)
  cb = shared.callback(cb, ...)
  
  if saving then
    local serializer = shared.createSerializer(shared.meta)
    serializer.encodeObject(object)
    cb(serializer.flush())
    return false
  else
    return addThread(create(function()
      local serializer = shared.createSerializer(shared.meta, yield)
      serializer.encodeObject(object)
      cb(serializer.flush())
    end), iconCompress)
  end
end

function shared.onUnserialize(encoded_data, cb, ...)
  cb = shared.callback(cb, ...)
  
  if saving then
    cb(shared.createUnserializer(encoded_data, shared.meta).decodeObject())
    return false
  else
    return addThread(create(function()
      cb(shared.createUnserializer(encoded_data, shared.meta, yield).decodeObject())
    end), iconDecompress)
  end
end
