local game = Game()

--these aren't local so that they can be saved and reloaded, or cleared in the Mod Config Menu
EID.BoC = {}
EID.BoC.CurrentPickupCount = -1
EID.BoC.BagItems = {}
EID.BoC.BagItemsOverride = nil
EID.BoC.RoomQueries = {}
EID.BoC.RoomOverride = nil -- Override items displayed for current room's content
EID.BoC.InventoryQuery = {}
EID.BoC.InventoryOverride = nil -- Override items the player has in its inventory (Cards, pills, etc.)
EID.BoC.FloorQuery = {}
EID.BoC.FloorOverride = nil -- Override total items displayed to be as floor content

EID.RefreshBagTextbox = false

local pickupValues = {
  0x00000000, -- 0 None
  -- Hearts
  0x00000001, -- 1 Red Heart
  0x00000004, -- 2 Soul Heart
  0x00000005, -- 3 Black Heart
  0x00000005, -- 4 Eternal Heart
  0x00000005, -- 5 Gold Heart
  0x00000005, -- 6 Bone Heart
  0x00000001, -- 7 Rotten Heart
  -- Pennies
  0x00000001, -- 8 Penny
  0x00000003, -- 9 Nickel
  0x00000005, -- 10 Dime
  0x00000008, -- 11 Lucky Penny
  -- Keys
  0x00000002, -- 12 Key
  0x00000007, -- 13 Golden Key
  0x00000005, -- 14 Charged Key
  -- Bombs
  0x00000002, -- 15 Bomb
  0x00000007, -- 16 Golden Bomb
  0x0000000a, -- 17 Giga Bomb
  -- Batteries
  0x00000002, -- 18 Micro Battery
  0x00000004, -- 19 Lil' Battery
  0x00000008, -- 20 Mega Battery
  -- Usables
  0x00000002, -- 21 Card
  0x00000002, -- 22 Pill
  0x00000004, -- 23 Rune
  0x00000004, -- 24 Dice Shard
  0x00000002, -- 25 Cracked Key
  -- Added in Update
  0x00000007, -- 26 Golden Penny
  0x00000007, -- 27 Golden Pill
  0x00000007, -- 28 Golden Battery
  0x00000000, -- 29 Tainted ??? Poop
  
  0x00000001,
}
local pickupIDLookup = {
	["10.1"] = {1}, -- Red heart
	["10.2"] = {1}, -- half heart
	["10.3"] = {2}, -- soul heart
	["10.4"] = {4}, -- eternal heart
	["10.5"] = {1, 1}, -- double heart
	["10.6"] = {3}, -- black heart
	["10.7"] = {5}, -- gold heart
	["10.8"] = {2}, -- half soul heart
	["10.9"] = {1}, -- scared red heart
	["10.10"] = {2, 1}, -- blended heart
	["10.11"] = {6}, -- Bone heart
	["10.12"] = {7}, -- Rotten heart
	["20.1"] = {8}, -- Penny
	["20.2"] = {9}, -- Nickel
	["20.3"] = {10}, -- Dime
	["20.4"] = {8, 8}, -- Double penny
	["20.5"] = {11}, -- Lucky Penny
	["20.6"] = {9}, -- Sticky Nickel
	["20.7"] = {26}, -- Golden Penny
	["30.1"] = {12}, -- Key
	["30.2"] = {13}, -- golden Key
	["30.3"] = {12,12}, -- Key Ring
	["30.4"] = {14}, -- charged Key
	["40.1"] = {15}, -- bomb
	["40.2"] = {15,15}, -- double bomb
	["40.4"] = {16}, -- golden bomb
	["40.7"] = {17}, -- giga bomb
	["42.0"] = {29}, -- poop nugget
	["42.1"] = {29}, -- big poop nugget
	["70.14"] = {27}, -- golden pill
	["70.2062"] = {27}, -- golden horse pill
	["90.1"] = {19}, -- Lil Battery
	["90.2"] = {18}, -- Micro Battery
	["90.3"] = {20}, -- Mega Battery
	["90.4"] = {28}, -- Golden Battery
	["300.49"] = {24}, -- Dice shard
	["300.50"] = {21}, -- Emergency Contact
	["300.78"] = {25}, -- Cracked key
}

local function IsTaintedCain()
	-- this check is necessary for tracking Bag usage since Tainted Cain's pocket bag works differently than everyone else's
	return EID.bagPlayer:GetPlayerType() == 23
end

local componentShifts = {
	{0x00000001, 0x00000005, 0x00000010},
	{0x00000001, 0x00000005, 0x00000013},
	{0x00000001, 0x00000009, 0x0000001D},
	{0x00000001, 0x0000000B, 0x00000006},
	{0x00000001, 0x0000000B, 0x00000010},
	{0x00000001, 0x00000013, 0x00000003},
	{0x00000001, 0x00000015, 0x00000014},
	{0x00000001, 0x0000001B, 0x0000001B},
	{0x00000002, 0x00000005, 0x0000000F},
	{0x00000002, 0x00000005, 0x00000015},
	{0x00000002, 0x00000007, 0x00000007},
	{0x00000002, 0x00000007, 0x00000009},
	{0x00000002, 0x00000007, 0x00000019},
	{0x00000002, 0x00000009, 0x0000000F},
	{0x00000002, 0x0000000F, 0x00000011},
	{0x00000002, 0x0000000F, 0x00000019},
	{0x00000002, 0x00000015, 0x00000009},
	{0x00000003, 0x00000001, 0x0000000E},
	{0x00000003, 0x00000003, 0x0000001A},
	{0x00000003, 0x00000003, 0x0000001C},
	{0x00000003, 0x00000003, 0x0000001D},
	{0x00000003, 0x00000005, 0x00000014},
	{0x00000003, 0x00000005, 0x00000016},
	{0x00000003, 0x00000005, 0x00000019},
	{0x00000003, 0x00000007, 0x0000001D},
	{0x00000003, 0x0000000D, 0x00000007},
	{0x00000003, 0x00000017, 0x00000019},
	{0x00000003, 0x00000019, 0x00000018},
	{0x00000003, 0x0000001B, 0x0000000B},
	{0x00000004, 0x00000003, 0x00000011},
	{0x00000004, 0x00000003, 0x0000001B},
	{0x00000004, 0x00000005, 0x0000000F},
	{0x00000005, 0x00000003, 0x00000015},
	{0x00000005, 0x00000007, 0x00000016},
	{0x00000005, 0x00000009, 0x00000007},
	{0x00000005, 0x00000009, 0x0000001C},
	{0x00000005, 0x00000009, 0x0000001F},
	{0x00000005, 0x0000000D, 0x00000006},
	{0x00000005, 0x0000000F, 0x00000011},
	{0x00000005, 0x00000011, 0x0000000D},
	{0x00000005, 0x00000015, 0x0000000C},
	{0x00000005, 0x0000001B, 0x00000008},
	{0x00000005, 0x0000001B, 0x00000015},
	{0x00000005, 0x0000001B, 0x00000019},
	{0x00000005, 0x0000001B, 0x0000001C},
	{0x00000006, 0x00000001, 0x0000000B},
	{0x00000006, 0x00000003, 0x00000011},
	{0x00000006, 0x00000011, 0x00000009},
	{0x00000006, 0x00000015, 0x00000007},
	{0x00000006, 0x00000015, 0x0000000D},
	{0x00000007, 0x00000001, 0x00000009},
	{0x00000007, 0x00000001, 0x00000012},
	{0x00000007, 0x00000001, 0x00000019},
	{0x00000007, 0x0000000D, 0x00000019},
	{0x00000007, 0x00000011, 0x00000015},
	{0x00000007, 0x00000019, 0x0000000C},
	{0x00000007, 0x00000019, 0x00000014},
	{0x00000008, 0x00000007, 0x00000017},
	{0x00000008, 0x00000009, 0x00000017},
	{0x00000009, 0x00000005, 0x0000000E},
	{0x00000009, 0x00000005, 0x00000019},
	{0x00000009, 0x0000000B, 0x00000013},
	{0x00000009, 0x00000015, 0x00000010},
	{0x0000000A, 0x00000009, 0x00000015},
	{0x0000000A, 0x00000009, 0x00000019},
	{0x0000000B, 0x00000007, 0x0000000C},
	{0x0000000B, 0x00000007, 0x00000010},
	{0x0000000B, 0x00000011, 0x0000000D},
	{0x0000000B, 0x00000015, 0x0000000D},
	{0x0000000C, 0x00000009, 0x00000017},
	{0x0000000D, 0x00000003, 0x00000011},
	{0x0000000D, 0x00000003, 0x0000001B},
	{0x0000000D, 0x00000005, 0x00000013},
	{0x0000000D, 0x00000011, 0x0000000F},
	{0x0000000E, 0x00000001, 0x0000000F},
	{0x0000000E, 0x0000000D, 0x0000000F},
	{0x0000000F, 0x00000001, 0x0000001D},
	{0x00000011, 0x0000000F, 0x00000014},
	{0x00000011, 0x0000000F, 0x00000017},
	{0x00000011, 0x0000000F, 0x0000001A}
}

-- The icon each item pool will use in the "Item Probability" display
local poolToIcon = { [0]="{{TreasureRoom}}",[1]="{{Shop}}",[2]="{{BossRoom}}",[3]="{{DevilRoom}}",[4]="{{AngelRoom}}",
[5]="{{SecretRoom}}",[7]="{{PoopRoomIcon}}",[8]="{{GoldenChestRoomIcon}}",[9]="{{RedChestRoomIcon}}",[12]="{{CursedRoom}}",[26]="{{Planetarium}}" }

-- local copies of our XML data in case it's slightly faster
local CraftingMaxItemID = EID.XMLMaxItemID
local CraftingFixedRecipes = EID.XMLRecipes
local CraftingItemPools = EID.XMLItemPools

local CraftingItemQualities = {}
local CraftingItemAllowed = {}

--These are recipes that have already been calculated, plus the contents of recipes.xml
local calculatedRecipes = {}
--If the seed changes, the above two tables will be wiped
local lastSeedUsed = 0
-- Test a few specific items' availability for if we should wipe our cached recipes due to availability change
-- Currently accounts for TLost/LostBR with Guppy's Paw, Sacred Orb with Lemon Mishap, and NoKeeper with A Pound of Flesh
-- Use non-achievement-locked items that fulfill specific tag/quality criteria
-- Currently checks Guppy's Paw, Lemon Mishap, A Pound of Flesh
local lastItemStatus = { [133] = true, [56] = true, [672] = true }

--A list of item IDs, sorted by quality, then by name, to help with sorting our recipe list faster
local sortedIDs = {}

local function sortAllItems()
	sortedIDs = {}
	local objectNames = {}
	
	for i = 1, CraftingMaxItemID do
		if CraftingItemQualities[i] ~= nil then
			table.insert(sortedIDs, i)
			objectNames[i] = EID:getObjectName(5, 100, i)
		end
	end

	table.sort(sortedIDs, function(a, b)
		if CraftingItemQualities[a] == CraftingItemQualities[b] then
			return (objectNames[a] < objectNames[b])
		else
			return (CraftingItemQualities[a] > CraftingItemQualities[b])
		end
	end)
end
-- delay the initial sort until needed, in case of modded items
local sortNeeded = true
local recheckPickups = false

local customRNGSeed = 0x77777770
local customRNGShift = {0,0,0}

-- Use local RNG functions to possibly reduce processing time a little bit
local function RNGNext()
	local num = customRNGSeed
	num = num ~ ((num >> customRNGShift[1]) & 4294967295)
	num = num ~ ((num << customRNGShift[2]) & 4294967295)
	num = num ~ ((num >> customRNGShift[3]) & 4294967295)
	customRNGSeed = num >> 0;
	return customRNGSeed;
end

local function nextFloat()
	local multi = 2.3283061589829401E-10;
	return RNGNext() * multi;
end

-- Convert a pickup's ID into what ingredient it counts as
function EID:getBagOfCraftingID(Variant, SubType)
	local entry = pickupIDLookup[Variant.."."..SubType]
	if entry ~= nil then
		return entry
	elseif Variant == 300 then
		if SubType == 0 then -- player:GetCard() returned 0
			return nil
		elseif EID.runeIDs[SubType] then -- runes
			return {23}
		else -- cards
			return {21}
		end 
	elseif Variant == 70 then -- pills
		if SubType == 0 then -- player:GetPill() returned 0
			return nil
		else
			return {22}
		end
	end
	return nil
end

-- "Item Probability" MODE: Get percentages of what quality / item pool a given set of 8 ingredients can yield
function EID:simulateBagOfCrafting(componentsTable)
	local components = componentsTable
	local compTotalWeight = 0
	local compCounts = {}
	for i = 1, #componentShifts do
		compCounts[i] = 0
	end
	for _, compId in ipairs(components) do
		if (_ > 8) then break end
		compCounts[compId + 1] = compCounts[compId + 1] + 1
		compTotalWeight = compTotalWeight + pickupValues[compId + 1]
	end
	
	local poolWeights = {
		{idx = 0, weight = 1, totalWeight = 0},
		{idx = 1, weight = 2, totalWeight = 0},
		{idx = 2, weight = 2, totalWeight = 0},
		{idx = 3, weight = compCounts[4] * 10, totalWeight = 0},
		{idx = 4, weight = compCounts[5] * 10, totalWeight = 0},
		{idx = 5, weight = compCounts[7] * 5, totalWeight = 0},
		{idx = 7, weight = compCounts[30] * 10, totalWeight = 0},
		{idx = 8, weight = compCounts[6] * 10, totalWeight = 0},
		{idx = 9, weight = compCounts[26] * 10, totalWeight = 0},
		{idx = 12, weight = compCounts[8] * 10, totalWeight = 0},
	}
	if compCounts[9] + compCounts[2] + compCounts[13] + compCounts[16] == 0 then
		table.insert(poolWeights, {idx = 26, weight = compCounts[24] * 10, totalWeight = 0})
	end
	
	local totalWeight = 0
	
	local qualityWeights = {[0] = 0, 0, 0, 0, 0}
	
	for _, poolWeight in ipairs(poolWeights) do
		if poolWeight.weight > 0 then
			local qualityMin = 0
			local qualityMax = 1
			local n = compTotalWeight
			if (poolWeight.idx >= 3) and (poolWeight.idx <= 5) then
				n = n - 5
			end
			if n > 34 then
				qualityMin = 4
				qualityMax = 4
			elseif n > 26 then
				qualityMin = 3
				qualityMax = 4
			elseif n > 22 then
				qualityMin = 2
				qualityMax = 4
			elseif n > 18 then
				qualityMin = 2
				qualityMax = 3
			elseif n > 14 then
				qualityMin = 1
				qualityMax = 2
			elseif n > 8 then
				qualityMin = 0
				qualityMax = 2
			end
			local pool = CraftingItemPools[poolWeight.idx + 1]
			
			for _, item in ipairs(pool) do
				local quality = CraftingItemQualities[item[1]]
				if quality >= qualityMin and quality <= qualityMax  then
					local w = item[2] * poolWeight.weight
					poolWeight.totalWeight = poolWeight.totalWeight + w
					qualityWeights[quality] = qualityWeights[quality] + w
					totalWeight = totalWeight + w
				end
			end
		end
	end
	
	local poolString = ""
	local firstAfterBoss = false
	for k,v in ipairs(poolWeights) do
		if (v.totalWeight > 0) then
			--line break after boss pool
			if (firstAfterBoss) then poolString = poolString .. " " end
			poolString = poolString .. poolToIcon[v.idx] .. ":" .. math.floor(v.totalWeight/totalWeight*100+0.5) .. "%,"
			firstAfterBoss = (k == 3)
		end
	end
	poolString = string.sub(poolString,1,-2) .. "#"
	for i=0,4 do
		local v = qualityWeights[i]
		if (v > 0) then
			poolString = poolString .. "{{Quality" .. i .. "}}:" .. math.floor(v/totalWeight*100+0.5) .. "%,"
		end
	end
	poolString = string.sub(poolString,1,-2)
	
	return compTotalWeight, poolString
end

-- The main function that takes 8 ingredients and tells you what collectible you will get in return
-- Only pass in a table with 8 valid ingredients!!!
function EID:calculateBagOfCrafting(componentsTable)
	-- ingredients must be sorted by ID for the RNG shifting to be accurate, so make a local copy
	local components = {table.unpack(componentsTable)}
	table.sort(components)
	local componentsAsString = table.concat(components, ",")

	-- Check the fixed recipes
	local cacheResult = CraftingFixedRecipes[componentsAsString]
	if cacheResult ~= nil then
		if EID:isCollectibleAvailable(cacheResult) then
			return cacheResult
		end
	end
	-- Check the recipes already calculated for this seed
	cacheResult = calculatedRecipes[componentsAsString]
	if cacheResult ~= nil then
		return cacheResult
	end
	
	-- Count up the ingredients, and shift the RNG based on the components in the bag
	customRNGSeed = lastSeedUsed
	local compTotalWeight = 0
	local compCounts = {}
	for i = 1, #componentShifts do
		compCounts[i] = 0
	end
	for _, compId in ipairs(components) do
		compCounts[compId + 1] = compCounts[compId + 1] + 1
		compTotalWeight = compTotalWeight + pickupValues[compId + 1]
		customRNGShift = componentShifts[compId + 1]
		RNGNext()
	end
	customRNGShift = componentShifts[7]
	
	local poolWeights = {
		{idx = 0, weight = 1},
		{idx = 1, weight = 2},
		{idx = 2, weight = 2},
		{idx = 3, weight = compCounts[4] * 10},
		{idx = 4, weight = compCounts[5] * 10},
		{idx = 5, weight = compCounts[7] * 5},
		{idx = 7, weight = compCounts[30] * 10},
		{idx = 8, weight = compCounts[6] * 10},
		{idx = 9, weight = compCounts[26] * 10},
		{idx = 12, weight = compCounts[8] * 10},
	}
	if compCounts[9] + compCounts[2] + compCounts[13] + compCounts[16] == 0 then
		table.insert(poolWeights, {idx = 26, weight = compCounts[24] * 10})
	end

	local totalWeight = 0
	local itemWeights = {}
	for i = 1, CraftingMaxItemID do
		itemWeights[i] = 0
	end

	for _, poolWeight in ipairs(poolWeights) do
		if poolWeight.weight > 0 then
			local qualityMin = 0
			local qualityMax = 1
			local n = compTotalWeight
			-- Devil, Angel, and Secret Room Pools have a 5 point penalty
			if (poolWeight.idx >= 3) and (poolWeight.idx <= 5) then
				n = n - 5
			end
			if n > 34 then
				qualityMin = 4
				qualityMax = 4
			elseif n > 26 then
				qualityMin = 3
				qualityMax = 4
			elseif n > 22 then
				qualityMin = 2
				qualityMax = 4
			elseif n > 18 then
				qualityMin = 2
				qualityMax = 3
			elseif n > 14 then
				qualityMin = 1
				qualityMax = 2
			elseif n > 8 then
				qualityMin = 0
				qualityMax = 2
			end
			local pool = CraftingItemPools[poolWeight.idx + 1]
			
			for _, item in ipairs(pool) do
				local quality = CraftingItemQualities[item[1]]
				if CraftingItemAllowed[item[1]] and quality >= qualityMin and quality <= qualityMax then
					local w = item[2] * poolWeight.weight
					itemWeights[item[1]] = itemWeights[item[1]] + w
					totalWeight = totalWeight + w
				end
			end
		end
	end
	
	for i=1,20 do
		local t = nextFloat() -- random number between 0 and 1
		local target = t * totalWeight -- number between 0 and total weight of possible results
		for k,v in ipairs(itemWeights) do
			target = target - v
			if target < 0 then
				-- check item:IsAvailable, otherwise reroll
				if EID:isCollectibleAvailable(k) then
					calculatedRecipes[componentsAsString] = k
					return k
				else
					break
				end
			end
		end
	end
	return 25
end

local function calcHeldItems()
	EID.BoC.InventoryQuery = {}
	for i = 0, game:GetNumPlayers() - 1 do
		local player = Isaac.GetPlayer(i)
		for j = 0, 3 do
			local card = EID:getBagOfCraftingID(300, player:GetCard(j))
			local pill = EID:getBagOfCraftingID(70, player:GetPill(j))
			-- assume the card/pill is only 1 ingredient
			if card then table.insert(EID.BoC.InventoryQuery, card[1]) end
			if pill then table.insert(EID.BoC.InventoryQuery, pill[1]) end
		end
	end
end
local function calcFloorItems()
	EID.BoC.FloorQuery = {}
	for _,roomQuery in pairs(EID.BoC.RoomQueries) do
		for _,v1 in ipairs(roomQuery[1]) do
			table.insert(EID.BoC.FloorQuery, v1)
		end
	end
end
local function qualitySort(a, b)
	if (pickupValues[a+1] == pickupValues[b+1]) then
		return a > b
	else
		return pickupValues[a+1] > pickupValues[b+1]
	end
end

local moddedCrafting = false
local function GameStartCrafting()
	for i=1, EID.XMLMaxItemID do
		local item = EID.itemConfig:GetCollectible(i)
		if item ~= nil then
			CraftingItemQualities[item.ID] = item.CraftingQuality or item.Quality
			CraftingItemAllowed[item.ID] = EID:isCollectibleAllowed(item.ID)
		end
	end
	if not EID:PlayersHaveCollectible(CollectibleType.COLLECTIBLE_TMTRAINER) then
		-- Check for modded items past the known max item ID on game start (can also support game updates)
		-- Only works if the new items are at Weight 1.0 in their item pools, but better than nothing
		if EID.Config["BagOfCraftingModdedRecipes"] and EID.itemConfig:GetCollectible(EID.XMLMaxItemID+1) ~= nil and not moddedCrafting then
			-- Save last pool
			local itemPool = game:GetItemPool()
			local lastPool = itemPool:GetLastPool()
			-- Items past max ID detected
			CraftingMaxItemID = EID.XMLMaxItemID -- XMLMaxItemID is never modified
			-- Add new item qualities
			local coll = EID.itemConfig:GetCollectible(CraftingMaxItemID+1)
			while coll ~= nil do
				CraftingMaxItemID = CraftingMaxItemID + 1
				CraftingItemQualities[coll.ID] = coll.CraftingQuality or coll.Quality
				CraftingItemAllowed[coll.ID] = true
				coll = EID.itemConfig:GetCollectible(CraftingMaxItemID+1)
			end
			-- Add new items to the crafting item pools, assuming Weight 1.0
			for poolNum,_ in pairs(poolToIcon) do
				for i=1,EID.XMLMaxItemID do itemPool:AddRoomBlacklist(i) end
				
				local collID = itemPool:GetCollectible(poolNum, false, 1, 25)
				local attempts = CraftingMaxItemID
				while collID ~= 25 and collID ~= 642 and collID > 0 and attempts > 0 do
					attempts = attempts - 1
					table.insert(CraftingItemPools[poolNum+1], {collID, 1.0})
					itemPool:AddRoomBlacklist(collID)
					collID = itemPool:GetCollectible(poolNum, false, 1, 25)
				end

				-- Do getCollectible again to revert last pool
				itemPool:GetCollectible(lastPool, false, 1, 25)
				
				itemPool:ResetRoomBlacklist()
			end
			moddedCrafting = true
		end
		
		sortNeeded = true
	end
end
EID:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, GameStartCrafting)

------------------------------------------
------------------------------------------
-----------Bag content detection ---------
------------------------------------------
------------------------------------------

local pickupsCollected = {} -- table of collected pickup indexes, reset each room
local pickupsJustTouched = {} -- flags of pickups a player/pickup-collector has touched, so the bag doesn't think it collected it

EID:AddCallback(ModCallbacks.MC_PRE_PICKUP_COLLISION, function(_, pickup,collider,_)
	if collider.Type == EntityType.ENTITY_PLAYER or collider.Type == EntityType.ENTITY_FAMILIAR or 
		collider.Type == EntityType.ENTITY_BUMBINO or collider.Type == EntityType.ENTITY_ULTRA_GREED then
		pickupsJustTouched[pickup.Index] = true
	end
end)

-- Formerly a MC_POST_PICKUP_UPDATE, but moved to this so that it's only called when we own a bag
local function checkForPickups()
	for _,pickup in ipairs(Isaac.FindByType(EntityType.ENTITY_PICKUP, -1, -1, false, false)) do
		if pickup:GetSprite():GetAnimation() == "Collect" and not pickupsCollected[pickup.Index] then
			pickupsCollected[pickup.Index] = true
			if not pickupsJustTouched[pickup.Index] then
				local craftingIDs = EID:getBagOfCraftingID(pickup.Variant, pickup.SubType)
				if craftingIDs ~= nil then
					recheckPickups = true
					for _,v in ipairs(craftingIDs) do
						if #EID.BoC.BagItems >= 8 then table.remove(EID.BoC.BagItems, 1) end
						table.insert(EID.BoC.BagItems, v)
					end
				end
			end
		end
		pickupsJustTouched[pickup.Index] = nil
	end
end

EID:AddCallback(ModCallbacks.MC_POST_NEW_ROOM, function(_)
	-- We're using the pickup indexes for quick checking, which reset on each new room
	pickupsCollected = {}
end)

-- Using a Card/Pill will change our inventory craftable items, so force a refresh then
-- (Note: Items that directly add a card/pill to you, i.e. Bottle of Pills, also need a refresh, but aren't tracked for performance)
EID:AddCallback(ModCallbacks.MC_USE_CARD, function(_)
	recheckPickups = true
end)
EID:AddCallback(ModCallbacks.MC_USE_PILL, function(_)
	recheckPickups = true
end)

--Tainted Cain "hold to craft" check
local holdCounter = 0
local icount = 0
local function trackBagHolding()
	if not IsTaintedCain() then return end
	local isCardHold = Input.IsActionPressed(ButtonAction.ACTION_PILLCARD, EID.bagPlayer.ControllerIndex)
	local animationName = EID.bagPlayer:GetSprite():GetAnimation()
	if isCardHold and string.match(animationName, "PickupWalk") and #EID.BoC.BagItems>=8 then
		holdCounter = holdCounter + 1
		if holdCounter < 30 then
			icount = EID.bagPlayer:GetCollectibleCount()
		end
	else
		if isCardHold and holdCounter >= 30 and (string.match(animationName, "Walk") and not string.match(animationName, "Pickup") or (EID.bagPlayer:GetCollectibleCount() ~= icount)) then
			EID.BoC.BagItems = {}
		else
			holdCounter = 0
		end
	end
end

--Active slot "press to craft" check
EID:AddCallback(ModCallbacks.MC_PRE_USE_ITEM, function(_, _, _, _, _, slot)
	if slot ~= 0 or #EID.BoC.BagItems < 8 then return end
	EID.BoC.BagItems = {}
end, CollectibleType.COLLECTIBLE_BAG_OF_CRAFTING)

local function shiftBagContent()
	local newContent = {}
	for i=2,#EID.BoC.BagItems do
		table.insert(newContent, EID.BoC.BagItems[i])
	end
	table.insert(newContent, EID.BoC.BagItems[1])
	EID.BoC.BagItems = newContent
end
-- only Tainted Cain's consumable slot bag can have its ingredients shifted
local function detectBagContentShift()
	if Input.IsActionTriggered(ButtonAction.ACTION_DROP, EID.bagPlayer.ControllerIndex) and IsTaintedCain() then
		shiftBagContent()
	end
end

-----------------------------
-----------------------------
-----------RENDERING---------
-----------------------------
-----------------------------

local randResultCache = {}
local calcResultCache = {}
local numResults = 0

local bagOfCraftingOffset = 0
local lockedResults = nil
local refreshNextTick = false
local refreshPosition = 0
local bagOfCraftingRefreshes = 0
local downHeld = 0
local upHeld = 0
local resetBagCounter = 0

local craftingIsHidden = false
local showCraftingResult = false

local prevDesc = ""

--this combination algorithm was adopted from this Java code: https://stackoverflow.com/a/16256122
--note that it will run into duplicates, for example if you have eight pennies and a key, it can't tell the difference between
--PPPPPPPK (pennies 1-7) and PPPPPPPK (pennies 2-8) and PPPPPPPK (pennies 1-4,6-8) etc..., I don't know of a way to prevent that
local coTimer = 0
-- number of milliseconds we allow these to run without yielding (1/60th of a second = 16.66667 milliseconds)
local coTimerLength = 5
local function combinations(arr, length, startPos, tempResult, randResults, newResults)
	if Isaac.GetTime() > coTimer + coTimerLength then
		coroutine.yield()
		coTimer = Isaac.GetTime()
	end
	length = length or 8
	startPos = startPos or 1
	tempResult = tempResult or {}
	if (length == 0) then
		local resultString = table.concat(tempResult,",")
		if (randResults[resultString] == nil) then
			randResults[resultString] = {table.unpack(tempResult)}
			newResults[resultString] = {table.unpack(tempResult)}
		end
		return
	end
	for i = startPos, #arr-length+1 do
		tempResult[8-length+1] = arr[i]
		combinations(arr,length-1, i+1, tempResult, randResults, newResults)
	end
end																								  
--code from InputHelper in MCM
local HotkeyToString = {}
for key,num in pairs(Keyboard) do
	local keyString = key
	local keyStart, keyEnd = string.find(keyString, "KEY_")
	keyString = string.sub(keyString, keyEnd+1, string.len(keyString))
	keyString = string.gsub(keyString, "_", " ")
	HotkeyToString[num] = keyString
end
--convert controller enum to buttons
local ControllerToString = { [0] = "{{ButtonDLeft}}", "{{ButtonDRight}}", "{{ButtonDUp}}", "{{ButtonDDown}}",
"{{ButtonA}}", "{{ButtonB}}", "{{ButtonX}}", "{{ButtonY}}", "{{ButtonLB}}", "{{ButtonLT}}", "{{ButtonLStick}}", 
"{{ButtonRB}}", "{{ButtonRT}}", "{{ButtonRStick}}", "{{ButtonSelect}}", "{{ButtonMenu}}" }

local function getHotkeyString()
	if (not EID.Config["BagOfCraftingShowControls"]) then return "" end
	local hotkeyString = ""
	local hideDesc = EID:getDescriptionEntry("CraftingHideKey")
	local previewDesc = EID:getDescriptionEntry("CraftingPreviewKey")
	
	local controllerEnabled = EID.bagPlayer.ControllerIndex > 0
	local hideKey = HotkeyToString[EID.Config["CraftingHideKey"]]
	local hideButton = controllerEnabled and ControllerToString[EID.Config["CraftingHideButton"]]
	local previewKey = HotkeyToString[EID.Config["CraftingResultKey"]]
	local previewButton = controllerEnabled and ControllerToString[EID.Config["CraftingResultButton"]]
	
	if hideKey or hideButton then hotkeyString = hideDesc .. " " end
	if hideKey and hideButton then
		hotkeyString = hotkeyString .. hideKey .. "/" .. hideButton
	else
		hotkeyString = hotkeyString .. (hideKey or hideButton)
	end
	
	if #EID.BoC.BagItems >= 8 and EID.Config["BagOfCraftingDisplayRecipesMode"] ~= "Preview Only" then
		if previewKey or previewButton then hotkeyString = hotkeyString .. ", " .. previewDesc .. " " end
		if previewKey and previewButton then
			hotkeyString = hotkeyString .. previewKey .. "/" .. previewButton
		else
			hotkeyString = hotkeyString .. (previewKey or previewButton)
		end
	end
	if hotkeyString ~= "" then
		hotkeyString = "!!! " .. hotkeyString .. "#"
	end
	
	return hotkeyString
end

local function getFloorItemsString(showPreviews, roomItems)
	local floorString = ""
	local bagItems = EID.BoC.BagItemsOverride or EID.BoC.BagItems
	if #bagItems >0 then
		if showPreviews and #bagItems == 8 then
			local recipe = EID:calculateBagOfCrafting(bagItems)
			floorString = floorString .. "{{Collectible"..recipe.."}} "
		end
		local bagDesc = EID:getDescriptionEntry("CraftingBagContent")
		floorString = floorString .. bagDesc.. EID:tableToCraftingIconsMerged(bagItems).."#"
	end
	local curRoomItems = EID.BoC.RoomOverride or roomItems
	if #curRoomItems >0 then
		if showPreviews and #curRoomItems == 8 then
			local recipe = EID:calculateBagOfCrafting(curRoomItems)
			floorString = floorString .. "{{Collectible"..recipe.."}} "
		end
		local roomDesc = EID:getDescriptionEntry("CraftingRoomContent")
		floorString = floorString .. roomDesc..EID:tableToCraftingIconsMerged(curRoomItems).."#"
	end
	local floorQuery = EID.BoC.FloorOverride or EID.BoC.FloorQuery
	if #floorQuery >0 and #curRoomItems ~= #floorQuery then
		if showPreviews and #floorQuery == 8 then
			local recipe = EID:calculateBagOfCrafting(floorQuery)
			floorString = floorString .. "{{Collectible"..recipe.."}} "
		end
		local floorDesc = EID:getDescriptionEntry("CraftingFloorContent")
		floorString = floorString .. floorDesc..EID:tableToCraftingIconsMerged(floorQuery)
	end
	local inventoryQuery= EID.BoC.InventoryOverride or EID.BoC.InventoryQuery
	if #inventoryQuery > 0 then
		floorString = floorString .. "(+" .. EID:tableToCraftingIconsMerged(inventoryQuery) .. ")#"
	else
		floorString = floorString .. "#"
	end
	return floorString
end

-- This list will be modified once the coroutine finishes; until then it will have the last finished list
local currentRecipesList = {}

local itemQuery = {}
local mostValuable = {}
local randResults = {}
local newResults = {}
local skipRandom = false
local isRefresh = false
local queryString = ""
local displayingRecipeList = false

local function RecipeCrunchCoroutine()
	coTimer = Isaac.GetTime()
	
	-- Fill randResults/newResults with every possible combination of our most valuable ingredients
	-- The number is has an option to limit it in the config, since the number of total combinations quickly grows (nCr):
	-- 12 = 495, 13 = 1287, 14 = 3003, 15 = 6435, 16 = 12870
	combinations(mostValuable, nil, nil, nil, randResults, newResults)
	
	--do random pulls for some more recipe choices
	if (not skipRandom) then
		for i = 0, EID.Config["BagOfCraftingRandomResults"] do
			if Isaac.GetTime() > coTimer + coTimerLength then
				coroutine.yield()
				coTimer = Isaac.GetTime()
			end
			local newTable = {}
			local tableCopy = {table.unpack(itemQuery)}
			for k = 1, 8 do
				local pos = math.random(1, #tableCopy)
				table.insert(newTable, tableCopy[pos])
				table.remove(tableCopy, pos)
			end
			table.sort(newTable, qualitySort)
			local resultString = table.concat(newTable,",")
			if (randResults[resultString] == nil) then
				randResults[resultString] = {table.unpack(newTable)}
				newResults[resultString] = {table.unpack(newTable)}
			end
		end
	end
	
	local sortedResults = {}
	if (calcResultCache[queryString]) then
		sortedResults = calcResultCache[queryString]
	else
		for _, v in ipairs(sortedIDs) do
			sortedResults[v] = {}
		end
	end
	
	for _, v in pairs(newResults) do
		if Isaac.GetTime() > coTimer + coTimerLength then
			coroutine.yield()
			coTimer = Isaac.GetTime()
		end
		local resultID = EID:calculateBagOfCrafting(v)
		table.insert(sortedResults[resultID], {v, resultID})
	end
	calcResultCache[queryString] = sortedResults
	randResultCache[queryString] = randResults
	currentRecipesList = sortedResults
	
	numResults = 0
	for _,v in ipairs(sortedIDs) do
		-- keep our cursor position if we're not at the top of the list, and bag's contents don't matter for list size
		if (isRefresh and bagOfCraftingOffset > 0 and v == refreshPosition and (IsTaintedCain() or #EID.BoC.BagItems == 0)) then
			--jump to the item we were looking at before, so you can more easily refresh for variants of recipes
			bagOfCraftingOffset = numResults
		end
		numResults = numResults + #currentRecipesList[v]
	end
	
	if not isRefresh then
		bagOfCraftingOffset = 0
		bagOfCraftingRefreshes = 0
	end
	isRefresh = false
	EID.RefreshBagTextbox = true
end

function EID:BOCHandleCurseOfMaze()
	-- switches tracked room contents when rooms are switched by curse of maze
	if game:GetLevel():GetCurses() & LevelCurse.CURSE_OF_MAZE ~= LevelCurse.CURSE_OF_MAZE then
		return
	end
	local rooms = game:GetLevel():GetRooms()
	local changedRooms = {}
	for i = 0, rooms.Size - 1 do
		local roomDesc = rooms:Get(i)
		if roomDesc and roomDesc.Data then
			local savedRoomContent = EID.BoC.RoomQueries[roomDesc.ListIndex .. ""]
			if not savedRoomContent then
				EID.BoC.RoomQueries[roomDesc.ListIndex .. ""] = { {}, roomDesc.Data.Variant }
			end
			if savedRoomContent and savedRoomContent[2] ~= roomDesc.Data.Variant then
				table.insert(changedRooms, roomDesc.ListIndex)
			end
		end
	end
	if #changedRooms >= 2 then
		local temp = EID.BoC.RoomQueries[changedRooms[2] .. ""]
		EID.BoC.RoomQueries[changedRooms[2] .. ""] = EID.BoC.RoomQueries[changedRooms[1] .. ""]
		EID.BoC.RoomQueries[changedRooms[1] .. ""] = temp
	end
end

local prevOffset = 0
-- Called 60 times a second so we can read input properly
function EID:handleBagOfCraftingUpdating()
	-- don't do any updating if on a Repentance version before v1.7.9b
	if (Isaac.RunCallback == nil) then return end
	-- reset our calculated recipes when the game seed changes, or certain item's availability changes
	local curSeed = game:GetSeeds():GetStartSeed()
	local updatedItemAvailability = false
	-- check the availability of our tracked items, for Sacred Orb / TLost/LostBR
	for k,v in pairs(lastItemStatus) do
		local curStatus = EID.itemConfig:GetCollectible(k):IsAvailable()
		if v ~= curStatus then updatedItemAvailability = true end
		lastItemStatus[k] = curStatus
	end
	if (curSeed ~= lastSeedUsed or updatedItemAvailability) then
		calculatedRecipes = {}
		calcResultCache = {}
		randResultCache = {}
		EID.itemAvailableStates = {}
		lockedResults = nil
	end
	lastSeedUsed = curSeed
	
	-- watch for holding the Craft button, and pressing the ingredient shift button
	trackBagHolding()
	detectBagContentShift()
	if EID.GameRenderCount % 2 == 0 then checkForPickups() end
	
	-- Check for Hide/Preview hotkeys; prevent them from triggering while in MCM
	if not ModConfigMenu or not ModConfigMenu.IsVisible then
		if Input.IsButtonTriggered(EID.Config["CraftingHideKey"], 0) or Input.IsButtonTriggered(EID.Config["CraftingHideButton"], EID.bagPlayer.ControllerIndex) then
			craftingIsHidden = not craftingIsHidden
		end
		
		if Input.IsButtonTriggered(EID.Config["CraftingResultKey"], 0) or Input.IsButtonTriggered(EID.Config["CraftingResultButton"], EID.bagPlayer.ControllerIndex) then
			showCraftingResult = not showCraftingResult
		end
	end
	
	-- Check for Hold Tab key inputs
	if displayingRecipeList and Input.IsActionPressed(EID.Config["BagOfCraftingToggleKey"], EID.bagPlayer.ControllerIndex) then
		EID.TabDescThisFrame = true
		EID.bagPlayer.ControlsCooldown = 2
		if Input.IsActionTriggered(ButtonAction.ACTION_SHOOTDOWN, EID.bagPlayer.ControllerIndex) then
			bagOfCraftingOffset = math.min(numResults-(numResults%EID.Config["BagOfCraftingResults"]), bagOfCraftingOffset + EID.Config["BagOfCraftingResults"])
			downHeld = Isaac.GetTime()
		elseif Input.IsActionTriggered(ButtonAction.ACTION_SHOOTUP, EID.bagPlayer.ControllerIndex) then
			bagOfCraftingOffset = math.max(0, bagOfCraftingOffset - EID.Config["BagOfCraftingResults"])
			upHeld = Isaac.GetTime()
		--lock the current results so you can actually do a recipe that you've scrolled down to without losing it
		elseif Input.IsActionTriggered(ButtonAction.ACTION_SHOOTLEFT, EID.bagPlayer.ControllerIndex) then
			EID.RefreshBagTextbox = true
			if (lockedResults == nil) then lockedResults = queryString
			else lockedResults = nil end
		--refresh the recipes
		elseif Input.IsActionTriggered(ButtonAction.ACTION_SHOOTRIGHT, EID.bagPlayer.ControllerIndex) then
			if (lockedResults == nil) then
				refreshNextTick = true
			end
		end
		--scroll pages quickly if the button is held
		if Input.IsActionPressed(ButtonAction.ACTION_SHOOTDOWN, EID.bagPlayer.ControllerIndex) and Isaac.GetTime() - downHeld > 750 then
			bagOfCraftingOffset = math.min(numResults-(numResults%EID.Config["BagOfCraftingResults"]), bagOfCraftingOffset + EID.Config["BagOfCraftingResults"])
			downHeld = Isaac.GetTime() - 700
		elseif Input.IsActionPressed(ButtonAction.ACTION_SHOOTUP, EID.bagPlayer.ControllerIndex) and Isaac.GetTime() - upHeld > 750 then
			bagOfCraftingOffset = math.max(0, bagOfCraftingOffset - EID.Config["BagOfCraftingResults"])
			upHeld = Isaac.GetTime() - 700
		end
		--reset bag contents when holding Use Pill/Card
		if Input.IsActionPressed(ButtonAction.ACTION_PILLCARD, EID.bagPlayer.ControllerIndex) then
			resetBagCounter = resetBagCounter + 1
			if resetBagCounter > 120 then
				EID.BoC.BagItems = {}
				recheckPickups = true
				resetBagCounter = 0
			end
		else
			resetBagCounter = 0
		end
	end
	--fix bug with being allowed to go to an empty page if recipe count = multiple of page size (or if we refresh on last page)
	if (bagOfCraftingOffset >= numResults) then bagOfCraftingOffset = bagOfCraftingOffset - EID.Config["BagOfCraftingResults"] end
end

-- Called when needed based on EID.Config["RefreshRate"]
function EID:handleBagOfCraftingRendering(ignoreRefreshRate)
	-- Determine if we should display anything at all
	if not EID:RefreshThisFrame() and not ignoreRefreshRate then
		return false
	end
	displayingRecipeList = false
	if ((EID.isHidden or craftingIsHidden) and EID.MCMCompat_isDisplayingEIDTab ~= "Crafting") or game.Challenge == Challenge.CHALLENGE_CANTRIPPED then
		return false
	elseif EID.Config["BagOfCraftingHideInBattle"] and (Isaac.CountBosses() > 0 or Isaac.CountEnemies() > 0) then
		return false
	elseif EID.Config["DisplayBagOfCrafting"] == "never" then
		return false
	elseif EID.Config["DisplayBagOfCrafting"] == "hold" and not string.find(EID.bagPlayer:GetSprite():GetAnimation(), "PickupWalk") then
		return false
	elseif game:GetRoom():GetFrameCount() < 2 then
		return false
	end
	
	local bagItems = EID.BoC.BagItemsOverride or EID.BoC.BagItems
	-- Display the result of the 8 items in our bag if applicable
	if (showCraftingResult or EID.Config["BagOfCraftingDisplayRecipesMode"] == "Preview Only") and #bagItems == 8 then
		if EID.Config["BagOfCraftingDisplayRecipesMode"] ~= "Recipe List" and EID:hasCurseBlind() and EID.Config["DisableOnCurse"] then
			showCraftingResult = false
			return false
		end
		local craftingResult = EID:calculateBagOfCrafting(bagItems)
		local descriptionObj = EID:getDescriptionObj(5, 100, craftingResult)
		-- prepend the Hide/Preview hotkeys to the description
		descriptionObj.Description = getHotkeyString() .. descriptionObj.Description
		EID:addDescriptionToPrint(descriptionObj)
		return true
	end
	-- if we're in Preview Only mode, then we have nothing more to do
	if (EID.Config["BagOfCraftingDisplayRecipesMode"] == "Preview Only") then return false end
	
	-- Check what pickups are available in this room
	local curRoomIndex = game:GetLevel():GetCurrentRoomDesc().ListIndex
	local roomItems = {}
	local pickups = Isaac.FindByType(5, -1, -1, true, false)

	if EID.BoC.CurrentPickupCount ~= #pickups or recheckPickups then
		recheckPickups = false
		for _, entity in ipairs(pickups) do
			local craftingIDs = EID:getBagOfCraftingID(entity.Variant, entity.SubType)
			if craftingIDs ~= nil and not entity:ToPickup():IsShopItem() and entity:GetSprite():GetAnimation() ~= "Collect" then
				for _,v in ipairs(craftingIDs) do
					table.insert(roomItems, v)
				end
			end
		end
		EID.BoC.RoomQueries[curRoomIndex .. ""] = { roomItems, game:GetLevel():GetCurrentRoomDesc().Data.Variant }
		EID.BoC.CurrentPickupCount = #pickups
		calcHeldItems()
		calcFloorItems()
		EID.RefreshBagTextbox = true
	else
		roomItems = EID.BoC.RoomQueries[curRoomIndex .. ""] and EID.BoC.RoomQueries[curRoomIndex .. ""][1] or {}
	end
	
	itemQuery = {}
	local itemCount = {}
	
	-- Merge our list of the floor's pickups, held cards/pills, and our bag's pickups
	-- max 8 copies of a single item in our list, to avoid repeat recipes
	local floorQuery = EID.BoC.FloorOverride or EID.BoC.FloorQuery
	local inventoryQuery= EID.BoC.InventoryOverride or EID.BoC.InventoryQuery
	local tablesToMerge = { floorQuery, inventoryQuery, bagItems }
	for _, tbl in ipairs(tablesToMerge) do
		for _, v in ipairs(tbl) do
			if (not itemCount[v] or itemCount[v] < 8) then
					table.insert(itemQuery, v)
					if (not itemCount[v]) then itemCount[v] = 1
					else itemCount[v] = itemCount[v] + 1 end
			end
		end
	end
	
	-- Nothing to do if there's less than 8 pickups detected
	if #itemQuery < 8 then
		return false
	end
	-- sort by ingredient quality, as high quality recipes are more important to check
	table.sort(itemQuery, qualitySort)
	queryString = table.concat(itemQuery,",")
	
	----------------------------------------------
	
	local customDescObj = EID:getDescriptionObj(5, 100, 710)
	customDescObj.Description = ""
	
	-- load the function we need for Show Recipes as Groups / 8 Icons
	local tableToCraftingIconsFunc = EID.tableToCraftingIconsMerged
	if EID.Config["BagOfCraftingDisplayIcons"] then tableToCraftingIconsFunc = EID.tableToCraftingIconsFull end
	
	-- Pickups Only / Item Probability Mode display
	if EID.Config["BagOfCraftingDisplayRecipesMode"] == "Pickups Only" then
		EID:appendToDescription(customDescObj, getHotkeyString())
		EID:appendToDescription(customDescObj, getFloorItemsString(false, roomItems))
		EID:addDescriptionToPrint(customDescObj)
		return true
	elseif EID.Config["BagOfCraftingDisplayRecipesMode"] == "Item Probability" then
		if not EID.RefreshBagTextbox and prevDesc ~= "" and not EID.OptionChanged then
			EID:appendToDescription(customDescObj, prevDesc)
			EID:addDescriptionToPrint(customDescObj)
			return true
		end
		
		prevDesc = ""
		EID.RefreshBagTextbox = false
		
		prevDesc = prevDesc .. getHotkeyString()
		prevDesc = prevDesc .. getFloorItemsString(false, roomItems)
		
		local mostValuableBag = {}
		for i=1,8 do
			mostValuableBag[i] = itemQuery[i]
		end
		local bagQuality, bagResult = EID:simulateBagOfCrafting(bagItems)
		local bestQuality, bestResult = EID:simulateBagOfCrafting(mostValuableBag)
		local bagQualityDesc = EID:getDescriptionEntry("CraftingBagQuality")
		local bestQualityDesc = EID:getDescriptionEntry("CraftingBestQuality")
		
		if (#bagItems > 0) then prevDesc = prevDesc .. bagQualityDesc .. " " .. bagQuality .. "#" .. bagResult .. "#" end
		if (bestQuality > bagQuality) then prevDesc = prevDesc .. bestQualityDesc .. " " .. bestQuality .. "#{{Blank}} " .. tableToCraftingIconsFunc(self,mostValuableBag, true) .. "#" .. bestResult .. "#" end
		
		EID:appendToDescription(customDescObj, prevDesc)
		EID:addDescriptionToPrint(customDescObj)
		return true
	end
	
	-- Recipe List display
	displayingRecipeList = true
	if sortNeeded then
		sortAllItems()
		sortNeeded = false
	end
	
	if lockedResults ~= nil then
		currentRecipesList = calcResultCache[lockedResults]
	elseif (calcResultCache[queryString] == nil or refreshNextTick) and EID.Coroutines["RecipeCrunch"] == nil then
		isRefresh = refreshNextTick
		--build on top of our previous recipe lists, if possible
		randResults = randResultCache[queryString] or {}
		newResults = {}
		skipRandom = false
		mostValuable = {}
		
		--shift our thorough check forward one ingredient each refresh (it will find duplicates, but spamming refresh will get a lot of variety)
		if (refreshNextTick) then
			bagOfCraftingRefreshes = bagOfCraftingRefreshes + 1
			if (#itemQuery >= EID.Config["BagOfCraftingCombinationMax"] + bagOfCraftingRefreshes) then
				for i=1,EID.Config["BagOfCraftingCombinationMax"] do
					mostValuable[i] = itemQuery[i+bagOfCraftingRefreshes]
				end
			end
		--we have less items than our threshold; every single recipe will be listed
		elseif (#itemQuery <= EID.Config["BagOfCraftingCombinationMax"]) then
			mostValuable = itemQuery
			skipRandom = true
		else
		--grab the X most valuable ingredients
			for i=1,EID.Config["BagOfCraftingCombinationMax"] do
				mostValuable[i] = itemQuery[i]
			end
		end
		
		EID:addCoroutine("RecipeCrunch", RecipeCrunchCoroutine)
		refreshNextTick = false
	else
		currentRecipesList = calcResultCache[queryString] or currentRecipesList
	end
	
	if numResults == 0 then
		bagOfCraftingOffset = 0
		return false
	end
	
	if not EID.RefreshBagTextbox and prevDesc ~= "" and bagOfCraftingOffset == prevOffset and not EID.OptionChanged then
		EID:appendToDescription(customDescObj, prevDesc)
		EID:addDescriptionToPrint(customDescObj)
		return true
	end
	
	prevOffset = bagOfCraftingOffset
	prevDesc = ""
	EID.RefreshBagTextbox = false
	
	prevDesc = prevDesc .. getHotkeyString()
	prevDesc = prevDesc .. getFloorItemsString(true, roomItems)
	if (EID.Config["BagOfCraftingShowControls"]) then
		local resultDesc = EID:getDescriptionEntry("CraftingResults")
		prevDesc = prevDesc .. resultDesc
	end
	
	local prevItem = 0
	
	local qualities = { [0] = "{{ColorSilver}}", "{{ColorLime}}", "{{ColorPastelBlue}}", "{{ColorLavender}}", "{{ColorLightOrange}}" }
	local prefix = "#{{Blank}} "
	if (lockedResults) then
		prefix = "#{{Trinket159}} "
	end
	
	
	local filteredRecipesList = {}
	local filteredNumResults = 0
	-- If we aren't Tainted Cain, we should filter out recipes that don't use everything in our bag
	if not IsTaintedCain() then
		for _,id in ipairs(sortedIDs) do
			filteredRecipesList[id] = {}
			for _, v in ipairs(currentRecipesList[id]) do
				if (EID:bagContainsCount(v[1]) == #bagItems) then
					table.insert(filteredRecipesList[id], v)
					filteredNumResults = filteredNumResults + 1
				end
			end
		end
	else
		filteredRecipesList = currentRecipesList
		filteredNumResults = numResults
	end
	
	-- Keeping the offset doesn't work at all with non-Tainted-Cain bag-filtered results;
	-- just reset us to 0 if we end up past the end of the list for now...
	if (bagOfCraftingOffset >= filteredNumResults) then bagOfCraftingOffset = 0 end
	
	--filteredRecipesList is a table of tables for each item, so we have to iterate over the table using sortedIDs
	if (bagOfCraftingOffset > 0) then
		prevDesc = prevDesc .. prefix .. "...+"..bagOfCraftingOffset.." more"
	end
	local curOffset = 0
	refreshPosition = -1
	for _,id in ipairs(sortedIDs) do
		if (curOffset + #filteredRecipesList[id] <= bagOfCraftingOffset) then curOffset = curOffset + #filteredRecipesList[id]
		else
			if (refreshPosition == -1) then refreshPosition = id end
			for _, v in ipairs(filteredRecipesList[id]) do
				curOffset = curOffset + 1
				if (curOffset > bagOfCraftingOffset+EID.Config["BagOfCraftingResults"]) then break end
				if not v then break end
				if (curOffset > bagOfCraftingOffset) then
					if not EID.Config["BagOfCraftingDisplayNames"] then
						prevDesc = prevDesc .."#{{Collectible"..v[2].."}} "
						--color the equals sign with the item quality, so the order of the list can make sense
						prevDesc = prevDesc .. qualities[CraftingItemQualities[v[2]]] .. "={{CR}}"
					--only display the item name if it's the first occurrence
					else
						if prevItem ~= v[2] then
							--substring the first 18 characters of the item name so it fits on one line; is there a way to get around desc line length limits?
							prevDesc = prevDesc .."#{{Collectible"..v[2].."}} ".. qualities[CraftingItemQualities[v[2]]] ..
							string.sub(EID:getObjectName(5, 100, v[2]),1,18).."#"
						else
							prevDesc = prevDesc .."#"
						end
					end
					
					prevDesc = prevDesc .. tableToCraftingIconsFunc(self, v[1], true)
					prevItem = v[2]
				end
			end
		end
	end
	if (bagOfCraftingOffset + EID.Config["BagOfCraftingResults"] < filteredNumResults) then
		prevDesc = prevDesc .. prefix .. "...+"..(filteredNumResults-EID.Config["BagOfCraftingResults"]-bagOfCraftingOffset).." more"
	end

	EID:appendToDescription(customDescObj, prevDesc)
	EID:addDescriptionToPrint(customDescObj)
	return true
end
