local lib = {}

lib.idToName = function(id)
	local mainPart, descriptor = id:match(".+:([^/]+)(/?.*)")

	if mainPart then
		-- Convert underscores to spaces and capitalize the first letter of each word
		mainPart = mainPart:gsub("_", " ")
		mainPart = mainPart:gsub("(%l)(%w*)", function(a, b) return string.upper(a) .. b end)

		if descriptor and descriptor ~= "" then
			-- Remove the leading slash from descriptor
			descriptor = descriptor:sub(2)
			-- Convert underscores to spaces and capitalize the first letter of each word
			descriptor = descriptor:gsub("_", " ")
			descriptor = descriptor:gsub("(%l)(%w*)", function(a, b) return string.upper(a) .. b end)
			return mainPart .. " (" .. descriptor .. ")"
		else
			return mainPart
		end
	else
		return "Invalid ID"
	end
end


lib.getChests = function(filter)
	return {peripheral.find("inventory",filter)}
end

lib.getSetup = function()
	local input,output
	local chestCache = {["left"]=true,["right"]=true,["back"]=true,["bottom"]=true,["top"]=true}
	local furnaceCache = {}
	return lib.getChests(
		function(n,p) 
			if chestCache[n] then return end
			if n:find("furnace") or p.size() < 9 or n:find("shulker") then return end
			chestCache[n] = p
			local detail = p.getItemDetail(1)
			if detail and detail.displayName then
				if detail.displayName == "input" then
					input = p
				elseif detail.displayName == "output" then
					output = p
				else
					return true
				end
				return false
			else
				return true
			end
		end
	), input, output, {peripheral.find("minecraft:furnace", function(n,p) if furnaceCache[n] then return end furnaceCache[n] = p return true end)}
end

local detailCache = {}

local function count(tbl)
	local i = 0
	for k,v in pairs(tbl) do
		i = i + 1
	end
	return i
end

lib.removeChest = function(inventory, chest)
	local loopThrough = {inventory}
	for _, tag in pairs(inventory.tags) do
		table.insert(loopThrough, tag)
	end
	for _, group in ipairs(inventory.groups) do
		table.insert(loopThrough, group)
	end
	for _,inv in ipairs(loopThrough) do
		for i=#inv, 1, -1 do
			local item = inv[i]
			if item.items then
				for t=#item.items, 1, -1 do
					if item.items[t].chest == chest then
						item.count = item.count - item.items[t].count
						table.remove(item.items, t)
					end
				end
			elseif item.chest == chest then
				table.remove(inv, i)
			end
		end
	end
	inventory.chests[chest] = nil
end

local itemCache = {}

lib.expandInventory = function(inventory, chest)
	if inventory.chests[chest] then
		lib.removeChest(inventory, chest)
	end
	inventory.chests[chest] = {occupied=0,limit=chest.size()}
	local iChest = inventory.chests[chest]

	if not itemCache[inventory] then
		itemCache[inventory] = {}
	end
	local itemCache = itemCache[inventory]
	
	local ls = chest.list()
	for slot,item in pairs(ls) do
		iChest.occupied = iChest.occupied + 1
		local fItem = {}
		local details

		if not detailCache[item.name] then
			local d = chest.getItemDetail(slot)
			if d.nbt then
				fItem.nbt = true
				details = d
			elseif d.maxCount > 1 then
				detailCache[item.name] = {
					name = lib.idToName(item.name),
					maxCount = d.maxCount,
					displayName = d.displayName
				}
				details = detailCache[item.name]
			else
				details = d
			end
			
			local oTags = d.tags
			details.tags = {}
			for k,v in pairs(oTags) do
				local tag = lib.idToName(k)
				details.tags[tag] = true
			end
			
			details.groups = {}
			if #d.itemGroups == 0 then
				d.itemGroups[1] = {displayName="Miscellaneous", id="misc"}
			end
			for k,v in pairs(d.itemGroups) do
				details.groups[v.displayName] = v.id
			end
		else
			details = detailCache[item.name]
		end
		
		fItem.id = item.name
		fItem.name = details.displayName or details.name
		fItem.count = item.count
		fItem.enchantments = details.enchantments
		fItem.chest = chest
		fItem.slot = slot
		fItem.tags = details.tags
		fItem.groups = details.groups
		
		if not fItem.nbt and not itemCache[fItem.id] then
			local ic = {
				id = fItem.id,
				name = lib.idToName(fItem.id),
				count = 0,
				tags = details.tags,
				groups = details.groups,
				items = {},
			}
			
			itemCache[fItem.id] = ic
			
			table.insert(inventory, ic)
			table.insert(iChest, ic)
			
			for tag,_ in pairs(ic.tags) do
				if not inventory.tags[tag] then
					inventory.tags[tag] = {}
				end
				table.insert(inventory.tags[tag], ic)
			end
			
			for group,_ in pairs(ic.groups) do
				if not inventory.groups[group] then
					inventory.groups[group] = {}
				end
				table.insert(inventory.groups[group], ic)
			end
		end
		
		if fItem.nbt then
			table.insert(inventory, fItem)
			table.insert(iChest, fItem)
			
			for tag,_ in pairs(details.tags) do
				if not inventory.tags[tag] then
					inventory.tags[tag] = {}
				end
				table.insert(inventory.tags[tag], fItem)
			end
			
			for group,_ in pairs(details.groups) do
				if not inventory.groups[group] then
					inventory.groups[group] = {}
				end
				table.insert(inventory.groups[group], fItem)
			end
		else
			local ic = itemCache[fItem.id]
			table.insert(ic.items, fItem)
			ic.count = ic.count + fItem.count
		end
	end
end

lib.getInventory = function(chests)
	local inventory = {groups={},tags={},chests={}}
	local cors = {}
	for _,chest in ipairs(chests) do
		table.insert(cors, function()
			lib.expandInventory(inventory, chest)
		end)
	end
	parallel.waitForAll(unpack(cors))
	return inventory
end

lib.outputItem = function(item,count,output,inventory,slot)
	if type(item) == "string" then
		for i,t in ipairs(inventory) do
			if t.id == item or t.name == item then
				item = t
				break
			end
		end
	end
	if type(item) == "string" then
		return false,"item not found"
	end
	if item.items then
		for i=#item.items,1,-1 do
			local t = item.items[i]
			local c = 0
			while c == 0 do
				c = output.pullItems(peripheral.getName(t.chest), t.slot, count, slot)
				if c == 0 then
					os.sleep(1)
				end
			end
			if count then
				count = count-c
			end
			t.count = t.count - c
			item.count = item.count - c
			if t.count <= 0 then
				table.remove(item.items, i)
			end
			if count and count <= 0 then
				break
			end
		end
	else
		if not item.chest then
			_G.weirditem = item
			error("wtf")
		end
		local c = 0
		while c == 0 do
			c = output.pullItems(peripheral.getName(item.chest), item.slot, count, slot)
			if c == 0 then
				os.sleep(1)
			end
		end
		item.count = item.count-c
	end
	return true
end

lib.inputItem = function(slot, input, inventory)
	local details = input.getItemDetail(slot)
	if not details then return false,"no item" end
	if not details.nbt and details.maxCount > 1 then
		local items
		for i,item in ipairs(inventory) do
			if item.id == details.name then
				if item.items then
					items = item.items
				else
					items = {item}
				end
				break
			end
		end
		if items then
			for _,item in ipairs(items) do
				if item.count < details.maxCount then
					if input.pushItems(peripheral.getName(item.chest), slot, details.maxCount - item.count, item.slot) == details.count then
						return true
					else
						details = input.getItemDetail(slot)
					end
				end
			end
		end
	end
	
	for chest, data in pairs(inventory.chests) do
		if data.occupied < data.limit then
			if input.pushItems(peripheral.getName(chest), slot) == details.count then
				return true
			else
				details = input.getItemDetail(slot)
			end
		end
	end
	
	return false,"no space"
end

lib.smeltItemAsync = function(item,count,furnaces,inventory)
	local furs = {}
	local furCount = math.ceil(count/64)
	for i, furnace in ipairs(furnaces) do
		if not furnace.getItemDetail(1) then
			table.insert(furs, furnace)
			if #furs >= furCount then
				break
			end
		end
	end
	local transferred = 0
	
	if type(item) == "string" then
		for i,t in ipairs(inventory) do
			if t.id == item or t.name == item then
				item = t
				break
			end
		end
	end
	if type(item) == "string" then
		return false,"item not found"
	end
	
	if item.count < count then
		return false, "not enough items"
	end
	
	if not item.items then
		return false, "invalid item"
	end
	
	local itemsDone = 0
	
	local function update()
		-- input items
		if transferred < count then
			--print((#item.items).." of "..item.id.." ("..item.count..")")
			for i=#item.items,1,-1 do
				local t = item.items[i]
				
				--print(peripheral.getName(t.chest).. " slot "..t.slot..": "..t.count.." items ("..i..")")
				
				for _, furnace in ipairs(furs) do
					local detail = furnace.getItemDetail(1)
					local oCount = 0
					if detail then
						oCount = detail.count
					end
					local tCount = math.min(count-transferred, 64-oCount, t.count)
					--print("Transferring "..tCount.." items to "..peripheral.getName(furnace))
					if tCount > 0 then
						local c = furnace.pullItems(peripheral.getName(t.chest), t.slot, tCount, 1)
						--print("Transferred "..c.." items.")
						transferred = transferred+tCount
						t.count = t.count - c
						item.count = item.count - c
						if t.count <= 0 then
							table.remove(item.items, i)
							break
						end
						if transferred >= count then
							break
						end
					end
				end
				
				if transferred >= count then
					break
				end
			end
		end
		
		-- transfer coal & output items
		for i, furnace in ipairs(furs) do
			-- check coal
			local detail = furnace.getItemDetail(2)
			if not detail or detail.count < 1 then
				local c = detail and detail.count or 0
				lib.outputItem("minecraft:coal", 1, furnace, inventory, 2)
			end
			
			-- check output
			local detail = furnace.getItemDetail(3)
			if detail then
				itemsDone = itemsDone+detail.count
				lib.inputItem(3,furnace,inventory)
			end
		end
		
		if itemsDone >= count then
			return true, itemsDone, transferred
		else
			return false, itemsDone, transferred
		end
	end
	
	return update
end

lib.smeltItem = function(item,count,furnaces,inventory)
	local update = lib.smeltItemAsync(item,count,furnaces,inventory)
	while true do
		update()
		os.sleep(10)
	end
end
return lib