Quantcast
local minItem, maxItem, chunkSize = 1, 99999, 20000
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

	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 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
	else
		frame:SetScript("OnUpdate", nil)
	end
end

local startTime, roundStartTime, numProcessed, roundNumProcessed, valid, roundValid, chunkStart, chunkEnd
local function itemParseCoroutine(start, tooltip, threadNumber)
	if threadNumber == 1 then
		chunkStart, chunkEnd = chunkSize * math.floor(start / chunkSize), chunkSize * math.floor((start + chunkSize) / chunkSize) - 1
		IS_status.items.scan_active = true
		IS_status.items.finished = false
		coroutine.yield()
		startTime = GetTime()
		roundStartTime = startTime
		IS_status.items.last_scanned = start - 1
		numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0
		print(string.format("ItemScanner: starting scan at item %d", start))
	else
		coroutine.yield()
	end

	local function scan(i, scanningAll)
		IS_status.items.last_scanned = i
		local itemStartTime = GetTime()
		if scanItemLink("item:" .. i .. ":0:0:0:0:0:0:0:85", tooltip, threadNumber) then
			if scanningAll then
				if IS_status.items.invalid[i] ~= false then
					valid, roundValid = valid + 1, roundValid + 1
				end
			else
				if IS_status.items.scan_active then
					IS_status.items.last_valid = i
				end
				valid, roundValid = valid + 1, roundValid + 1
			end
			-- Reset invalid count
			IS_status.items.invalid[i] = false
		else
			-- start with 3 if no previous value
			IS_status.items.invalid[i] = (IS_status.items.invalid[i] or 2) + 1
		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

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

	local chunkName = chunkStart .. "-" .. chunkEnd
	while IS_status.items.scan_active do
		local i = (IS_status.items.last_invalid[chunkName] or chunkStart - 1)
		repeat
			i = i + 1
			if i > chunkEnd or i > maxItem then
				i = chunkStart
			end
		until IS_status.items.invalid[i] ~= true
		IS_status.items.last_invalid[chunkName] = i
		scan(i, true)
	end

	if threadNumber == 1 then
		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))
	end

	return true
end

local function commandHandler(msg)
	if 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"))
			local currentTime = GetTime()
			print("roundTime = " .. tostring(currentTime - roundStartTime))
			print("elapsedTime = " .. tostring(currentTime - startTime))
		else
			print("Scanning not active")
			return
		end
	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 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 = {}
		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(itemParseCoroutine))
			frame.itemco[i](start + i - 1, tooltip, i)
		end
		frame:SetScript("OnUpdate", OnUpdate)
	else
		print("Usage: /is <arg> (or /itemscanner <arg>")
		print("  debug       prints scanning status")
		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

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

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

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

	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)