-- IndustrialTest
-- Copyright (C) 2023 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.Reactor=table.copy(industrialtest.ActivatedElectricMachine)
industrialtest.internal.unpackTableInto(industrialtest.Reactor,{
	name="industrialtest:nuclear_reactor",
	description=S("Nuclear Reactor"),
	tiles={
		"industrialtest_machine_block.png^industrialtest_nuclear_reactor_top.png",
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png",
		"industrialtest_machine_block.png^industrialtest_nuclear_reactor_front.png"
	},
	sounds="metal",
	requiresWrench=true,
	facedir=true,
	storageLists={
		"charged",
		"fuel"
	},
	powerLists={
		{
			list="charged",
			direction="o"
		}
	},
	active={
		tiles={
			"industrialtest_machine_block.png^industrialtest_nuclear_reactor_top.png",
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png",
			"industrialtest_machine_block.png^industrialtest_nuclear_reactor_front_active.png"
		},
		lightSource=2
	},
	capacity=industrialtest.api.evPowerFlow,
	flow=industrialtest.api.evPowerFlow,
	hasPowerOutput=true,
	ioConfig="oooooo"
})

function industrialtest.Reactor.onConstruct(self,pos)
	local meta=minetest.get_meta(pos)
	local inv=meta:get_inventory()
	inv:set_size("fuel",4)
	inv:set_size("charged",1)
	meta:set_int("heat",0)
	meta:set_int("size",6)
	meta:set_int("enabled",0)
	meta:set_int("stateChanged",0)
	industrialtest.ActivatedElectricMachine.onConstruct(self,pos)
end

function industrialtest.Reactor.onDestruct(self,pos)
	local meta=minetest.get_meta(pos)
	local chambers=minetest.deserialize(meta:get_string("chambers")) or {}
	for _,chamber in ipairs(chambers) do
		minetest.remove_node(chamber)
		minetest.add_item(chamber,"industrialtest:nuclear_reactor_chamber")
	end
	industrialtest.ActivatedElectricMachine.onDestruct(self,pos)
end

function industrialtest.Reactor.getFormspec(self,pos)
	local parentFormspec=industrialtest.ActivatedElectricMachine.getFormspec(self,pos)
	local meta=minetest.get_meta(pos)
	local charged=meta:get_int("industrialtest.powerAmount")/meta:get_int("industrialtest.powerCapacity")
	local size=math.floor(meta:get_int("size")/3)
	local switchText=(meta:get_int("enabled")==0 and S("Start") or S("Stop"))
	local formspec={
		"list[context;fuel;1,1;"..size..","..size.."]",
		industrialtest.internal.getItemSlotBg(1,1,size,size),
		"list[context;charged;7,2.8;1,1]",
		industrialtest.internal.getItemSlotBg(7.7,2.8,1,1),
		"button[7.7,1;1,0.8;toggle;"..minetest.formspec_escape(switchText).."]",
		self.createPowerIndicatorWidget(charged,9,1),
		"listring[context;fuel]"
	}
	return parentFormspec..table.concat(formspec,"")
end

function industrialtest.Reactor.allowMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
	local meta=minetest.get_meta(pos)
	local inv=meta:get_inventory()
	local movedItemStack=inv:get_stack(fromList,fromIndex)
	local def=movedItemStack:get_definition()
	if toList=="fuel" and (not def or not def.groups._industrialtest_placedInNuclearReactor) then
		return 0
	end
	return industrialtest.ActivatedElectricMachine.allowMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
end

function industrialtest.Reactor.allowMetadataInventoryPut(self,pos,listname,index,stack,player)
	local def=stack:get_definition()
	if listname=="fuel" and (not def or not def.groups._industrialtest_placedInNuclearReactor) then
		return 0
	end
	return industrialtest.ActivatedElectricMachine.allowMetadataInventoryPut(self,pos,listname,index,stack,player)
end

function industrialtest.Reactor.onMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
	industrialtest.ActivatedElectricMachine.onMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
	self.synchronizeChambers(pos)
end

function industrialtest.Reactor.onMetadataInventoryPut(self,pos,listname,index,stack,player)
	industrialtest.ActivatedElectricMachine.allowMetadataInventoryPut(self,pos,listname,index,stack,player)
	self.synchronizeChambers(pos)
end

function industrialtest.Reactor.onMetadataInventoryTake(self,pos,listname,index,stack)
	industrialtest.ActivatedElectricMachine.onMetadataInventoryTake(self,pos,listname,index,stack)
	self.synchronizeChambers(pos)
end

function industrialtest.Reactor.onReceiveFields(self,pos,formname,fields)
	if not fields.toggle then
		return
	end
	local meta=minetest.get_meta(pos)
	if not self.hasFuel(pos) and meta:get_int("enabled")==0 then
		return
	end
	if meta:get_int("enabled")==0 then
		meta:set_int("enabled",1)
	else
		meta:set_int("enabled",0)
		self:updateFormspec(pos)
		self.synchronizeChambers(pos)
	end
	meta:set_int("stateChanged",1)
	self:triggerIfNeeded(pos)
end

function industrialtest.Reactor.shouldActivate(self,pos)
	local meta=minetest.get_meta(pos)

	if meta:get_int("stateChanged")==0 then
		return false
	end

	return meta:get_int("enabled")>0 and self.hasFuel(pos)
end

function industrialtest.Reactor.shouldDeactivate(self,pos)
	local meta=minetest.get_meta(pos)
	return meta:get_int("enabled")==0 or not self.hasFuel(pos)
end

function industrialtest.Reactor.afterActivation(self,pos)
	local meta=minetest.get_meta(pos)
	meta:set_int("stateChanged",0)
	self.synchronizeChambers(pos)
end

function industrialtest.Reactor.afterDeactivation(self,pos)
	local meta=minetest.get_meta(pos)
	meta:set_int("enabled",0)
	self:updateFormspec(pos)
	self.synchronizeChambers(pos)
end

function industrialtest.Reactor.activeUpdate(self,pos,elapsed,meta,inv)
	local size=math.floor(meta:get_int("size")/3)
	local fuelList=inv:get_list("fuel")
	local shouldRerunTimer=meta:get_int("enabled")>0
	local shouldUpdateFormspec=false

	local maxCluster=self.findMaxFuelCluster(size,fuelList)
	for _,stack in ipairs(maxCluster) do
		local index=stack.y*size+stack.x
		local usedStack,_=self.useFuel(fuelList[index],5)
		inv:set_stack("fuel",index,usedStack)
	end
	local generatedPowerAmount=math.pow(3,#maxCluster)
	if industrialtest.api.addPower(meta,generatedPowerAmount)>0 then
		shouldUpdateFormspec=true
	end

	local heat=meta:get_int("heat")+#maxCluster
	local coolant=self.findCoolant(fuelList)
	if coolant>0 then
		local coolantStack,used=self.useFuel(fuelList[coolant],#maxCluster*50)
		heat=math.max(0,heat-used)
		inv:set_stack("fuel",coolant,coolantStack)
	end
	if heat>200 then
		minetest.remove_node(pos)
		industrialtest.internal.explode(pos,#maxCluster*4)
		return false
	end
	meta:set_int("heat",heat)

	self.synchronizeChambers(pos)

	return shouldUpdateFormspec
end

function industrialtest.Reactor.changeSize(self,pos,diff)
	local meta=minetest.get_meta(pos)
	local inv=meta:get_inventory()
	local size=meta:get_int("size")+diff
	local actualSize=math.floor(size/3)
	meta:set_int("size",size)
	inv:set_size("fuel",actualSize*actualSize)
	self:updateFormspec(pos)
end

function industrialtest.Reactor.synchronizeToChamber(self,pos)
	local meta=minetest.get_meta(pos)
	local inv=meta:get_inventory()
	local fuelList=inv:get_list("fuel")
	local chargedList=inv:get_list("charged")

	local reactorPos=minetest.deserialize(meta:get_string("reactor"))
	local reactorMeta=minetest.get_meta(reactorPos)
	local reactorInv=reactorMeta:get_inventory()
	reactorInv:set_list("fuel",fuelList)
	reactorInv:set_list("charged",chargedList)

	self.synchronizeChambers(reactorPos)
end

function industrialtest.Reactor.hasFuel(pos)
	local meta=minetest.get_meta(pos)
	local inv=meta:get_inventory()
	local fuelList=inv:get_list("fuel")
	for _,stack in ipairs(fuelList) do
		if stack:get_name()=="industrialtest:uranium_cell" then
			return true
		end
	end
	return false
end

function industrialtest.Reactor.findMaxFuelCluster(size,fuelList)
	local maxCluster={}
	for y=1,size do
		for x=1,size do
			local iy=y-1
			local stack=fuelList[iy*size+x]
			local def=minetest.registered_tools[stack:get_name()]
			if def and def.groups._industrialtest_nuclearReactorFuel then
				local cluster={
					[1]={
						x=x,
						y=iy
					}
				}
				if x>1 and fuelList[iy*size+x-1]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x-1,
						y=iy
					})
				end
				if x<size and fuelList[iy*size+x+1]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x+1,
						y=iy
					})
				end
				if y>1 and fuelList[(iy-1)*size+x]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x,
						y=iy-1
					})
				end
				if y<size and fuelList[(iy+1)*size+x]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x,
						y=iy+1
					})
				end
				if x>1 and y>1 and fuelList[(iy-1)*size+x-1]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x-1,
						y=iy-1
					})
				end
				if x<size and y>1 and fuelList[(iy-1)*size+x+1]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x+1,
						y=iy-1
					})
				end
				if x>1 and y<size and fuelList[(iy+1)*size+x-1]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x-1,
						y=iy+1
					})
				end
				if x<size and y<size and fuelList[(iy+1)*size+x+1]:get_name()==stack:get_name() then
					table.insert(cluster,{
						x=x+1,
						y=iy+1
					})
				end
				if #cluster==9 then
					return cluster
				end
				if #cluster>#maxCluster then
					maxCluster=cluster
				end
			end
		end
	end
	return maxCluster
end

function industrialtest.Reactor.findCoolant(fuelList)
	for i=1,#fuelList do
		local stack=fuelList[i]
		local def=minetest.registered_tools[stack:get_name()]
		if def and def.groups._industrialtest_nuclearReactorCoolant then
			return i
		end
	end
	return 0
end

function industrialtest.Reactor.useFuel(stack,use)
	local used=math.min(65535-stack:get_wear(),use)
	if used<use then
		stack:replace("industrialtest:empty_cell")
	else
		stack:set_wear(stack:get_wear()+used)
	end
	return stack,used
end

function industrialtest.Reactor.synchronizeChambers(pos)
	local meta=minetest.get_meta(pos)
	local chambers=meta:contains("chambers") and minetest.deserialize(meta:get_string("chambers")) or {}
	for _,chamber in ipairs(chambers) do
		industrialtest.ReactorChamber:synchronize(chamber,pos)
	end
end

industrialtest.Reactor:register()

minetest.register_craft({
	type="shaped",
	output="industrialtest:nuclear_reactor",
	recipe={
		{"","industrialtest:advanced_electronic_circuit",""},
		{"industrialtest:nuclear_reactor_chamber","industrialtest:nuclear_reactor_chamber","industrialtest:nuclear_reactor_chamber"},
		{"","industrialtest:generator",""}
	}
})

industrialtest.ReactorChamber=table.copy(industrialtest.Machine)
industrialtest.internal.unpackTableInto(industrialtest.ReactorChamber,{
	name="industrialtest:nuclear_reactor_chamber",
	description=S("Nuclear Reactor Chamber"),
	tiles={"industrialtest_machine_block.png^industrialtest_nuclear_reactor_top.png"},
	sounds="metal",
	storageLists={
		"charged",
		"fuel"
	},
	requiresWrench=true
})

function industrialtest.ReactorChamber.onDestruct(self,pos)
	industrialtest.Machine.onDestruct(self,pos)

	local meta=minetest.get_meta(pos)

	if not meta:contains("reactor") then
		return
	end

	local reactorPos=minetest.deserialize(meta:get_string("reactor"))
	local reactorMeta=minetest.get_meta(reactorPos)
	if not reactorMeta or not reactorMeta:contains("chambers") then
		return
	end
	local chambers=minetest.deserialize(reactorMeta:get_string("chambers"))
	for i,chamber in ipairs(chambers) do
		if chamber.x==pos.x and chamber.y==pos.y and chamber.z==pos.z then
			table.remove(chambers,i)
			break
		end
	end
	reactorMeta:set_string("chambers",minetest.serialize(chambers))

	industrialtest.Reactor:changeSize(reactorPos,-1)
	industrialtest.Reactor.synchronizeChambers(reactorPos)
end

function industrialtest.ReactorChamber.afterPlaceNode(self,pos)
	local neighbours={
		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 reactorPos=nil
	for _,neighbour in ipairs(neighbours) do
		local node=minetest.get_node(neighbour)
		if node.name=="industrialtest:nuclear_reactor" or node.name=="industrialtest:nuclear_reactor_active" then
			reactorPos=neighbour
		end
	end
	if not reactorPos then
		minetest.remove_node(pos)
		return true
	end

	local meta=minetest.get_meta(pos)
	meta:set_string("reactor",minetest.serialize(reactorPos))

	industrialtest.Reactor:changeSize(reactorPos,1)
	industrialtest.Reactor.synchronizeChambers(reactorPos)

	local reactorMeta=minetest.get_meta(reactorPos)
	local chambers=reactorMeta:contains("chambers") and minetest.deserialize(reactorMeta:get_string("chambers")) or {}
	table.insert(chambers,pos)
	reactorMeta:set_string("chambers",minetest.serialize(chambers))

	industrialtest.api.createNetworkMapForNode(reactorPos)

	self:synchronize(pos,reactorPos)
end

function industrialtest.ReactorChamber.getFormspec(self,pos)
	local meta=minetest.get_meta(pos)
	if not meta:contains("reactor") then
		return ""
	end

	local reactorPos=minetest.deserialize(meta:get_string("reactor"))
	return industrialtest.Reactor:getFormspec(reactorPos)
end

function industrialtest.ReactorChamber.allowMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
	return industrialtest.Reactor:allowMetadataInventoryMove(pos,fromList,fromIndex,toList,toIndex,count)
end

function industrialtest.ReactorChamber.allowMetadataInventoryPut(self,pos,listname,index,stack,player)
	return industrialtest.Reactor:allowMetadataInventoryPut(pos,listname,index,stack,player)
end

function industrialtest.ReactorChamber.onMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
	industrialtest.Machine.onMetadataInventoryMove(self,pos,fromList,fromIndex,toList,toIndex,count)
	industrialtest.Reactor:synchronizeToChamber(pos)
end

function industrialtest.ReactorChamber.onMetadataInventoryPut(self,pos,listname,index,stack,player)
	industrialtest.Machine.allowMetadataInventoryPut(self,pos,listname,index,stack,player)
	industrialtest.Reactor:synchronizeToChamber(pos)
end

function industrialtest.ReactorChamber.onMetadataInventoryTake(self,pos,listname,index,stack)
	industrialtest.Machine.onMetadataInventoryTake(self,pos,listname,index,stack)
	industrialtest.Reactor:synchronizeToChamber(pos)
end

function industrialtest.ReactorChamber.onReceiveFields(self,pos,formname,fields)
	local meta=minetest.get_meta(pos)
	local reactorPos=minetest.deserialize(meta:get_string("reactor"))
	industrialtest.Reactor:onReceiveFields(reactorPos,formname,fields)
end

function industrialtest.ReactorChamber.createDefinitionTable(self)
	local def=industrialtest.Machine.createDefinitionTable(self)
	def.groups._industrialtest_cable=1
	def._industrialtest_cableFlow=industrialtest.api.evPowerFlow
	return def
end

function industrialtest.ReactorChamber.synchronize(self,pos,reactor)
	self:updateFormspec(pos)

	local meta=minetest.get_meta(pos)
	local inv=meta:get_inventory()
	local reactorMeta=minetest.get_meta(reactor)
	local reactorInv=reactorMeta:get_inventory()
	local fuelList=reactorInv:get_list("fuel")
	local chargedList=reactorInv:get_list("charged")
	inv:set_size("fuel",#fuelList)
	inv:set_size("charged",#chargedList)
	inv:set_list("fuel",fuelList)
	inv:set_list("charged",chargedList)
end

industrialtest.ReactorChamber:register()

minetest.register_craft({
	type="shaped",
	output="industrialtest:nuclear_reactor_chamber",
	recipe={
		{"","industrialtest:lead_plate",""},
		{"industrialtest:lead_plate","industrialtest:machine_block","industrialtest:lead_plate"},
		{"","industrialtest:lead_plate",""}
	}
})
