-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

local _, TSM = ...
local Scanner = TSM.Init("Service.AuctionScanClasses.Scanner")
local Delay = TSM.Include("Util.Delay")
local FSM = TSM.Include("Util.FSM")
local Future = TSM.Include("Util.Future")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Event = TSM.Include("Util.Event")
local ItemInfo = TSM.Include("Service.ItemInfo")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local Util = TSM.Include("Service.AuctionScanClasses.Util")
local ResultRow = TSM.Include("Service.AuctionScanClasses.ResultRow")
local private = {
	resolveSellers = nil,
	pendingFuture = nil,
	query = nil,
	browseResults = nil,
	callback = nil,
	browseId = 1,
	browseIsNoScan = false,
	browseIndex = 1,
	browsePendingIndexes = {},
	searchRow = nil,
	useCachedData = nil,
	retryCount = 0,
	requestFuture = Future.New("AUCTION_SCANNER_FUTURE"),
	requestResult = nil,
	fsm = nil,
}



-- ============================================================================
-- Module Loading
-- ============================================================================

Scanner:OnModuleLoad(function()
	private.requestFuture:SetScript("OnCleanup", function()
		Delay.Cancel("AUCTION_SCANNER_DONE")
		private.fsm:ProcessEvent("EV_CANCEL")
	end)

	if TSM.IsWowClassic() then
		Event.Register("AUCTION_ITEM_LIST_UPDATE", function()
			private.fsm:SetLoggingEnabled(false)
			private.fsm:ProcessEvent("EV_BROWSE_RESULTS_UPDATED")
			private.fsm:SetLoggingEnabled(true)
		end)
	else
		Event.Register("COMMODITY_SEARCH_RESULTS_UPDATED", function()
			private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED")
		end)
		Event.Register("ITEM_SEARCH_RESULTS_UPDATED", function()
			private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED")
		end)
	end

	private.fsm = FSM.New("AUCTION_SCANNER_FSM")
		:AddState(FSM.NewState("ST_INIT")
			:SetOnEnter(function()
				private.query = nil
				private.resolveSellers = nil
				private.useCachedData = nil
				private.searchRow = nil
				private.callback = nil
				private.retryCount = 0
				Delay.Cancel("AUCTION_SCANNER_RETRY")
				if private.pendingFuture then
					private.pendingFuture:Cancel()
					private.pendingFuture = nil
				end
			end)
			:AddTransition("ST_BROWSE_SORT")
			:AddTransition("ST_BROWSE_CHECKING")
			:AddTransition("ST_SEARCH_GET_KEY")
			:AddEvent("EV_START_BROWSE", function(_, query, resolveSellers, browseResults, callback)
				assert(not private.query)
				private.query = query
				private.resolveSellers = resolveSellers
				private.browseResults = browseResults
				private.browseId = private.browseId + 1
				private.browseIsNoScan = false
				private.callback = callback
				return "ST_BROWSE_SORT"
			end)
			:AddEvent("EV_START_BROWSE_NO_SCAN", function(_, query, itemKeys, browseResults, callback)
				assert(not TSM.IsWowClassic())
				assert(not private.query)
				private.query = query
				private.browseResults = browseResults
				private.browseId = private.browseId + 1
				private.browseIsNoScan = true
				private.callback = callback
				for _, itemKey in ipairs(itemKeys) do
					local baseItemString = ItemString.GetBaseFromItemKey(itemKey)
					private.ProcessBrowseResult(baseItemString, itemKey)
				end
				return "ST_BROWSE_CHECKING"
			end)
			:AddEvent("EV_START_SEARCH", function(_, query, resolveSellers, useCachedData, searchRow, callback)
				assert(not TSM.IsWowClassic())
				assert(not private.query)
				private.query = query
				private.resolveSellers = resolveSellers
				private.useCachedData = useCachedData
				private.searchRow = searchRow
				private.callback = callback
				private.searchRow:SearchReset()
				return "ST_SEARCH_GET_KEY"
			end)
		)
		:AddState(FSM.NewState("ST_BROWSE_SORT")
			:SetOnEnter(function()
				if not private.query:_SetSort() then
					Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
					return
				end
				return "ST_BROWSE_SEND"
			end)
			:AddTransition("ST_BROWSE_SORT")
			:AddTransition("ST_BROWSE_SEND")
			:AddTransition("ST_CANCELING")
			:AddEventTransition("EV_RETRY", "ST_BROWSE_SORT")
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_BROWSE_SEND")
			:SetOnEnter(function()
				private.HandleAuctionHouseWrapperResult(private.query:_SendWowQuery())
			end)
			:AddTransition("ST_BROWSE_SEND")
			:AddTransition("ST_BROWSE_CHECKING")
			:AddTransition("ST_CANCELING")
			:AddEvent("EV_FUTURE_SUCCESS", function()
				if TSM.IsWowClassic() then
					private.browseIndex = 1
					wipe(private.browsePendingIndexes)
				else
					for _, result in ipairs(C_AuctionHouse.GetBrowseResults()) do
						local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey)
						private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity)
					end
				end
				return "ST_BROWSE_CHECKING"
			end)
			:AddEventTransition("EV_RETRY", "ST_BROWSE_SEND")
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_BROWSE_CHECKING")
			:SetOnEnter(function()
				if not private.query:_BrowseIsPageValid() then
					-- this page isn't valid, so go to the next page
					return "ST_BROWSE_REQUEST_MORE"
				elseif not private.CheckBrowseResults() then
					-- result's aren't valid yet, so check again
					Delay.AfterFrame("AUCTION_SCANNER_RETRY", 1, private.RetryHandler)
					return
				end
				-- we're done with this set of browse results
				if private.callback then
					private.callback(private.query)
				end
				if private.browseIsNoScan or private.query:_BrowseIsDone() then
					-- we're done
					return "ST_BROWSE_DONE"
				else
					-- move on to the next page
					return "ST_BROWSE_REQUEST_MORE"
				end
			end)
			:AddTransition("ST_BROWSE_CHECKING")
			:AddTransition("ST_BROWSE_DONE")
			:AddTransition("ST_BROWSE_REQUEST_MORE")
			:AddTransition("ST_CANCELING")
			:AddEventTransition("EV_RETRY", "ST_BROWSE_CHECKING")
			:AddEventTransition("EV_BROWSE_RESULTS_UPDATED", "ST_BROWSE_CHECKING")
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_BROWSE_REQUEST_MORE")
			:SetOnEnter(function(_, isRetry)
				if private.query:_BrowseIsDone(isRetry) then
					return "ST_BROWSE_CHECKING"
				else
					private.HandleAuctionHouseWrapperResult(private.query:_BrowseRequestMore(isRetry))
				end
			end)
			:AddTransition("ST_BROWSE_REQUEST_MORE")
			:AddTransition("ST_BROWSE_CHECKING")
			:AddTransition("ST_CANCELING")
			:AddEvent("EV_FUTURE_SUCCESS", function(_, ...)
				if TSM.IsWowClassic() then
					private.browseIndex = 1
					wipe(private.browsePendingIndexes)
				else
					local newResults = ...
					for _, result in ipairs(newResults) do
						local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey)
						private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity)
					end
				end
				return "ST_BROWSE_CHECKING"
			end)
			:AddEvent("EV_RETRY", function()
				return "ST_BROWSE_REQUEST_MORE", true
			end)
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_BROWSE_DONE")
			:SetOnEnter(function()
				private.HandleRequestDone(true)
				return "ST_INIT"
			end)
			:AddTransition("ST_INIT")
		)
		:AddState(FSM.NewState("ST_SEARCH_GET_KEY")
			:SetOnEnter(function()
				assert(not TSM.IsWowClassic())
				if not private.searchRow:SearchIsReady() then
					Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
					return
				end
				return "ST_SEARCH_SEND"
			end)
			:AddTransition("ST_SEARCH_GET_KEY")
			:AddTransition("ST_SEARCH_SEND")
			:AddTransition("ST_CANCELING")
			:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_SEND")
			:AddEventTransition("EV_RETRY", "ST_SEARCH_GET_KEY")
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_SEARCH_SEND")
			:SetOnEnter(function()
				assert(not TSM.IsWowClassic())
				if not AuctionHouseWrapper.IsOpen() then
					return "ST_CANCELING"
				end
				if private.useCachedData and private.searchRow:HasCachedSearchData() then
					return "ST_SEARCH_REQUEST_MORE"
				end
				local future, delayTime = private.searchRow:SearchSend()
				if future then
					private.HandleAuctionHouseWrapperResult(future)
				else
					if not delayTime then
						Log.Err("Failed to send search query - retrying")
						delayTime = 0.5
					end
					-- try again after a delay
					Delay.AfterTime("AUCTION_SCANNER_RETRY", delayTime, private.RetryHandler)
				end
			end)
			:AddTransition("ST_SEARCH_SEND")
			:AddTransition("ST_SEARCH_REQUEST_MORE")
			:AddTransition("ST_CANCELING")
			:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_REQUEST_MORE")
			:AddEventTransition("EV_RETRY", "ST_SEARCH_SEND")
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_SEARCH_REQUEST_MORE")
			:SetOnEnter(function()
				assert(not TSM.IsWowClassic())
				local baseItemString = private.searchRow:GetBaseItemString()
				-- get if the item is a commodity or not
				local isCommodity = ItemInfo.IsCommodity(baseItemString)
				if isCommodity == nil then
					Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
					return
				end

				local isDone, future = private.searchRow:SearchCheckStatus()
				if isDone then
					return "ST_SEARCH_CHECKING"
				elseif future then
					private.HandleAuctionHouseWrapperResult(future)
				else
					Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
				end
			end)
			:AddTransition("ST_SEARCH_SEND")
			:AddTransition("ST_SEARCH_CHECKING")
			:AddTransition("ST_CANCELING")
			:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_CHECKING")
			:AddEventTransition("EV_RETRY", "ST_SEARCH_SEND")
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_SEARCH_CHECKING")
			:SetOnEnter(function()
				assert(not TSM.IsWowClassic())
				Delay.Cancel("AUCTION_SCANNER_RETRY")
				private.searchRow:PopulateSubRows(private.browseId)

				-- check if all the sub rows have their data
				local isDone = true
				for _, subRow in private.searchRow:SubRowIterator(true) do
					if not subRow:HasRawData() or not subRow:HasItemString() then
						isDone = false
					elseif private.resolveSellers and not subRow:HasOwners() and not private.query:_IsFiltered(subRow, true) then
						-- waiting for owner info
						isDone = false
					end
				end

				if not isDone and private.retryCount >= 100 then
					-- out of retries, so give up
					return "ST_SEARCH_DONE", false
				elseif not isDone then
					-- we'll try again
					private.retryCount = private.retryCount + 1
					Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
					return
				end

				-- filter the sub rows we don't care about
				private.searchRow:FilterSubRows(private.query)

				if private.callback then
					private.callback(private.query, private.searchRow)
				end
				if private.searchRow:SearchNext() then
					-- there is more to search
					return "ST_SEARCH_GET_KEY"
				else
					-- scanned everything
					return "ST_SEARCH_DONE", true
				end
			end)
			:AddTransition("ST_SEARCH_GET_KEY")
			:AddTransition("ST_SEARCH_CHECKING")
			:AddTransition("ST_SEARCH_DONE")
			:AddTransition("ST_CANCELING")
			:AddEventTransition("EV_RETRY", "ST_SEARCH_CHECKING")
			:AddEventTransition("EV_SEARCH_RESULTS_UPDATED", "ST_SEARCH_CHECKING")
			:AddEventTransition("EV_CANCEL", "ST_CANCELING")
		)
		:AddState(FSM.NewState("ST_SEARCH_DONE")
			:SetOnEnter(function(_, result)
				assert(not TSM.IsWowClassic())
				private.HandleRequestDone(result)
				return "ST_INIT"
			end)
			:AddTransition("ST_INIT")
		)
		:AddState(FSM.NewState("ST_CANCELING")
			:SetOnEnter(function()
				Delay.Cancel("AUCTION_SCANNER_DONE")
				return "ST_INIT"
			end)
			:AddTransition("ST_INIT")
		)
		:Init("ST_INIT", nil)
end)



-- ============================================================================
-- Module Functions
-- ============================================================================

function Scanner.Browse(query, resolveSellers, results, callback)
	private.requestFuture:Start()
	private.fsm:ProcessEvent("EV_START_BROWSE", query, resolveSellers, results, callback)
	return private.requestFuture
end

function Scanner.BrowseNoScan(query, itemKeys, results, callback)
	assert(not TSM.IsWowClassic())
	private.requestFuture:Start()
	private.fsm:ProcessEvent("EV_START_BROWSE_NO_SCAN", query, itemKeys, results, callback)
	return private.requestFuture
end

function Scanner.Search(query, resolveSellers, useCachedData, browseRow, callback)
	assert(not TSM.IsWowClassic())
	private.requestFuture:Start()
	private.fsm:ProcessEvent("EV_START_SEARCH", query, resolveSellers, useCachedData, browseRow, callback)
	return private.requestFuture
end

function Scanner.Cancel()
	private.requestFuture:Done(false)
end



-- ============================================================================
-- Private Helper Functions
-- ============================================================================

function private.ProcessBrowseResult(baseItemString, ...)
	if private.browseResults[baseItemString] then
		private.browseResults[baseItemString]:Merge(...)
	else
		private.browseResults[baseItemString] = ResultRow.Get(private.query, ...)
	end
	return private.browseResults[baseItemString]
end

function private.PendingFutureDoneHandler()
	local result = private.pendingFuture:GetValue()
	private.pendingFuture = nil
	if result then
		private.fsm:ProcessEvent("EV_FUTURE_SUCCESS", result)
	else
		Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
	end
end

function private.RetryHandler()
	private.fsm:SetLoggingEnabled(false)
	private.fsm:ProcessEvent("EV_RETRY")
	private.fsm:SetLoggingEnabled(true)
end

function private.RequestDoneHandler()
	local result = private.requestResult
	private.requestResult = nil
	private.requestFuture:Done(result)
end

function private.HandleAuctionHouseWrapperResult(future)
	if future then
		private.pendingFuture = future
		private.pendingFuture:SetScript("OnDone", private.PendingFutureDoneHandler)
	else
		Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
	end
end

function private.HandleRequestDone(result)
	private.requestResult = result
	-- delay a bit so that we complete our current FSM transition
	Delay.AfterTime("AUCTION_SCANNER_DONE", 0, private.RequestDoneHandler)
end

function private.CheckBrowseResults()
	if TSM.IsWowClassic() then
		-- process as many auctions as we can
		local numAuctions = GetNumAuctionItems("list")
		for i = #private.browsePendingIndexes, 1, -1 do
			local index = private.browsePendingIndexes[i]
			if private.ProcessBrowseResultClassic(index) then
				tremove(private.browsePendingIndexes, i)
			end
		end
		local index = private.browseIndex
		while index <= numAuctions and #private.browsePendingIndexes < 50 do
			if not private.ProcessBrowseResultClassic(index) then
				tinsert(private.browsePendingIndexes, index)
			end
			index = index + 1
		end
		private.browseIndex = index
		if private.browseIndex <= numAuctions or #private.browsePendingIndexes > 0 then
			return false
		end
	end

	-- check if there's data still pending
	local hasPendingData = false
	for _, row in pairs(private.browseResults) do
		if not row:PopulateBrowseData() then
			hasPendingData = true
			-- keep going so we issue requests for all pending rows
		end
	end
	if hasPendingData then
		return false
	end

	-- filter the results
	local numRemoved = 0
	for baseItemString, row in pairs(private.browseResults) do
		-- filter the itemKeys we don't care about and rows which don't match the query
		if row:IsFiltered(private.query) then
			private.browseResults[baseItemString] = nil
			numRemoved = numRemoved + 1
		end
		if TSM.IsWowClassic() then
			if row:FilterSubRows(private.query) then
				-- no more subRows, so filter the entire row
				private.browseResults[baseItemString] = nil
				numRemoved = numRemoved + 1
			end
		end
	end
	if numRemoved > 0 then
		Log.Info("Removed %d results", numRemoved)
	end

	return true
end

function private.ProcessBrowseResultClassic(index)
	local rawName, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", index)
	local itemLink = GetAuctionItemLink("list", index)
	local baseItemString = ItemString.GetBase(itemLink)
	local timeLeft = GetAuctionItemTimeLeft("list", index)
	seller = Util.FixSellerName(seller, sellerFull)
	if not rawName or rawName == "" or not baseItemString or not buyout or not stackSize or not timeLeft or (not seller and private.resolveSellers) then
		return false
	end
	local row = private.ProcessBrowseResult(baseItemString, itemLink)
	-- amazingly, GetAuctionItemLink could return nil the next time it's called (within the same frame), so pass through our itemLink
	row:PopulateSubRows(private.browseId, index, itemLink)
	return true
end
