-- IndustrialTest
-- Copyright (C) 2025 mrkubax10

-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.

-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.

-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

local S=minetest.get_translator("industrialtest")
industrialtest.Pump=table.copy(industrialtest.ActivatedElectricMachine)
industrialtest.internal.unpackTableInto(industrialtest.Pump,{
	name="industrialtest:pump",
	description=S("Pump"),
	tiles={
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png^industrialtest_pump_side.png",
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png^industrialtest_pump_front.png"
	},
	sounds="metal",
	facedir=true,
	storageLists={
		"src",
		"dst",
		"powerStorage"
	},
	powerLists={
		{
			list="powerStorage",
			direction="i"
		}
	},
	active={
		tiles={
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png^industrialtest_pump_side.png",
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png^industrialtest_pump_front_active.png"
		}
	},
	capacity=industrialtest.api.lvPowerFlow*2,
	flow=industrialtest.api.lvPowerFlow,
	ioConfig="iiiiii",
	requiresWrench=true,
	hasPowerInput=true,
	_fluidCapacity=5000,
	_opPower=300,
	_pumpTime=10
})

function industrialtest.Pump.onConstruct(self,pos)
	local meta=minetest.get_meta(pos)
	local inv=meta:get_inventory()
	inv:set_size("src",1)
	inv:set_size("dst",1)
	inv:set_size("powerStorage",1)
	meta:set_float("srcTime",0)
	industrialtest.api.addFluidStorage(meta,self._fluidCapacity)
	self.determinePumpTargets(pos)
	industrialtest.ActivatedElectricMachine.onConstruct(self,pos)
end

function industrialtest.Pump.getFormspec(self,pos)
	local parentFormspec=industrialtest.ActivatedElectricMachine.getFormspec(self,pos)
	local meta=minetest.get_meta(pos)
	local powerPercent=meta:get_int("industrialtest.powerAmount")/meta:get_int("industrialtest.powerCapacity")*100
	local srcPercent=meta:get_float("srcTime")/self._pumpTime*100
	local fluidType=meta:get_string("industrialtest.fluidType")
	local fluidPercent=0
	if meta:contains("industrialtest.fluidAmount") and meta:contains("industrialtest.fluidCapacity") then
		fluidPercent=meta:get_int("industrialtest.fluidAmount")/meta:get_int("industrialtest.fluidCapacity")*100
	end
	local pumpFluid=industrialtest.api.getPumpFluid(fluidType)
	local tile=(pumpFluid and pumpFluid.texture or "industrialtest_gui_fluid_bg.png")
	local formspec={
		industrialtest.internal.getItemSlotBg(3.2,1.7,1,1),
		"list[context;src;3.2,1.7;1,1]",
		industrialtest.internal.getItemSlotBg(4.6,1.7,1,1),
		"list[context;dst;4.6,1.7;1,1]",
		industrialtest.internal.getItemSlotBg(3.9,3.7,1,1),
		"list[context;powerStorage;3.9,3.7;1,1]",
		(powerPercent>0 and "image[3.9,2.7;1,1;industrialtest_gui_electricity_bg.png^[lowpart:"..powerPercent..":industrialtest_gui_electricity_fg.png]"
		 or "image[3.9,2.7;1,1;industrialtest_gui_electricity_bg.png]"),
		(srcPercent>0 and "image[6.7,2.7;1,1;gui_furnace_arrow_bg.png^[lowpart:"..srcPercent..":gui_furnace_arrow_fg.png]"
		 or "image[6.7,2.7;1,1;gui_furnace_arrow_bg.png]"),
		(fluidPercent>0 and "image[7.7,2.7;1,1;industrialtest_gui_fluid_bg.png^[lowpart:"..fluidPercent..":"..tile.."]" or "image[7.7,2.7;1,1;industrialtest_gui_fluid_bg.png]"),
		"label[3.2,1.35;"..S("Input").."]",
		"label[4.6,1.35;"..S("Output").."]",
		"listring[context;src]",
		"listring[context;dst]"
	}
	return parentFormspec..table.concat(formspec,"")
end

function industrialtest.Pump.allowMetadataInventoryMove(self,pos,fromList,fromIndex,toList,count)
	if toList=="dst" then
		return 0
	end
	return industrialtest.ActivatedElectricMachine.allowMetadataInventoryMove(self,pos,fromList,fromIndex,toList,count)
end

function industrialtest.Pump.allowMetadataInventoryPut(self,pos,listname,index,stack,player)
	if listname=="dst" then
		return 0
	end
	return industrialtest.ActivatedElectricMachine.allowMetadataInventoryPut(self,pos,listname,index,stack,player)
end

function industrialtest.Pump.allowMetadataInventoryTake(self,pos,listname,index,stack,player)
	if listname=="src" then
		local meta=minetest.get_meta(pos)
		local inv=meta:get_inventory()
		local srcSlot=inv:get_stack("src",1)
		if stack:get_count()==srcSlot:get_count() and not meta:get_int("hasOutputTarget") then
			meta:set_float("srcTime",0)
			self:updateFormspec(pos)
		end
	end
	return industrialtest.ActivatedElectricMachine.allowMetadataInventoryTake(self,pos,listname,index,stack,player)
end

function industrialtest.Pump.onMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
	if fromList=="src" then
		local meta=minetest.get_meta(pos)
		local inv=meta:get_inventory()
		local srcSlot=inv:get_stack("src",1)
		if count==srcSlot:get_count() and not meta:get_int("hasOutputTarget") then
			meta:set_float("srcTime",0)
			self:updateFormspec(pos)
		end
	elseif toList=="src" then
		self:triggerIfNeeded(pos)
	end
	industrialtest.ActivatedElectricMachine.onMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
end

function industrialtest.Pump.onMetadataInventoryPut(self,pos,listname,index,stack)
	if listname=="src" then
		self:triggerIfNeeded(pos)
	end
	industrialtest.ActivatedElectricMachine.onMetadataInventoryPut(self,pos,listname,index,stack)
end

function industrialtest.Miner.onMetadataInventoryTake(self,pos,listname,index,stack)
	if listname=="dst" then
		self:triggerIfNeeded(pos)
	end
end

function industrialtest.Pump.shouldActivate(self,pos)
	local meta=minetest.get_meta(pos)
	if meta:get_int("industrialtest.powerAmount")<self._opPower then
		return false
	end

	local hasInput=false
	local nodeUnder=minetest.get_node(vector.offset(pos,0,-1,0))
	local fluidAmount=meta:get_int("industrialtest.fluidAmount")
	if fluidAmount>0 then
		hasInput=true
	else
		-- Check if there is node that can be pumped under pump
		if industrialtest.api.getPumpFluid(nodeUnder.name) then
			hasInput=true
		end
	end

	-- Check if any input target can provide with fluid
	if not hasInput and meta:get_int("hasInputTarget")>0 then
		local polledTargets=self.pollInputTargets(pos)
		hasInput=#polledTargets>0
	end

	if not hasInput then
		return false
	end

	local hasOutput=false
	-- First check if pump can push into any neighour node
	if meta:get_int("hasOutputTarget") then
		local outputTargets=minetest.deserialize(meta:get_string("outputTargets"))
		for _,target in ipairs(outputTargets) do
			local node=minetest.get_node(target)
			local def=minetest.registered_nodes[node.name]
			hasOutput=(def and def._industrialtest_self and def._industrialtest_self:canPushFluid(target,nodeUnder.name,fluidAmount))
			if hasOutput then
				break
			end
		end
	end

	-- Check if it's possible to pump fluid into item
	if not hasOutput then
		local inv=meta:get_inventory()
		local srcSlot=inv:get_stack("src",1)
		if not srcSlot:is_empty() then
			local def=srcSlot:get_definition()
			if def.groups._industrialtest_simpleFluidStorage and def._industrialtest_simpleFluidStorageCapacity and fluidAmount>=def._industrialtest_simpleFluidStorageCapacity and
				def._industrialtest_getResultingFluidStorageItemByNode then
				local fluidType=meta:get_string("industrialtest.fluidType")
				local resulting=def._industrialtest_getResultingFluidStorageItemByNode(fluidType)
				if resulting then
					local dstSlot=inv:get_stack("dst",1)
					hasOutput=dstSlot:item_fits(ItemStack(resulting.name))
				end
			end
		end
	end

	-- Check if pump storage is not full
	if not hasOutput then
		local fluidCapacity=meta:get_int("industrialtest.fluidCapacity")
		hasOutput=fluidCapacity-fluidAmount>=industrialtest.api.nodeFluidCapacity
	end

	return hasOutput
end

function industrialtest.Pump.shouldDeactivate(self,pos)
	return not self:shouldActivate(pos)
end

function industrialtest.Pump.afterDeactivation(self,pos)
	-- If machine was deactivated then make sure to update formspec
	local meta=minetest.get_meta(pos)
	meta:set_float("srcTime",0)
	self:updateFormspec(pos)
end

function industrialtest.Pump.activeUpdate(self,pos,elapsed,meta,inv)
	local nodeUnderPos=vector.offset(pos,0,-1,0)
	local nodeUnder=minetest.get_node(nodeUnderPos)
	local fluidAmount=meta:get_int("industrialtest.fluidAmount")
	local fluidCapacity=meta:get_int("industrialtest.fluidCapacity")
	local shouldUpdateFormspec=false

	-- First try to pump fluid under
	-- Check if there is node that can be pumped under pump
	if fluidCapacity-fluidAmount>=industrialtest.api.nodeFluidCapacity and industrialtest.api.getPumpFluid(nodeUnder.name) then
		local srcTime=meta:get_float("srcTime")+elapsed*industrialtest.api.getMachineSpeed(meta)
		if srcTime>=self._pumpTime then
			fluidAmount=fluidAmount+industrialtest.api.nodeFluidCapacity
			meta:set_string("industrialtest.fluidType",nodeUnder.name)
			meta:set_int("industrialtest.fluidAmount",fluidAmount)
			minetest.remove_node(nodeUnderPos)
			srcTime=0
		end
		industrialtest.api.addPower(meta,-self._opPower)
		meta:set_float("srcTime",srcTime)
		shouldUpdateFormspec=true
	end

	if meta:get_int("hasInputTarget")>0 then
		local polledTargets=self.pollInputTargets(pos)
		local fluidType=meta:get_string("industrialtest.fluidType")
		for _,target in ipairs(polledTargets) do
			local moved=math.min(fluidCapacity-fluidAmount,target.fluidInfo.remaining)
			fluidAmount=fluidAmount+moved
			target.pullFluid(moved)
			meta:set_string("industrialtest.fluidType",target.fluidInfo.fluidType)
			shouldUpdateFormspec=true
			if fluidCapacity-fluidAmount<=0 then
				break
			end
		end
		meta:set_int("industrialtest.fluidAmount",fluidAmount)
	end

	-- Try to push fluid into item if available
	local inv=meta:get_inventory()
	local srcSlot=inv:get_stack("src",1)
	if not srcSlot:is_empty() then
		local def=srcSlot:get_definition()
		if def.groups._industrialtest_simpleFluidStorage and def._industrialtest_simpleFluidStorageCapacity and fluidAmount>=def._industrialtest_simpleFluidStorageCapacity and
			def._industrialtest_getResultingFluidStorageItemByNode then
			local fluidType=meta:get_string("industrialtest.fluidType")
			local resulting=def._industrialtest_getResultingFluidStorageItemByNode(fluidType)
			if resulting then
				local dstSlot=inv:get_stack("dst",1)
				local resultingStack=ItemStack(resulting.name)
				if dstSlot:item_fits(resultingStack) then
					dstSlot:add_item(resultingStack)
					inv:set_stack("dst",1,dstSlot)
					srcSlot:take_item()
					inv:set_stack("src",1,srcSlot)
					fluidAmount=fluidAmount-def._industrialtest_simpleFluidStorageCapacity
					meta:set_int("industrialtest.fluidAmount",fluidAmount)
					shouldUpdateFormspec=true
				end
			end
		end
	end

	-- Try to push fluid into neighbour target
	if meta:get_int("hasOutputTarget")>0 then
		local outputTargets=minetest.deserialize(meta:get_string("outputTargets"))
		for _,targetPos in ipairs(outputTargets) do
			local targetNode=minetest.get_node(targetPos)
			local targetDef=minetest.registered_nodes[targetNode.name]
			local fluidType=meta:get_string("industrialtest.fluidType")
			if targetDef and targetDef._industrialtest_self and targetDef._industrialtest_self.canPushFluid and
				targetDef._industrialtest_self.onPumpFluidPush and targetDef._industrialtest_self:canPushFluid(targetPos,fluidType,fluidAmount) then
				fluidAmount=targetDef._industrialtest_self:onPumpFluidPush(targetPos,pos,fluidType,fluidAmount)
			end
		end
		meta:set_int("industrialtest.fluidAmount",fluidAmount)
	end

	return shouldUpdateFormspec
end

function industrialtest.Pump.action(self,pos)
	self.determinePumpTargets(pos)
	self:triggerIfNeeded(pos)
end

-- Scans neighbour positions for pump targets
function industrialtest.Pump.determinePumpTargets(pos)
	local neighbourPositions={
		vector.offset(pos,-1,0,0),
		vector.offset(pos,1,0,0),
		vector.offset(pos,0,-1,0),
		vector.offset(pos,0,1,0),
		vector.offset(pos,0,0,-1),
		vector.offset(pos,0,0,1)
	}
	local inputTargets={}
	local outputTargets={}
	for _,neighbour in ipairs(neighbourPositions) do
		local node=minetest.get_node(neighbour)
		local targetDef=industrialtest.api.getPumpTarget(node.name)
		if targetDef then
			if targetDef.direction=="i" then
				table.insert(inputTargets,neighbour)
			elseif targetDef.direction=="o" then
				table.insert(outputTargets,neighbour)
			end
		end
	end
	local meta=minetest.get_meta(pos)
	meta:set_string("inputTargets",minetest.serialize(inputTargets))
	meta:set_int("hasInputTarget",#inputTargets>0 and 1 or 0)
	meta:set_string("outputTargets",minetest.serialize(outputTargets))
	meta:set_int("hasOutputTarget",#outputTargets>0 and 1 or 0)
end

-- \brief Checks all input targets if they have any fluid incoming
-- \param pos vector
-- \returns table
function industrialtest.Pump.pollInputTargets(pos)
	local meta=minetest.get_meta(pos)
	local inputTargets=minetest.deserialize(meta:get_string("inputTargets"))
	local fluidType=meta:get_string("industrialtest.fluidType")
	local result={}
	for _,targetPos in ipairs(inputTargets) do
		local targetNode=minetest.get_node(targetPos)
		local targetDef=minetest.registered_nodes[targetNode.name]
		if targetDef and targetDef._industrialtest_self and targetDef._industrialtest_self.pullFluid then
			local fluidInfo=targetDef._industrialtest_self:pullFluid(targetPos,0)
			if fluidInfo and (fluidInfo.fluidType==fluidType or fluidType=="ignore") then
				table.insert(result,{
					pos=targetPos,
					fluidInfo=fluidInfo,
					pullFluid=function(amount)
						return targetDef._industrialtest_self:pullFluid(targetPos,amount)
					end
				})
				fluidType=fluidInfo.fluidType
			end
		end
	end
	return result
end

industrialtest.Pump:register()

minetest.register_abm({
	label="Pump pumping",
	nodenames={"industrialtest:pump"},
	interval=industrialtest.config.updateDelay,
	chance=1,
	action=function(pos)
		industrialtest.Pump:action(pos)
	end
})

minetest.register_craft({
	type="shaped",
	output="industrialtest:pump",
	recipe={
		{"industrialtest:empty_cell","industrialtest:electronic_circuit","industrialtest:empty_cell"},
		{"industrialtest:empty_cell","industrialtest:machine_block","industrialtest:empty_cell"},
		{"industrialtest:mining_pipe",industrialtest.elementKeys.treetap,"industrialtest:mining_pipe"}
	}
})
