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

local _, TSM = ...
local Util = TSM.Auctioning:NewPackage("Util")
local TempTable = TSM.Include("Util.TempTable")
local Vararg = TSM.Include("Util.Vararg")
local String = TSM.Include("Util.String")
local Math = TSM.Include("Util.Math")
local CustomPrice = TSM.Include("Service.CustomPrice")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local private = {
	priceCache = {},
}
local INVALID_PRICE = {}
local VALID_PRICE_KEYS = {
	minPrice = true,
	normalPrice = true,
	maxPrice = true,
	undercut = true,
	cancelRepostThreshold = true,
	priceReset = true,
	aboveMax = true,
	postCap = true,
	stackSize = true,
	keepQuantity = true,
	maxExpires = true,
}
local IS_GOLD_PRICE_KEY = {
	minPrice = true,
	normalPrice = true,
	maxPrice = true,
	undercut = TSM.IsWowClassic(),
	priceReset = true,
	aboveMax = true,
}



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

function Util.GetPrice(key, operation, itemString)
	assert(VALID_PRICE_KEYS[key])
	local cacheKey = key..tostring(operation)..itemString
	if private.priceCache.updateTime ~= GetTime() then
		wipe(private.priceCache)
		private.priceCache.updateTime = GetTime()
	end
	if not private.priceCache[cacheKey] then
		local value = nil
		if key == "aboveMax" or key == "priceReset" then
			-- redirect to the selected price (if applicable)
			local priceKey = operation[key]
			if VALID_PRICE_KEYS[priceKey] then
				value = Util.GetPrice(priceKey, operation, itemString)
			end
		else
			value = CustomPrice.GetValue(operation[key], itemString, not IS_GOLD_PRICE_KEY[key])
		end
		if not TSM.IsWowClassic() and IS_GOLD_PRICE_KEY[key] then
			value = value and Math.Ceil(value, COPPER_PER_SILVER) or nil
		else
			value = value and Math.Round(value) or nil
		end
		local minValue, maxValue = TSM.Operations.Auctioning.GetMinMaxValues(key)
		private.priceCache[cacheKey] = (value and value >= minValue and value <= maxValue) and value or INVALID_PRICE
	end
	if private.priceCache[cacheKey] == INVALID_PRICE then
		return nil
	end
	return private.priceCache[cacheKey]
end

function Util.GetLowestAuction(subRows, itemString, operationSettings, resultTbl)
	if not TSM.IsWowClassic() then
		local foundLowest = false
		for _, subRow in ipairs(subRows) do
			local _, itemBuyout = subRow:GetBuyouts()
			local quantity = subRow:GetQuantities()
			local timeLeft = subRow:GetListingInfo()
			if not foundLowest and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
				local ownerStr = subRow:GetOwnerInfo()
				local _, auctionId = subRow:GetListingInfo()
				local _, itemMinBid = subRow:GetBidInfo()
				local firstSeller = strsplit(",", ownerStr)
				resultTbl.buyout = itemBuyout
				resultTbl.bid = itemMinBid
				resultTbl.seller = firstSeller
				resultTbl.auctionId = auctionId
				resultTbl.isWhitelist = TSM.db.factionrealm.auctioningOptions.whitelist[strlower(firstSeller)] and true or false
				resultTbl.isPlayer = PlayerInfo.IsPlayer(firstSeller, true, true, true)
				if not subRow:HasOwners() then
					resultTbl.hasInvalidSeller = true
				end
				foundLowest = true
			end
		end
		return foundLowest
	else
		local hasInvalidSeller = nil
		local ignoreWhitelist = nil
		local lowestItemBuyout = nil
		local lowestAuction = nil
		for _, subRow in ipairs(subRows) do
			local _, itemBuyout = subRow:GetBuyouts()
			local quantity = subRow:GetQuantities()
			local timeLeft = subRow:GetListingInfo()
			if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
				assert(itemBuyout and itemBuyout > 0)
				lowestItemBuyout = lowestItemBuyout or itemBuyout
				if itemBuyout == lowestItemBuyout then
					local ownerStr = subRow:GetOwnerInfo()
					local _, auctionId = subRow:GetListingInfo()
					local _, itemMinBid = subRow:GetBidInfo()
					local temp = TempTable.Acquire()
					temp.buyout = itemBuyout
					temp.bid = itemMinBid
					temp.seller = ownerStr
					temp.auctionId = auctionId
					temp.isWhitelist = TSM.db.factionrealm.auctioningOptions.whitelist[strlower(ownerStr)] and true or false
					temp.isPlayer = PlayerInfo.IsPlayer(ownerStr, true, true, true)
					if not temp.isWhitelist and not temp.isPlayer then
						-- there is a non-whitelisted competitor, so we don't care if a whitelisted competitor also posts at this price
						ignoreWhitelist = true
					end
					if not subRow:HasOwners() and next(TSM.db.factionrealm.auctioningOptions.whitelist) then
						hasInvalidSeller = true
					end
					if operationSettings.blacklist then
						for _, player in Vararg.Iterator(strsplit(",", operationSettings.blacklist)) do
							if String.SeparatedContains(strlower(ownerStr), ",", strlower(strtrim(player))) then
								temp.isBlacklist = true
							end
						end
					end
					if not lowestAuction then
						lowestAuction = temp
					elseif private.LowestAuctionCompare(temp, lowestAuction) then
						TempTable.Release(lowestAuction)
						lowestAuction = temp
					else
						TempTable.Release(temp)
					end
				end
			end
		end
		if not lowestAuction then
			return false
		end
		for k, v in pairs(lowestAuction) do
			resultTbl[k] = v
		end
		TempTable.Release(lowestAuction)
		if resultTbl.isWhitelist and ignoreWhitelist then
			resultTbl.isWhitelist = false
		end
		resultTbl.hasInvalidSeller = hasInvalidSeller
		return true
	end
end

function Util.GetPlayerAuctionCount(subRows, itemString, operationSettings, findBid, findBuyout, findStackSize)
	local playerQuantity = 0
	for _, subRow in ipairs(subRows) do
		local _, itemBuyout = subRow:GetBuyouts()
		local quantity = subRow:GetQuantities()
		local timeLeft = subRow:GetListingInfo()
		if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
			local _, itemMinBid = subRow:GetBidInfo()
			if itemMinBid == findBid and itemBuyout == findBuyout and (not TSM.IsWowClassic() or quantity == findStackSize) then
				local count = private.GetPlayerAuctionCount(subRow)
				if not TSM.IsWowClassic() and count == 0 and playerQuantity > 0 then
					-- there's another player's auction after ours, so stop counting
					break
				end
				playerQuantity = playerQuantity + count
			end
		end
	end
	return playerQuantity
end

function Util.GetPlayerLowestBuyout(subRows, itemString, operationSettings)
	local lowestItemBuyout, lowestItemAuctionId = nil, nil
	for _, subRow in ipairs(subRows) do
		local _, itemBuyout = subRow:GetBuyouts()
		local quantity = subRow:GetQuantities()
		local timeLeft, auctionId = subRow:GetListingInfo()
		if not lowestItemBuyout and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) > 0 then
			lowestItemBuyout = itemBuyout
			lowestItemAuctionId = auctionId
		end
	end
	return lowestItemBuyout, lowestItemAuctionId
end

function Util.GetLowestNonPlayerAuctionId(subRows, itemString, operationSettings, lowestItemBuyout)
	local lowestItemAuctionId = nil
	for _, subRow in ipairs(subRows) do
		local _, itemBuyout = subRow:GetBuyouts()
		local quantity = subRow:GetQuantities()
		local timeLeft, auctionId = subRow:GetListingInfo()
		if not lowestItemAuctionId and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) == 0 and itemBuyout == lowestItemBuyout then
			lowestItemAuctionId = auctionId
		end
	end
	return lowestItemAuctionId
end

function Util.IsPlayerOnlySeller(subRows, itemString, operationSettings)
	local isOnly = true
	for _, subRow in ipairs(subRows) do
		local _, itemBuyout = subRow:GetBuyouts()
		local quantity = subRow:GetQuantities()
		local timeLeft = subRow:GetListingInfo()
		if isOnly and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) < (TSM.IsWowClassic() and 1 or quantity) then
			isOnly = false
		end
	end
	return isOnly
end

function Util.GetNextLowestItemBuyout(subRows, itemString, lowestAuction, operationSettings)
	local nextLowestItemBuyout = nil
	for _, subRow in ipairs(subRows) do
		local _, itemBuyout = subRow:GetBuyouts()
		local quantity = subRow:GetQuantities()
		local timeLeft, auctionId = subRow:GetListingInfo()
		local isLower = itemBuyout > lowestAuction.buyout or (itemBuyout == lowestAuction.buyout and auctionId < lowestAuction.auctionId)
		if not nextLowestItemBuyout and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and isLower then
			nextLowestItemBuyout = itemBuyout
		end
	end
	return nextLowestItemBuyout
end

function Util.GetQueueStatus(query)
	local numProcessed, numConfirmed, numFailed, totalNum = 0, 0, 0, 0
	query:OrderBy("auctionId", true)
	for _, row in query:Iterator() do
		local rowNumStacks, rowNumProcessed, rowNumConfirmed, rowNumFailed = row:GetFields("numStacks", "numProcessed", "numConfirmed", "numFailed")
		totalNum = totalNum + rowNumStacks
		numProcessed = numProcessed + rowNumProcessed
		numConfirmed = numConfirmed + rowNumConfirmed
		numFailed = numFailed + rowNumFailed
	end
	query:Release()
	return numProcessed, numConfirmed, numFailed, totalNum
end

function Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft)
	if timeLeft <= operationSettings.ignoreLowDuration then
		-- ignoring low duration
		return true
	elseif TSM.IsWowClassic() and operationSettings.matchStackSize and quantity ~= Util.GetPrice("stackSize", operationSettings, itemString) then
		-- matching stack size
		return true
	elseif operationSettings.priceReset == "ignore" then
		local minPrice = Util.GetPrice("minPrice", operationSettings, itemString)
		local undercut = Util.GetPrice("undercut", operationSettings, itemString)
		if minPrice and itemBuyout - undercut < minPrice then
			-- ignoring auctions below threshold
			return true
		end
	end
	return false
end

function Util.GetFilteredSubRows(query, itemString, operationSettings, result)
	for _, subRow in query:ItemSubRowIterator(itemString) do
		local _, itemBuyout = subRow:GetBuyouts()
		local quantity = subRow:GetQuantities()
		local timeLeft = subRow:GetListingInfo()
		if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
			tinsert(result, subRow)
		end
	end
	sort(result, private.SubRowSortHelper)
end



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

function private.SubRowSortHelper(a, b)
	local _, aItemBuyout = a:GetBuyouts()
	local _, bItemBuyout = b:GetBuyouts()
	if aItemBuyout ~= bItemBuyout then
		return aItemBuyout < bItemBuyout
	end
	local _, aAuctionId = a:GetListingInfo()
	local _, bAuctionId = b:GetListingInfo()
	return aAuctionId > bAuctionId
end

function private.LowestAuctionCompare(a, b)
	if a.isBlacklist ~= b.isBlacklist then
		return a.isBlacklist
	end
	if a.isWhitelist ~= b.isWhitelist then
		return a.isWhitelist
	end
	if a.auctionId ~= b.auctionId then
		return a.auctionId > b.auctionId
	end
	if a.isPlayer ~= b.isPlayer then
		return b.isPlayer
	end
	return tostring(a) < tostring(b)
end

function private.GetPlayerAuctionCount(subRow)
	local ownerStr, numOwnerItems = subRow:GetOwnerInfo()
	if TSM.IsWowClassic() then
		return PlayerInfo.IsPlayer(ownerStr, true, true, true) and select(2, subRow:GetQuantities()) or 0
	else
		return numOwnerItems
	end
end
