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

local _, TSM = ...
local MyAuctions = TSM:NewPackage("MyAuctions")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local ItemInfo = TSM.Include("Service.ItemInfo")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local private = {
	pendingDB = nil,
	ahOpen = false,
	pendingHashes = {},
	expectedCounts = {},
	auctionInfo = { numPosted = 0, numSold = 0, postedGold = 0, soldGold = 0 },
	dbHashFields = {},
	pendingFuture = nil,
}



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

function MyAuctions.OnInitialize()
	private.pendingDB = Database.NewSchema("MY_AUCTIONS_PENDING")
		:AddUniqueNumberField("index")
		:AddNumberField("hash")
		:AddBooleanField("isPending")
		:AddNumberField("pendingAuctionId")
		:AddIndex("index")
		:Commit()
	for field in AuctionTracking.DatabaseFieldIterator() do
		if field ~= "index" and field ~= "auctionId" then
			tinsert(private.dbHashFields, field)
		end
	end

	Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowEventHandler)
	Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseHideEventHandler)
	Event.Register("CHAT_MSG_SYSTEM", private.ChatMsgSystemEventHandler)
	Event.Register("UI_ERROR_MESSAGE", private.UIErrorMessageEventHandler)
	AuctionTracking.RegisterCallback(private.OnAuctionsUpdated)
end

function MyAuctions.CreateQuery()
	local query = AuctionTracking.CreateQuery()
		:LeftJoin(private.pendingDB, "index")
		:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
		:VirtualField("group", "string", private.AuctionsGetGroupText, "itemString")
	if TSM.IsWowClassic() then
		query:OrderBy("index", false)
	else
		query:OrderBy("saleStatus", false)
		query:OrderBy("name", true)
		query:OrderBy("auctionId", true)
	end
	return query
end

function MyAuctions.CancelAuction(auctionId)
	local row = private.pendingDB:NewQuery()
		:Equal("pendingAuctionId", auctionId)
		:GetFirstResultAndRelease()
	local hash = row:GetField("hash")
	assert(hash)

	Log.Info("Canceling (auctionId=%d, hash=%d)", auctionId, hash)
	if TSM.IsWowClassic() then
		CancelAuction(auctionId)
	else
		private.pendingFuture = AuctionHouseWrapper.CancelAuction(auctionId)
		if not private.pendingFuture then
			Log.PrintUser(L["Failed to cancel auction due to the auction house being busy. Ensure no other addons are scanning the AH and try again."])
			return
		end
		private.pendingFuture:SetScript("OnDone", private.PendingFutureOnDone)
	end

	if private.expectedCounts[hash] and private.expectedCounts[hash] > 0 then
		private.expectedCounts[hash] = private.expectedCounts[hash] - 1
	else
		private.expectedCounts[hash] = private.GetNumRowsByHash(hash) - 1
	end
	assert(private.expectedCounts[hash] >= 0)
	assert(not row:GetField("isPending"))
	row:SetField("isPending", true)
		:Update()
	row:Release()

	tinsert(private.pendingHashes, hash)
end

function MyAuctions.CanCancel(index)
	if TSM.IsWowClassic() then
		local numPending = private.pendingDB:NewQuery()
			:Equal("isPending", true)
			:LessThanOrEqual("index", index)
			:CountAndRelease()
		return numPending == 0
	else
		return not private.pendingFuture
	end
end

function MyAuctions.GetNumPending()
	if TSM.IsWowClassic() then
		return private.pendingDB:NewQuery()
			:Equal("isPending", true)
			:CountAndRelease()
	else
		return private.pendingFuture and 1 or 0
	end
end

function MyAuctions.GetAuctionInfo()
	if not private.ahOpen then
		return
	end
	return private.auctionInfo.numPosted, private.auctionInfo.numSold, private.auctionInfo.postedGold, private.auctionInfo.soldGold
end



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

function private.AuctionHouseShowEventHandler()
	private.ahOpen = true
end

function private.AuctionHouseHideEventHandler()
	private.ahOpen = false
	if private.pendingFuture then
		private.pendingFuture:Cancel()
		private.pendingFuture = nil
	end
end

function private.ChatMsgSystemEventHandler(_, msg)
	if msg == ERR_AUCTION_REMOVED and #private.pendingHashes > 0 and TSM.IsWowClassic() then
		local hash = tremove(private.pendingHashes, 1)
		assert(hash)
		Log.Info("Confirmed (hash=%d)", hash)
	end
end

function private.UIErrorMessageEventHandler(_, _, msg)
	if (msg == ERR_ITEM_NOT_FOUND or msg == ERR_NOT_ENOUGH_MONEY) and #private.pendingHashes > 0 and TSM.IsWowClassic() then
		local hash = tremove(private.pendingHashes, 1)
		assert(hash)
		Log.Info("Failed to cancel (hash=%d)", hash)
		if private.expectedCounts[hash] then
			private.expectedCounts[hash] = private.expectedCounts[hash] + 1
		end
	end
end

function private.PendingFutureOnDone()
	local result = private.pendingFuture:GetValue()
	private.pendingFuture = nil
	local hash = tremove(private.pendingHashes, 1)
	assert(hash)
	if result then
		Log.Info("Confirmed (hash=%d)", hash)
	else
		Log.Info("Failed to cancel (hash=%d)", hash)
		if private.expectedCounts[hash] then
			private.expectedCounts[hash] = private.expectedCounts[hash] + 1
		end
		private.OnAuctionsUpdated()
		AuctionTracking.QueryOwnedAuctions()
	end
end

function private.GetNumRowsByHash(hash)
	return private.pendingDB:NewQuery()
		:Equal("hash", hash)
		:CountAndRelease()
end

function private.AuctionsGetGroupText(itemString)
	local groupPath = TSM.Groups.GetPathByItem(itemString)
	if not groupPath then
		return ""
	end
	return groupPath
end

function private.OnAuctionsUpdated()
	local minPendingIndexByHash = TempTable.Acquire()
	local numByHash = TempTable.Acquire()
	local query = AuctionTracking.CreateQuery()
		:OrderBy("index", true)
	for _, row in query:Iterator() do
		local index = row:GetField("index")
		local hash = row:CalculateHash(private.dbHashFields)
		numByHash[hash] = (numByHash[hash] or 0) + 1
		if not minPendingIndexByHash[hash] and private.pendingDB:GetUniqueRowField("index", index, "isPending") then
			minPendingIndexByHash[hash] = index
		end
	end
	local numUsed = TempTable.Acquire()
	private.pendingDB:TruncateAndBulkInsertStart()
	for _, row in query:Iterator() do
		local hash = row:CalculateHash(private.dbHashFields)
		assert(numByHash[hash] > 0)
		local expectedCount = private.expectedCounts[hash]
		local isPending = nil
		if not expectedCount then
			-- this was never pending
			isPending = false
		elseif numByHash[hash] <= expectedCount then
			-- this is no longer pending
			isPending = false
			private.expectedCounts[hash] = nil
		elseif row:GetField("index") >= (minPendingIndexByHash[hash] or math.huge) then
			local numPending = numByHash[hash] - expectedCount
			assert(numPending > 0)
			numUsed[hash] = (numUsed[hash] or 0) + 1
			isPending = numUsed[hash] <= numPending
		else
			-- it's a later auction which is pending
			isPending = false
		end
		private.pendingDB:BulkInsertNewRow(row:GetField("index"), hash, isPending, row:GetField("auctionId"))
	end
	private.pendingDB:BulkInsertEnd()
	TempTable.Release(numByHash)
	TempTable.Release(numUsed)
	TempTable.Release(minPendingIndexByHash)

	-- update the player's auction status
	private.auctionInfo.numPosted = 0
	private.auctionInfo.numSold = 0
	private.auctionInfo.postedGold = 0
	private.auctionInfo.soldGold = 0
	for _, row in query:Iterator() do
		local itemString, saleStatus, buyout, currentBid, stackSize = row:GetFields("itemString", "saleStatus", "buyout", "currentBid", "stackSize")
		if saleStatus == 1 then
			private.auctionInfo.numSold = private.auctionInfo.numSold + 1
			-- if somebody did a buyout, then bid will be equal to buyout, otherwise it'll be the winning bid
			private.auctionInfo.soldGold = private.auctionInfo.soldGold + currentBid
		else
			private.auctionInfo.numPosted = private.auctionInfo.numPosted + 1
			if ItemInfo.IsCommodity(itemString) then
				private.auctionInfo.postedGold = private.auctionInfo.postedGold + (buyout * stackSize)
			else
				private.auctionInfo.postedGold = private.auctionInfo.postedGold + buyout
			end
		end
	end
	query:Release()
end
