Quantcast
local minItem, maxItem, chunkSize = 1, 99999, 20000
local CONSECUTIVE_INVALID_TO_IGNORE = 5
local VALID_DELAY = 0.025
local INVALID_DELAY = 5

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)
	local textL, textR

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

	local startTime = GetTime()
	local isPattern, hasReq = false, false
	while true do
		local numLines = ItemScannerHiddenTooltip:NumLines()
		local done = numLines > 1
		if done then
			if not isPattern then
				local text = _G["ItemScannerHiddenTooltipTextLeft1"]: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["ItemScannerHiddenTooltipTextLeft" .. 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]) >= ItemScannerHiddenTooltip:NumLines() then
		return true
	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, ItemScannerHiddenTooltip:NumLines() do
		textL = _G["ItemScannerHiddenTooltipTextLeft" .. i]:GetText()
		textR = _G["ItemScannerHiddenTooltipTextRight" .. 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 ~= ItemScannerHiddenTooltip: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 frame = CreateFrame("Frame")
local function OnUpdate(self)
	local status, err
	if self.itemco then
		status, err = pcall(self.itemco)
		if status == false then
			print("ItemScanner: item scan error, aborting.")
			self.itemco = nil
			IS_status.items.scan_active = false
			error(err)
			return
		elseif err == true then
			self.itemco = nil
		end
	else
		frame:SetScript("OnUpdate", nil)
	end
end

local function itemParseCoroutine(start)
	local chunkStart, chunkEnd = chunkSize * math.floor(start / chunkSize), chunkSize * math.floor((start + chunkSize) / chunkSize) - 1
	IS_status.items.scan_active = true
	coroutine.yield()
	local startTime = GetTime()
	local roundStartTime = startTime
	local numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0
	print(string.format("ItemScanner: starting scan at item %d", start))
	for i = start, math.min(chunkEnd, maxItem) do
		if not IS_status.items.invalid[i] or (type(IS_status.items.invalid[i]) == "number" and IS_status.items.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE) then
			IS_status.items.last_scanned = i
			local itemStartTime = GetTime()
			if scanItemLink("item:" .. i .. ":0:0:0:0:0:0:0:85") then
				IS_status.items.last_valid = i
				-- Reset invalid count
				IS_status.items.invalid[i] = false
				valid, roundValid = valid + 1, roundValid + 1
			else
			-- start with 3 if no previous value
				IS_status.items.invalid[i] = (IS_status.items.invalid[i] or 2) + 1
			end
			if not IS_status.items.scan_active then
				break
			end
			numProcessed = numProcessed + 1
			roundNumProcessed = roundNumProcessed + 1
			while GetTime() - itemStartTime < VALID_DELAY do
				coroutine.yield()
			end
			local currentTime = GetTime()
			if currentTime - roundStartTime >= 120 then
				local elapsedTime = currentTime - startTime
				print(string.format("ItemScanner: at item %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
	end

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

	return true
end

local function commandHandler(msg)
	if 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 data but does not stop a running scan")
			print("  reset       clears item 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.itemco = coroutine.wrap(itemParseCoroutine)
		frame.itemco(start)
		frame:SetScript("OnUpdate", OnUpdate)
	else
		print("Usage: /is <arg> (or /itemscanner <arg>")
		print("  items       scans all items")
	end
end

if not IS_item_info then
	IS_item_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

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

-- Autoresumes on login
local function resume()
	if IS_status.items.scan_active then
		if IS_status.items.finished then
			commandHandler("items next-chunk")
		else
			commandHandler("items")
		end
	end
	if not frame.itemco then
		print("Nothing seems to be active, restarting")
		commandHandler("items reset")
	end
end

frame:SetScript("OnUpdate", resume)