local lib = ...
local V = vector.new
local ofs = {
	neighbors = {
		V( 0,  1,  0);
		V( 0, -1,  0);
		V( 1,  0,  0);
		V(-1,  0,  0);
		V( 0,  0,  1);
		V( 0,  0, -1);
	};
	corners = {
		V( 1,  0,  1);
		V(-1,  0,  1);
		V(-1,  0, -1);
		V( 1,  0, -1);
	};
	planecorners = {
		V( 1,  0,  1);
		V(-1,  0,  1);
		V(-1,  0, -1);
		V( 1,  0, -1);

		V( 1,  1,  0);
		V(-1,  1,  0);
		V(-1, -1,  0);
		V( 1, -1,  0);
	};
	cubecorners = {
		V( 1,  1,  1);
		V(-1,  1,  1);
		V(-1, -1,  1);
		V(-1, -1, -1);
		V( 1, -1, -1);
		V( 1,  1, -1);
		V( 1, -1,  1);
		V(-1,  1, -1);
	};
	nextto = {
		V( 1,  0,  0);
		V(-1,  0,  0);
		V( 0,  0,  1);
		V( 0,  0, -1);
	};
	cardinal = {
		V( 1, 0,  0);
		V(-1, 0,  0);
		V( 0, 0,  1);
		V( 0, 0, -1);
	};
}

ofs.adjoining = lib.tbl.append(lib.tbl.append(
	ofs.neighbors,ofs.planecorners),ofs.cubecorners)

local purge_container = function(only, pos,node,meta,user)
	local offset = function(pos,range)
		local r = function(min,max)
			return (math.random() * (max - min)) + min
		end
		return {
			x = pos.x + r(0 - range, range);
			y = pos.y;
			z = pos.z + r(0 - range, range);
		}
	end
	for name, inv in pairs(meta.inventory) do
		if only and not lib.tbl.has(only,name) then goto skip end
		for _, item in pairs(inv) do
			if not item:is_empty() then
				core.add_item(offset(pos,0.4), item)
			end
		end
	::skip::end
end;

local force = function(pos,preload_for)
	local n = core.get_node_or_nil(pos)
	if preload_for then lib.node.preload(pos,preload_for) end
	if n then return n end

	core.load_area(pos)
	return core.get_node(pos)
end;

local amass = function(startpoint,names,directions)
	if not directions then directions = ofs.neighbors end
	local check = function(n)
		return lib.tbl.has(names, n.name, function(check,against)
			return lib.item.groupmatch(against,check)
		end)-- match found
	end
	if type(names) == 'function' then check = names end
	local nodes, positions, checked = {},{},{}
	local checkedp = function(pos)
		for _,v in pairs(checked) do
			if vector.equals(pos,v) then return true end
		end
		return false
	end
	local i,stack = 1,{startpoint} repeat
		local pos = stack[i]
		local n = force(pos)
		if check(n, pos, nodes, positions) then
			-- record the find
			nodes[pos] = n.name
			if positions[n.name]
				then positions[n.name][#positions[n.name]+1] = pos
				else positions[n.name] = {pos}
			end

			-- check selected neighbors to see if any need scanning
			for _,d in pairs(directions) do
				local sum = vector.add(pos, d)
				if not checkedp(sum) then
					stack[#stack + 1] = sum
					checked[#checked+1] = sum
				end
			end
		end
		checked[#checked+1] = pos
		i = i + 1
	until i > #stack
	return nodes, positions
end;

local is_air = function(pos)
	local n = force(pos)
	if n.name == 'air' then return true end
	local d = core.registered_nodes[n.name]
	if not d then return false end
	return (d.walkable == false) and (d.drawtype == 'airlike' or d.buildable_to == true)
end;

local is_clear = function(pos)
	if not lib.node.is_air(pos) then return false end
	local ents = core.get_objects_inside_radius(pos,0.5)
	if #ents > 0 then return false end
	return true
end;

local function boxwarp(nb, mogrifier, par)
	if nb == nil then
		return
	elseif nb.type then
		if nb.type == 'fixed' or nb.type == 'leveled' then
			for i, b in ipairs(nb.fixed) do
				boxwarp(b, mogrifier, nb)
			end
		elseif nb.type == 'wallmounted' then
			boxwarp(nb.wall_top,    mogrifier, nb)
			boxwarp(nb.wall_bottom, mogrifier, nb)
			boxwarp(nb.wall_side,   mogrifier, nb)
		elseif nb.type == 'connected' then
			for _, state in pairs{'connect', 'disconnected'} do
				for _, dir in pairs{'top','bottom','front','left','back','right'} do
					boxwarp(nb[state ..'_'.. dir], mogrifier, nb)
				end
				boxwarp(nb.disconnected, mogrifier, nb)
				boxwarp(nb.disconnected_sides, mogrifier, nb)
			end
		elseif nb.type == 'regular' then
			nb.type = 'fixed'
			nb.fixed = {-.5, -.5, -.5; .5, .5, .5};
			boxwarp(nb.fixed, mogrifier, nb);
		end
	elseif nb[1] then
		mogrifier(nb, par)
	end
end

local function boxwarped(box, warp) --oof
	local c = lib.tbl.deepcopy(box)
	boxwarp(c, warp)
	return c
end

return {
	offsets = ofs;
	purge_container = function(...) return purge_container(nil, ...) end;
	purge_only = function(lst)
		return function(...)
			return purge_container(lst, ...)
		end
	end; 

	is_air = is_air;
	is_clear = is_clear;

	insert = function(item, slot, npos, user, inv)
		inv = inv or core.get_meta(npos):get_inventory()
		if inv:room_for_item(slot,item) then
			inv:add_item(slot,item)
		else repeat
			if user then
				local ui = user:get_inventory()
				if ui:room_for_item('main', item) then
					ui:add_item('main', item)
					break
				end
			end
			core.add_item(npos, item)
		until true end
	end;

	install_bed = function(bed, where, dir)
		local bottom = bed .. '_bottom'
		local top = bed .. '_top'
		local d
		if type(dir) == 'number' then
			d = dir
			dir = core.facedir_to_dir(d)
		else
			d = core.dir_to_facedir(dir)
		end
		if not is_clear(where) and is_clear(where - dir) then return false end
		core.set_node(where,       {name = top, param2 = d})
		core.set_node(where - dir, {name = bottom, param2 = d})
		return true
	end;

	get_arrival_point = function(pos)
		local try = function(p)
			local air = lib.node.is_clear
			if air(p) then
				if air(vector.offset(p,0,1,0))  then return p end
				if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end
			end
			return false
		end
		
		do local t = try(pos) if t then return t end end
		for _,o in pairs(ofs.neighbors) do
			local p = vector.add(pos, o)
			do local t = try(p) if t then return t end end
		end
	end;


	forneighbor = function(pos, n, fn)
		for _,p in pairs(n) do
			local sum = vector.add(pos, p)
			local n = core.get_node(sum)
			if n.name == 'ignore' then
				core.load_area(sum)
				n = core.get_node(sum)
			end
			if fn(sum, n) == false then break end
		end
	end;

	amass = amass;
	
	force = force;

	blockpos = function(pos)
		return {
			x = math.floor(pos.x / 16);
			y = math.floor(pos.y / 16);
			z = math.floor(pos.z / 16);
		}
	end;

	preload = function(pos, user)
		core.load_area(pos)
		user:send_mapblock(lib.node.blockpos(pos))
	end;

	discharger = function(pos)
		local below = force(vector.subtract(pos,{x=0,y=1,z=0}))
		if below.name == 'hopper:hopper'
		or below.name == 'hopper:hopper_side' then
			local hopper = core.get_meta(below):get_inventory()
			return function(i)
				if hopper:room_for_item('main',i) then
					return hopper:add_item('main',i), true
				end
				return i, false
			end
		else
			return function(i) return i, false end
		end
	end;

	autopreserve = function(id, tbl)
		tbl.drop = tbl.drop or {
			max_items = 1;
			items = {
				{ items = {id} };
			};
		}
		local next_apn = tbl.after_place_node
		tbl.after_place_node = function(...) local pos, who, stack = ...
			core.get_meta(pos):from_table(stack:get_meta():to_table())
			if next_apn then return next_apn(...) end
		end
		local next_pm = tbl.preserve_metadata
		tbl.preserve_metadata = function(...) local pos, node, meta, drops = ...
			drops[1]:get_meta():from_table({fields = meta})
			if next_pm then return next_pm(...) end
		end
		return tbl
	end;
	reg_autopreserve = function(id, tbl)
		core.register_node(id, lib.node.autopreserve(id, tbl))
	end;

	boxwarp = boxwarp;
	boxwarped = boxwarped;
}
