local constants = {
	xmit_wattage = 0.4;
	-- the amount of power per second needed to transmit an item from
	-- one displacer to another

	rcpt_wattage = 0.15;
	-- the amount of power needed to broadcast a receptor's availability
}

local gettxr = function(pos)
	local txrcomps = {
		'sorcery:displacer';
		'sorcery:displacer_transmit_gem';
		'sorcery:displacer_transmit_attune';
		'sorcery:displacer_receive_gem';
		'sorcery:displacer_receive_attune';
	}

	local devs = sorcery.lib.node.amass(pos,txrcomps,sorcery.lib.node.offsets.neighbors)
	local r = {
		receptacles = {};
		connections = {};
		counts = {
			receptacles = 0;
			transmitters = 0;
			receptors = 0;
		};
	}
	local getcode = function(pos)
		local inv = minetest.get_meta(pos):get_inventory()
		local code = {}
		local empty = true
		for i=1,inv:get_size('code') do
			if not inv:get_stack('code',i):is_empty()
				then empty = false end

			code[i] = inv:get_stack('code',i):get_name()
		end
		if empty then return nil
		         else return code end
	end
	for pos, dev in pairs(devs) do
		if dev == 'sorcery:displacer_receive_gem' then
			r.counts.receptors = r.counts.receptors + 1
			r.connections[#r.connections+1] = {
				pos = pos; mode = 'receive';
				code = getcode(pos); -- TODO retrieve code
			}
		elseif dev == 'sorcery:displacer_receive_attune' then
			local tune = sorcery.attunement.verify(pos)
			if tune then
				r.counts.receptors = r.counts.receptors + 1
				r.connections[#r.connections+1] = {
					pos = pos; mode = 'receive';
					partner = tune.partner;
				}
			end
		elseif dev == 'sorcery:displacer_transmit_gem' then
			r.counts.transmitters = r.counts.transmitters + 1
			r.connections[#r.connections+1] = {
				pos = pos; mode = 'transmit';
				code = getcode(pos); -- TODO retrieve code
			}
		elseif dev == 'sorcery:displacer_transmit_attune' then
			local tune = sorcery.attunement.verify(pos)
			if tune then
				r.counts.transmitters = r.counts.transmitters + 1
				r.connections[#r.connections+1] = {
					pos = pos; mode = 'transmit';
					partner = tune.partner;
				}
			end
		elseif dev == 'sorcery:displacer' then
			r.counts.receptacles = r.counts.receptacles + 1
			r.receptacles[#r.receptacles+1] = pos
		end
	end
	return r
end

local autoselect = function(pos)
	local dev = gettxr(pos)
	local active
	if dev.counts.receptors == 0 and dev.counts.transmitters == 1 then
		active = dev.connections[1].pos
	end
	for _,rcp in pairs(dev.receptacles) do
		local meta = minetest.get_meta(rcp)
		meta:set_string('active-device',active and minetest.pos_to_string(active) or '')
	end
	return active ~= nil
end

minetest.register_node('sorcery:displacer', {
	description = 'Displacer Receptacle';
	paramtype2 = 'facedir';
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		minetest.get_node_timer(pos):start(1)
		inv:set_size('cache', 6)
		meta:set_string('infotext','displacer')
		meta:set_string('active-device','')
		meta:set_string('formspec', [[
			formspec_version[3] size[10.25,8]
			list[context;cache;3.125,0.25;3,2]
			list[current_player;main;0.25,3;8,4]
			listring[]
		]])
	end;

	-- vararg wrapping necessary to discard the return value,
	-- as a return value of true will prevent the item from
	-- being removed from the user's inventory after placement
	after_place_node = function(...) autoselect(...) end;
	after_dig_node = function(...)
		autoselect(...)
		sorcery.lib.node.purge_container(...)
	end;
	on_metadata_inventory_put = function(pos)
		minetest.get_node_timer(pos):start(1)
	end;
	on_timer = function(pos,delta)
		local meta = minetest.get_meta(pos)
		if not meta:contains('active-device') then return false end

		local probe = sorcery.spell.probe(pos)
		if probe.disjunction then return true end

		local inv = meta:get_inventory()
		if inv:is_empty('cache') then return false end

		local dev = gettxr(pos)
		local active = minetest.string_to_pos(meta:get_string('active-device'))

		local ad
		for _,d in pairs(dev.connections) do
			if vector.equals(d.pos, active) then ad = d break end
		end
		if not ad then
			meta:set_string('active-device','')
			return false
		end

		local remote
		if ad.partner then
			remote = gettxr(ad.partner)
		elseif ad.code then
			local net = sorcery.farcaster.junction(pos,constants.xmit_wattage)
			for _,n in pairs(net) do
				for _,d in pairs(n.caps.net.devices.consume) do
					if d.id == 'sorcery:displacer' then
						local dp = sorcery.spell.probe(d.pos)
						if not dp.disjunction then
							local t = gettxr(d.pos)
							for _,d in pairs(t.connections) do
								if d.mode == 'receive' and d.code then
									local match = true
									for i=1,#d.code do
										if d.code[i] ~= ad.code[i] then
											match = false break
										end
									end
									if match then
										remote = t
										break
									end
								end
							end
						end
					end
					if remote then break end
				end
				if remote then break end
			end
		end

		if not remote then return false end


		local n = sorcery.ley.netcaps(pos,delta,nil,constants.xmit_wattage)
		if n.self.powerdraw == n.self.maxpower then
			-- fully powered for transmission; find an object to transmit
			local transmission
			for i=1,inv:get_size('cache') do
				local s = inv:get_stack('cache',i)
				if not (s:is_empty() or minetest.get_item_group(s:get_name(), 'sorcery_nontranslocatable') ~= 0) then
					local quantity = 1
					local tq = minetest.get_item_group(s:get_name(), 'sorcery_translocate_pack')
					if tq ~= 0 then quantity = math.min(tq, s:get_count()) end
					transmission = s:take_item(quantity)
					inv:set_stack('cache',i,s)
					break
				end
			end
			if not transmission then return false end
			-- iterate through available receptacles and see if there's room
			-- in any of them. otherwise, fail
			for _,r in pairs(remote.receptacles) do
				local i = minetest.get_meta(r):get_inventory()
				transmission = i:add_item('cache',transmission)
				if transmission:is_empty() then break end
			end
			if not transmission:is_empty() then inv:add_item('cache',transmission) end
			return true
		elseif n.maxpower >= n.self.maxpower then
			-- other devices are currently drawing power and might stop,
			-- making enough available for us; keep iterating just in case
			return true
		else
			-- the system does not have the capability to generate
			-- sufficient power, no point in continuing to fuck around
			return false
		end
	end;
	groups = {
		cracky = 2;
		sorcery_ley_device = 1;
		sorcery_magitech = 1;
	};
	tiles = {
		'sorcery_displacer_top.png';
		'sorcery_displacer_top.png';
		'sorcery_displacer_side.png';
		'sorcery_displacer_side.png';
		'sorcery_displacer_side.png';
		'sorcery_displacer_front.png';
	};

	_sorcery = {
		on_leychange = function(pos)
			minetest.get_node_timer(pos):start(1)
		end;
		ley = {
			mode = 'consume', affinity = {'mandatic'};
			power = function(pos,time)
				local meta = minetest.get_meta(pos)
				local power = 0
				if meta:contains('active-device') then
					power = constants.xmit_wattage
				end

				local dev = gettxr(pos)
				power = power + constants.rcpt_wattage * dev.counts.receptors

				return (power / dev.counts.receptacles) * time
			end;
		};
	};
})

for mode,m in pairs {
	gem={
		desc = 'Gem-Coded';
		construct = function(pos)
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			inv:set_size('code',6)
			meta:set_string('formspec', [[
				formspec_version[3] size[10.25,7]
				list[context;code;1.5,0.25;6,1]
				list[current_player;main;0.25,1.75;8,4]
				listring[]
			]])
		end;
		allowput = function(pos,list,idx,stack)
			if list == 'code' then
				if sorcery.itemclass.get(stack:get_name(),'gem') then return 1 end
			end
			return 0
		end;
	};
	attune={
		desc = 'Attuned';
	};
} do for kind,n in pairs {
		transmit = {
			name = 'Transmission';
			button = function(pos)
				minetest.sound_play('doors_steel_door_open', {pos = pos})
				local n = minetest.get_node(pos)
				local dev = gettxr(pos)
				if dev.counts.receptacles > 0 then
					for _,r in pairs(dev.receptacles) do
						local m = minetest.get_meta(r)
						m:set_string('active-device',minetest.pos_to_string(pos))
						minetest.get_node_timer(r):start(1)
					end
				end
			end;
			attune = {
				target = true, accepts = 'sorcery:displacer';
				reciprocal = true;
			};
		};
		receive = {
			name = 'Reception';
			attune = {
				source = true, class = 'sorcery:displacer';
				reciprocal = true;
			}
		};
	} do local id = 'sorcery:displacer_' .. kind .. '_' .. mode
		minetest.register_node(id, {
			description = m.desc .. ' ' .. n.name .. ' Module';
			paramtype2 = 'facedir';
			tiles = {
				'sorcery_displacer_top.png';
				'sorcery_displacer_top.png';
				'sorcery_displacer_side.png';
				'sorcery_displacer_side.png';
				'sorcery_displacer_side.png';
				'sorcery_displacer_module_' .. kind .. '.png';
			};
			on_construct = m.construct;
			on_rightclick = mode ~= 'gem' and n.button or nil;
			on_punch = n.button and function(pos,node,puncher)
				if puncher and puncher:get_wielded_item():is_empty() then
					n.button(pos)
				end
			end or nil;
			after_place_node = function(...)
				autoselect(...)
				-- these are not ley devices but they still affect the
				-- energy consumption of the ley device they are attached
				-- to, so we need to manually run the notification routine
				-- when they are placed or dug
				sorcery.ley.notify(...)
			end;
			allow_metadata_inventory_put = m.allowput;
			_sorcery = {
				attune = (mode == 'attune') and n.attune or nil;
			};
			after_dig_node = function(...)
				autoselect(...)
				sorcery.lib.node.purge_container(...)
				sorcery.lib.node.notifyneighbors(...)
			end;
			groups = {
				cracky = 2;
				sorcery_magitech = 1;
			};
		})
	end
end
