
local MAJOR, MINOR = "LibAuctionScan-1.0", 1
local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end


lib.mixinTargets = lib.mixinTargets or {}
local mixins = {"StartScan", "StopScan", "GetAuctionQueryInfo", "GetCommonAuctionQueryInfo", "GetPageProgress", "FindAuction", "StopFindScan", "NewAuctionItem", "SortAuctions", "StartGetAllScan", "IsFindScanning"}

function lib:Embed(target)
	for _,name in pairs(mixins) do
		target[name] = lib[name]
	end
	lib.mixinTargets[target] = true
end





--[[-------------------------------------------------------------------------
	LibAuctionScan Utility Functions
---------------------------------------------------------------------------]]

local BASE_DELAY = 0.10 -- time to delay for before trying to scan a page again when it isn't fully loaded

-- Converts an itemLink into an itemString
local function GetItemString(itemLink)
	if type(itemLink) ~= "string" and type(itemLink) ~= "number" then return end
	itemLink = select(2, GetItemInfo(itemLink)) or itemLink
	
	-- it's an itemId and we couldn't get the itemLink so guess
	if tonumber(itemLink) then
		return "item:"..itemLink..":0:0:0:0:0:0"
	end
	
	local itemInfo = {strfind(itemLink, "|?c?f?f?(%x*)|?H?([^:]*):?(%d+):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%-?%d*):?(%-?%d*):?(%-?%d*):?(%d*)|?h?%[?([^%[%]]*)%]?|?h?|?r?")}
	if not itemInfo[11] then return end
	
	return table.concat(itemInfo, ":", 4, 11), itemInfo[5]
end

local delays = {}

-- Cancels a time delay
local function CancelFrame(label)
	local delayFrame
	for i, frame in pairs(delays) do
		if frame.label == label then
			delayFrame = frame
		end
	end
	
	if delayFrame then
		delayFrame:Hide()
		delayFrame.label = nil
		delayFrame.inUse = false
		delayFrame.validate = nil
		delayFrame.timeLeft = nil
		delayFrame:SetScript("OnUpdate", nil)
	end
end

-- Creates a time delay
-- the callback is called after the specified amount of time has passed
local function CreateTimeDelay(label, duration, callback)
	if not (label and type(duration) == "number" and type(callback) == "function") then return end

	local frameNum
	for i, frame in pairs(delays) do
		if frame.label == label then return end
		if not frame.inUse then
			frameNum = i
		end
	end
	
	if not frameNum then
		local delay = CreateFrame("Frame")
		delay:Hide()
		tinsert(delays, delay)
		frameNum = #delays
	end
	
	local frame = delays[frameNum]
	frame.inUse = true
	frame.label = label
	frame.timeLeft = duration
	frame:SetScript("OnUpdate", function(self, elapsed)
		self.timeLeft = self.timeLeft - elapsed
		if self.timeLeft <= 0 then
			CancelFrame(self.label)
			callback()
		end
	end)
	frame:Show()
end

local function AuctionDataIsBad(temp, resolveSeller)
	local shown = GetNumAuctionItems("list")
	local badData = false

	for i=1, shown do
		-- checks to make sure all the data has been sent to the client
		-- if not, the data is bad and we'll wait / try again
		local count, _, _, _, _, _, buyout, _, _, seller = select(3, GetAuctionItemInfo("list", i))
		local itemString = GetItemString(GetAuctionItemLink("list", i))
		temp[i] = {itemString=itemString, index=i}
		if not (itemString and buyout and count and (seller or not resolveSeller)) then
			badData = true
		end
	end
	
	return badData
end




--[[-------------------------------------------------------------------------
	LibAuctionScan Scanning Functions (lib:StartScan, lib:StopScan)
---------------------------------------------------------------------------]]

do
	lib.scanFrame = lib.scanFrame or CreateFrame("Frame")

	local private = {}
	local status = {page=0, retries=0, timeDelay=0, AH=false, filterlist = {}}

	local function eventHandler(frame, event)
		if event == "AUCTION_HOUSE_SHOW" then
			-- auction house was opened
			status.AH = true
		elseif event == "AUCTION_HOUSE_CLOSED" then
			-- auction house was closed, make sure all scanning is stopped
			frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
			status.AH = false
			if status.isScanning then -- stop scanning if we were scanning (pass true to specify it was interrupted)
				private:StopScanning(true)
			end
		elseif event == "AUCTION_ITEM_LIST_UPDATE" then
			-- gets called whenever the AH window is updated (something is shown in the results section)
			frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
			if not status.isScanning then return end
			CancelFrame("updateDelay")
			
			-- now that our query was successful, we can get our data
			private:ScanAuctions()
		end
	end
	lib.scanFrame:SetScript("OnEvent", eventHandler)
	lib.scanFrame:RegisterEvent("AUCTION_HOUSE_CLOSED")
	lib.scanFrame:RegisterEvent("AUCTION_HOUSE_SHOW")

	local function DoCallback(...)
		if type(status.callbackHandler) == "function" then
			status.callbackHandler(...)
		end
	end

	-- gets the number of pages a certain query results in
	-- used to determine whether combined filters should be split up
	local function GetNumPages(filter, callbackFunc)
		lib:StopScan()
		if not status.AH then
			return
		elseif not CanSendAuctionQuery() then
			local delay = CreateFrame("Frame")
			delay:SetScript("OnUpdate", function(self)
					if CanSendAuctionQuery() then
						self:Hide()
						if status.isScanning == nil then return end
						GetNumPages(unpack(self.params))
					end
				end)
			delay:Show()
			delay.params = {filter, callbackFunc}
			return
		end

		local eventFrame = CreateFrame("Frame")
		eventFrame:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
		eventFrame:SetScript("OnEvent", function(self)
				local _, total = GetNumAuctionItems("list")
				local totalPages = math.ceil(total / 50)
				self:UnregisterAllEvents()
				self:Hide()
				if status.isScanning ~= false then return end
				callbackFunc(totalPages)
			end)
		eventFrame:Show()

		QueryAuctionItems(filter.name, filter.minLevel, filter.maxLevel, filter.invType, filter.class, filter.subClass, 0, filter.usable, filter.quality)
	end
	
	-- splits the combined filter into individual item filters
	local function SplitCurrentFilterItems()
		local newFilters = {}
		for _, item in ipairs(status.filter.arg) do
			local name = GetItemInfo(item)
			local temp = CopyTable(status.filter)
			temp.arg = item
			temp.isCombinedFilter = nil
			temp.name = name
			tinsert(newFilters, temp)
		end

		tremove(status.filterList, 1)
		for _, filter in ipairs(newFilters) do
			tinsert(status.filterList, 1, filter)
		end
		status.filter = status.filterList[1]
		DoCallback("UPDATE_TOTAL_FILTERS", #status.filterList)
	end
	
	local function SortAuctionsAscending(header)
		SortAuctionItems("list", header)
		if IsAuctionSortReversed("list", header) then
			SortAuctionItems("list", header)
		end
	end

	local function IsDuplicatePage()
		if not private.pageTemp or GetNumAuctionItems("list") == 0 then return false end
		
		local numLinks, prevLink = 0, nil
		for i=1, GetNumAuctionItems("list") do
			local _, _, count, _, _, _, minBid, minInc, buyout, bid, _, seller = GetAuctionItemInfo("list", i)
			local link = GetAuctionItemLink("list", i)
			local temp = private.pageTemp[i]
			
			if not prevLink then
				prevLink = link
			elseif prevLink ~= link then
				prevLink = link
				numLinks = numLinks + 1
			end
			
			if not temp or temp.count ~= count or temp.minBid ~= minBid or temp.minInc ~= minInc or temp.buyout ~= buyout or temp.bid ~= bid or temp.seller ~= seller or temp.link ~= link then
				return false
			end
		end
		
		if numLinks > 1 and private.pageTemp.shown == GetNumAuctionItems("list") then
			return false
		end
		
		return true
	end
	
	local function PopulatePageTemp()
		local shown = GetNumAuctionItems("list")
		private.pageTemp = {numShown=shown}

		for i=1, shown do
			-- checks to make sure all the data has been sent to the client
			-- if not, the data is bad and we'll wait / try again
			local _, _, count, _, _, _, minBid, minInc, buyout, bid, _, seller = GetAuctionItemInfo("list", i)
			local link = GetAuctionItemLink("list", i)
			
			private.pageTemp[i] = {count=count, minBid=minBid, minInc=minInc, buyout=buyout, bid=bid, seller=seller, link=link}
		end
	end
	
	-- Starts a scan of the auction house.
	--		scanQueue - A list of queries. Each entry represents a unique set of QueryAuctionItem paramters:
	--			name, minLevel, maxLevel, invType, class, subClass, usable, quality
	-- 	options - special options for the scan (all optional)
	--			sellerResolution - resolve seller names
	--			missingSellerName - allows a missing seller (only applies if sellerResolution is set) and sets the name to this string
	--			passiveRequest - if there is a scan going on, ignore this request (otherwise stop it and run this request)
	--			maxRetries - sets the max number of retries, 3 by default
	--			retryDelay - how long the scan should wait before retrying the query, 4 sec by default
	function lib:StartScan(scanQueue, callbackHandler, scanOptions)
		lib:StopFindScan()
		if status.isScanning then
			if scanOptions.passiveRequest then
				return -4
			else
				lib:StopScan()
			end
		end

		if scanOptions.ShouldStop then
			-- sort the auction house by buyout < name
			SortAuctionsAscending("name")
			SortAuctionsAscending("buyout")
		else
			-- sort the auction house by name < buyout
			SortAuctionsAscending("buyout")
			SortAuctionsAscending("name")
		end

		if not status.AH then
			return -1 -- the auction house isn't open (return code -1)
		elseif type(scanQueue) ~= "table" or #scanQueue == 0 then
			return -2 -- the scan queue is empty or not a table (return code -2)
		elseif not CanSendAuctionQuery() then
			local delay = CreateFrame("Frame")
			delay:SetScript("OnUpdate", function(self)
					if CanSendAuctionQuery() then
						self:Hide()
						lib:StartScan(unpack(self.params))
					end
				end)
			delay:Show()
			delay.params = {scanQueue, callbackHandler, scanOptions}
			return 0 -- the query will start as soon as it can but did not start immediately (return code 0)
		end

		for i=1, #scanQueue do
			if type(scanQueue[i]) ~= "table" then
				local isNum = tonumber(scanQueue[i]) and true
				local itemString = GetItemString(scanQueue[i])
				if itemString then
					scanQueue[i] = lib:GetAuctionQueryInfo(itemString)
					scanQueue[i].exactOnly = not isNum
					scanQueue[i].arg = itemString
				else
					return -3 -- the scan queue contained invalid entries (return code -3)
				end
			end
			scanQueue[i].scanTemp = {didCallback={}}
		end
		
		-- set defaults
		scanOptions.maxRetries = scanOptions.maxRetries or 3
		scanOptions.retryDelay = scanOptions.retryDelay or 2

		-- initialize all the values of the status table
		-- filter = current category being scanned for {class, subClass, invSlot}
		-- filterList = queue of categories to scan for
		status.data = {} -- the data we've scanned so far
		status.page = 0 -- what page we are currently scanning (starts at 0)
		status.retries = 0 -- how many times we've done a hard retry so far
		status.hardRetry = nil -- if a page hasn't loaded after we've tried a delay, we'll do a hard retry and re-send the query
		status.filterList = scanQueue -- list of filters
		status.filter = scanQueue[1] -- current filter
		status.isScanning = true -- used to prevent functions from running when we're not supposed to be scanning
		status.callbackHandler = callbackHandler
		status.options = scanOptions -- any special options for this scan
		status.ShouldStop = function(link, count, buyout)
			if scanOptions.ShouldStop and link and select(8, GetItemInfo(link)) == count and buyout then
				return scanOptions.ShouldStop(link, buyout/count)
			end
		end

		--starts scanning
		private:SendQuery()
		return 1 -- scan started successfully (return code 1)
	end

	-- sends a query to the AH frame once it is ready to be queried (uses frame as a delay)
	function private:SendQuery()
		if not status.isScanning then return end
		if CanSendAuctionQuery() then
			-- stop delay timer
			CancelFrame("queryDelay")

			if status.page == 0 and status.filter.isCombinedFilter and type(status.filter.arg) == "table" and not status.filter.scanTemp.checkedPages then
				status.filter.scanTemp.checkedPages = true
				status.isScanning = false
				GetNumPages(status.filter, function(numPages)
						if status.isScanning == nil then return end
						if numPages > #status.filter.arg then
							SplitCurrentFilterItems()
						end
						status.isScanning = true
						private:SendQuery()
					end)
				return
			end

			-- Query the auction house (then waits for AUCTION_ITEM_LIST_UPDATE to fire)
			lib.scanFrame:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
			QueryAuctionItems(status.filter.name, status.filter.minLevel, status.filter.maxLevel, status.filter.invType, status.filter.class, status.filter.subClass, status.page, status.filter.usable, status.filter.quality)
		else
			-- run delay timer then try again to scan
			CreateTimeDelay("queryDelay", 0.05, private.SendQuery)
		end
	end
	
	--scans the currently shown page of auctions and collects all the data
	function private:ScanAuctions()
		if not status.isScanning then return end
		
		local shown, total = GetNumAuctionItems("list")
		local totalPages = math.ceil(total / 50)
		local temp = {}
		local dataIsBad = AuctionDataIsBad(temp, status.options.sellerResolution)
		
		-- check that we have good data
		if dataIsBad or IsDuplicatePage() then
			if status.retries < status.options.maxRetries then
				if status.hardRetry then
					-- Hard retry
					-- re-sends the entire query
					status.retries = status.retries + 1
					status.timeDelay = 0
					status.hardRetry = nil
					private:SendQuery()
				else
					-- Soft retry
					-- runs a delay and then tries to scan the query again
					status.timeDelay = status.timeDelay + BASE_DELAY
					CreateTimeDelay("updateDelay", BASE_DELAY, private.ScanAuctions)

					-- If after 2 seconds of retrying we still don't have data, will go and requery to try and solve the issue
					-- if we still don't have data, we try to scan it anyway and move on.
					if status.timeDelay >= status.options.retryDelay then
						status.hardRetry = true
					end
				end
				return
			elseif dataIsBad and status.retries == status.options.maxRetries and status.options.sellerResolution and not status.options.missingSellerName then
				-- give up
				DoCallback("SCAN_TIMEOUT", status.filter)
				status.hardRetry = nil
				status.retries = 0
				status.timeDelay = 0
				private:RemoveCurrentFilter()
				return
			end
		end
		
		status.hardRetry = nil
		status.retries = 0
		status.timeDelay = 0
		DoCallback("SCAN_STATUS_UPDATE", status.page+1, totalPages, #status.filterList)
		PopulatePageTemp()
		local isDone = false
		
		-- now that we know our query is good, time to verify and then store our data
		for _, v in ipairs(temp) do
			local newItem, shouldStop = private:AddAuctionRecord(v.index)
			if newItem and status.filter.isCombinedFilter then
				if type(newItem) == "table" then
					for _, itemString in ipairs(newItem) do
						DoCallback("NEW_ITEM_DATA", {item=itemString, data=status.data, filter=status.filter})
						status.filter.scanTemp.didCallback[itemString] = true
					end
				else
					DoCallback("NEW_ITEM_DATA", {item=newItem, data=status.data, filter=status.filter})
					status.filter.scanTemp.didCallback[newItem] = true
				end
				
				isDone = true
				for _, itemString in ipairs(status.filter.arg) do
					if not status.filter.scanTemp.didCallback[itemString] then
						isDone = false
						break
					end
				end
			elseif shouldStop and not status.filter.isCombinedFilter and not newItem then
				isDone = true
				status.filter.exactOnly = not status.options.ShouldStop and "done"
				break
			end
		end
		
		if not isDone or status.filter.exactOnly == "done" then
			-- This query has more pages to scan
			-- increment the page # and send the new query
			if status.filter.exactOnly ~= "done" and totalPages > (status.page + 1) then
				status.page = status.page + 1
				private:SendQuery()
				return
			end

			
			if not status.filter.isCombinedFilter then
				DoCallback("QUERY_FINISHED", {filter=status.filter, data=status.data, left=#status.filterList})
			else
				-- do callbacks for the last item and items that weren't on the AH
				if status.filter.scanTemp.currentItem then
					DoCallback("NEW_ITEM_DATA", {item=status.filter.scanTemp.currentItem.itemString, data=status.data, filter=status.filter})
				end
				for _, item in ipairs(status.filter.arg) do
					if not status.data[item] then
						DoCallback("NEW_ITEM_DATA", {item=item, data=status.data, filter=status.filter})
					end
				end
			end
		end
		
		-- done with this filter so remove it
		private:RemoveCurrentFilter()
	end
	
	-- called when we are done with the current filter
	function private:RemoveCurrentFilter()
		-- Removes the current filter from the filterList as we are done scanning for that item
		for i=1, #(status.filterList) do
			if status.filterList[i] == status.filter then
				tremove(status.filterList, i)
				break
			end
		end

		-- Query the next filter if we have one
		if status.filterList[1] then
			status.filter = status.filterList[1]
			status.page = 0
			private:SendQuery()
			return
		end

		-- we are done scanning!
		private:StopScanning()
	end

	-- Add a new record to the status.data table
	function private:AddAuctionRecord(index)
		local name, _, count, _, _, _, minBid, minIncrement, buyout, bid, highBidder, seller = GetAuctionItemInfo("list", index)
		local timeLeft = GetAuctionItemTimeLeft("list", index)
		local link = GetAuctionItemLink("list", index)
		local itemString, itemID = GetItemString(link)
		local newItem
		
		if not itemString then return end

		if status.filter.isCombinedFilter then
			status.filter.scanTemp.currentItem = status.filter.scanTemp.currentItem or {}
			if status.filter.scanTemp.currentItem.name ~= name then
				newItem = status.filter.scanTemp.currentItem.itemString
				status.filter.scanTemp.currentItem = {name=name, itemString=itemString}
				if status.filter.scanTemp[name] then
					DoCallback("SCAN_ERROR", name)
				end
				status.filter.scanTemp[name] = true
			elseif status.filter.scanTemp.currentItem.itemString ~= itemString then
				if type(status.filter.scanTemp.currentItem.itemString) == "table" then
					local isNew = true
					for _, v in ipairs(status.filter.scanTemp.currentItem.itemString) do
						if v == itemString then
							isNew = false
							break
						end
					end
					
					if isNew then
						tinsert(status.filter.scanTemp.currentItem.itemString, itemString)
					end
				else
					local oldItemString = status.filter.scanTemp.currentItem.itemString
					status.filter.scanTemp.currentItem.itemString = {oldItemString, itemString}
				end
			end
		elseif status.filter.isItemIDFilter then
			itemString = itemID
		end

		-- Create a new entry in the table
		if not status.data[itemString] then
			status.data[itemString] = lib:NewAuctionItem()
			status.data[itemString]:SetItemLink(link)
		end
		status.data[itemString]:AddAuctionRecord(count, minBid, minIncrement, buyout, bid, highBidder, seller or status.options.missingSellerName, timeLeft)

		local exactOnly = status.filter.exactOnly ~= 0 and status.filter.exactOnly
		if exactOnly and not status.filter.isCombinedFilter and not newItem and not status.options.ShouldStop then
			return nil, strlower(name) > strlower(status.filter.name)
		end
		return itemString, status.ShouldStop(link, count, buyout)
	end

	-- stops the scan when we are finished scanning, it was interrupted, or somebody stopped it
	function private:StopScanning(interrupted)
		if not status.isScanning then return end

		if interrupted then
			-- fires if the scan was interrupted
			DoCallback("SCAN_INTERRUPTED")
		else
			-- fires if the scan completed sucessfully
			DoCallback("SCAN_COMPLETE", status.data)
		end

		-- cancel any delays that might still be running
		CancelFrame("queryDelay")
		CancelFrame("updateDelay")
		status.isScanning = nil
		private.pageTemp = nil

		return true
	end
	
	-- gets the current page progress
	function lib:GetPageProgress()
		local shown, total = GetNumAuctionItems("list")
		local totalPages = ceil(total / 50)
		return status.page + 1, totalPages
	end

	-- API for stopping the scan
	-- returns true/false if we were/weren't actually scanning
	function lib:StopScan()
		lib:StopFindScan()
		return private:StopScanning(true)
	end
	
	
	do
		--[[-------------------------------------------------------------------------
			GetAll Scan Code
		---------------------------------------------------------------------------]]
		local function GetAllScanFrameUpdate(self)
			if not AuctionFrame:IsVisible() then self:Hide() end
			
			-- get data for at most 200 auctions per update to avoid excessive lag
			for i=1, 200 do
				local link = GetAuctionItemLink("list", self.num)
				local _, _, quantity, _, _, _, _, _, buyout = GetAuctionItemInfo("list", self.num)
				if self.tries == 0 or (link and quantity and buyout) then
					self.num = self.num + 1
					self.tries = 3
					if link then
						private:AddAuctionRecord(self.num)
					end
					
					DoCallback("GETALL_UPDATE", self.num, self.numShown)
					
					-- check if we are done scanning or not
					if self.num == self.numShown then
						-- bug with getall scan only being able to return a max of 42554 auctions
						if self.num ~= self.totalNum then
							DoCallback("GETALL_BUG")
						end
						
						self:Hide()
						private:StopScanning()
						break
					end
				else
					self.tries = self.tries - 1
					break
				end
			end
		end

		local scanFrame = CreateFrame("Frame")
		scanFrame:Hide()
		scanFrame:SetScript("OnUpdate", GetAllScanFrameUpdate)

		-- every half second we check to see if there are more than 50 auctions
		-- if so, we show the scan frame and start scanning the data
		local function DataAvailableFrameUpdate(self, elapsed)
			if not AuctionFrame:IsVisible() then self:Hide() end

			self.delay = self.delay - elapsed
			self.totalDelay = self.totalDelay - elapsed
			
			DoCallback("GETALL_WAITING", 20-self.totalDelay)
			
			if self.delay <= 0 then
				if GetNumAuctionItems("list") > 50 then
					-- data is ready to be scanned!
					scanFrame.numShown, scanFrame.totalNum = GetNumAuctionItems("list")
					self:Hide()
					scanFrame:Show()
				else
					-- wait another half second
					self.delay = 1
				end
			end
		end

		local	dataAvailableFrame = CreateFrame("Frame")
		dataAvailableFrame:Hide()
		dataAvailableFrame:SetScript("OnUpdate", DataAvailableFrameUpdate)

		function lib:StartGetAllScan(callbackHandler)
			lib:StopScan()

			if not status.AH then
				return -1 -- the auction house isn't open (return code -1)
			elseif not CanSendAuctionQuery() then
				local delay = CreateFrame("Frame")
				delay:SetScript("OnUpdate", function(self)
						if CanSendAuctionQuery() then
							self:Hide()
							lib:StartGetAllScan(unpack(self.params))
						end
					end)
				delay:Show()
				delay.params = {callbackHandler}
				return 0 -- the query will start as soon as it can but did not start immediately (return code 0)
			elseif not select(2, CanSendAuctionQuery()) then
				return -2 -- getall scan not ready
			end

			-- initialize all the values of the status table
			status.options = {}
			status.filter = {}
			status.data = {} -- the data we've scanned so far
			status.isScanning = "getAll" -- used to prevent functions from running when we're not supposed to be scanning
			status.callbackHandler = callbackHandler
			status.ShouldStop = function() end
		
			QueryAuctionItems("", "", "", nil, nil, nil, nil, nil, nil, true)
			
			scanFrame.num = 0
			scanFrame.tries = 3
			dataAvailableFrame.totalDelay = 20
			dataAvailableFrame.delay = 2
			dataAvailableFrame:Show()
			
			return 1 -- scan started successfully (return code 1)
		end
	end
end





--[[-------------------------------------------------------------------------
	LibAuctionScan Finding Functions (lib:FindAuction, lib:StopFindScan)
---------------------------------------------------------------------------]]

do
	lib.findFrame = lib.findFrame or CreateFrame("Frame")
	
	local equipLocLookup = {
		[INVTYPE_HEAD]=1, [INVTYPE_NECK]=2, [INVTYPE_SHOULDER]=3, [INVTYPE_BODY]=4, [INVTYPE_CHEST]=5,
		[INVTYPE_WAIST]=6, [INVTYPE_LEGS]=7, [INVTYPE_FEET]=8, [INVTYPE_WRIST]=9, [INVTYPE_HAND]=10,
		[INVTYPE_FINGER]=11, [INVTYPE_TRINKET]=12, [INVTYPE_CLOAK]=13, [INVTYPE_HOLDABLE]=14,
		[INVTYPE_WEAPONMAINHAND]=15, [INVTYPE_ROBE]=16, [INVTYPE_TABARD]=17, [INVTYPE_BAG]=18,
		[INVTYPE_2HWEAPON]=19, [INVTYPE_RANGED]=20, [INVTYPE_SHIELD]=21, [INVTYPE_WEAPON]=22
	}
	
	local private, status = {}, {}
	
	local function eventHandler(frame, event)
		if event == "AUCTION_HOUSE_SHOW" then
			-- auction house was opened
			status.AH = true
		elseif event == "AUCTION_HOUSE_CLOSED" then
			frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
			status.AH = false
			if status.isScanning then -- stop scanning if we were scanning (pass true to specify it was interrupted)
				lib:StopFindScan()
			end
		elseif event == "AUCTION_ITEM_LIST_UPDATE" then
			frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
			if status.isScanning then
				status.timeDelay = 0
				CancelFrame("auctionFindScanDelay")
				
				-- now that our query was successful we can get our data
				private:ScanAuctions()
			end
		end
	end
	lib.findFrame:SetScript("OnEvent", eventHandler)
	lib.findFrame:RegisterEvent("AUCTION_HOUSE_CLOSED")
	lib.findFrame:RegisterEvent("AUCTION_HOUSE_SHOW")

	function lib:GetAuctionQueryInfo(itemString)
		local name, _, rarity, _, minLevel, class, subClass, _, equipLoc = GetItemInfo(itemString)
		if not name then return end
		return {name=name, minLevel=minLevel, maxLevel=minLevel, invType=(equipLocLookup[equipLoc] or 0), class=class, subClass=subClass, quality=rarity}
	end

	function lib:GetCommonAuctionQueryInfo(items, nameFilter)
		if not nameFilter or not items or #items == 0 then return end
		local result = {name=nameFilter, minLevel=nil, maxLevel=nil, invType=-1, class=-1, subClass=-1, quality=4}
		
		for _, itemString in ipairs(items) do
			local itemFilters = lib:GetAuctionQueryInfo(itemString)
			if not itemFilters or not strfind(strlower(itemFilters.name), nameFilter) then return end
			
			if not result.minLevel or itemFilters.minLevel < result.minLevel then
				result.minLevel = itemFilters.minLevel
			end
			
			if not result.maxLevel or itemFilters.maxLevel > result.maxLevel then
				result.maxLevel = itemFilters.maxLevel
			end
			
			if result.invType == -1 then
				result.invType = itemFilters.invType
			elseif result.invType ~= itemFilters.invType then
				result.invType = 0
			end
			
			if result.class == -1 then
				result.class = itemFilters.class
			elseif result.class ~= itemFilters.class then
				result.class = 0
			end
			
			if result.subClass == -1 then
				result.subClass = itemFilters.subClass
			elseif result.subClass ~= itemFilters.subClass then
				result.subClass = 0
			end
			
			if result.quality > itemFilters.quality then
				result.quality = itemFilters.quality
			end
		end
		
		return result
	end
	
	local function IsTargetAuction(index)
		local itemString, itemID = GetItemString(GetAuctionItemLink("list", index))
		local _, _, count, _, _, _, minBid, bidIncrement, buyout, bidAmount, _, seller = GetAuctionItemInfo("list", index)
		local bid = bidAmount == 0 and minBid or bidAmount
		local info = status.targetInfo
		if type(info.itemString) == "number" then
			itemString = itemID
		end
		
		return (not info.itemString or itemString == info.itemString) and (not info.count or count == info.count) and (not info.bid or bid == info.bid) and (not info.buyout or buyout == info.buyout) and (not info.seller or seller == info.seller)
	end

	-- valid targetInfo keys: itemString, count, bid, buyout, seller
	function lib:FindAuction(callback, targetInfo)
		if status.isScanning then lib:StopFindScan() end
		
		local name, _, rarity, _, minLevel, class, subClass, _, equipLoc = GetItemInfo(targetInfo.itemString:match("item:%d+:%d+:%d+:%d+:%d+"))
		status.query = {name=name, minLevel=minLevel, maxLevel=minLevel, invSlot=(equipLocLookup[equipLoc] or 0), class=class, subClass=subClass, rarity=rarity}
		status.targetInfo = targetInfo
		status.callback = callback
		
		status.page = 0
		status.isScanning = true
		status.retries = 0
		status.hardRetry = nil
		
		-- check if the item is on the current page
		for i=1, GetNumAuctionItems("list") do
			if IsTargetAuction(i) then
				lib:StopFindScan()
				CreateTimeDelay("queryFoundDelay", 0.1, function() status.callback(i) end)
				return
			end
		end
		
		private:SendQuery()
	end

	-- sends a query to the AH frame once it is ready to be queried (uses frame as a delay)
	function private:SendQuery()
		if not status.isScanning then return end
		if CanSendAuctionQuery() then
			-- stop delay timer
			CancelFrame("auctionFindQueryDelay")
			
			-- query the auction house (then waits for AUCTION_ITEM_LIST_UPDATE to fire)
			lib.findFrame:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
			QueryAuctionItems(status.query.name, status.query.minLevel, status.query.maxLevel, status.query.invType, status.query.class, status.query.subClass, status.page, 0, status.query.rarity)
		else
			-- run delay timer then try again to scan
			CreateTimeDelay("auctionFindQueryDelay", 0.05, function() private:SendQuery() end)
		end
	end

	-- scans the currently shown page of auctions and collects all the data
	function private:ScanAuctions()
		if not status.isScanning then return end
		-- collects data on the query:
			-- # of auctions on current page
			-- # of pages total
		local shown, total = GetNumAuctionItems("list")
		local totalPages = math.ceil(total / 50)
		local temp = {}
		
		-- Check for bad data
		if status.retries < 3 then
			if AuctionDataIsBad(temp) then
				if status.hardRetry then
					-- Hard retry
					-- re-sends the entire query
					status.retries = status.retries + 1
					private:SendQuery()
				else
					-- Soft retry
					-- runs a delay and then tries to scan the query again
					status.timeDelay = status.timeDelay + BASE_DELAY
					CreateTimeDelay("auctionFindScanDelay", BASE_DELAY, private.ScanAuctions)
		
					-- If after 4 seconds of retrying we still don't have data, will go and requery to try and solve the issue
					-- if we still don't have data, we try to scan it anyway and move on.
					if status.timeDelay >= 4 then
						status.hardRetry = true
						status.retries = 0
					end
				end
				
				return
			end
		end
		
		status.hardRetry = nil
		status.retries = 0
		
		-- now that we know our query is good, time to verify and then store our data
		for i=1, shown do
			if IsTargetAuction(temp[i].index) then
				lib:StopFindScan()
				return status.callback(temp[i].index)
			end
		end

		-- This query has more pages to scan
		-- increment the page # and send the new query
		if totalPages > (status.page + 1) then
			status.page = status.page + 1
			private:SendQuery()
			return
		end
		
		-- we are done scanning!
		lib:StopFindScan()
		return status.callback()
	end

	-- stops the scan because it was either interrupted or it was completed successfully
	function lib:StopFindScan()
		lib.findFrame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
		status.isScanning = nil
		CancelFrame("auctionFindQueryDelay")
		CancelFrame("auctionFindScanDelay")
	end
	
	function lib:IsFindScanning()
		return status.isScanning and CopyTable(status.targetInfo)
	end
end




-- update the mixins
for target,_ in pairs(lib.mixinTargets) do
	lib:Embed(target)
end