
local GetTime = QuestHelper_GetTime

QuestHelper_File["recycle.lua"] = "4.0.1.$svnversion$"
QuestHelper_Loadtime["recycle.lua"] = GetTime()

--[[

"Zorba, why are you doing manual memory allocation in Lua? That's incredibly stupid! You, as well, must be incredibly stupid. Why are you so stupid?"

Yeah. Yeah, that's what I thought too. It turns out things are more complicated than I thought.

There's a few good reasons to do something this ugly.

First off, it makes it real, real easy to track where allocations are going. That's what the whole "tag" thing is for - all created tables are tagged. This is useful. This is very, very useful, as it lets me track down memory leaks extraordinarily easily. This is also obsoleted slightly by the technique in bst_pre.lua (check it out.)

Second, it deals far better with table churn. I don't know if this is a WoW issue, but in WoW at least, tables can hang around for quite a while before getting garbage-collected. If you're making a dozen tables per frame, you can rapidly eat 10 or 20 megs of RAM that you're not actually using. Rigging an explicit thing like this allows you to recycle those tables instead of just wasting them.

It's ugly. I'm not arguing that. But it really, really helps.

]]

QuestHelper.used_tables = 0
QuestHelper.free_tables = setmetatable({}, {__mode="k"})  -- But Zorba, the only thing you're storing here is unused table values! Yeah, that's right, *unused* table values, if the garbage collector wants to have a field day, go for it

local function crashy(tab, name)
  QuestHelper: Assert(false, "Tried to access " .. name .. " from released table")
end

local unused_meta = {__index=crashy, __newindex=crashy}

QuestHelper.used_textures = 0
QuestHelper.free_textures = {}

QuestHelper.used_text = 0
QuestHelper.free_text = {}

QuestHelper.used_frames = 0
QuestHelper.free_frames = {}

-- This little table rigs up a basic typing system to assist with debugging. It has weak-reference keys so it shouldn't ever lead to leaks of any kind.
QuestHelper.recycle_tabletyping = setmetatable({}, {__mode="k"})

local toomanytables_warned = false
local function mark(table, item, tag) table[item] = tag end
function QuestHelper:CreateTable(tag)
  local tbl = next(self.free_tables)
  self.used_tables = self.used_tables + 1
  
  if not tbl then
    tbl = {}
  else
    self.free_tables[tbl] = nil
    setmetatable(tbl, nil)
  end
  
  tag = tag or string.gsub(debugstack(2, 1, 1), "\n.*", "")
  if type(tag) ~= "string" then tag = tostring(tag) .. " ((weird)) " .. string.gsub(debugstack(2, 1, 1), "\n.*", "") end
  
  if QH_RegisterTable then QH_RegisterTable(tbl, true, tag) end
  if not pcall(mark, self.recycle_tabletyping, tbl, tag) then
    local freq = {}
    for _, v in pairs(self.recycle_tabletyping) do
      freq[v] = (freq[v] or 0) + 1
    end
    
    local fqx = {}
    for k, v in pairs(freq) do
      table.insert(fqx, {k, v})
    end
    
    table.sort(fqx, function(a, b) return a[2] < b[2] end)
    
    local stt = "recycle overflow error (too many tables)\n"
    
    for _, v in ipairs(fqx) do
      stt = stt .. string.format("        %d: %s\n", v[2], v[1])
    end
    
    local pcscaught = QH_ClearPathcache(true)
    collectgarbage("collect")
    
    stt = stt .. string.format("        (pathcache cleared %d)\n", pcscaught)
    
    if not pcall(mark, self.recycle_tabletyping, tbl, tag) then
      QuestHelper: Assert(false, stt)
    end
    
    QuestHelper_ErrorCatcher_ExplicitError(false, stt .. "        (recovered)\n")
    if not toomanytables_warned then
      QuestHelper:TextOut("Something has gone wrong! QuestHelper should continue working, but Zorba would really appreciate it if you type |cffbbffd6/qh error|r and went to report that on the QuestHelper homepage.")
      toomanytables_warned = true
    end
  end
  
  return tbl
end

local release_cycle = 0
function QuestHelper:ReleaseTable(tbl)
  QuestHelper: Assert(type(tbl) == "table")
  QuestHelper: Assert(not self.free_tables[tbl])
  
  wipe(tbl)
  
  self.used_tables = self.used_tables - 1
  self.recycle_tabletyping[tbl] = nil
  
  if QH_RegisterTable or self.used_tables < 500 or release_cycle < 100 then -- this is actually plenty. you'd be horrified how much table churn there is in this thing
    self.free_tables[setmetatable(tbl, unused_meta)] = true
    release_cycle = release_cycle + 1
  else
    self.recycle_tabletyping[tbl] = (self.recycle_tabletyping[tbl] or "((unknown))") .. "((released))"
    release_cycle = 0
  end
end

function QuestHelper:RecycleClear()
  local ct = QuestHelper:TableSize(QuestHelper.free_tables)
  QuestHelper.free_tables = {}
  return ct
end

function QuestHelper:DumpTableTypeFrequencies(silent)
  local freq = {}
  for k, v in pairs(self.recycle_tabletyping) do
    freq[v] = (freq[v] or 0) + 1
  end
  
  if not silent then
    local flist = {}
    for k, v in pairs(freq) do
      table.insert(flist, {count=v, name=k})
    end
    
    table.sort(flist, function(a, b) return a.count < b.count end)
    
    for k, v in pairs(flist) do
      self:TextOut(v.count .. ": " .. v.name)
    end
  end
  
  return freq
end

function QuestHelper:CreateFrame(parent)
  self.used_frames = self.used_frames + 1
  local frame = table.remove(self.free_frames)
  
  if frame then
    frame:SetParent(parent)
  else
    frame = CreateFrame("Button", string.format("QuestHelperFrame%d",self.used_frames), parent)
  end
  
  frame:SetFrameLevel((parent or UIParent):GetFrameLevel()+1)
  frame:SetFrameStrata("MEDIUM")
  frame:Show()
  
  return frame
end

local frameScripts =
 {
  "OnChar",
  "OnClick",
  "OnDoubleClick",
  "OnDragStart",
  "OnDragStop",
  "OnEnter",
  "OnEvent",
  "OnHide",
  "OnKeyDown",
  "OnKeyUp",
  "OnLeave",
  "OnLoad",
  "OnMouseDown",
  "OnMouseUp",
  "OnMouseWheel",
  "OnReceiveDrag",
  "OnShow",
  "OnSizeChanged",
  "OnUpdate",
  "PostClick",
  "PreClick"
 }

function QuestHelper:ReleaseFrame(frame)
  --[[ assert(type(frame) == "table") ]]
  for i,t in ipairs(self.free_frames) do --[[ assert(t ~= frame) ]] end
  
  for key in pairs(frame) do
    -- Remove all keys except 0, which seems to hold some special data.
    if key ~= 0 then
      frame[key] = nil
    end
  end
  
  for _, script in ipairs(frameScripts) do
    QH_Hook(frame, script, nil)
  end
  
  frame:Hide()
  frame:SetParent(QuestHelper)
  frame:ClearAllPoints()
  frame:SetMovable(false)
  frame:RegisterForDrag()
  frame:RegisterForClicks()
  frame:SetBackdrop(nil)
  frame:SetScale(1)
  frame:SetAlpha(1)
  
  self.used_frames = self.used_frames - 1
  table.insert(self.free_frames, frame)
end

function QuestHelper:CreateText(parent, text_str, text_size, text_font, r, g, b, a)
  self.used_text = self.used_text + 1
  local text = table.remove(self.free_text)
  
  if text then
    text:SetParent(parent)
  else
    text = parent:CreateFontString()
  end
  
  text:SetFont(text_font or QuestHelper.font.sans or ChatFontNormal:GetFont(), text_size or 12)
  text:SetDrawLayer("OVERLAY")
  text:SetJustifyH("CENTER")
  text:SetJustifyV("MIDDLE")
  text:SetTextColor(r or 1, g or 1, b or 1, a or 1)
  text:SetText(text_str or "")
  text:SetShadowColor(0, 0, 0, 0.3)
  text:SetShadowOffset(1, -1)
  text:Show()
  
  return text
end

function QuestHelper:ReleaseText(text)
  --[[ assert(type(text) == "table") ]]
  for i,t in ipairs(self.free_text) do --[[ assert(t ~= text) ]] end
  
  for key in pairs(text) do
    -- Remove all keys except 0, which seems to hold some special data.
    if key ~= 0 then
      text[key] = nil
    end
  end
  
  text:Hide()
  text:SetParent(UIParent)
  text:ClearAllPoints()
  self.used_text = self.used_text - 1
  table.insert(self.free_text, text)
end

function QuestHelper:CreateTexture(parent, r, g, b, a)
  self.used_textures = self.used_textures + 1
  local tex = table.remove(self.free_textures)
  
  if tex then
    tex:SetParent(parent)
  else
    tex = parent:CreateTexture()
  end
  
  if not tex:SetTexture(r, g, b, a) and
     not tex:SetTexture("Interface\\Icons\\Temp.blp") then
    tex:SetTexture(1, 0, 1, 0.5)
  end
  
  tex:ClearAllPoints()
  tex:SetTexCoord(0, 1, 0, 1)
  tex:SetVertexColor(1, 1, 1, 1)
  tex:SetDrawLayer("ARTWORK")
  tex:SetBlendMode("BLEND")
  tex:SetWidth(12)
  tex:SetHeight(12)
  tex:Show()
  
  return tex
end

function QuestHelper:CreateIconTexture(parent, id)
  local icon = self:CreateTexture(parent, "Interface\\AddOns\\QuestHelper\\Art\\Icons.tga")
  
  local w, h = 1/8, 1/8
  local x, y = ((id-1)%8)*w, math.floor((id-1)/8)*h
  
  icon:SetTexCoord(x, x+w, y, y+h)
  
  return icon
end

function QuestHelper:CreateDotTexture(parent)
  local icon = self:CreateIconTexture(parent, 13)
  icon:SetWidth(5)
  icon:SetHeight(5)
  icon:SetVertexColor(0, 0, 0, 0.35)
  return icon
end

function QuestHelper:CreateGlowTexture(parent)
  local tex = self:CreateTexture(parent, "Interface\\Addons\\QuestHelper\\Art\\Glow.tga")
  
  local angle = math.random()*6.28318530717958647692528676655900576839433879875021164
  local x, y = math.cos(angle)*0.707106781186547524400844362104849039284835937688474036588339869,
               math.sin(angle)*0.707106781186547524400844362104849039284835937688474036588339869
  
  -- Randomly rotate the texture, so they don't all look the same.
  tex:SetTexCoord(x+0.5, y+0.5, y+0.5, 0.5-x, 0.5-y, x+0.5, 0.5-x, 0.5-y)
  tex:ClearAllPoints()
  
  return tex
end

function QuestHelper:ReleaseTexture(tex)
  --[[ assert(type(tex) == "table") ]]
  for i,t in ipairs(self.free_textures) do --[[ assert(t ~= tex) ]] end
  
  for key in pairs(tex) do
    -- Remove all keys except 0, which seems to hold some special data.
    if key ~= 0 then
      tex[key] = nil
    end
  end
  
  tex:Hide()
  tex:SetParent(UIParent)
  tex:ClearAllPoints()
  self.used_textures = self.used_textures - 1
  table.insert(self.free_textures, tex)
end

QuestHelper.recycle_active_cached_tables = {}
QuestHelper.recycle_decache_queue = {}

function QuestHelper:CacheRegister(obj)
  if not self.recycle_active_cached_tables[obj] then
    self.recycle_active_cached_tables[obj] = true
    table.insert(self.recycle_decache_queue, obj)
  end
end

function QuestHelper:CacheCleanup(obj)
  local target = self.recycle_decache_queue[1]
  
  if not target then return end
  table.remove(self.recycle_decache_queue, 1)
  self.recycle_active_cached_tables[target] = nil
  
  if target.distance_cache then
    for k, v in pairs(target.distance_cache) do
      self:ReleaseTable(v)
    end
    self:ReleaseTable(target.distance_cache)
    target.distance_cache = self:CreateTable("objective.distance_cache cleaned")
  end
end

function QuestHelper:DumpCacheData(obj)
  local caches = 0
  local cached = 0
  for k, v in pairs(self.recycle_decache_queue) do
    caches = caches + 1
    if v.distance_cache then
      for q, w in pairs(v.distance_cache) do
        cached = cached + 1
      end
    end
  end
  
  self:TextOut(caches .. " queued caches with a total of " .. cached .. " cached items")
end
