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

-- This file contains code for scanning the auction house
local TSM = select(2, ...)
local private = {}
TSMAPI:RegisterForTracing(private, "TradeSkillMaster.AuctionQueryUtil_private")


local ITEM_CLASS_LOOKUP = {}
for i, class in ipairs({GetAuctionItemClasses()}) do
	ITEM_CLASS_LOOKUP[class] = {}
	ITEM_CLASS_LOOKUP[class].index = i
	for j, subclass in pairs({GetAuctionItemSubClasses(i)}) do
		ITEM_CLASS_LOOKUP[class][subclass] = j
	end
end

local function GetItemClasses(itemString)
	local class, subClass = select(6, TSMAPI:GetSafeItemInfo(itemString))
	if not class or not ITEM_CLASS_LOOKUP[class] then return end
	return ITEM_CLASS_LOOKUP[class].index, ITEM_CLASS_LOOKUP[class][subClass]
end

function TSMAPI:GetAuctionQueryInfo(itemString)
	local name, _, rarity, _, minLevel, class, subClass, _, equipLoc = TSMAPI:GetSafeItemInfo(itemString)
	local class, subClass = GetItemClasses(itemString)
	if not name then return end
	return {name=name, minLevel=minLevel, maxLevel=minLevel, invType=0, class=class, subClass=subClass, quality=rarity}
end

local function GetCommonQueryInfo(name, items)
	local queries = {}
	for _, itemString in ipairs(items) do
		local itemQuery = TSMAPI:GetAuctionQueryInfo(itemString)
		local existingQuery
		for _, query in ipairs(queries) do
			if query.class == itemQuery.class then
				existingQuery = query
				break
			end
		end
		if existingQuery then
			existingQuery.minLevel = min(existingQuery.minLevel, itemQuery.minLevel)
			existingQuery.maxLevel = max(existingQuery.maxLevel, itemQuery.maxLevel)
			existingQuery.quality = (existingQuery.quality == itemQuery.quality) and existingQuery.quality or nil
			if existingQuery.subClass ~= itemQuery.subClass then
				existingQuery.subClass = nil
			end
			tinsert(existingQuery.items, itemString)
		else
			itemQuery.name = name
			itemQuery.items = {itemString}
			tinsert(queries, itemQuery)
		end
	end
	return queries
end

local function GetCommonQueryInfoClass(class, items)
	local resultQuery = TSMAPI:GetAuctionQueryInfo(items[1])
	resultQuery.name = ""
	resultQuery.class = class
	for i=2, #items do
		local itemQuery = TSMAPI:GetAuctionQueryInfo(items[i])
		resultQuery.minLevel = min(resultQuery.minLevel, itemQuery.minLevel)
		resultQuery.maxLevel = max(resultQuery.maxLevel, itemQuery.maxLevel)
		resultQuery.quality = (resultQuery.quality == itemQuery.quality) and resultQuery.quality or nil
		if resultQuery.subClass ~= itemQuery.subClass then resultQuery.subClass = nil end
	end
	resultQuery.items = items
	return {resultQuery}
end

local function GreatestSubstring(str1, str2)
	local parts1 = {(" "):split(str1)}
	local parts2 = {(" "):split(str2)}
	for i=1, #parts1 do
		if parts1[i] ~= parts2[i] then
			local subStr = table.concat(parts1, " ", 1, i-1)
			return subStr ~= "" and subStr
		end
	end
	return table.concat(parts1, " ")
end

local function ReduceStrings(strList)
	local didReduction = true
	while didReduction do
		didReduction = false
		for i=1, #strList-1 do
			if i > #strList-1 then break end
			local subStr = GreatestSubstring(strList[i], strList[i+1])
			if subStr then
				strList[i] = subStr
				tremove(strList, i+1)
				didReduction = true
			end
		end
		if not private.thread then return end
		private.thread:Yield()
	end
	return true
end

local function NumPagesCallback(event, numPages)
	if event == "NUM_PAGES" then
		local skippedItems = {}
		local score = max(#private.combinedQueries[1].items-numPages, 0)
		if private.combinedQueries[1].name == "" then
			-- This is a common class term so determine if we should use this or not.
			local cost = 0
			for _, query in ipairs(private.queries) do
				if query.score and query.class == private.combinedQueries[1].class then
					cost = cost + query.score
				end
			end
			if score >= cost and score > 0 then
				-- use the common class term
				for i=#private.queries, 1, -1 do
					local query = private.queries[i]
					local shouldRemove = (query.class == private.combinedQueries[1].class)
					if shouldRemove then
						tremove(private.queries, i)
					end
				end
				tinsert(private.queries, private.combinedQueries[1])
			end
		else
			if numPages > #private.combinedQueries[1].items then
				for _, itemString in ipairs(private.combinedQueries[1].items) do
					local query = TSMAPI:GetAuctionQueryInfo(itemString)
					query.items = {itemString}
					query.score = 0
					tinsert(private.queries, query)
				end
			elseif numPages == 0 then
				for _, itemString in ipairs(private.combinedQueries[1].items) do
					tinsert(skippedItems, itemString)
				end
			else
				-- use the common search term
				private.combinedQueries[1].score = score
				tinsert(private.queries, private.combinedQueries[1])
			end
		end
		tremove(private.combinedQueries, 1)
		private.callback("QUERY_UPDATE", private.totalQueries-#private.combinedQueries, private.totalQueries, skippedItems)
	end
	private:CheckNextCombinedQuery()
end

function private:CheckNextCombinedQuery()
	if not private.isScanning then return end
	
	if #private.combinedQueries == 0 then
		-- we're done
		sort(private.queries, function(a, b) return a.name < b.name end)
		TSM:StopGeneratingQueries()
		TSMAPI:CreateTimeDelay("queryUtilCallbackDelay", 0.05, function() private.callback("QUERY_COMPLETE", private.queries) end)
		return
	end
	
	for _, itemString in ipairs(private.combinedQueries[1].items) do
		if strlower(private.combinedQueries[1].name) == strlower(TSMAPI:GetSafeItemInfo(itemString)) then
			-- One of the items in this combined query is the same as the common search term,
			-- so it's always worth using this common search term.
			NumPagesCallback("NUM_PAGES", 1)
			return
		end
	end
	
	TSMAPI.AuctionScan:GetNumPages(private.combinedQueries[1], NumPagesCallback)
end

local function GenerateSearchTerms(names, itemList, isReversed)
	sort(names)
	if not ReduceStrings(names) then return end -- run the reduction
	
	-- create a table associating all the reduced names to a list of items
	local temp = {}
	for i, filterName in ipairs(names) do
		for j, itemString in ipairs(itemList) do
			local itemName = TSMAPI:GetSafeItemInfo(itemString)
			itemName = itemName and isReversed and strrev(itemName) or itemName -- reverse item name if necessary
			if itemName and strfind(itemName, "^"..TSMAPI:StrEscape(filterName)) then
				temp[filterName] = temp[filterName] or {}
				tinsert(temp[filterName], itemString)
			end
		end
		if not private.thread then return end
		private.thread:Yield()
	end
	
	return temp
end

local function GenerateQueriesThread(self)
	private.thread = self
	local function GenerateFilters(reverse)
		-- create a list of all item names
		local names = {}
		for _, itemString in ipairs(private.itemList) do
			local name = TSMAPI:GetSafeItemInfo(itemString)
			if type(name) == "string" and name ~= "" then
				tinsert(names, reverse and strrev(name) or name)
			end
		end
		if not private.thread then return end

		local filters, tempFilters, tempItems  = {}, {}, {}
		local numFilters = 0
		local tbl = GenerateSearchTerms(names, private.itemList, reverse)
		if not tbl then return end
		for filterName, items in pairs(tbl) do
			if #items > 1 then
				filters[reverse and strrev(filterName) or filterName] = items
				numFilters = numFilters + 1
			else
				tinsert(tempFilters, strrev(filterName)) -- reverse name for second pass
				for _, itemString in ipairs(items) do
					tinsert(tempItems, itemString)
				end
			end
		end
		
		-- try to find common search terms of reversed item names
		local tbl = GenerateSearchTerms(tempFilters, tempItems, not reverse)
		if not tbl then return end
		for filterName, items in pairs(tbl) do
			filters[reverse and filterName or strrev(filterName)] = items
			numFilters = numFilters + 1
		end
		
		return filters, numFilters
	end
	
	local endTime = debugprofilestop() + 5000
	while debugprofilestop() < endTime do
		-- request all the item info
		local tryAgain = false
		for _, itemString in ipairs(private.itemList) do
			if not TSMAPI:GetSafeItemInfo(itemString) then
				tryAgain = true
			end
		end
		if not tryAgain then break end
		self:Sleep(0.1)
	end
	
	local filters1, num1 = GenerateFilters()
	local filters2, num2 = GenerateFilters(true)
	if not filters1 or not filters2 then return end
	local filters = num2 < num1 and filters2 or filters1
	
	-- generate class filters
	local itemClasses = {}
	local classes = {GetAuctionItemClasses()}
	for _, itemString in ipairs(private.itemList) do
		local classIndex = GetItemClasses(itemString)
		if classIndex then
			itemClasses[classIndex] = itemClasses[classIndex] or {}
			tinsert(itemClasses[classIndex], itemString)
		end
	end
	
	-- create the actual queries
	local queries, combinedQueries = {}, {}
	for filterName, items in pairs(filters) do
		for _, query in ipairs(GetCommonQueryInfo(filterName, items)) do
			if #query.items > 1 then
				tinsert(combinedQueries, query)
			else
				tinsert(queries, query)
			end
		end
	end
	for class, items in pairs(itemClasses) do
		for _, query in ipairs(GetCommonQueryInfoClass(class, items)) do
			if #query.items > 1 then
				tinsert(combinedQueries, query)
			end
		end
	end
	
	private.isScanning = true
	private.queries = queries
	private.combinedQueries = combinedQueries
	private.totalQueries = #combinedQueries
end

function TSMAPI:GenerateQueries(itemList, callback)
	if private.thread then return end
	private.itemList = itemList
	private.callback = callback
	
	local function ThreadDone()
		if private.thread then
			private.thread = nil
			private:CheckNextCombinedQuery()
		end
	end
	TSMAPI.Threading:Start(GenerateQueriesThread, 0.5, ThreadDone)
end

function TSM:StopGeneratingQueries()
	private.thread = nil
	private.isScanning = nil
end