--[[
	Swatter - An AddOn debugging aid for World of Warcraft.
	$Id$
	Copyright (C) 2006 Norganna

	This library is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation; either
	version 2.1 of the License, or (at your option) any later version.

	This library 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
	Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
]]

-- Check to see if another debugging aid has been loaded.
for addon, name in pairs({
	['!buggrabber'] = 'BugGrabber',
	['!improvederrorframe'] = 'ImprovedErrorFrame',
}) do
  local enabled = select(4, GetAddOnInfo(addon))
  if enabled then
	  DEFAULT_CHAT_FRAME:AddMessage("|cffffaa11Swatter is not loaded, because you are running "..name.."|r")
	  return
  end
end

Swatter = {
	origHandler = geterrorhandler(),
	origItemRef = SetItemRef,
	nilFrame = {
		GetName = function() return "Global" end
	},
	errorOrder = {},
	HISTORY_SIZE = 50,
}
local origItemRef = Swatter.origItemRef

Swatter.Version="3.9.0.1301"
if (Swatter.Version == "<%".."version%>") then
	Swatter.Version = "3.9-DEV"
end
SWATTER_VERSION = Swatter.Version

SwatterData = {
	enabled = true,
	autoshow = true,
	errors = {},
}

function Swatter.ChatMsg(msg)
	DEFAULT_CHAT_FRAME:AddMessage(msg)
end

local chat = Swatter.ChatMsg

function Swatter.IsEnabled()
	return SwatterData.enabled
end

-- Test:  /run Swatter.OnError("Test")
function Swatter.OnError(msg, frame, stack, etype, ...)
	if (not SwatterData.enabled) then 
		if (not etype) then
			return Swatter.origHandler(msg, frame)
		else
			return UIParent_OnEvent(etype, ...)
		end
	end

	msg = msg or ""
	frame = frame or Swatter.nilFrame
	stack = stack or debugstack(2, 20, 20)

	local context
	if (not frame.Swatter) then frame.Swatter = {} end
	local id = frame.Swatter[msg]

	if (not id) then
		context = "Anonymous"
		if (frame) then
			context = "Unnamed"
			if (frame:GetName()) then
				context = frame:GetName()
			end
		end
		local ts = date("%Y-%m-%d %H:%M:%S");
		local addons = Swatter.GetAddOns()
		table.insert(SwatterData.errors, {
			context = context,
			timestamp = ts,
			addons = addons,
			message = msg,
			stack = stack,
			count = 0,
		})
		id = table.getn(SwatterData.errors)
		frame.Swatter[msg] = id
	else
		context = SwatterData.errors[id].context
		for pos, errid in ipairs(Swatter.errorOrder) do
			if (errid == id) then
				table.remove(Swatter.errorOrder, pos)
				break
			end
		end
	end
	table.insert(Swatter.errorOrder, id)

	local err = SwatterData.errors[id]
	local count = err.count or 0
	if (count < 1000) then err.count = count + 1 end
	if (count == 0) then
		if (etype == "ADDON_ACTION_BLOCKED") then
			if (not Swatter.blockWarn) then
				chat("|cffffaa11Warning only: Swatter found blocked actions:|r |Hswatter:"..id.."|h|cffff3311["..context.."]|r|h")
				chat("|cffffaa11Note: Swatter will continue to catch blocked actions but this is the last time this session that we'll tell you about it.|r")
				Swatter.blockWarn = true
			end
		elseif (SwatterData.autoshow) then
			Swatter.ErrorUpdate()
			Swatter.Error:Show()
		else
			chat("|cffffaa11Swatter caught error:|r |Hswatter:"..id.."|h|cffff3311["..context.."]|r|h")
		end
	end
end

function Swatter.NamedFrame(name)
	if (not Swatter.named) then Swatter.named = {} end
	if (not Swatter.named[name]) then
		Swatter.named[name] = {
			name = name,
			GetName = function(obj) return obj.name end,
		}
	end
	return Swatter.named[name]
end

function Swatter.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 (version) then
				addlist = addlist.."  "..name..", v"..version.."\n"
			else
				addlist = addlist.."  "..name.."\n" 
			end
		end
	end
	return addlist
end


-- Error occured in: Global
-- Date: "%m/%d/%y %H:%M:%S"
-- Count: 1
-- Message: [string "bla()"] line 1:
--   attempt to call global 'bla' (a nil value)
-- Debug:
-- [C]: in function `bla'
-- [string "bla()"]:1: in main chunk
-- [C]: in function `RunScript'
-- Interface\FrameXML\ChatFrame.lua:1788: in function `value'
-- Interface\FrameXML\ChatFrame.lua:3008: in function `ChatEdit_ParseText'
-- Interface\FrameXML\ChatFrame.lua:2734: in function `ChatEdit_SendText'
-- Interface\FrameXML\ChatFrame.lua:2756: in function `ChatEdit_OnEnterPressed'
-- [string "ChatFrameEditBox:OnEnterPressed"]:2: in function <[string "ChatFrameEditBox:OnEnterPressed"]:1>

function Swatter.OnEvent(frame, event, ...)
	if (event == "ADDON_LOADED") then
		local addon = select(1, ...)
		if (addon:lower() == "!swatter") then

			-- Check to see if we still exist
			if (not SwatterData) then
				if (BugGrabber) then
					-- We've been buggrabber-nabbed. Give up.
					DEFAULT_CHAT_FRAME:AddMessage("|cffffaa11Warning: Swatter has been disabled by BugGrabber. If you want to run Swatter instead of BugGrabber/BugSack, disable those two addons in you addon list and re-enable Swatter. Otherwise, enjoy BugGrabber!|r");
				end
				SetItemRef = origItemRef
				return
			end

			-- We need to cleanup our error history
			if (not SwatterData.errors) then SwatterData.errors = {} end
			local ec = table.getn(SwatterData.errors) or 0
			if (ec > Swatter.HISTORY_SIZE) then
				local remove = ec - Swatter.HISTORY_SIZE
				for i=1, remove do
					table.remove(SwatterData.errors, 1)
				end
			end
			for pos, err in ipairs(SwatterData.errors) do
				table.insert(Swatter.errorOrder, pos)
			end
			frame:UnregisterEvent("ADDON_LOADED")
			return
		end
	elseif (event == "ADDON_ACTION_BLOCKED") then
		local addon, func = select(1, ...)
		Swatter.OnError(string.format("Warning: AddOn %s attempted to call a protected function (%s) from a tainted execution path.", addon, func), Swatter.NamedFrame("AddOn: "..addon), debugstack(2, 20, 20), event, ...)
	elseif (event == "ADDON_ACTION_FORBIDDEN") then
		local addon, func = select(1, ...)
		Swatter.OnError(string.format("Warning: AddOn %s attempted to call a forbidden function (%s) from a tainted execution path.", addon, func), Swatter.NamedFrame("AddOn: "..addon), debugstack(2, 20, 20), event, ...)
	end
end

function Swatter.SetItemRef(...)
	local msg = select(1, ...)
	local id = select(3, msg:find("^swatter:(%d+)"))
	id = tonumber(id)
	if (id) then
		if (Swatter) then
			for pos, errid in ipairs(Swatter.errorOrder) do
				if (errid == id) then
					Swatter.Error:Show()
					return Swatter.ErrorDisplay(pos)
				end
			end
		end
	else
		if (not Swatter) then
			SetItemRef = origItemRef
			return origItemRef(...)
		end
		return Swatter.origItemRef(...)
	end
end

function Swatter.ErrorShow()
	Swatter.Error.pos = table.getn(Swatter.errorOrder)
	Swatter.ErrorDisplay()
end

function Swatter.ErrorDisplay(id)
	if id then Swatter.Error.pos = id else id = Swatter.Error.pos end
	Swatter.ErrorUpdate()

	local errid = Swatter.errorOrder[id]
	if (not errid) then 
		Swatter.Error.curError = "Unknown error at position "..id
		Swatter.ErrorUpdate()
		return
	end
	local err = SwatterData.errors[errid]
	if (not err) then
		Swatter.Error.curError = "Unknown error at index "..errid
		Swatter.ErrorUpdate()
		return
	end
	
	local ts = err.timestamp or "Unavailable"
	local addlist = err.addons or "  Unavailable"

	local message = err.message:gsub("(.-):(%d+): ", "%1 line %2:\n   "):gsub("Interface(\\%w+\\)", "..%1"):gsub(": in function `(.-)`", ": %1"):gsub("|", "||")
	local trace = "   "..err.stack:gsub("Interface\\AddOns\\", ""):gsub("Interface(\\%w+\\)", "..%1"):gsub(": in function `(.-)'", ": %1()"):gsub(": in function <(.-)>", ":\n   %1"):gsub(": in main chunk ", ": "):gsub("\n$",""):gsub("\n", "\n   ")
	local count = err.count
	if (count > 999) then count = "\226\136\158" --[[Infinity]] end

	
	Swatter.Error.curError = "Date: "..ts.."\nID: "..id.."\nError occured in: "..(err.context or "Anonymous").."\nCount: "..count.."\nMessage: "..message.."\nDebug:\n"..trace.."\nAddOns:\n"..addlist.."\n"
	Swatter.Error.selected = false
	Swatter.ErrorUpdate()
	Swatter.Error:Show()
end


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

function Swatter.ErrorPrev()
	local cur = Swatter.Error.pos or 1
	if (cur > 1) then
		Swatter.ErrorDisplay(cur - 1)
	else
		Swatter.ErrorUpdate()
	end
end

function Swatter.ErrorNext()
	local cur = Swatter.Error.pos or 1
	local max = table.getn(Swatter.errorOrder) or 0
	if (cur < max) then
		Swatter.ErrorDisplay(cur + 1)
	else
		Swatter.ErrorUpdate()
	end
end

function Swatter.UpdateNextPrev()
	local cur = Swatter.Error.pos or 1
	local max = table.getn(Swatter.errorOrder) or 0
	if (max > cur) then Swatter.Error.Next:Enable() else Swatter.Error.Next:Disable() end
	if (cur > 1) then Swatter.Error.Prev:Enable() else Swatter.Error.Prev:Disable() end
end

function Swatter.ErrorUpdate()
	if (not Swatter.Error.curError) then Swatter.Error.curError = "" end
	Swatter.Error.Box:SetText(Swatter.Error.curError)
	Swatter.Error.Scroll:UpdateScrollChildRect()
	Swatter.Error.Box:ClearFocus()
	Swatter.UpdateNextPrev()
end

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

-- Create our error message frame
Swatter.Error = CreateFrame("Frame", "", UIParent)
Swatter.Error:Hide()
Swatter.Error:SetPoint("CENTER", "UIParent", "CENTER")
Swatter.Error:SetFrameStrata("TOOLTIP")
Swatter.Error:SetHeight(280)
Swatter.Error:SetWidth(500)
Swatter.Error:SetBackdrop({
	bgFile = "Interface/Tooltips/UI-Tooltip-Background",
	edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
	tile = true, tileSize = 32, edgeSize = 32,
	insets = { left = 9, right = 9, top = 9, bottom = 9 }
})
Swatter.Error:SetBackdropColor(0,0,0, 0.8)
Swatter.Error:SetScript("OnShow", Swatter.ErrorShow)

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

Swatter.Error.Next = CreateFrame("Button", "", Swatter.Error, "OptionsButtonTemplate")
Swatter.Error.Next:SetText("Next >")
Swatter.Error.Next:SetPoint("BOTTOMRIGHT", Swatter.Error.Done, "BOTTOMLEFT", -5, 0)
Swatter.Error.Next:SetScript("OnClick", Swatter.ErrorNext)

Swatter.Error.Prev = CreateFrame("Button", "", Swatter.Error, "OptionsButtonTemplate")
Swatter.Error.Prev:SetText("< Prev")
Swatter.Error.Prev:SetPoint("BOTTOMRIGHT", Swatter.Error.Next, "BOTTOMLEFT", -5, 0)
Swatter.Error.Prev:SetScript("OnClick", Swatter.ErrorPrev)

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

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

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

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

seterrorhandler(Swatter.OnError)
Swatter.Frame = CreateFrame("Frame")
Swatter.Frame:Show()
Swatter.Frame:SetScript("OnEvent", Swatter.OnEvent)
Swatter.Frame:RegisterEvent("ADDON_LOADED")
Swatter.Frame:RegisterEvent("ADDON_ACTION_FORBIDDEN")
Swatter.Frame:RegisterEvent("ADDON_ACTION_BLOCKED")
SetItemRef = Swatter.SetItemRef

UIParent:UnregisterEvent("ADDON_ACTION_FORBIDDEN")
UIParent:UnregisterEvent("ADDON_ACTION_BLOCKED")

SLASH_SWATTER1 = "/swatter"
SLASH_SWATTER2 = "/swat"
SlashCmdList["SWATTER"] = function(msg)
	if (not msg or msg == "" or msg == "help") then
		chat("Swatter help:")
		chat("  /swat enable    -  Enables swatter")
		chat("  /swat disable   -  Disables swatter")
		chat("  /swat show      -  Shows the last error box again")
		chat("  /swat autoshow  -  Enables swatter autopopup upon error")
		chat("  /swat noauto    -  Swatter will only show an error in chat")
	elseif (msg == "show") then
		Swatter.Error:Show()
	elseif (msg == "enable") then
		SwatterData.enabled = true
		chat("Swatter will now catch errors")
	elseif (msg == "disable") then
		SwatterData.enabled = false
		chat("Swatter will no longer catch errors")
	elseif (msg == "autoshow") then
		SwatterData.autoshow = true
		chat("Swatter will popup the first time it sees an error")
	elseif (msg == "noautoshow") then
		SwatterData.autoshow = false
		chat("Swatter will print into chat instead of popping up")
	end
end

