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

--[[
  Much of this code is ganked wholesale from Swatter, and is Copyright (C) 2006 Norganna. Licensed under LGPL v3.0.
]]

local debug_output = false
if QuestHelper_File["error.lua"] == "Development Version" then debug_output = true end

QuestHelper_local_version = QuestHelper_File["error.lua"]
QuestHelper_toc_version = GetAddOnMetadata("QuestHelper", "Version")

local origHandler = geterrorhandler()

local QuestHelper_ErrorCatcher = { }

local startup_errors = {}
local completely_started = false
local yelled_at_user = false

local first_error = nil

QuestHelper_Errors = {}

function QuestHelper_ErrorCatcher.TextError(text)
  DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff8080QuestHelper Error Handler: |r%s", text))
end


-- ganked verbatim from Swatter
function QuestHelper_ErrorCatcher.GetPlayerInfo()
  local race, rfile = UnitRace("player")
  local class, cfile = UnitClass("player")
  return string.format("Level %s %s %s on %s", UnitLevel("player"), race, class, GetRealmName())
end

function QuestHelper_ErrorCatcher.GetQuests()
  local return_string = ""

  for q = 1, GetNumQuestLogEntries() do
    local title, _, _, _, header, _, _, _, id = GetQuestLogTitle(q)
    if header then
      if id then
        return_string = return_string .. string.format("%s (%d)\n", title, id)
      else
        return_string = return_string .. string.format("[%s]\n", title)
      end
    else
      return_string = return_string .. string.format("\t%s (%d)\n", title, id)
    end
  end

  return return_string
end
function QuestHelper_ErrorCatcher.GetAddOns()
	local addlist = ""
	for i = 1, GetNumAddOns() do
		local name, title, notes, enabled, loadable, reason, security = GetAddOnInfo(i)

		local loaded = IsAddOnLoaded(i)
		if (loaded) then
			if not name then name = "Anonymous" end
			name = name:gsub("[^a-zA-Z0-9]+", "")
			local version = GetAddOnMetadata(i, "Version")
			local class = getglobal(name)
			if not class or type(class)~='table' then class = getglobal(name:lower()) end
			if not class or type(class)~='table' then class = getglobal(name:sub(1,1):upper()..name:sub(2):lower()) end
			if not class or type(class)~='table' then class = getglobal(name:upper()) end
			if class and type(class)=='table' then
				if (class.version) then
					version = class.version
				elseif (class.Version) then
					version = class.Version
				elseif (class.VERSION) then
					version = class.VERSION
				end
			end
			local const = getglobal(name:upper().."_VERSION")
			if (const) then version = const end

			if type(version)=='table' then
        local allstr = true
        for k, v in pairs(version) do
          if type(v) ~= "string" then allstr = false end
        end
        
        if allstr then
          version = table.concat(version,":")
        end
			elseif type(version) == 'function' then
        local yay, v = pcall(version)
        
        if yay then version = v end
      end

			if (version) then
				addlist = addlist.."  "..name..", v"..tostring(version).."\n"
			else
				addlist = addlist.."  "..name.."\n"
			end
		end
	end
	return addlist
end

local error_uniqueness_whitelist = {
  ["count"] = true,
  ["timestamp"] = true,
}

-- here's the logic
function QuestHelper_ErrorCatcher.CondenseErrors()
  if completely_started then
    while next(startup_errors) do
      _, err = next(startup_errors)
      table.remove(startup_errors)
      
      if not QuestHelper_Errors[err.type] then QuestHelper_Errors[err.type] = {} end
      
      local found = false
      
      for _, item in ipairs(QuestHelper_Errors[err.type]) do
        local match = true
        for k, v in pairs(err.dat) do
          if not error_uniqueness_whitelist[k] and item[k] ~= v then match = false break end
        end
        if match then for k, v in pairs(item) do
          if not error_uniqueness_whitelist[k] and err.dat[k] ~= v then match = false break end
        end end
        if match then
          found = true
          item.count = (item.count or 1) + 1
          break
        end
      end
      
      if not found then
        table.insert(QuestHelper_Errors[err.type], err.dat)
      end
    end
  end
end

function QuestHelper_ErrorCatcher_RegisterError(typ, dat)
  table.insert(startup_errors, {type = typ, dat = dat})
  QuestHelper_ErrorCatcher.CondenseErrors()
end

function QuestHelper_ErrorPackage(depth)
  return {
    timestamp = date("%Y-%m-%d %H:%M:%S"),
    local_version = QuestHelper_local_version,
    toc_version = QuestHelper_toc_version,
    game_version = GetBuildInfo(),
    locale = GetLocale(),
    mutation_passes_exceeded = QuestHelper and QuestHelper.mutation_passes_exceeded,
    stack = debugstack(depth or 4, 20, 20),
  }
end

StaticPopupDialogs["QH_EXPLODEY"] = {
	text = "QuestHelper has broken. You may have to restart WoW. Type \"/qh error\" for a detailed error message.",
	button1 = OKAY,
	OnAccept = function(self)
	end,
	timeout = 0,
	whileDead = 1,
	hideOnEscape = 1
};

function QuestHelper_ErrorCatcher_ExplicitError(loud, o_msg, o_frame, o_stack, ...)
  local msg = o_msg or ""  

  -- We toss it into StartupErrors, and then if we're running properly, we'll merge it into the main DB.
  local terror = QuestHelper_ErrorPackage()
  
  terror.message = msg
  terror.addons = QuestHelper_ErrorCatcher.GetAddOns()
  terror.stack = o_stack or terror.stack
  terror.silent = not loud
  terror.quests = QuestHelper_ErrorCatcher.GetQuests()
  terror.pinfo = QuestHelper_ErrorCatcher.GetPlayerInfo()
  
  QuestHelper_ErrorCatcher_RegisterError("crash", terror)
  
  if first_error and first_error.silent and not first_error.next_loud and not terror.silent then first_error.next_loud = terror first_error.addons = "" end
  if not first_error or first_error.generated then first_error = terror end
  
  QuestHelper_ErrorCatcher.CondenseErrors()

  if (--[[debug_output or]] loud) and not yelled_at_user then
    --print("qhbroken")
    StaticPopupDialogs["QH_EXPLODEY"] = {
      text = "QuestHelper has broken. You may have to restart WoW. Type \"/qh error\" for a detailed error message.",
      button1 = OKAY,
      OnAccept = function(self)
      end,
      timeout = 0,
      whileDead = 1,
      hideOnEscape = 1
    }
    
    StaticPopup_Show("QH_EXPLODEY")
    yelled_at_user = true
  end
end

function QuestHelper_ErrorCatcher_GenerateReport()
  if first_error then return end -- don't need to generate one
  
  local terror = QuestHelper_ErrorPackage()
  
  terror.message = "(Full report)"
  terror.addons = QuestHelper_ErrorCatcher.GetAddOns()
  terror.stack = ""
  terror.silent = "(Full report)"
  terror.generated = true
  terror.quests = QuestHelper_ErrorCatcher.GetQuests()
  terror.pinfo = QuestHelper_ErrorCatcher.GetPlayerInfo()
  
  first_error = terror
end

function QuestHelper_ErrorCatcher.OnError(o_msg, o_frame, o_stack, o_etype, ...)
  local errorize = false
  local loud = false
  if o_msg and string.find(o_msg, "QuestHelper") and not string.find(o_msg, "Cannot find a library with name") then loud = true end
  
  for lin in string.gmatch(debugstack(2, 20, 20), "([^\n]*)") do
    if string.find(lin, "QuestHelper") and not string.find(lin, "QuestHelper\\AstrolabeQH\\DongleStub.lua") then errorize = true end
  end
  
  if string.find(o_msg, "SavedVariables") then errorize, loud = false, false end
  if string.find(o_msg, "C stack overflow") then
    if loud then errorize = true end
    loud = false
  end
  
  if loud then errorize = true end
  
  if errorize then QuestHelper_ErrorCatcher_ExplicitError(loud, o_msg, o_frame, o_stack) end
  
  --[[
  if o_msg and
    (
      (
        string.find(o_msg, "QuestHelper")  -- Obviously we care about our bugs
      )
      or (
        string.find(debugstack(2, 20, 20), "QuestHelper")  -- We're being a little overzealous and catching any bug with "QuestHelper" in the stack. This possibly should be removed, I'm not sure it's ever caught anything interesting.
      )
    )
    and not string.match(o_msg, "WTF\\Account\\.*")  -- Sometimes the WTF file gets corrupted. This isn't our fault, since we weren't involved in writing it, and there's also nothing we can do about it - in fact we can't even retrieve the remnants of the old file. We may as well just ignore it. I suppose we could pop up a little dialog saying "clear some space on your hard drive, dufus" but, meh.
    and not (string.find(o_msg, "Cannot find a library with name") and string.find(debugstack(2, 20, 20), "QuestHelper\\AstrolabeQH\\DongleStub.lua")) -- We're catching errors caused by other people mucking up their dongles. Ughh.
    then
      QuestHelper_ErrorCatcher_ExplicitError(o_msg, o_frame, o_stack)
  end]]
  
  return origHandler(o_msg, o_frame, o_stack, o_etype, unpack(arg or {}))  -- pass it on
end

seterrorhandler(QuestHelper_ErrorCatcher.OnError) -- at this point we can catch errors

function QuestHelper_ErrorCatcher.CompletelyStarted()
  completely_started = true
  
  -- Our old code generated a horrifying number of redundant items. My bad. I considered going and trying to collate them into one chunk, but I think I'm just going to wipe them - it's easier, faster, and should fix some performance issues.
  if not QuestHelper_Errors.version or QuestHelper_Errors.version ~= 1 then
    QuestHelper_Errors = {version = 1}
  end
  
  QuestHelper_ErrorCatcher.CondenseErrors()
end

function QuestHelper_ErrorCatcher_CompletelyStarted()
  QuestHelper_ErrorCatcher.CompletelyStarted()
end



-- and here is the GUI

local QHE_Gui = {}

function QHE_Gui.ErrorUpdate()
  QHE_Gui.ErrorTextinate()
  QHE_Gui.Error.Box:SetText(QHE_Gui.Error.curError)
  QHE_Gui.Error.Scroll:UpdateScrollChildRect()
	QHE_Gui.Error.Box:ClearFocus()
end

function TextinateError(err)
  local tserr = string.format("msg: %s\ntoc: %s\nv: %s\ngame: %s\nlocale: %s\ntimestamp: %s\nmutation: %s\nsilent: %s\n\n%s\naddons:\n%s", err.message, err.toc_version, err.local_version, err.game_version, err.locale, err.timestamp, tostring(err.mutation_passes_exceeded), tostring(err.silent), err.stack, err.addons)
  if err.next_loud then
    tserr = tserr .. "\n\n---- Following loud error\n\n" .. TextinateError(err.next_loud)
  end
  return tserr
end

function QHE_Gui.ErrorTextinate()
  if first_error then
    QHE_Gui.Error.curError = TextinateError(first_error)
  else
    QHE_Gui.Error.curError = "None"
  end
end

function QHE_Gui.ErrorClicked()
	if (QHE_Gui.Error.selected) then return end
	QHE_Gui.Error.Box:HighlightText()
	QHE_Gui.Error.selected = true
end

function QHE_Gui.ErrorDone()
	QHE_Gui.Error:Hide()
end


-- Create our error message frame. Most of this is also ganked from Swatter.
QHE_Gui.Error = CreateFrame("Frame", "QHE_GUIErrorFrame", UIParent)
QHE_Gui.Error:Hide()
QHE_Gui.Error:SetPoint("CENTER", "UIParent", "CENTER")
QHE_Gui.Error:SetFrameStrata("TOOLTIP")
QHE_Gui.Error:SetHeight(300)
QHE_Gui.Error:SetWidth(600)
QHE_Gui.Error:SetBackdrop({
	bgFile = "Interface/Tooltips/ChatBubble-Background",
	edgeFile = "Interface/Tooltips/ChatBubble-BackDrop",
	tile = true, tileSize = 32, edgeSize = 32,
	insets = { left = 32, right = 32, top = 32, bottom = 32 }
})
QHE_Gui.Error:SetBackdropColor(0.2,0,0, 1)
QHE_Gui.Error:SetScript("OnShow", QHE_Gui.ErrorShow)
QHE_Gui.Error:SetMovable(true)

QHE_Gui.ProxyFrame = CreateFrame("Frame", "QHE_GuiProxyFrame")
QHE_Gui.ProxyFrame:SetParent(QHE_Gui.Error)
QHE_Gui.ProxyFrame.IsShown = function() return QHE_Gui.Error:IsShown() end
QHE_Gui.ProxyFrame.escCount = 0
QHE_Gui.ProxyFrame.timer = 0
QHE_Gui.ProxyFrame.Hide = (
	function( self )
		local numEscapes = QHE_Gui.numEscapes or 1
		self.escCount = self.escCount + 1
		if ( self.escCount >= numEscapes ) then
			self:GetParent():Hide()
			self.escCount = 0
		end
		if ( self.escCount == 1 ) then
			self.timer = 0
		end
	end
)
QHE_Gui.ProxyFrame:SetScript("OnUpdate",
	function( self, elapsed )
		local timer = self.timer + elapsed
		if ( timer >= 1 ) then
			self.escCount = 0
		end
		self.timer = timer
	end
)
table.insert(UISpecialFrames, "QHE_GuiProxyFrame")

QHE_Gui.Drag = CreateFrame("Button", nil, QHE_Gui.Error)
QHE_Gui.Drag:SetPoint("TOPLEFT", QHE_Gui.Error, "TOPLEFT", 10,-5)
QHE_Gui.Drag:SetPoint("TOPRIGHT", QHE_Gui.Error, "TOPRIGHT", -10,-5)
QHE_Gui.Drag:SetHeight(8)
QHE_Gui.Drag:SetHighlightTexture("Interface\\FriendsFrame\\UI-FriendsFrame-HighlightBar")

QHE_Gui.Drag:SetScript("OnMouseDown", function() QHE_Gui.Error:StartMoving() end)
QHE_Gui.Drag:SetScript("OnMouseUp", function() QHE_Gui.Error:StopMovingOrSizing() end)

QHE_Gui.Error.Done = CreateFrame("Button", "", QHE_Gui.Error, "OptionsButtonTemplate")
QHE_Gui.Error.Done:SetText("Close")
QHE_Gui.Error.Done:SetPoint("BOTTOMRIGHT", QHE_Gui.Error, "BOTTOMRIGHT", -10, 10)
QHE_Gui.Error.Done:SetScript("OnClick", QHE_Gui.ErrorDone)

QHE_Gui.Error.Mesg = QHE_Gui.Error:CreateFontString("", "OVERLAY", "GameFontNormalSmall")
QHE_Gui.Error.Mesg:SetJustifyH("LEFT")
QHE_Gui.Error.Mesg:SetPoint("TOPRIGHT", QHE_Gui.Error.Prev, "TOPLEFT", -10, 0)
QHE_Gui.Error.Mesg:SetPoint("LEFT", QHE_Gui.Error, "LEFT", 15, 0)
QHE_Gui.Error.Mesg:SetHeight(20)
QHE_Gui.Error.Mesg:SetText("Select All and Copy the above error message to report this bug.")

QHE_Gui.Error.Scroll = CreateFrame("ScrollFrame", "QHE_GUIErrorInputScroll", QHE_Gui.Error, "UIPanelScrollFrameTemplate")
QHE_Gui.Error.Scroll:SetPoint("TOPLEFT", QHE_Gui.Error, "TOPLEFT", 20, -20)
QHE_Gui.Error.Scroll:SetPoint("RIGHT", QHE_Gui.Error, "RIGHT", -30, 0)
QHE_Gui.Error.Scroll:SetPoint("BOTTOM", QHE_Gui.Error.Done, "TOP", 0, 10)

QHE_Gui.Error.Box = CreateFrame("EditBox", "QHE_GUIErrorEditBox", QHE_Gui.Error.Scroll)
QHE_Gui.Error.Box:SetWidth(500)
QHE_Gui.Error.Box:SetHeight(85)
QHE_Gui.Error.Box:SetMultiLine(true)
QHE_Gui.Error.Box:SetAutoFocus(false)
QHE_Gui.Error.Box:SetFontObject(GameFontHighlight)
QHE_Gui.Error.Box:SetScript("OnEscapePressed", QHE_Gui.ErrorDone)
QHE_Gui.Error.Box:SetScript("OnTextChanged", QHE_Gui.ErrorUpdate)
QHE_Gui.Error.Box:SetScript("OnEditFocusGained", QHE_Gui.ErrorClicked)

QHE_Gui.Error.Scroll:SetScrollChild(QHE_Gui.Error.Box)

function QuestHelper_ErrorCatcher_ReportError()
  QHE_Gui.Error.selected = false
	QHE_Gui.ErrorUpdate()
	QHE_Gui.Error:Show()
end
