Quantcast
--[[
	Auctioneer - ScanData
	Version: 5.9.4961 (WhackyWallaby)
	Revision: $Id: ScanData.lua 4840 2010-08-04 21:44:00Z Nechckn $
	URL: http://auctioneeraddon.com/

	This is an addon for World of Warcraft that adds statistical history to the auction data that is collected
	when the auction is scanned, so that you can easily determine what price
	you will be able to sell an item for at auction or at a vendor whenever you
	mouse-over an item in the game

	License:
		This program 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 2
		of the License, or (at your option) any later version.

		This program 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 this program(see GPL.txt); if not, write to the Free Software
		Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Note:
		This AddOn's source code is specifically designed to work with
		World of Warcraft's interpreted AddOn system.
		You have an implicit license to use this AddOn with these facilities
		since that is its designated purpose as per:
		http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat
--]]
if not AucAdvanced then return end

local libType, libName = "Util", "ScanData"
local lib,parent,private = AucAdvanced.NewModule(libType, libName)
if not lib then return end

local DATABASE_VERSION = 1.3
local INTERFACE_VERSION = "A" -- must match CoreScan's SCANDATA_VERSION

local aucPrint,decode,_,_,replicate,empty,get,set,default,debugPrint,fill = AucAdvanced.GetModuleLocals()

private.distributionCache = {}
private.worthCache = {}

local Const = AucAdvanced.Const
local QueryImage = AucAdvanced.API.QueryImage
local PriceCalcLevel = AucAdvanced.Modules.Util.PriceLevel and AucAdvanced.Modules.Util.PriceLevel.CalcLevel

local type = type
local pairs = pairs
local format = format
local floor = floor
local tostring, strjoin = tostring, strjoin
local tinsert, tremove, tconcat, unpack, wipe = tinsert, tremove, table.concat, unpack, wipe

local colorDist = {
	exact = { red=0, orange=0, yellow=0, green=0, blue=0 },
	suffix = { red=0, orange=0, yellow=0, green=0, blue=0 },
	base = { red=0, orange=0, yellow=0, green=0, blue=0 },
    stack = { },
	all = { red=0, orange=0, yellow=0, green=0, blue=0 },
}

--[[ MODULE FUNCTIONS ]]--

lib.Processors = {}
--[[
function lib.Processors.tooltip(callbackType, ...)
	private.ProcessTooltip(...)
end
--]]
function lib.Processors.scanstats()
	wipe(private.distributionCache)
	wipe(private.worthCache)
end
function lib.Processors.load(callbackType, addon)
	if addon == "auc-scandata" then
		if private.OnLoad then
			private.OnLoad()
		end
	end
end


local tmp = {}
function lib.Colored(doIt, counts, alt, shorten)
	local n=0
	if (counts.blue > 0) then
		n=n+1
		if shorten and counts.blue>=1000 then
			tmp[n] = format("|cff3399ff%dk|r", floor(counts.blue/1000+0.5))
		else
			tmp[n] = format("|cff3399ff%d|r", counts.blue)
		end
	end
	if (counts.green > 0) then
		n=n+1
		if shorten and counts.green>=1000 then
			tmp[n] = format("|cff33ff44%dk|r", floor(counts.green/1000+0.5))
		else
			tmp[n] = format("|cff33ff44%d|r", counts.green)
		end
	end
	if (counts.yellow > 0) then
		n=n+1
		if shorten and counts.yellow>=1000 then
			tmp[n] = format("|cffffff00%dk|r", floor(counts.yellow/1000+0.5))
		else
			tmp[n] = format("|cffffff00%d|r", counts.yellow)
		end
	end
	if (counts.orange > 0) then
		n=n+1
		if shorten and counts.orange>=1000 then
			tmp[n] = format("|cffff9900%dk|r", floor(counts.orange/1000+0.5))
		else
			tmp[n] = format("|cffff9900%d|r", counts.orange)
		end
	end
	if (counts.red > 0) then
		n=n+1
		if shorten and counts.red>=1000 then
			tmp[n] = format("|cffff0000%dk|r", floor(counts.red/1000+0.5))
		else
			tmp[n] = format("|cffff0000%d|r", counts.red)
		end
	end
	local text = tconcat(tmp, " / ", 1, n)
	if alt then
		if text and text ~= "" then
			text = "( "..text.." )"
		else
			text = alt
		end
	end
	return text
end

local query3 = {} -- 3 fields itemId, suffix & factor
function lib.GetImageCounts(hyperlink, maxPrice, items, serverKey)
	if type(hyperlink) == "number" then
		query3.itemId = hyperlink
		query3.suffix = 0
		query3.factor = 0
	else
		local iType, iID, iSuffix, iFactor = decode(hyperlink)
		if iType == "item" then
			query3.itemId = iID
			query3.suffix = iSuffix
			query3.factor = iFactor
		else
			return
		end
	end
	local image = QueryImage(query3, serverKey)

	local totalBid, totalBuy = 0, 0

	for i=1, #image do
		local item = image[i]
		local count = item[Const.COUNT]
		local bid = item[Const.PRICE]
		local buy = item[Const.BUYOUT]

		local matched = false
		if maxPrice then
			if buy > 0 and buy <= maxPrice then
				totalBuy = totalBuy + count
				matched = true
			elseif bid <= maxPrice then
				totalBid = totalBid + count
				matched = true
			end
		else
			if buy > 0 then
				totalBuy = totalBuy + count
				matched = true
			else
				totalBid = totalBid + count
				matched = true
			end
		end
		if items and matched then
			tinsert(items, item)
		end
	end

	return totalBuy, totalBid
end

local query1 = {} -- only 1 field itemId
function lib.GetDistribution(hyperlink)
	local iType, iID, iSuffix, iFactor = decode(hyperlink)
	if iType ~= "item" then return end
	local sig = strjoin(":", iID, iSuffix, iFactor)
	if private.distributionCache[sig] then return unpack(private.distributionCache[sig]) end

	local exact, suffix, base, myColors = 0,0,0,{}
	for k,v in pairs(colorDist) do
		myColors[k] = {}
		for c,n in pairs(v) do
			myColors[k][c] = 0
		end
	end

	query1.itemId = iID
	local image = QueryImage(query1)
	local sigTemplate = iID..":%d:%d"
	for i=1, #image do
		local item = image[i]
		local vSuffix = item[Const.SUFFIX]
		local vFactor = item[Const.FACTOR]
		local vCount = item[Const.COUNT]

		local vColor
		if (PriceCalcLevel) then
			local _
			local vLink = item[Const.LINK]
			local vBid = item[Const.PRICE]
			local vBuy = item[Const.BUYOUT]
			local vSig = sigTemplate:format(vSuffix, vFactor)
			_,_,_,_,_, vColor, private.worthCache[vSig] = PriceCalcLevel(vLink, vCount, vBid, vBuy, private.worthCache[vSig])
		end

		if (vSuffix == iSuffix) then
			if (vFactor == iFactor) then
				exact = exact + vCount
				if (vColor) then
					myColors.exact[vColor] = myColors.exact[vColor] + vCount
				end
			else
				suffix = suffix + vCount
				if (vColor) then
					myColors.suffix[vColor] = myColors.suffix[vColor] + vCount
				end
			end
		else
			base = base + vCount
			if (vColor) then
				myColors.base[vColor] = myColors.base[vColor] + vCount
			end
		end
		if (vColor) then
			myColors.all[vColor] = myColors.all[vColor] + vCount
			-- Set up colours per stack size as well.
			if not myColors.stack[vCount] then myColors.stack[vCount] =  { red=0, orange=0, yellow=0, green=0, blue=0 } end
			myColors.stack[vCount][vColor] = myColors.stack[vCount][vColor] + vCount
		end
	end

	private.distributionCache[sig] = {exact, suffix, base, myColors}
	return exact, suffix, base, myColors
end

function lib.Processors.tooltip(callbackType, tooltip, name, hyperlink, quality, quantity, cost)
	if not get("scandata.tooltip.display") then return  end

	tooltip:SetColor(0.3, 0.9, 0.8)

	local doColor = true
	local exact, suffix, base, dist = lib.GetDistribution(hyperlink)

	if base+suffix+exact <= 0 then
		tooltip:AddLine("No matches in image.")
	else
		if get("scandata.tooltip.modifier") and IsShiftKeyDown() then
			tooltip:AddLine("Items in image:")
			if (exact > 0) then
				tooltip:AddLine("  |cffddeeff"..exact.."|r exact "..lib.Colored(doColor, dist.exact, "matches"))
			end
			if (suffix > 0) then
				tooltip:AddLine("  |cffddeeff"..suffix.."|r suffix "..lib.Colored(doColor, dist.suffix, "matches"))
			end
			if (base > 0) then
				tooltip:AddLine("  |cffddeeff"..base.."|r base "..lib.Colored(doColor, dist.base, "matches"))
			end
			if (dist.stack) then
				for stackSize, stackColor in pairs(dist.stack) do
					tooltip:AddLine("  Stacks of "..stackSize.."  "..lib.Colored(doColor, stackColor, "in image"))
				end
			end
		else
			if (suffix+base > 0) then
				tooltip:AddLine("|cffddeeff"..exact.." +"..(suffix+base).."|r matches "..lib.Colored(doColor, dist.all, "in image"))
			else
				tooltip:AddLine("|cffddeeff"..exact.."|r matches "..lib.Colored(doColor, dist.exact, "in image"))
			end
		end
	end
end

--[[ DATABASE FUNCTIONS ]]--
function lib.GetAddOnInfo()
	return private.isLoaded, INTERFACE_VERSION
end

private.dataCache = {}
function lib.GetScanData(serverKey)
	local cache = private.dataCache[serverKey]
	if cache then return cache end

	local realm, faction = AucAdvanced.SplitServerKey(serverKey)
	if not realm then
		debugPrint("AucScanData: invalid serverKey passed to GetScanData: "..tostring(serverKey), "ScanData", "Invalid serverKey", "Error")
		return
	end

	local realmdata = AucScanData.scans[realm]
	if not realmdata then return end -- not in database

	local livedata = serverKey == Const.ServerKeyHome or serverKey == Const.ServerKeyNeutral -- 'live' data can be changed by scanning
	local scandata = realmdata[faction]
	if scandata then
		if not livedata then
			-- Copy scandata info into a clone table and call Unpack on that
			-- The original does not get unpacked, so does not need repacking
			local clone = {
				image = scandata.image, -- will be overwritten by unpack
				ropes = scandata.ropes, -- will be deleted by unpack
				scanstats = replicate(scandata.scanstats)
			}
			scandata = clone
		end
		if type(scandata.scanstats) ~= "table" then
			scandata.scanstats = {ImageUpdated = scandata.time or time()}
		end
		if not scandata.image then
			scandata.image = {}
			scandata.scanstats.ImageUpdated = time()
		end
		-- delete obsolete entries
		scandata.nextID = nil
		scandata.time = nil
		scandata.LastFullScan = nil
		scandata.LastGetAll = nil
	else
		scandata = {image = {}, scanstats = {ImageUpdated = time()} }
		if livedata then
			realmdata[faction] = scandata
		end
	end

	private.Unpack(scandata)
	private.dataCache[serverKey] = scandata
	return scandata
end

function lib.ClearScanData(command)
	local report, serverKey
	local keyword, extra = "faction", "" -- default

	if type(command) == "string" then
		local _, ind, key = strfind(command, "(%S+)")
		if key then
			key = AucAdvanced.API.IsKeyword(key)
			if key then
				keyword = key -- recognised keyword
				extra = strtrim(strsub(command, ind+1))
			else
				extra = strtrim(command) -- processor will try to resolve whole command
			end
		end
	elseif command then -- only valid types are string or nil
		error("Unrecognised parameter type to ClearScanData: "..type(command)..":"..tostring(command))
	end

	if keyword == "ALL" then
		if extra == "" then
			wipe(AucScanData.scans)
			report = "All realms"
		end
	else
		if keyword == "server" then
			if extra == "" then extra = Const.PlayerRealm end
		elseif keyword == "faction" then
			if extra == "" then extra = AucAdvanced.GetFaction() end
		end
		if AucScanData.scans[extra] then -- it's a realm name in our database
			AucScanData.scans[extra] = nil
			report = extra
		else
			local fac = AucAdvanced.IsFaction(extra)
			if fac then -- convert faction group to serverKey
				extra = Const.PlayerRealm.."-"..fac
			end
			local realm, faction, text = AucAdvanced.SplitServerKey(extra)
			if faction and AucScanData.scans[realm] then
				AucScanData.scans[realm][faction] = nil
				report = text
				serverKey = extra
			end
		end
	end

	wipe(private.dataCache)
	-- Our functions expect home faction to exist - create a new one if it has just been deleted
	if not AucScanData.scans[Const.PlayerRealm] then AucScanData.scans[Const.PlayerRealm] = {} end
	lib.GetScanData(Const.ServerKeyHome) -- force create (if needed) and put back in cache
	if report then
		aucPrint("Auctioneer: ScanData cleared for {{"..report.."}}.")
		local clearstats = {
			source = "clear",
			clearType = "scandata",
			clearRequest = command,
			clearReport = report,
			serverKey = serverKey,
		}
		AucAdvanced.SendProcessorMessage("scanstats", clearstats) -- notify modules to flush caches
	else
		aucPrint("Auctioneer: Unable to clear ScanData for {{"..command.."}}")
	end
end

function private.Unpack(scandata)
	if type(scandata.image) == "string" then
		if scandata.image ~= "rope" then
			scandata.ropes = { scandata.image }
		end

		scandata.image = {}
		for pos, rope in ipairs(scandata.ropes) do
			local loader, err = loadstring(rope)
			if loader then
				local test, items = pcall(loader)
				if test then
					for pos, item in ipairs(items) do
						tinsert(scandata.image, item)
					end
					err = nil
				else
					err = items
				end
			end
			if err then
				aucPrint("Error loading scan image: {{", err, "}}")
				-- if we get an error from any rope, assume the whole packed image is corrupt
				scandata.image = {}
				scandata.scanstats.ImageUpdated = time()
				break
			end
		end
	elseif type(scandata.image) ~= "table" then
		scandata.image = {}
		scandata.scanstats.ImageUpdated = time()
	end
	scandata.ropes = nil
end

function private.OnLoad()
	private.OnLoad = nil
	private.UpgradeDB()
	if not AucScanData.scans[Const.PlayerRealm] then AucScanData.scans[Const.PlayerRealm] = {} end
	lib.GetScanData(Const.ServerKeyHome) -- force unpack of home faction data
	private.isLoaded = true
end

function private.UpgradeDB()
	private.UpgradeDB = nil

	if AucScanData then
		if type(AucScanData.scans) ~= "table" then AucScanData.scans = {} end
		if AucScanData.Version == DATABASE_VERSION then return end

		if AucScanData.Version == "1.2" then
			-- version "1.2" to version 1.3
			-- Database structure is virtually the same, we won't try to update the whole database here
			-- Each time GetScanData is called it will check/update that record as needed
			aucPrint("Auc-ScanData is upgrading database version 1.2 to 1.3")
			AucScanData.Version = DATABASE_VERSION
			return
		end

		-- Unknown version - wipe and start from fresh
		aucPrint("Auc-ScanData database error: unknown version, resetting database")
		wipe(AucScanData.scans)
		AucScanData.Version = DATABASE_VERSION
	else
		AucScanData = { Version = DATABASE_VERSION, scans = {} }
	end
end

function lib.OnUnload()
	local StringRope = LibStub:GetLibrary("StringRope")
	local rope = StringRope:New(-1)

	local maxLen = 2^22

	if not (AucScanData and AucScanData.scans) then return end

	-- Convert all image data to loadstring strings
	for server, sData in pairs(AucScanData.scans) do
		for faction, fData in pairs(sData) do
			if fData.image and type(fData.image) == "table" then
				fData.ropes = {}
				rope:Add("return {")
				local fCount = #fData.image
				for i = 1, fCount do
					local item = fData.image[i]
					if item and type(item) == "table" then
						rope:Add("{")
						local pos = 1
						while item[pos] or item[pos+1] or item[pos+2] or item[pos+3] do
							local v = item[pos]
							if v == nil then
								rope:Add("nil,")
							else
								local t = type(v)
								if t == "string" then
									rope:Add(("%q,"):format(v))
								elseif t == "number" then
									rope:Add(v..",")
								elseif t == "boolean" then
									rope:Add(tostring(v)..",")
								else
									rope:Add("nil--[["..t.."]],")
								end
							end
							pos = pos + 1
						end
						rope:Add("},")
					elseif item == nil then
						rope:Add("nil,")
					else
						rope:Add("nil--[["..type(item).."]],")
					end
					if rope.len and rope.len > maxLen then
						rope:Add("}");
						tinsert(fData.ropes, rope:Get())
						rope:Clear()
						rope:Add("return {")
					end
				end
				rope:Add("}")
				fData.image = "rope"
				tinsert(fData.ropes, rope:Get())
				rope:Clear()
			end
		end
	end
end

AucAdvanced.RegisterRevision("$URL: http://svn.norganna.org/auctioneer/branches/5.9/Auc-ScanData/ScanData.lua $", "$Rev: 4840 $")