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

local _, TSM = ...
local Buy = TSM.Vendoring:NewPackage("Buy")
local Database = TSM.Include("Util.Database")
local Delay = TSM.Include("Util.Delay")
local Event = TSM.Include("Util.Event")
local Log = TSM.Include("Util.Log")
local TempTable = TSM.Include("Util.TempTable")
local Theme = TSM.Include("Util.Theme")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Inventory = TSM.Include("Service.Inventory")
local private = {
	merchantDB = nil,
	pendingIndex = nil,
	pendingQuantity = 0,
}
local FIRST_BUY_TIMEOUT = 5
local FIRST_BUY_TIMEOUT_PER_STACK = 1
local CONSECUTIVE_BUY_TIMEOUT = 5



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

function Buy.OnInitialize()
	private.merchantDB = Database.NewSchema("MERCHANT")
		:AddUniqueNumberField("index")
		:AddStringField("itemString")
		:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
		:AddNumberField("price")
		:AddStringField("costItemsText")
		:AddStringField("firstCostItemString")
		:AddNumberField("stackSize")
		:AddNumberField("numAvailable")
		:Commit()
	Event.Register("MERCHANT_SHOW", private.MerchantShowEventHandler)
	Event.Register("MERCHANT_CLOSED", private.MerchantClosedEventHandler)
	Event.Register("MERCHANT_UPDATE", private.MerchantUpdateEventHandler)
	Event.Register("CHAT_MSG_LOOT", private.ChatMsgLootEventHandler)
end

function Buy.CreateMerchantQuery()
	return private.merchantDB:NewQuery()
end

function Buy.NeedsRepair()
	local _, needsRepair = GetRepairAllCost()
	return needsRepair
end

function Buy.CanGuildRepair()
	return Buy.NeedsRepair() and not TSM.IsWowClassic() and CanGuildBankRepair()
end

function Buy.DoGuildRepair()
	RepairAllItems(true)
end

function Buy.DoRepair()
	RepairAllItems()
end

function Buy.GetMaxCanAfford(index)
	local maxCanAfford = math.huge
	local _, _, price, stackSize, _, _, _, extendedCost = GetMerchantItemInfo(index)
	local numAltCurrencies = GetMerchantItemCostInfo(index)
	-- bug with big keech vendor returning extendedCost = true for gold only items
	if numAltCurrencies == 0 then
		extendedCost = false
	end

	-- check the price
	if price > 0 then
		maxCanAfford = min(floor(GetMoney() / price), maxCanAfford)
	end

	-- check the extended cost
	if extendedCost then
		assert(numAltCurrencies > 0)
		for i = 1, numAltCurrencies do
			local _, costNum, costItemLink, currencyName = GetMerchantItemCostItem(index, i)
			local costItemString = ItemString.Get(costItemLink)
			local costNumHave = nil
			if costItemString then
				costNumHave = Inventory.GetBagQuantity(costItemString) + Inventory.GetBankQuantity(costItemString) + Inventory.GetReagentBankQuantity(costItemString)
			elseif currencyName then
				if TSM.IsShadowlands() then
					for j = 1, C_CurrencyInfo.GetCurrencyListSize() do
						local info = C_CurrencyInfo.GetCurrencyListInfo(j)
						if not info.isHeader and info.name == currencyName then
							costNumHave = info.quantity
							break
						end
					end
				else
					for j = 1, GetCurrencyListSize() do
						local name, isHeader, _, _, _, count = GetCurrencyListInfo(j)
						if not isHeader and name == currencyName then
							costNumHave = count
							break
						end
					end
				end
			end
			if costNumHave then
				maxCanAfford = min(floor(costNumHave / costNum), maxCanAfford)
			end
		end
	end

	return maxCanAfford * stackSize
end

function Buy.BuyItem(itemString, quantity)
	local index = private.GetFirstIndex(itemString)
	if not index then
		return
	end
	private.BuyIndex(index, quantity)
end

function Buy.BuyItemIndex(index, quantity)
	private.BuyIndex(index, quantity)
end

function Buy.CanBuyItem(itemString)
	local index = private.GetFirstIndex(itemString)
	return index and true or false
end



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

function private.MerchantShowEventHandler()
	Delay.AfterFrame("UPDATE_MERCHANT_DB", 1, private.UpdateMerchantDB)
end

function private.MerchantClosedEventHandler()
	private.ClearPendingContext()
	Delay.Cancel("UPDATE_MERCHANT_DB")
	Delay.Cancel("RESCAN_MERCHANT_DB")
	private.merchantDB:Truncate()
end

function private.MerchantUpdateEventHandler()
	Delay.AfterFrame("UPDATE_MERCHANT_DB", 1, private.UpdateMerchantDB)
end

function private.UpdateMerchantDB()
	local needsRetry = false
	private.merchantDB:TruncateAndBulkInsertStart()
	for i = 1, GetMerchantNumItems() do
		local itemLink = GetMerchantItemLink(i)
		local itemString = ItemString.Get(itemLink)
		if itemString then
			ItemInfo.StoreItemInfoByLink(itemLink)
			local _, _, price, stackSize, numAvailable, _, _, extendedCost = GetMerchantItemInfo(i)
			local numAltCurrencies = GetMerchantItemCostInfo(i)
			-- bug with big keech vendor returning extendedCost = true for gold only items
			if numAltCurrencies == 0 then
				extendedCost = false
			end
			local costItemsText, firstCostItemString = "", ""
			if extendedCost then
				assert(numAltCurrencies > 0)
				local costItems = TempTable.Acquire()
				for j = 1, numAltCurrencies do
					local _, costNum, costItemLink = GetMerchantItemCostItem(i, j)
					local costItemString = ItemString.Get(costItemLink)
					local texture = nil
					if not costItemLink then
						needsRetry = true
					elseif costItemString then
						firstCostItemString = firstCostItemString ~= "" and firstCostItemString or costItemString
						texture = ItemInfo.GetTexture(costItemString)
					elseif strmatch(costItemLink, "currency:") then
						if TSM.IsShadowlands() then
							texture = C_CurrencyInfo.GetCurrencyInfoFromLink(costItemLink).iconFileID
						else
							_, _, texture = GetCurrencyInfo(costItemLink)
						end
						firstCostItemString = strmatch(costItemLink, "(currency:%d+)")
					else
						error(format("Unknown item cost (%d, %d, %s)", i, costNum, tostring(costItemLink)))
					end
					if TSM.Vendoring.Buy.GetMaxCanAfford(i) < stackSize then
						costNum = Theme.GetFeedbackColor("RED"):ColorText(costNum)
					end
					tinsert(costItems, costNum.." |T"..(texture or "")..":12|t")
				end
				costItemsText = table.concat(costItems, " ")
				TempTable.Release(costItems)
			end
			private.merchantDB:BulkInsertNewRow(i, itemString, price, costItemsText, firstCostItemString, stackSize, numAvailable)
		end
	end
	private.merchantDB:BulkInsertEnd()

	if needsRetry then
		Log.Err("Failed to scan merchant")
		Delay.AfterTime("RESCAN_MERCHANT_DB", 0.2, private.UpdateMerchantDB)
	else
		Delay.Cancel("RESCAN_MERCHANT_DB")
	end
end

function private.GetFirstIndex(itemString)
	local index = Buy.CreateMerchantQuery()
		:Equal("itemString", itemString)
		:OrderBy("index", true)
		:Select("index")
		:GetFirstResultAndRelease()
	if not index and ItemString.GetBaseFast(itemString) == itemString then
		index = Buy.CreateMerchantQuery()
			:Equal("baseItemString", itemString)
			:OrderBy("index", true)
			:Select("index")
			:GetFirstResultAndRelease()
	end
	return index
end

function private.BuyIndex(index, quantity)
	local maxStack = GetMerchantItemMaxStack(index)
	quantity = min(quantity, Buy.GetMaxCanAfford(index))
	if quantity == 0 then
		return
	end
	private.ClearPendingContext()
	private.pendingIndex = index
	local numStacks = 0
	while quantity > 0 do
		local buyQuantity = min(quantity, maxStack)
		BuyMerchantItem(index, buyQuantity)
		private.pendingQuantity = private.pendingQuantity + buyQuantity
		quantity = quantity - buyQuantity
		numStacks = numStacks + 1
	end
	Log.Info("Buying %d of %d (%d stacks)", private.pendingQuantity, index, numStacks)
	Delay.AfterTime("VENDORING_BUY_TIMEOUT", numStacks * FIRST_BUY_TIMEOUT_PER_STACK + FIRST_BUY_TIMEOUT, private.BuyTimeout)
end

function private.ChatMsgLootEventHandler(_, msg)
	if not private.pendingIndex then
		return
	end
	local link = GetMerchantItemLink(private.pendingIndex)
	if not link then
		Log.Err("Failed to get link (%s)", private.pendingIndex)
		private.ClearPendingContext()
		return
	end
	local quantity = nil
	if msg == format(LOOT_ITEM_PUSHED_SELF, link) then
		quantity = 1
	else
		for i = 1, GetMerchantItemMaxStack(private.pendingIndex) do
			if msg == format(LOOT_ITEM_PUSHED_SELF_MULTIPLE, link, i) then
				quantity = i
				break
			end
		end
	end
	Log.Info("Got CHAT_MSG_LOOT(%s) with a quantity of %s (%d pending)", msg, tostring(quantity), private.pendingQuantity)
	if not quantity then
		return
	end
	private.pendingQuantity = private.pendingQuantity - quantity
	if private.pendingQuantity <= 0 then
		-- we're done
		private.ClearPendingContext()
		return
	end

	-- reset the timeout
	Delay.Cancel("VENDORING_BUY_TIMEOUT")
	Delay.AfterTime("VENDORING_BUY_TIMEOUT", CONSECUTIVE_BUY_TIMEOUT, private.BuyTimeout)
end

function private.BuyTimeout()
	Log.Warn("Retrying buying (%d, %d)", private.pendingIndex, private.pendingQuantity)
	Buy.BuyItemIndex(private.pendingIndex, private.pendingQuantity)
end

function private.ClearPendingContext()
	private.pendingIndex = nil
	private.pendingQuantity = 0
	Delay.Cancel("VENDORING_BUY_TIMEOUT")
end
