Quantcast
local minItem, maxItem, chunkSize, minSuffix, maxSuffix, minGem, maxGem = 1, 99999, 20000, -500, 3000, 1, 6000
local CONSECUTIVE_INVALID_TO_IGNORE = 5
local VALID_DELAY = 0.025
local INVALID_DELAY = 10
local NUM_THREADS = 4

local patterns = {
	"^Design:",
	"^Formula:",
	"^Manual:",
	"^Pattern:",
	"^Plans:",
	"^Recipe:",
	"^Schematic:",
	"^Technique:",
}

local skippedLines = {
	"^Already known$",
	"^Prospectable$",
	"^Disenchanting requires Enchanting %(%d+%)$",
	"^Cannot be disenchanted",
	"^Collected %(%d/%d%)$",
}

local function scanItemLink(link, tooltip, threadNumber)
	local textL, textR
	local tooltipName = tooltip:GetName()

	-- Populate hidden tooltip
	tooltip:ClearLines()
	tooltip:SetHyperlink(link)

	local startTime = GetTime()
	local isPattern, hasReq = false, false
	while true do
		local numLines = tooltip:NumLines()
		local done = numLines > 1
		if done then
			if not isPattern then
				local text = _G[tooltipName .. "TextLeft1"]:GetText()
				for _, pattern in ipairs(patterns) do
					if text:find(pattern) then
						isPattern = true
						break
					end
				end
			end
			-- We can skip the first line because it will only be RETRIEVING_ITEM_INFO if we only have one line
			for i = 2, numLines do
				local text = _G[tooltipName .. "TextLeft" .. i]:GetText()
				if text:find(RETRIEVING_ITEM_INFO) then
					done = false
					break
				elseif text:find("%(%d/[01]%)$") then
					done = false
					break
				elseif isPattern and not hasReq then
					hasReq = text:find("^\nRequires")
				end
			end
			if isPattern and not hasReq then
				done = false
			end
			if done then
				break
			end
		end
		coroutine.yield()
		if GetTime() - startTime >= INVALID_DELAY then
			-- If we got anything useful, keep it
			if numLines > 1 then
				break
			end
			return false
		end
	end

	if IS_item_info[link] and #(IS_item_info[link]) >= tooltip:NumLines() then
		return true
	end
	-- Avoids recording blank random suffixes
	if link:find("^item:11993:") and not link:find("^item:11993:0:0:0:0:0:0:") then
		if _G[tooltipName .. "TextLeft1"]:GetText():find("^Clay Ring ?$") and tooltip:NumLines() == 5 then
			return false
		end
	end

	IS_item_info[link] = {
		itemInfo = {GetItemInfo(link)},
		itemSpell = {GetItemSpell(link)},
		statTable = GetItemStats(link),
		itemUniqueness = {GetItemUniqueness(link)},
		consumableItem = IsConsumableItem(link),
		equippable = IsEquippableItem(link),
		helpful = IsHelpfulItem(link),
		harmful = IsHarmfulItem(link),
	}
	local numSkipped = 0
	for i = 1, tooltip:NumLines() do
		textL = _G[tooltipName .. "TextLeft" .. i]:GetText()
		textR = _G[tooltipName .. "TextRight" .. i]:GetText()
		local skip = false
		for _, pattern in ipairs(skippedLines) do
			if textL:find(pattern) then
				skip = true
				numSkipped = numSkipped + 1
				break
			end
		end
		if not skip and (i ~= tooltip:NumLines() or (textL ~= " " and textL ~= "" and textR ~= " " and textR ~= "")) then
			IS_item_info[link][i - numSkipped] = {
				left = textL,
				right = textR,
			}
		end
	end

	return true
end

local function scanGem(gemId)
	local results = {GetItemGem("item:25:0:" .. gemId .. ":0:0:0:0:0:0:0", 1)}
	for _ in pairs(results) do
		IS_gem_info[gemId] = results
		return true
	end
	return false
end

local frame = CreateFrame("Frame")
local nextSlot = 1
local function OnUpdate(self)
	local status, err
	if self.itemco then
		if nextSlot > #(self.itemco) then
			nextSlot = 1
		end
		if self.itemco[nextSlot] then
			status, err = pcall(self.itemco[nextSlot])
			if status == false then
				print("ItemScanner: item scan error, aborting thread " .. nextSlot .. ".")
				table.remove(self.itemco, nextSlot)
				if #(self.itemco) == 0 then
					IS_status.items.scan_active = false
					self.itemco = nil
				end
				error(err)
				return
			elseif err == true then
				table.remove(self.itemco, nextSlot)
				if #(self.itemco) == 0 then
					self.itemco = nil
				end
				return
			end
			nextSlot = nextSlot + 1
		else
			self.itemco = nil
		end
	elseif self.suffco then
		status, err = pcall(self.suffco)
		if status == false then
			print("ItemScanner: suffix scan error, aborting.")
			self.suffco = nil
			IS_status.suffixes.scan_active = false
			error(err)
			return
		elseif err == true then
			self.suffco = nil
		end
	elseif self.gemco then
		status, err = pcall(self.gemco)
		if status == false then
			print("ItemScanner: gem scan error, aborting.")
			self.gemco = nil
			IS_status.gems.scan_active = false
			error(err)
			return
		elseif err == true then
			self.gemco = nil
		end
	else
		frame:SetScript("OnUpdate", nil)
	end
end

local startTime, roundStartTime, numProcessed, roundNumProcessed, valid, roundValid
local function genericCoroutine(start, min, max, name, linkTemplate, delay, statusVars, tooltip, threadNumber)
	if not threadNumber or threadNumber == 1 then
		statusVars.scan_active = true
		statusVars.finished = false
		coroutine.yield()
		startTime = GetTime()
		roundStartTime = startTime
		statusVars.last_scanned = start - 1
		numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0
		print(string.format("ItemScanner: starting scan at %s %d", name, start))
	else
		coroutine.yield()
	end

	local function scan(i, scanningAll)
		statusVars.last_scanned = i
		local itemStartTime = GetTime()
		if scanItemLink(string.format(linkTemplate, i), tooltip, threadNumber) then
			if scanningAll then
				if statusVars.invalid[i] ~= false then
					valid, roundValid = valid + 1, roundValid + 1
				end
			else
				if statusVars.scan_active then
					statusVars.last_valid = i
				end
				valid, roundValid = valid + 1, roundValid + 1
			end
			-- Reset invalid count
			statusVars.invalid[i] = false
		else
			-- start with 3 if no previous value
			statusVars.invalid[i] = (statusVars.invalid[i] or 2) + 1
		end
		numProcessed = numProcessed + 1
		roundNumProcessed = roundNumProcessed + 1
		while GetTime() - itemStartTime < delay do
			coroutine.yield()
		end

		local currentTime = GetTime()
		if currentTime - roundStartTime >= 120 then
			local elapsedTime = currentTime - startTime
			print(string.format("ItemScanner: at %s %d, %d in %.1f sec. (%.2f/min.) (%d valid), %d this round (%d valid)", name, i, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid, roundNumProcessed, roundValid))
			roundStartTime = roundStartTime + 120
			roundNumProcessed, roundValid = 0, 0
		end
	end

	while statusVars.scan_active and not statusVars.finished do
		local i = statusVars.last_scanned
		if i == nil then
			break
		end
		repeat
			i = i + 1
			if i > max then
				statusVars.finished = true
				break
			end
		until not statusVars.invalid[i] or (type(statusVars.invalid[i]) == "number" and statusVars.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE)
		if statusVars.finished then
			break
		end
		scan(i, false)
	end

	while statusVars.scan_active do
		local i = (statusVars.last_invalid or min - 1)
		repeat
			i = i + 1
			if i > max then
				i = min
			end
		until statusVars.invalid[i] ~= true
		statusVars.last_invalid = i
		scan(i, true)
	end

	if not threadNumber or threadNumber == 1 then
		local elapsedTime = GetTime() - startTime
		if elapsedTime == 0 then
			elapsedTime = 0.001
		end
		print(string.format("ItemScanner: scan stopped at %s %d, %d in %.1f sec. (%.2f/min.) (%d valid)", name, statusVars.last_scanned, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid))
	end

	return true
end

local function gemParseCoroutine(start)
	IS_status.gems.scan_active = true
	IS_status.gems.finished = false
	coroutine.yield()
	local startTime = GetTime()
	local roundStartTime = startTime
	local numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0
	print(string.format("ItemScanner: starting scan at gem %d", start))

	local function scan(i, scanningAll)
		IS_status.gems.last_scanned = i
		local itemStartTime = GetTime()
		if scanGem(i) then
			if scanningAll then
				if IS_status.gems.invalid[i] ~= false then
					valid, roundValid = valid + 1, roundValid + 1
				end
			else
				IS_status.gems.last_valid = i
				valid, roundValid = valid + 1, roundValid + 1
			end
			-- Reset invalid count
			IS_status.gems.invalid[i] = false
		else
			-- start with 3 if no previous value
			IS_status.gems.invalid[i] = (IS_status.gems.invalid[i] or 2) + 1
		end
		while GetTime() - itemStartTime < VALID_DELAY do
			coroutine.yield()
		end
		numProcessed = numProcessed + 1
		roundNumProcessed = roundNumProcessed + 1

		local currentTime = GetTime()
		if currentTime - roundStartTime >= 120 then
			local elapsedTime = currentTime - startTime
			print(string.format("ItemScanner: at gem %d, %d in %.1f sec. (%.2f/min.) (%d valid), %d this round (%d valid)", i, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid, roundNumProcessed, roundValid))
			roundStartTime = roundStartTime + 120
			roundNumProcessed, roundValid = 0, 0
		end
	end

	for i = start, maxGem do
		if not IS_status.gems.invalid[i] or (type(IS_status.gems.invalid[i]) == "number" and IS_status.gems.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE) then
			if not IS_status.gems.scan_active then
				break
			end
			scan(i, false)
		end
	end
	IS_status.gems.finished = true
	while IS_status.gems.scan_active do
		local i = (IS_status.gems.last_invalid or minGem - 1)
		repeat
			i = i + 1
			if i > maxGem then
				i = minGem
			end
		until IS_status.gems.invalid[i] ~= true
		IS_status.gems.last_invalid = i
		scan(i, true)
	end

	local elapsedTime = GetTime() - startTime
	if elapsedTime == 0 then
		elapsedTime = 0.001
	end
	print(string.format("ItemScanner: scan stopped at gem %d, %d in %.1f sec. (%.2f/min.) (%d valid)", IS_status.gems.last_scanned, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid))

	return true
end

local function commandHandler(msg)
	if string.match(msg, "^all") then
		commandHandler(string.gsub(msg, "all", "items"))
		commandHandler(string.gsub(msg, "all", "suffixes"))
		commandHandler(string.gsub(msg, "all", "gems"))
	elseif string.match(msg, "^debug") then
		if IS_status.items.scan_active then
			if IS_status.items.finished then
				print("Scanning all items with " .. #(frame.itemco) .. " threads")
			else
				print("Scanning known valid items with " .. #(frame.itemco) .. " threads")
			end
			print("Last item: " .. tostring(IS_status.items.last_scanned or "none"))
		elseif IS_status.suffixes.scan_active then
			if IS_status.suffixes.finished then
				print("Scanning all random suffixes")
			else
				print("Scanning known valid random suffixes")
			end
			print("Last suffix: " .. tostring(IS_status.suffixes.last_scanned or "none"))
		elseif IS_status.gems.scan_active then
			if IS_status.gems.finished then
				print("Scanning all gems")
			else
				print("Scanning known valid gems")
			end
			print("Last gem: " .. tostring(IS_status.gems.last_scanned or "none"))
		else
			print("Scanning not active")
			return
		end
		local currentTime = GetTime()
		print("roundTime = " .. tostring(currentTime - roundStartTime))
		print("elapsedTime = " .. tostring(currentTime - startTime))
	elseif string.match(msg, "^gems") then
		local start

		if msg == "gems" or msg == "gems start" then
			start = IS_status.gems.last_valid or IS_status.gems.last_scanned or minGem
		elseif msg == "gems clear" then
			IS_status.gems.last_scanned = nil
			IS_status.gems.last_valid = nil
			IS_gem_info = {}
			return
		elseif msg == "gems reset" then
			commandHandler("gems clear")
			commandHandler("gems restart")
			return
		elseif msg == "gems restart" then
			start = minGem
			IS_status.gems.last_scanned = nil
			IS_status.gems.last_valid = nil
		elseif msg == "gems stop" then
			IS_status.gems.scan_active = false
			return
		else
			print("Usage: /is gems <arg> (or /itemscanner gems <arg>")
			print("  clear       clears gem data but does not stop a running scan")
			print("  reset       clears gem data and starts over")
			print("  restart     restarts the current scan but leaves existing data intact")
			print("  start        (default) starts where you last left off")
			print("  stop        stops scanning but leaves existing data intact")
			return
		end
		frame.gemco = coroutine.wrap(gemParseCoroutine)
		frame.gemco(start)
		frame:SetScript("OnUpdate", OnUpdate)
	elseif string.match(msg, "^items") then
		local start

		if msg == "items" or msg == "items start" then
			start = IS_status.items.last_valid or IS_status.items.last_scanned or minItem
		elseif msg == "items clear" then
			IS_item_info = {}
			IS_status.items.last_scanned = nil
			IS_status.items.last_valid = nil
			return
		elseif msg == "items next-chunk" then
			start = IS_status.items.last_valid or IS_status.items.last_scanned or minItem
			start = chunkSize * math.ceil(start / chunkSize)
			if start > maxItem then
				commandHandler("items stop")
				return
			end
			commandHandler("items clear")
		elseif msg == "items reset" then
			commandHandler("items clear")
			commandHandler("items restart")
			return
		elseif msg == "items restart" then
			start = minItem
			IS_status.items.last_scanned = nil
			IS_status.items.last_valid = nil
		elseif msg == "items restart-chunk" then
			start = (IS_status.items.last_valid or IS_status.items.last_scanned or minItem)
			start = math.max(chunkSize * math.floor(start / chunkSize), minItem)
		elseif msg == "items stop" then
			IS_status.items.scan_active = false
			return
		else
			print("Usage: /is items <arg> (or /itemscanner items <arg>")
			print("  clear       clears item and suffix data but does not stop a running scan")
			print("  reset       clears item and suffix data and starts over")
			print("  restart     restarts the current scan but leaves existing data intact")
			print("  start        (default) starts where you last left off")
			print("  stop        stops scanning but leaves existing data intact")
			return
		end

		local chunkStart, chunkEnd = chunkSize * math.floor(start / chunkSize), chunkSize * math.floor((start + chunkSize) / chunkSize) - 1
		if chunkEnd > maxItem then
			chunkEnd = maxItem
		end

		frame.itemco = {}
		for i = 1, NUM_THREADS do
			local tooltip = _G["ItemScannerHiddenTooltip" .. i]
			if not tooltip then
				tooltip = CreateFrame("GameTooltip", "ItemScannerHiddenTooltip" .. i, nil, "ItemScannerHiddenTooltip")
			end
			table.insert(frame.itemco, coroutine.wrap(genericCoroutine))
			frame.itemco[i](start + i - 1, chunkStart, chunkEnd, "item", "item:%d:0:0:0:0:0:0:0:85", VALID_DELAY * NUM_THREADS, IS_status.items[chunkStart .. "-" .. chunkEnd], tooltip, i)
		end
		frame:SetScript("OnUpdate", OnUpdate)
	elseif string.match(msg, "^suffixes") then
		local start

		if msg == "suffixes" or msg == "suffixes start" then
			start = IS_status.suffixes.last_valid or IS_status.suffixes.last_scanned or minSuffix
		elseif msg == "suffixes clear" then
			IS_status.suffixes.last_scanned = nil
			IS_status.suffixes.last_valid = nil
			return
		elseif msg == "suffixes reset" then
			commandHandler("suffixes restart")
			return
		elseif msg == "suffixes restart" then
			start = minSuffix
			IS_status.suffixes.last_scanned = nil
			IS_status.suffixes.last_valid = nil
		elseif msg == "suffixes stop" then
			IS_status.suffixes.scan_active = false
			return
		else
			print("Usage: /is suffixes <arg> (or /itemscanner suffixes <arg>")
			print("  clear       clears suffix progress but does not stop a running scan")
			print("  reset       clears suffix progress and starts over")
			print("  restart     restarts the current scan but leaves existing data intact")
			print("  start        (default) starts where you last left off")
			print("  stop        stops scanning but leaves existing data intact")
			return
		end

		local tooltip = _G["ItemScannerHiddenTooltip1"]
		if not tooltip then
			tooltip = CreateFrame("GameTooltip", "ItemScannerHiddenTooltip1", nil, "ItemScannerHiddenTooltip")
		end

		frame.suffco = coroutine.wrap(genericCoroutine)
		frame.suffco(start, minSuffix, maxSuffix, "suffix", "item:11993:0:0:0:0:0:%d:100:85", VALID_DELAY, IS_status.suffixes, tooltip)
		frame:SetScript("OnUpdate", OnUpdate)
	else
		print("Usage: /is <arg> (or /itemscanner <arg>")
		print("  all          scans all scannable objects")
		print("  debug       prints scanning status")
		print("  gems         scans all gem ids")
		print("  items       scans all items")
		print("  suffixes   scans all random suffixes")
	end
end

if not IS_item_info then
	IS_item_info = {}
end

if not IS_gem_info then
	IS_gem_info = {}
end

if not IS_status then
	IS_status = {}
end

if not IS_status.items then
	IS_status.items = {}
end

if not IS_status.items.invalid then
	IS_status.items.invalid = {}
end

if not IS_status.items.last_invalid then
	IS_status.items.last_invalid = {}
end

if not IS_status.suffixes then
	IS_status.suffixes = {}
end

if not IS_status.suffixes.invalid then
	IS_status.suffixes.invalid = {}
end

if not IS_status.gems then
	IS_status.gems = {}
end

if not IS_status.gems.invalid then
	IS_status.gems.invalid = {}
end

SLASH_ITEMSCANNER1="/is"
SLASH_ITEMSCANNER2="/itemscanner"
SlashCmdList["ITEMSCANNER"] = commandHandler

-- Autoresumes on login
local function resume()
	if not IS_status.items then
		IS_status.items = {}
	end

	local metatable = {
		__index = IS_status.items,
		__newindex = function(tbl, key, val)
			if key == "last_invalid" then
				rawset(tbl, key, val)
			else
				IS_status.items[key] = val
			end
		end,
	}
	local t = IS_status.items
	for i = 0, maxItem, chunkSize do
		local chunkStart, chunkEnd = chunkSize * math.floor(i / chunkSize), chunkSize * math.floor((i + chunkSize) / chunkSize) - 1
		local chunkName = chunkStart .. "-" .. chunkEnd
		t[chunkName] = setmetatable(t[chunkName] or { last_invalid = (IS_status.items.last_invalid or {})[chunkName] }, metatable)
	end

	IS_status.items.last_invalid = nil

	if IS_status.items.scan_active then
		if IS_status.items.finished then
			commandHandler("items next-chunk")
		else
			commandHandler("items")
		end
	end
	if IS_status.suffixes.scan_active then
		if IS_status.suffixes.finished then
			IS_status.suffixes.scan_active = false
			IS_status.suffixes.finished = false
		else
			commandHandler("suffixes")
		end
	end
	if IS_status.gems.scan_active then
		if IS_status.gems.finished then
			IS_status.gems.scan_active = false
			IS_status.gems.finished = false
		else
			commandHandler("gems")
		end
	end
	if not frame.itemco and not frame.suffco and not frame.gemco then
		print("Nothing seems to be active, restarting all")
		commandHandler("all reset")
	end
end

frame:SetScript("OnUpdate", resume)