-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--          http://www.curse.com/addons/wow/tradeskillmaster_warehousing          --
--                                                                                --
--             A TradeSkillMaster Addon (http://tradeskillmaster.com)             --
--    All Rights Reserved* - Detailed license information included with addon.    --
-- ------------------------------------------------------------------------------ --

TSMAPI_FOUR.Util = {}
local private = { freeTempTables = {}, tempTableState = {}, filterTemp = {} }
private.iterContext = { arg = {}, index = {}, helperFunc = {}, cleanupFunc = {} }
setmetatable(private.iterContext.arg, { __mode = "k" })
setmetatable(private.iterContext.index, { __mode = "k" })
setmetatable(private.iterContext.helperFunc, { __mode = "k" })
setmetatable(private.iterContext.cleanupFunc, { __mode = "k" })
local NUM_TEMP_TABLES = 50
local MAGIC_CHARACTERS = { '[', ']', '(', ')', '.', '+', '-', '*', '?', '^', '$' }

-- setup the temporary tables
do
	local TEMP_TABLE_MT = {
		__newindex = function(self, key, value)
			assert(private.tempTableState[self], "Attempt to access temp table after release")
			rawset(self, key, value)
		end,
		__index = function(self, key)
			assert(private.tempTableState[self], "Attempt to access temp table after release")
			return rawget(self, key)
		end,
		__metatable = false,
	}
	for i = 1, NUM_TEMP_TABLES do
		local tempTbl = setmetatable({}, TEMP_TABLE_MT)
		tinsert(private.freeTempTables, tempTbl)
	end
end



-- ============================================================================
-- TSMAPI Functions - Lua Util
-- ============================================================================

function TSMAPI_FOUR.Util.SafeStrSplit(str, sep)
	local parts = {}
	local s = 1
	local sepLength = #sep
	if sepLength == 0 then
		tinsert(parts, str)
		return parts
	end
	while true do
		local e = strfind(str, sep, s)
		if not e then
			tinsert(parts, strsub(str, s))
			break
		end
		tinsert(parts, strsub(str, s, e - 1))
		s = e + sepLength
	end
	return parts
end

function TSMAPI_FOUR.Util.StrEscape(str)
	assert(not strmatch(str, "\001"), "Input string must not contain '\\001' characters")
	str = gsub(str, "%%", "\001")
	for _, char in ipairs(MAGIC_CHARACTERS) do
		str = gsub(str, "%"..char, "%%"..char)
	end
	str = gsub(str, "\001", "%%%%")
	return str
end

function TSMAPI_FOUR.Util.Round(value, sig)
	sig = sig or 1
	return floor((value / sig) + 0.5) * sig
end

function TSMAPI_FOUR.Util.Floor(value, sig)
	sig = sig or 1
	return floor(value / sig) * sig
end

function TSMAPI_FOUR.Util.Ceil(value, sig)
	sig = sig or 1
	return ceil(value / sig) * sig
end

function TSMAPI_FOUR.Util.Scale(value, fromMin, fromMax, toMin, toMax)
	assert(fromMax > fromMin and toMax > toMin)
	assert(value >= fromMin and value <= fromMax)
	return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin)
end

function TSMAPI_FOUR.Util.CalculateHash(data, hash)
	-- calculate the hash using the djb2 algorithm (http://www.cse.yorku.ca/~oz/hash.html)
	hash = hash or 5381
	local maxValue = 2 ^ 24
	if type(data) == "string" then
		for i = 1, #data do
			hash = (hash * 33 + strbyte(data, i)) % maxValue
		end
	elseif type(data) == "number" then
		assert(data == floor(data), "Invalid number")
		while data > 0 do
			hash = (hash * 33 + data % 256) % maxValue
			data = floor(data / 256)
		end
	elseif type(data) == "table" then
		local keys = TSMAPI_FOUR.Util.AcquireTempTable()
		for k in pairs(data) do
			tinsert(keys, k)
		end
		sort(keys)
		for _, key in TSMAPI_FOUR.Util.TempTableIterator(keys) do
			hash = TSMAPI_FOUR.Util.CalculateHash(key, hash)
			hash = TSMAPI_FOUR.Util.CalculateHash(data[key], hash)
		end
	else
		error("Invalid data")
	end
	return hash
end

function TSMAPI_FOUR.Util.VarargIntoTable(tbl, ...)
	for i = 1, select("#", ...) do
		tbl[i] = select(i, ...)
	end
end

function TSMAPI_FOUR.Util.TableIterator(tbl, helperFunc, arg, cleanupFunc)
	tbl._iterArg = arg
	tbl._iterIndex = 0
	tbl._iterHelperFunc = helperFunc
	tbl._iterCleanupFunc = cleanupFunc
	return private.TableIterator, tbl
end

function TSMAPI_FOUR.Util.VarargIterator(...)
	return TSMAPI_FOUR.Util.TempTableIterator(TSMAPI_FOUR.Util.AcquireTempTable(...))
end

function TSMAPI_FOUR.Util.IteratorGetFirst(iter, tbl, index)
	local temp = TSMAPI_FOUR.Util.AcquireTempTable(iter(tbl, index))
	index = temp[1]
	if index == nil then
		TSMAPI_FOUR.Util.ReleaseTempTable(temp)
	else
		return TSMAPI_FOUR.Util.UnpackAndReleaseTempTable(temp)
	end
end

function TSMAPI_FOUR.Util.TableKeyIterator(tbl)
	return private.TableKeyIterator, tbl, nil
end

function TSMAPI_FOUR.Util.In(value, ...)
	for i = 1, select("#", ...) do
		if value == select(i, ...) then
			return true
		end
	end
	return false
end



-- ============================================================================
-- TSMAPI Functions - Table Recycling
-- ============================================================================

function TSMAPI_FOUR.Util.AcquireTempTable(...)
	local tbl = tremove(private.freeTempTables, 1)
	local debugInfo = nil
	if not tbl then
		-- generate a debugInfo variable which will show in the error and put the highest-count one in the message itself
		debugInfo = {}
		for _, info in pairs(private.tempTableState) do
			debugInfo[info] = (debugInfo[info] or 0) + 1
		end
		local highestInfo = nil
		for info, count in pairs(debugInfo) do
			highestInfo = highestInfo or info
			if count > debugInfo[highestInfo] then
				highestInfo = info
			end
		end
		error("Could not acquire temp table: "..highestInfo)
	end
	private.tempTableState[tbl] = TSMAPI_FOUR.Util.GetDebugStackInfo(2)
	TSMAPI_FOUR.Util.VarargIntoTable(tbl, ...)
	return tbl
end

function TSMAPI_FOUR.Util.TempTableIterator(tbl, helperFunc, arg)
	assert(private.tempTableState[tbl])
	return TSMAPI_FOUR.Util.TableIterator(tbl, helperFunc, arg, TSMAPI_FOUR.Util.ReleaseTempTable)
end

function TSMAPI_FOUR.Util.ReleaseTempTable(tbl)
	private.TempTableReleaseHelper(tbl)
end

function TSMAPI_FOUR.Util.UnpackAndReleaseTempTable(tbl)
	return private.TempTableReleaseHelper(tbl, unpack(tbl))
end



-- ============================================================================
-- TSMAPI Functions - WoW Util
-- ============================================================================

function TSMAPI_FOUR.Util.ShowStaticPopupDialog(name)
	StaticPopupDialogs[name].preferredIndex = 4
	StaticPopup_Show(name)
	for i=1, 100 do
		if _G["StaticPopup" .. i] and _G["StaticPopup" .. i].which == name then
			_G["StaticPopup" .. i]:SetFrameStrata("TOOLTIP")
			break
		end
	end
end

function TSMAPI_FOUR.Util.SafeTooltipLink(link)
	if strmatch(link, "p:") then
		link = TSMAPI_FOUR.Item.GetLink(link)
	end
	if strmatch(link, "battlepet") then
		local _, speciesID, level, breedQuality, maxHealth, power, speed, battlePetID = strsplit(":", link)
		BattlePetToolTip_Show(tonumber(speciesID), tonumber(level) or 0, tonumber(breedQuality) or 0, tonumber(maxHealth) or 0, tonumber(power) or 0, tonumber(speed) or 0, gsub(gsub(link, "^(.*)%[", ""), "%](.*)$", ""))
	elseif strmatch(link, "currency") then
		local currencyID = strmatch(link, "currency:(%d+)")
		GameTooltip:SetCurrencyByID(currencyID)
	else
		GameTooltip:SetHyperlink(TSMAPI_FOUR.Item.GetLink(link))
	end
end

function TSMAPI_FOUR.Util.SafeItemRef(link)
	if type(link) ~= "string" then return end
	-- extract the Blizzard itemString for both items and pets
	local blizzItemString = strmatch(link, "^\124c[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]\124H(item:[^\124]+)\124.+$")
	blizzItemString = blizzItemString or strmatch(link, "^\124c[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]\124H(battlepet:[^\124]+)\124.+$")
	if blizzItemString then
		SetItemRef(blizzItemString, link)
	end
end

function TSMAPI_FOUR.Util.IsAddonInstalled(name)
	return select(2, GetAddOnInfo(name)) and true or false
end

function TSMAPI_FOUR.Util.IsAddonEnabled(name)
	return GetAddOnEnableState(UnitName("player"), name) == 2 and select(4, GetAddOnInfo(name)) and true or false
end

function TSMAPI_FOUR.Util.GetDebugStackInfo(targetLevel, thread)
	targetLevel = targetLevel + 1
	assert(targetLevel > 0)
	for level = 1, 100 do
		local stackLine = nil
		if thread then
			stackLine = debugstack(thread, level, 1, 0)
		else
			stackLine = debugstack(level, 1, 0)
		end
		if not stackLine then
			return
		end
		stackLine = gsub(gsub(stackLine, "%.%.%.", ""), "/", "\\")
		stackLine = strmatch(stackLine, "^([^:]+:[0-9]+):")
		if stackLine then
			stackLine = gsub(stackLine, "[^%.]+lMaster\\", "TSM\\")
			stackLine = strtrim(stackLine)
			-- ignore the class code's wrapper function
			if not strfind(stackLine, "\\Class%.lua:177") then
				targetLevel = targetLevel - 1
				if targetLevel == 0 then
					return stackLine
				end
			end
		end
	end
end



-- ============================================================================
-- TSMAPI Functions - Table Functions
-- ============================================================================

function TSMAPI_FOUR.Util.TableFilter(tbl, func, ...)
	assert(not next(private.filterTemp))
	for k, v in pairs(tbl) do
		if func(k, v, ...) then
			tinsert(private.filterTemp, k)
		end
	end
	for _, k in ipairs(private.filterTemp) do
		tbl[k] = nil
	end
	wipe(private.filterTemp)
end

function TSMAPI_FOUR.Util.TableRemoveByValue(tbl, value)
	for i = #tbl, 1, -1 do
		if tbl[i] == value then
			tremove(tbl, i)
		end
	end
end

function TSMAPI_FOUR.Util.TableIndexOf(table, value)
	for i, v in pairs(table) do
		if v == value then
			return i
		end
	end
end

function TSMAPI_FOUR.Util.Count(tbl)
	local count = 0

	for _ in pairs(tbl) do
		count = count + 1
	end
	return count
end

function TSMAPI_FOUR.Util.GetDistinctTableKey(tbl, value)
	local key = nil
	for k, v in pairs(tbl) do
		if v == value then
			assert(not key)
			key = k
		end
	end
	assert(key)
	return key
end

function TSMAPI_FOUR.Util.BinarySearchGetFirstIndex(tbl, value, key)
	local found, index = private.BinarySearchHelper(tbl, value, key, "FIRST")
	return found and index or nil
end

function TSMAPI_FOUR.Util.BinarySearchGetLastIndex(tbl, value, key)
	local found, index = private.BinarySearchHelper(tbl, value, key, "LAST")
	return found and index or nil
end

function TSMAPI_FOUR.Util.BinarySearchGetInsertIndex(tbl, value, key)
	local _, index = private.BinarySearchHelper(tbl, value, key, "ANY")
	return index
end



-- ============================================================================
-- Private Helper Functions
-- ============================================================================

function private.TableKeyIterator(tbl, prevKey)
	local key = next(tbl, prevKey)
	return key
end

function private.TableIterator(tbl)
	tbl._iterIndex = tbl._iterIndex + 1
	if tbl._iterIndex > #tbl then
		tbl._iterArg = nil
		tbl._iterIndex = nil
		tbl._iterHelperFunc = nil
		local cleanupFunc = tbl._iterCleanupFunc
		tbl._iterCleanupFunc = nil
		if cleanupFunc then
			cleanupFunc(tbl)
		end
		return
	end
	if tbl._iterHelperFunc then
		local result = TSMAPI_FOUR.Util.AcquireTempTable(tbl._iterHelperFunc(tbl._iterIndex, tbl[tbl._iterIndex], tbl._iterArg))
		if #result == 0 then
			TSMAPI_FOUR.Util.ReleaseTempTable(result)
			return private.TableIterator(tbl)
		end
		return TSMAPI_FOUR.Util.UnpackAndReleaseTempTable(result)
	else
		return tbl._iterIndex, tbl[tbl._iterIndex]
	end
end

function private.TempTableReleaseHelper(tbl, ...)
	assert(private.tempTableState[tbl])
	wipe(tbl)
	tinsert(private.freeTempTables, tbl)
	private.tempTableState[tbl] = nil
	return ...
end

function private.BinarySearchHelper(tbl, value, key, searchType)
	-- binary search for index
	local low, mid, high = 1, 0, #tbl
	while low <= high do
		mid = floor((low + high) / 2)
		local rowValue = key and tbl[mid][key] or tbl[mid]
		if rowValue == value then
			if searchType == "FIRST" then
				if mid == 1 or (key and tbl[mid-1][key] or tbl[mid]) ~= value then
					-- we've found the row we want
					return true, mid
				else
					-- we're too high
					high = mid - 1
				end
			elseif searchType == "LAST" then
				if mid == high or (key and tbl[mid+1][key] or tbl[mid]) ~= value then
					-- we've found the row we want
					return true, mid
				else
					-- we're too low
					low = mid + 1
				end
			elseif searchType == "ANY" then
				return true, mid
			else
				error("Invalid searchType: "..tostring(searchType))
			end
		elseif rowValue < value then
			-- we're too low
			low = mid + 1
		else
			-- we're too high
			high = mid - 1
		end
	end
	-- didn't find it but return where it should be inserted
	return false, low
end
