local helper = {}
--#region CONFIG----
local cfg = {}
cfg.localConfig = {
	--if true, the program will attempt to download and use the config from remoteConfigPath
	--useful if you have many turtles and you don't want to change the config of each one manually
	useRemoteConfig = false,
	remoteConfigPath = "http://localhost:33344/config.lua",
	--this command will be used when the program is started as "mine def" (mineLoop overrides this command)
	defaultCommand = "cube 3 3 8",
	--false: build walls/floor/ceiling everywhere, true: only where there is fluid
	plugFluidsOnly = true,
	--maximum taxicab distance from enterance point when collecting ores, 0 = disable ore traversal
	oreTraversalRadius = 0,
	--layer mining order, use "z" for branch mining, "y" for anything else
	--"y" - mine top to bottom layer by layer, "z" - mine forward in vertical slices
	layerSeparationAxis="y",
	--false: use regular chests, true: use entangled chests
	--if true, the turtle will place a single entangled chest to drop off items and break it afterwards.
	--tested with chests from https://www.curseforge.com/minecraft/mc-mods/kibe
	useEntangledChests = false,
	--false: refuel from inventory, true: refuel from (a different) entangled chest
	--if true, the turtle won't store any coal. Instead, when refueling, it will place the entangled chest, grab fuel from it, refuel, and then break the chest.
	useFuelEntangledChest = false,
	--true: use two chuck loaders to mine indefinitely without moving into unloaded chunks. 
	--This doesn't work with chunk loaders from https://www.curseforge.com/minecraft/mc-mods/kibe, but might work with some other mod.
	--After an area is mined, the turtle will shift by mineLoopOffset and execute mineLoopCommand
	--mineLoopCommand is used in place of defaultCommand when launching as "mine def"
	mineLoop = false,
	mineLoopOffset = {x=0, y=0, z=8},
	mineLoopCommand = "rcube 1 1 1"
}

cfg.getRemoteConfig = function(remotePath)
	local handle = http.get(remotePath)
	if not handle then
		helper.printError("Server not responding, using local")
		return nil
	end
	local data = handle.readAll()
	handle.close()
	local deser = textutils.unserialise(data)
	if not deser then
		helper.printError("Couldn't parse remote config, using local")
		return nil
	end
	for key,_ in pairs(cfg.localConfig) do
		if deser[key] == nil then
			helper.printError("No key", key, "in remote config, using local")
			return nil
		end
	end
	return deser
end

cfg.processConfig = function()
	local config = cfg.localConfig
	if cfg.localConfig.useRemoteConfig then
		helper.print("Downloading config..")
		local remoteConfig = cfg.getRemoteConfig(config.remoteConfigPath)
		config = remoteConfig or cfg.localConfig
	end
	return config
end

--#endregion
--#region CONSTANTS----

local SOUTH = 0
local WEST = 1
local NORTH = 2
local EAST = 3

local CHEST_SLOT = "chest_slot"
local BLOCK_SLOT = "block_slot"
local FUEL_SLOT = "fuel_slot"
local FUEL_CHEST_SLOT = "fuel_chest_slot"
local CHUNK_LOADER_SLOT = "chunk_loader_slot"
local MISC_SLOT = "misc_slot"

local ACTION = 0
local TASK = 1

local PATHFIND_INSIDE_AREA = 0
local PATHFIND_OUTSIDE_AREA = 1
local PATHFIND_INSIDE_NONPRESERVED_AREA = 2
local PATHFIND_ANYWHERE_NONPRESERVED = 3

local SUCCESS = 0
local FAILED_NONONE_COMPONENTCOUNT = 1
local FAILED_TURTLE_NOTINREGION = 2
local FAILED_REGION_EMPTY = 4

local REFUEL_THRESHOLD = 500
local RETRY_DELAY = 3

local FALLING_BLOCKS = {
	["minecraft:gravel"] = true,
	["minecraft:sand"] = true
}

--#endregion
--#region WRAPPER----

local wrapt = (function()
	local self = {
		selectedSlot = 1,
		direction = SOUTH,
		x = 0,
		y = 0,
		z = 0
	}

	local public = {}

	--wrap everything in turtle
	for key,value in pairs(turtle) do
		public[key] = value
	end
	--init turtle selected slot
	turtle.select(self.selectedSlot);

	public.select = function(slot)
		if self.selectedSlot ~= slot then
			turtle.select(slot)
			self.selectedSlot = slot
		end
	end

	public.forward = function()
		local success = turtle.forward()
		if not success then
			return success
		end
		if self.direction == EAST then
			self.x = self.x + 1
		elseif self.direction == WEST then
			self.x = self.x - 1
		elseif self.direction == SOUTH then
			self.z = self.z + 1
		elseif self.direction == NORTH then
			self.z = self.z - 1
		end
		return success
	end

	public.up = function()
		local success = turtle.up()
		if not success then
			return success
		end
		self.y = self.y + 1
		return success
	end

	public.down = function()
		local success = turtle.down()
		if not success then
			return success
		end
		self.y = self.y - 1
		return success
	end

	public.turnRight = function()
		local success = turtle.turnRight()
		if not success then
			return success
		end
		self.direction = self.direction + 1
		if self.direction > 3 then
			self.direction = 0
		end
		return success
	end

	public.turnLeft = function()
		local success = turtle.turnLeft()
		if not success then
			return success
		end
		self.direction = self.direction - 1
		if self.direction < 0 then
			self.direction = 3
		end
		return success
	end

	public.getX = function() return self.x end
	public.getY = function() return self.y end
	public.getZ = function() return self.z end
	public.getDirection = function() return self.direction end
	public.getPosition = function() return {x = self.x, y = self.y, z = self.z, direction = self.direction} end

	return public
end)()

--#endregion
--#region TYPE ALIASES---

-- use https://marketplace.visualstudio.com/items?itemName=sumneko.lua for intellisense

---@alias vec3 {x: number, y: number, z: number}
---@alias vec3dir {x: number, y: number, z: number, direction: integer}

--#endregion
--#region DEBUG FUNCTIONS----

local debug = {
	dumpPoints = function(points, filename)
		local file = fs.open(filename .. ".txt","w")
		for _, value in pairs(points) do
			local line =
				tostring(value.x)..","..
				tostring(value.y)..","..
				tostring(value.z)..","
			if value.adjacent then
				line = line .. "0,128,0"
			elseif value.inacc then
				line = line .. "255,0,0"
			elseif value.triple then
				line = line .. "0,255,0"
			elseif value.checkedInFirstPass then
				line = line .. "0,0,0"
			else
				helper.printError("Invalid block type when dumping points")
			end

			if math.abs(value.z) < 100 then
				file.writeLine(line)
			end
		end
		file.close()
	end,

	---@param points {[string]:vec3}
	---@param filename string
	dumpPath = function(points, filename)
		local file = fs.open(filename .. ".txt","w")
		for key, value in pairs(points) do
			if tonumber(key) then
				local line =
					tostring(value.x)..","..
					tostring(value.y)..","..
					tostring(value.z)
				file.writeLine(line)
			end
		end
		file.close()
	end,

	dumpLayers = function(layers, filename)
		for index, layer in ipairs(layers) do
			local file = fs.open(filename .. tostring(index) .. ".txt","w")
			for _, value in ipairs(layer) do
				local line =
					tostring(value.x)..","..
					tostring(value.y)..","..
					tostring(value.z)..","
				if value.adjacent then
					line = line .. "0,128,0"
				elseif value.inacc then
					line = line .. "255,0,0"
				elseif value.triple then
					line = line .. "0,255,0"
				elseif value.checkedInFirstPass then
					line = line .. "0,0,0"
				else
					helper.printError("Invalid block type when dumping layers")
				end
				file.writeLine(line)
			end
			file.close()
		end
	end
}

--#endregion
--#region HELPER FUNCTIONS----

---@param dX number
---@param dZ number
helper.deltaToDirection = function(dX, dZ)
	if dX > 0 then
		return EAST
	elseif dX < 0 then
		return WEST
	elseif dZ > 0 then
		return SOUTH
	elseif dZ < 0 then
		return NORTH
	end
	error("Invalid delta", 2)
end

---@param T table
---@return integer
helper.tableLength = function(T)
	local count = 0
	for _ in pairs(T) do count = count + 1 end
	return count
end

---@param x number
---@param y number
---@param z number
helper.getIndex = function(x, y, z)
	return tostring(x) .. "," .. tostring(y) .. "," .. tostring(z)
end

---@param pos1 vec3
---@param pos2 vec3
helper.isPosEqual = function(pos1, pos2)
	return pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z
end

---@param pos vec3
---@return {[string]: vec3}
helper.getSurroundings = function(pos)
	return {
		[helper.getIndex(pos.x + 1, pos.y, pos.z)] = {x = pos.x + 1, y = pos.y, z = pos.z},
		[helper.getIndex(pos.x - 1, pos.y, pos.z)] = {x = pos.x - 1, y = pos.y, z = pos.z},
		[helper.getIndex(pos.x, pos.y + 1, pos.z)] = {x = pos.x, y = pos.y + 1, z = pos.z},
		[helper.getIndex(pos.x, pos.y - 1, pos.z)] = {x = pos.x, y = pos.y - 1, z = pos.z},
		[helper.getIndex(pos.x, pos.y, pos.z + 1)] = {x = pos.x, y = pos.y, z = pos.z + 1},
		[helper.getIndex(pos.x, pos.y, pos.z - 1)] = {x = pos.x, y = pos.y, z = pos.z - 1}
	}
end

---@param currentPos vec3dir
---@return vec3
helper.getForwardPos = function(currentPos)
	local newPos = {x = currentPos.x, y = currentPos.y, z = currentPos.z}
	if currentPos.direction == EAST then
		newPos.x = newPos.x + 1
	elseif currentPos.direction == WEST then
		newPos.x = newPos.x - 1
	elseif currentPos.direction == SOUTH then
		newPos.z = newPos.z + 1
	elseif currentPos.direction == NORTH then
		newPos.z = newPos.z - 1
	end
	return newPos
end

---@param from vec3
---@param to vec3
---@return number
helper.distance = function(from, to)
	return math.abs(from.x - to.x) + math.abs(from.y - to.y) + math.abs(from.z - to.z)
end

---@param number number
---@return integer
helper.sign = function(number)
    return number > 0 and 1 or (number == 0 and 0 or -1)
end

---@param number number
---@param a number
---@param b number
helper.inRange = function(number, a, b)
	return number >= a and number <= b
end

helper.splitString = function(string, separator)
	if not separator then separator = "%s" end
	local split = {}
	for str in string.gmatch(string, "([^"..separator.."]+)") do
		table.insert(split, str)
	end
	return split
end

helper.stringEndsWith = function (str, ending)
	return ending == "" or str:sub(-#ending) == ending
end

helper.tableContains = function(table, element, comparison)
	for _, value in pairs(table) do
		if comparison(value, element) then
			return true
		end
	end
	return false
end

helper.readOnlyTable = function(t)
	local mt = {
		__index = t,
		__newindex = function(_, _, _)
			error("Cannot write into a read-only table", 2)
		end
	}
	local proxy = {}
	setmetatable(proxy, mt)
	return proxy
end

helper.osYield = function()
---@diagnostic disable-next-line: undefined-field
	os.queueEvent("fakeEvent");
---@diagnostic disable-next-line: undefined-field
	os.pullEvent();
end

helper.printError = function(...)
	term.setTextColor(colors.red)
	print(...)
end

helper.printWarning = function(...)
	term.setTextColor(colors.yellow)
	print(...)
end

helper.print = function(...)
	term.setTextColor(colors.white)
	print(...)
end

helper.write = function(...)
	term.setTextColor(colors.white)
	term.write(...)
end

helper.read = function()
	term.setTextColor(colors.lightGray)
	local data = read()
	term.setTextColor(colors.white)
	return data
end

--#endregion
--#region SHAPE LIBRARY----

local shapes = {}

shapes.custom = {
	command = "custom",
	shortDesc = "custom <filename>",
	longDesc = "Executes a function named \"generate\" from the specified file and uses the data it returns as a shape",
	args = {"str"},
	generate = function(filename)
		local env = {
			table=table,
			fs=fs,
			http=http,
			io=io,
			math=math,
			os=os,
			parallel=parallel,
			string=string,
			vector=vector,
			textutils=textutils
		}
		local chunk, err = loadfile(filename, "bt", env)
		if err then
			helper.printError("Couldn't load file:", err);
			return {}
		end
		chunk();
		if type(env.generate) ~= "function" then
			helper.printError("File does not contain generate function")
			return {}
		end
		local generated = env.generate()
		if type(generated) ~= "table" then
			helper.printError("Generate function didn't return a table")
			return {}
		end
		local blocks = {}
		for _, value in ipairs(generated) do
			if type(value.x) ~= "number" or type(value.y) ~= "number" or type(value.z) ~= "number" then
				helper.printError("Invalid coordinates entry:", textutils.serialize(value))
				return {}
			end
			blocks[helper.getIndex(value.x, value.y, value.z)] = {x = value.x, y = value.y, z = value.z}
		end
		return blocks
	end
}

shapes.sphere = {
	command = "sphere",
	shortDesc = "sphere <diameter>",
	longDesc = "Mine a sphere of diameter <diameter>, starting from it's bottom center",
	args = {"2.."},
	generate = function(diameter)
		local radius = math.ceil(diameter / 2.0)
		local radiusSq = (diameter / 2.0) * (diameter / 2.0)
		local blocks = {}
		local first = nil
		for j=-radius,radius do
			for i=-radius,radius do
				for k=-radius,radius do
					if diameter % 2 == 0 then
						if math.pow(i+0.5, 2) + math.pow(j+0.5, 2) + math.pow(k+0.5, 2) < radiusSq then
							if not first then
								first = j
							end
							blocks[helper.getIndex(i,j-first,k)] = {x = i, y = j-first, z = k}
						end
					else
						if math.pow(i, 2) + math.pow(j, 2) + math.pow(k, 2) < radiusSq then
							if not first then
								first = j
							end
							blocks[helper.getIndex(i,j-first,k)] = {x = i, y = j-first, z = k}
						end
					end
				end
			end
			helper.osYield()
		end
		return blocks
	end
}

shapes.cuboid = {
	command="cube",
	shortDesc = "cube <left> <up> <forward>",
	longDesc = "Mine a cuboid of a specified size. Use negative values to dig in an opposite direction",
	args= {"..-2 2..", "..-2 2..", "..-2 2.."},
	generate = function(x, y, z)
		local blocks = {}
		local sX = helper.sign(x)
		local sY = helper.sign(y)
		local sZ = helper.sign(z)
		local tX = sX * (math.abs(x) - 1)
		local tY = sY * (math.abs(y) - 1)
		local tZ = sZ * (math.abs(z) - 1)
		for i=0,tX,sX do
			for j=0,tY,sY do
				for k=0,tZ,sZ do
					blocks[helper.getIndex(i,j,k)] = {x = i, y = j, z = k}
				end
			end
			helper.osYield()
		end
		return blocks
	end
}

shapes.centeredCuboid = {
	command="rcube",
	shortDesc = "rcube <leftR> <upR> <forwardR>",
	longDesc = "Mine a cuboid centered on the turtle. Each dimension is a \"radius\", so typing \"rcube 1 1 1\" will yield a 3x3x3 cube",
	args={"1..", "1..", "1.."},
	generate = function(rX, rY, rZ)
		local blocks = {}
		for i=-rX,rX do
			for j=-rY,rY do
				for k=-rZ,rZ do
					blocks[helper.getIndex(i,j,k)] = {x = i, y = j, z = k}
				end
			end
			helper.osYield()
		end
		return blocks
	end
}

shapes.branch = {
	command="branch",
	shortDesc = "branch <branchLen> <shaftLen>",
	longDesc = "Branch-mining. <branchLen> is the length of each branch, <shaftLen> is the length of the main shaft",
	args={"0..", "3.."},
	generate = function(xRadius, zDepth)
		local blocks = {}
		--generate corridor
		for x=-1,1 do
			for y=0,2 do
				for z=0,zDepth-1 do
					blocks[helper.getIndex(x,y,z)] = {x = x, y = y, z = z}
				end
			end
		end
		--generate branches
		for z=2,zDepth-1,2 do
			local y = (z % 4 == 2) and 0 or 2
			for x=0,xRadius-1 do
				blocks[helper.getIndex(x+2,y,z)] = {x = x+2, y = y, z = z}
				blocks[helper.getIndex(-x-2,y,z)] = {x = -x-2, y = y, z = z}
			end
		end
		return blocks
	end
}

--#endregion
--#region REGION PROCESSING----

local region = {}
region.createShape = function()
	local blocks = {}
	for i=-7,7 do
		for j=0,7 do
			for k=-7,7 do
				if i*i + j*j + k*k < 2.5*2.5 then
					blocks[helper.getIndex(i,j,k)] = {x = i, y = j, z = k}
				end
			end
		end
	end
	return blocks
end

region.cloneBlocks = function(allBlocks)
	local cloned = {}
	for key,value in pairs(allBlocks) do
		local blockClone = {
			x = value.x,
			y = value.y,
			z = value.z
		}
		cloned[key] = blockClone
	end
	return cloned
end

--mark blocks that are next to walls
region.markAdjacentInPlace = function(allBlocks)
	for key,value in pairs(allBlocks) do
		local xMinus = allBlocks[helper.getIndex(value.x - 1, value.y, value.z)]
		local xPlus = allBlocks[helper.getIndex(value.x + 1, value.y, value.z)]
		local yMinus = allBlocks[helper.getIndex(value.x, value.y - 1, value.z)]
		local yPlus = allBlocks[helper.getIndex(value.x, value.y + 1, value.z)]
		local zMinus = allBlocks[helper.getIndex(value.x, value.y, value.z - 1)]
		local zPlus = allBlocks[helper.getIndex(value.x, value.y, value.z + 1)]
		if not xMinus or not xPlus or not yMinus or not yPlus or not zMinus or not zPlus then
			value.adjacent = true
			if yMinus then yMinus.checkedInFirstPass = true end
			if yPlus then yPlus.checkedInFirstPass = true end
		end
	end
end

--mark positions where the turtle can check both the block above and the block below
region.markTripleInPlace = function(allBlocks)
	local minY = 9999999;
	for key,value in pairs(allBlocks) do
		if not value.checkedInFirstPass and not value.adjacent then
			minY = math.min(minY, value.y)
		end
	end
	for key,value in pairs(allBlocks) do
		if not value.checkedInFirstPass and not value.adjacent then
			local offset = (value.y - minY) % 3;
			if offset == 0 then
				local blockAbove = allBlocks[helper.getIndex(value.x, value.y+1, value.z)]
				if blockAbove ~= nil and blockAbove.checkedInFirstPass ~= true then
					value.checkedInFirstPass = true
				else
					value.inacc = true
				end
			elseif offset == 1 then
				value.triple = true
			elseif offset == 2 and allBlocks[helper.getIndex(value.x, value.y-1, value.z)] ~= nil then
				local blockBelow = allBlocks[helper.getIndex(value.x, value.y-1, value.z)]
				if blockBelow ~= nil and blockBelow.checkedInFirstPass ~= true then
					value.checkedInFirstPass = true
				else
					value.inacc = true
				end
			end
		end
	end
end

region.findConnectedComponents = function(allBlocks)
	local visited = {}
	local components = {}
	local counter = 0
	local lastTime = os.clock()
	for key,value in pairs(allBlocks) do
		if not visited[key] then
			local component = {}
			local toVisit = {[key] = value}
			while true do
				local newToVisit = {}
				local didSomething = false
				for currentKey,current in pairs(toVisit) do
					didSomething = true
					visited[currentKey] = true
					component[currentKey] = current
					local minusX = helper.getIndex(current.x-1, current.y, current.z)
					local plusX = helper.getIndex(current.x+1, current.y, current.z)
					local minusY = helper.getIndex(current.x, current.y-1, current.z)
					local plusY = helper.getIndex(current.x, current.y+1, current.z)
					local minusZ = helper.getIndex(current.x, current.y, current.z-1)
					local plusZ = helper.getIndex(current.x, current.y, current.z+1)
					if allBlocks[minusX] and not visited[minusX] then newToVisit[minusX] = allBlocks[minusX] end
					if allBlocks[plusX] and not visited[plusX] then newToVisit[plusX] = allBlocks[plusX] end
					if allBlocks[minusY] and not visited[minusY] then newToVisit[minusY] = allBlocks[minusY] end
					if allBlocks[plusY] and not visited[plusY] then newToVisit[plusY] = allBlocks[plusY] end
					if allBlocks[minusZ] and not visited[minusZ] then newToVisit[minusZ] = allBlocks[minusZ] end
					if allBlocks[plusZ] and not visited[plusZ] then newToVisit[plusZ] = allBlocks[plusZ] end

					counter = counter + 1
					if counter % 50 == 0 then
						local curTime = os.clock()
						if curTime - lastTime > 1 then
							lastTime = curTime
							helper.osYield()
						end
					end
				end
				toVisit = newToVisit
				if not didSomething then break end
			end
			table.insert(components, component)
		end
	end
	return components
end

region.separateLayers = function(allBlocks, direction)
	if direction ~= "y" and direction ~= "z" then
		error("Invalid direction value", 2)
	end
	local layers = {}
	local min = 999999
	local max = -999999
	for key,value in pairs(allBlocks) do
		if not (not value.adjacent and value.checkedInFirstPass) then
			local index = direction == "y" and value.y or value.z
			if not layers[index] then 
				layers[index] = {} 
			end
			layers[index][key] = value
			min = math.min(min, index)
			max = math.max(max, index)
		end
	end
	if min == 999999 then
		error("There should be at least one block in passed table", 2)
	end
	
	local reassLayers = {}
	for key, value in pairs(layers) do
		local index = direction == "y" and (max - min+1) - (key - min) or (key - min + 1)
		reassLayers[index] = value
	end
	return reassLayers
end

region.sortFunction = function(a, b) 
	return (a.y ~= b.y and a.y < b.y or (a.x ~= b.x and a.x > b.x or a.z > b.z))
end

region.findClosestPoint = function(location, points, usedPoints)
	local surroundings = helper.getSurroundings(location)
	local existingSurroundings = {}
	local foundClose = false
	for key,value in pairs(surroundings) do
		if points[key] and not usedPoints[key] then
			table.insert(existingSurroundings, value)
			foundClose = true
		end
	end
	if foundClose then
		table.sort(existingSurroundings, region.sortFunction)
		local closest = table.remove(existingSurroundings)
		return points[helper.getIndex(closest.x, closest.y, closest.z)]
	end

	local minDist = 999999
	local minValue = nil
	for key,value in pairs(points) do
		if not usedPoints[key] then
			local dist = helper.distance(value,location)
			if dist < minDist then
				minDist = dist
				minValue = value
			end
		end
	end
	if not minValue then
		return nil
	end
	return minValue
end

--travelling salesman, nearest neighbour method
region.findOptimalBlockOrder = function(layers)
	local newLayers = {}
	local lastTime = os.clock()
	for index, layer in ipairs(layers) do
		local newLayer = {}
		local usedPoints = {}
		local current = region.findClosestPoint({x=0,y=0,z=0}, layer, usedPoints)
		repeat
			usedPoints[helper.getIndex(current.x, current.y, current.z)] = true
			table.insert(newLayer, current)
			current = region.findClosestPoint(current, layer, usedPoints)
			local curTime = os.clock()
			if curTime - lastTime > 1 then
				lastTime = curTime
				helper.osYield()
			end
		until not current
		newLayers[index]=newLayer
	end
	return newLayers
end

region.createLayersFromArea = function(diggingArea, direction)
	local blocksToProcess = region.cloneBlocks(diggingArea)
	region.markAdjacentInPlace(blocksToProcess)
	region.markTripleInPlace(blocksToProcess)
	local layers = region.separateLayers(blocksToProcess, direction)
	local orderedLayers = region.findOptimalBlockOrder(layers)
	return orderedLayers
end

region.shiftRegion = function(shape, delta)
	local newShape = {}
	for key,value in pairs(shape) do
		local newPos = {x = value.x + delta.x, y = value.y + delta.y, z = value.z + delta.z}
		newShape[helper.getIndex(newPos.x, newPos.y, newPos.z)] = newPos
	end
	return newShape
end

region.reserveChests = function(blocks)
	local blocksCopy = {}
	local counter = 0
	for _,value in pairs(blocks) do
		counter = counter + 1
		blocksCopy[counter] = value
	end
	table.sort(blocksCopy, function(a,b)
		if a.y ~= b.y then return a.y > b.y
		elseif a.z~=b.z then return a.z > b.z
		else return a.x > b.x end
	end)

	return {reserved=blocksCopy}
end

region.validateRegion = function(blocks)
	local result = SUCCESS
	--there must be only one connected component
	local components = region.findConnectedComponents(blocks)
	if helper.tableLength(components) == 0 then
		result = result + FAILED_REGION_EMPTY
	end
	if helper.tableLength(components) > 1 then
		result = result + FAILED_NONONE_COMPONENTCOUNT
	end
	--the turtle must be inside of the region
	if not blocks[helper.getIndex(0,0,0)] then
		result = result + FAILED_TURTLE_NOTINREGION
	end
	return result
end

--#endregion
--#region PATHFINDING----

local path = {}
path.pathfindUpdateNeighbour = function(data, neighbour, destination, origin, neighbourIndex)
	local originIndex = helper.getIndex(origin.x, origin.y, origin.z)
	if not data[neighbourIndex] then
		data[neighbourIndex] = {}
		data[neighbourIndex].startDist = data[originIndex].startDist + 1
		data[neighbourIndex].heuristicDist = helper.distance(neighbour, destination)
		data[neighbourIndex].previous = origin
	elseif data[originIndex].startDist + 1 < data[neighbourIndex].startDist then
		data[neighbourIndex].startDist = data[originIndex].startDist + 1
		data[neighbourIndex].previous = origin
	end
end

path.traversable = function(area, index, pathfindingType)
	if pathfindingType == PATHFIND_INSIDE_AREA then
		return area[index] ~= nil
	elseif pathfindingType == PATHFIND_INSIDE_NONPRESERVED_AREA then
		return not not (area[index] and not area[index].preserve)
	elseif pathfindingType == PATHFIND_OUTSIDE_AREA then
		return area[index] == nil
	elseif PATHFIND_ANYWHERE_NONPRESERVED then
		return area[index] == nil or not area[index].preserve
	end
	error("Unknown pathfinding type", 3)
end

path.pathfind = function(from, to, allBlocks, pathfindingType)
	if helper.isPosEqual(from,to) then
		return {length = 0}
	end
	local data = {}
	local openSet = {}
	local closedSet = {}

	local current = from
	local curIndex = helper.getIndex(from.x, from.y, from.z)
	openSet[curIndex] = current

	data[curIndex] = {}
	data[curIndex].startDist = 0
	data[curIndex].heuristicDist = helper.distance(current, to)

	while true do
		local surroundings = helper.getSurroundings(current)
		for key,value in pairs(surroundings) do
			if path.traversable(allBlocks,key,pathfindingType) and not closedSet[key] then 
				path.pathfindUpdateNeighbour(data, value, to, current, key)
				openSet[key] = value
			end
		end

		closedSet[curIndex] = current
		openSet[curIndex] = nil

		local minN = 9999999
		local minValue = nil
		for key,value in pairs(openSet) do
			local sum = data[key].startDist + data[key].heuristicDist
			if sum < minN then
				minN = sum
				minValue = value
			end
		end
		current = minValue;

		if current == nil then
			helper.printWarning("No path from", from.x, from.y, from.z, "to", to.x, to.y, to.z)
			return false
		end

		curIndex = helper.getIndex(current.x, current.y, current.z)
		if helper.isPosEqual(current,to) then
			break
		end
	end

	local almostFinalPath = {}
	local counter = 1
	while current ~= nil do
		almostFinalPath[counter] = current
		counter = counter + 1
		current = data[helper.getIndex(current.x, current.y, current.z)].previous
	end

	local reversedPath = {}
	local newCounter = 1
	for i=counter-1,1,-1 do
		reversedPath[newCounter] = almostFinalPath[i]
		newCounter = newCounter + 1
	end
	reversedPath.length = newCounter-1;
	return reversedPath
end

--#endregion
--#region SLOT MANAGER----
local slots = (function()
	local slotAssignments = {}
	local assigned = false
	local public = {}

	local slotDesc = nil

	local generateDescription = function(config)
		local desc = {
			[CHEST_SLOT] = "Chests",
			[BLOCK_SLOT] = "Cobblestone",
			[FUEL_SLOT] = "Fuel (only coal supported)",
			[FUEL_CHEST_SLOT] = "Fuel Entangled Chest",
			[CHUNK_LOADER_SLOT] = "2 Chunk Loaders",
		}
		if config.useEntangledChests then
			desc[CHEST_SLOT] = "Entangled Chest"
		end
		return desc
	end

	public.assignSlots = function(config)
		if assigned then
			error("Slots have already been assigned", 2)
		end
		assigned = true

		local currentSlot = 1
		slotAssignments[CHEST_SLOT] = currentSlot
		currentSlot = currentSlot + 1
		if config.useFuelEntangledChest then
			slotAssignments[FUEL_CHEST_SLOT] = currentSlot
			currentSlot = currentSlot + 1
		else
			slotAssignments[FUEL_SLOT] = currentSlot
			currentSlot = currentSlot + 1
		end
		if config.mineLoop then
			slotAssignments[CHUNK_LOADER_SLOT] = currentSlot
			currentSlot = currentSlot + 1
		end
		slotAssignments[BLOCK_SLOT] = currentSlot
		currentSlot = currentSlot + 1
		slotAssignments[MISC_SLOT] = currentSlot
		currentSlot = currentSlot + 1

		slotDesc = generateDescription(config)
	end

	public.get = function(slotId)
		if slotAssignments[slotId] then
			return slotAssignments[slotId]
		else
			error("Slot " .. tostring(slotId) .. " was not assigned", 2)
		end
	end

	public.printSlotInfo = function()
		local inverse = {}
		for key,value in pairs(slotAssignments) do
			inverse[value] = key
		end
		for key,value in ipairs(inverse) do
			if value ~= MISC_SLOT then
				helper.print("\tSlot", key, "-", slotDesc[value])
			end
		end
	end

	return public
end)()

--#endregion
--#region UI----

local ui = {}

ui.printInfo = function()
	helper.print("Current fuel level is", wrapt.getFuelLevel())
	helper.print("Item slots, can change based on config:")
	slots.printSlotInfo()
end

ui.printProgramsInfo = function()
	helper.print("Select a program:")
	helper.print("\thelp <program>")
	for _,value in pairs(shapes) do
		helper.print("\t"..value.shortDesc)
	end
end

ui.tryShowHelp = function(input)
	if not input then
		return false
	end
	local split = helper.splitString(input)
	if split[1] ~= "help" then
		return false
	end

	if #split == 1 then
		ui.printProgramsInfo()
		return true
	end

	if #split ~= 2 then
		return false
	end

	local program = split[2]
	local shape = nil
	for key, value in pairs(shapes) do
		if value.command == program then
			shape = value
		end
	end

	if not shape then
		helper.printError("Unknown program")
		return true
	end
	helper.print("Usage:", shape.shortDesc)
	helper.print("\t"..shape.longDesc)
	return true
end

ui.testRange = function(range, value)
	if range == "str" then
		return "string"
	end
	if type(value) ~= "number" then
		return false
	end

	local subRanges = helper.splitString(range, " ")
	for _, subRange in ipairs(subRanges) do
		local borders = helper.splitString(subRange, "..")
		local tableLength = helper.tableLength(borders)
		if tableLength == 2 then
			local left = tonumber(borders[1])
			local right = tonumber(borders[2])
			if helper.inRange(value, left, right) then
				return true
			end
		elseif tableLength == 1 then
			local isLeft = string.sub(subRange, 0, 1) ~= "."
			local border = tonumber(borders[1])
			local good = isLeft and (value >= border) or not isLeft and (value <= border)
			if good then
				return true
			end
		end
	end
	return false
end

ui.parseArgs = function(argPattern, args)
	if helper.tableLength(argPattern) ~= helper.tableLength(args) then
		return nil
	end
	
	local parsed = {}
	for _,value in ipairs(args) do
		local number = tonumber(value)
		if not number then
			table.insert(parsed, value)
		end
		table.insert(parsed, number)
	end

	for index,value in ipairs(argPattern) do
		local result = ui.testRange(value, parsed[index])
		if result == "string" then
			parsed[index] = args[index]
		elseif not result then
			return nil
		end
	end
	return parsed
end

ui.parseProgram = function(string)
	if not string then
		return nil
	end

	local split = helper.splitString(string)
	if not split or helper.tableLength(split) == 0 then
		return nil
	end
	
	local program = split[1]
	local shape = nil
	for _, value in pairs(shapes) do
		if value.command == program then
			shape = value
		end
	end
	if not shape then
		return nil
	end

	local args = {table.unpack(split, 2, #split)}
	local parsed = ui.parseArgs(shape.args, args)
	if not parsed then
		return nil
	end

	return {shape = shape, args = parsed}
end

ui.promptForShape = function()
	local shape
	while true do
		helper.write("> ")
		local input = helper.read()
		shape = ui.parseProgram(input)
		if not shape then
			if not ui.tryShowHelp(input) then
				helper.printError("Invalid program")
			end
		else
			break
		end
	end
	return shape
end

ui.showValidationError = function(validationResult) 
	local error = "Invalid mining volume:";
	if bit32.band(validationResult, FAILED_REGION_EMPTY) ~= 0 then
		helper.printError("Invalid mining volume: \n\tVolume is empty")
		return
	end
	if bit32.band(validationResult, FAILED_NONONE_COMPONENTCOUNT) ~= 0 then
		error = error .. "\n\tVolume has multiple disconnected parts"
	end
	if bit32.band(validationResult, FAILED_TURTLE_NOTINREGION) ~= 0 then
		error = error .. "\n\tTurtle (pos(0,0,0)) not in volume"
	end
	helper.printError(error)
end

--#endregion

----THE REST OF THE CODE----

local function execWithSlot(func, slot)
	wrapt.select(slots.get(slot))
	local data = func()
	wrapt.select(slots.get(MISC_SLOT))
	return data
end

local function isWaterSource(inspectFunction)
	local success, data = inspectFunction()
	if success and data.name == "minecraft:water" and data.state.level == 0 then
		return true
	end
	return false
end

local function isFluidSource(inspectFunction)
	local success, data = inspectFunction()
	if success and (data.name == "minecraft:lava" or data.name == "minecraft:water") and data.state.level == 0 then
		return true
	end
	return false
end

local function isFluid(inspectFunction)
	local success, data = inspectFunction()
	if success and (data.name == "minecraft:lava" or data.name == "minecraft:water") then
		return true
	end
	return false
end

local function isSand(inspectFunction)
	local success, data = inspectFunction()
	if success and FALLING_BLOCKS[data.name] then
		return true
	end
	return false
end

local function isOre(inspectFunction)
	local success, data = inspectFunction()
	if success and helper.stringEndsWith(data.name, "_ore") then
		return true
	end
	return false
end


local function dropInventory(dropFunction)
	local dropped = true
	for i=slots.get(MISC_SLOT),16 do
		if wrapt.getItemCount(i) > 0 then
			wrapt.select(i)
			dropped = dropFunction() and dropped
		end
	end
	wrapt.select(slots.get(MISC_SLOT))
	return dropped
end

local function forceMoveForward()
	if isWaterSource(wrapt.inspect) then
		execWithSlot(wrapt.place, BLOCK_SLOT)
	end
	repeat
		wrapt.dig()
	until wrapt.forward()
end

local function plugTop(onlyFluids)
	if isSand(wrapt.inspectUp) then
		wrapt.digUp()
	end
	if not wrapt.detectUp() and not onlyFluids or onlyFluids and isFluid(wrapt.inspectUp) then
		if wrapt.getItemCount(slots.get(BLOCK_SLOT)) > 0 then
			local tries = 0
			repeat tries = tries + 1 until execWithSlot(wrapt.placeUp, BLOCK_SLOT) or tries > 10
		end
	end
end

local function plugBottom(onlyFluids)
	if not onlyFluids or isFluidSource(wrapt.inspectDown) then
		execWithSlot(wrapt.placeDown, BLOCK_SLOT)
	end
end

local function digAbove()
	if isFluidSource(wrapt.inspectUp) then
		execWithSlot(wrapt.placeUp, BLOCK_SLOT)
	end
	wrapt.digUp()
end

local function digBelow()
	if isFluidSource(wrapt.inspectDown) then
		execWithSlot(wrapt.placeDown, BLOCK_SLOT)
	end
	wrapt.digDown()
end

local function digInFront()
	if isFluidSource(wrapt.inspect) then
		execWithSlot(wrapt.place, BLOCK_SLOT)
	end
	wrapt.dig()
end

local function turnTowardsDirection(targetDir)
	local delta = (targetDir - wrapt.getDirection()) % 4
	if delta == 1 then
		wrapt.turnRight()
	elseif delta == 2 then
		wrapt.turnRight()
		wrapt.turnRight()
	elseif delta == 3 then
		wrapt.turnLeft()
	end
	if targetDir ~= wrapt.getDirection() then
		error("Could not turn to requested direction")
	end
end



local function stepTo(tX, tY, tZ)
	local dX = tX - wrapt.getX()
	local dY = tY - wrapt.getY()
	local dZ = tZ - wrapt.getZ()
	if dY < 0 then
		repeat wrapt.digDown() until wrapt.down()
	elseif dY > 0 then
		repeat wrapt.digUp() until wrapt.up()
	else
		local dir = helper.deltaToDirection(dX, dZ)
		turnTowardsDirection(dir)
		forceMoveForward()
	end
end

local function goToCoords(curBlock, pathfindingArea, pathfindingType)
	local pos = wrapt.getPosition()
	local pth = path.pathfind(pos, curBlock, pathfindingArea, pathfindingType)
	if not pth then
		return false
	end

	for k=1,pth.length do
		if not (pth[k].x == pos.x and pth[k].y == pos.y and pth[k].z == pos.z) then
			stepTo(pth[k].x, pth[k].y, pth[k].z)
		end
	end
	return true
end



local function findOre(oresTable, startPos, rangeLimit)
	local ores = {}
	local isForwardCloseEnough = function() 
		return helper.distance(helper.getForwardPos(wrapt.getPosition()), startPos) <= rangeLimit
	end
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
	end
	local down = {x = wrapt.getX(), y = wrapt.getY() - 1, z = wrapt.getZ()}
	if helper.distance(down, startPos) <= rangeLimit and isOre(wrapt.inspectDown) then
		table.insert(ores, down)
	end
	local up = {x = wrapt.getX(), y = wrapt.getY() + 1, z = wrapt.getZ()}
	if helper.distance(up, startPos) <= rangeLimit and isOre(wrapt.inspectUp) then
		table.insert(ores, up)
	end
	wrapt.turnLeft()
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
	end
	wrapt.turnLeft()
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
	end
	wrapt.turnLeft()
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
	end
	for _,value in pairs(ores) do
		if not helper.tableContains(oresTable, value, helper.isPosEqual) then
			table.insert(oresTable, value)
		end
	end
end

--traverse an ore vein and return to original turtle position afterwards
local function traverseVein(blocks, rangeLimit)
	local startPos = wrapt.getPosition()
	local minePath = {}
	minePath[helper.getIndex(startPos.x, startPos.y, startPos.z)] = startPos

	local ores = {}
	local searchedPositions = {}
	while true do
		local currentIndex = helper.getIndex(wrapt.getX(),wrapt.getY(),wrapt.getZ())
		if not searchedPositions[currentIndex] then
			findOre(ores, startPos, rangeLimit)
			searchedPositions[currentIndex] = true
		end
		local targetOre = table.remove(ores)
		if not targetOre then
			goToCoords(startPos, minePath, PATHFIND_INSIDE_AREA)
			turnTowardsDirection(startPos.direction)
			break
		end
		local targetIndex = helper.getIndex(targetOre.x, targetOre.y, targetOre.z)
		if not blocks[targetIndex] or not blocks[targetIndex].preserve then
			minePath[helper.getIndex(targetOre.x, targetOre.y, targetOre.z)] = targetOre
			goToCoords(targetOre, minePath, PATHFIND_INSIDE_AREA)
		end
	end
end



--diggingOptions: plugFluidsOnly, oreTraversalRadius
local function processAdjacent(allBlocks, diggingOptions)
	local toPlace = {}

	local pos = wrapt.getPosition()

	local minusX = helper.getIndex(pos.x-1, pos.y, pos.z)
	local plusX = helper.getIndex(pos.x+1, pos.y, pos.z)
	local minusZ = helper.getIndex(pos.x, pos.y, pos.z-1)
	local plusZ = helper.getIndex(pos.x, pos.y, pos.z+1)

	if not allBlocks[minusX] then toPlace[minusX] = {x = pos.x - 1, y = pos.y, z = pos.z} end
	if not allBlocks[plusX] then toPlace[plusX] = {x = pos.x + 1, y = pos.y, z = pos.z} end
	if not allBlocks[minusZ] then toPlace[minusZ] = {x = pos.x, y = pos.y, z = pos.z - 1} end
	if not allBlocks[plusZ] then toPlace[plusZ] = {x = pos.x, y = pos.y, z = pos.z + 1} end

	for key,value in pairs(toPlace) do
		local dX = value.x - pos.x
		local dZ = value.z - pos.z
		local dir = helper.deltaToDirection(dX, dZ)
		turnTowardsDirection(dir)
		if diggingOptions.oreTraversalRadius > 0 and isOre(wrapt.inspect) then
			traverseVein(allBlocks, diggingOptions.oreTraversalRadius)
		end
		if not diggingOptions.plugFluidsOnly or isFluid(wrapt.inspect) then
			execWithSlot(wrapt.place, BLOCK_SLOT)
		end
	end

	local minusY = helper.getIndex(pos.x, pos.y-1, pos.z)
	local plusY = helper.getIndex(pos.x, pos.y+1, pos.z)

	if diggingOptions.oreTraversalRadius > 0 then
		if not allBlocks[minusY] and isOre(wrapt.inspectDown)
			or not allBlocks[plusY] and isOre(wrapt.inspectUp) then
			traverseVein(allBlocks, diggingOptions.oreTraversalRadius)
		end
	end

	if allBlocks[minusY] then
		if not allBlocks[minusY].preserve then digBelow() end
	else
		plugBottom(diggingOptions.plugFluidsOnly)
	end

	if allBlocks[plusY] then
		if not allBlocks[plusY].preserve then digAbove() end
	else
		plugTop(diggingOptions.plugFluidsOnly)
	end
end




local function processTriple(diggingArea)
	local pos = wrapt.getPosition()
	local minusY = helper.getIndex(pos.x, pos.y-1, pos.z)
	local plusY = helper.getIndex(pos.x, pos.y+1, pos.z)
	if not diggingArea[plusY].preserve then digAbove() end
	if not diggingArea[minusY].preserve then digBelow() end
end



local function sortInventory(sortFuel)
	--clear cobble slot
	local initCobbleData = wrapt.getItemDetail(slots.get(BLOCK_SLOT))
	if initCobbleData and initCobbleData.name ~= "minecraft:cobblestone" then
		wrapt.select(slots.get(BLOCK_SLOT))
		wrapt.drop()
	end

	--clear fuel slot
	if sortFuel then
		local initFuelData = wrapt.getItemDetail(slots.get(FUEL_SLOT))
		if initFuelData and initFuelData.name ~= "minecraft:coal" then
			wrapt.select(slots.get(FUEL_SLOT))
			wrapt.drop()
		end
	end

	--search inventory for cobble and fuel and put them in the right slots
	local fuelData = sortFuel and wrapt.getItemDetail(slots.get(FUEL_SLOT)) or {count=64}
	local cobbleData = wrapt.getItemDetail(slots.get(BLOCK_SLOT))

	if fuelData and cobbleData and fuelData.count > 32 and cobbleData.count > 32 then
		wrapt.select(slots.get(MISC_SLOT))
		return
	end

	for i=slots.get(MISC_SLOT),16 do
		local curData = wrapt.getItemDetail(i)
		if curData then
			if curData.name == "minecraft:cobblestone" then
				wrapt.select(i)
				wrapt.transferTo(slots.get(BLOCK_SLOT))
			elseif sortFuel and curData.name == "minecraft:coal" then
				wrapt.select(i)
				wrapt.transferTo(slots.get(FUEL_SLOT))
			end
		end
	end

	wrapt.select(slots.get(MISC_SLOT))
end



local function dropIntoEntangled(dropFunction)
	local result = false
	repeat
		result = dropInventory(dropFunction)
		if not result then
			helper.printWarning("Entangled chest is full, retrying..")
---@diagnostic disable-next-line: undefined-field
			os.sleep(RETRY_DELAY)
		end
	until result
end

local function findSuitableEntangledChestPos(diggingArea)
	local surroundings = helper.getSurroundings(wrapt.getPosition())
	local options = {}
	for key,value in pairs(surroundings) do
		if diggingArea[key] and not diggingArea[key].preserve then
			table.insert(options, value)
		end
	end
	local selectedOption = table.remove(options)
	if not selectedOption then
		error("Did the turtle just surround itself with chests?")
		return nil
	end
	return selectedOption
end

local function dropOffEntangledChest(diggingArea)
	local selectedOption = findSuitableEntangledChestPos(diggingArea)
	if not selectedOption then
		return
	end

	local curPosition = wrapt.getPosition()
	local delta = {x = selectedOption.x-curPosition.x, y = selectedOption.y-curPosition.y, z = selectedOption.z-curPosition.z}
	if delta.y < 0 then
		repeat digBelow() until execWithSlot(wrapt.placeDown, CHEST_SLOT)
		dropIntoEntangled(wrapt.dropDown)
		wrapt.select(slots.get(CHEST_SLOT))
		digBelow()
		wrapt.select(slots.get(MISC_SLOT))
	elseif delta.y > 0 then
		repeat digAbove() until execWithSlot(wrapt.placeUp, CHEST_SLOT)
		dropIntoEntangled(wrapt.dropUp)
		wrapt.select(slots.get(CHEST_SLOT))
		digAbove()
		wrapt.select(slots.get(MISC_SLOT))
	elseif delta.x ~= 0 or delta.y ~= 0 or delta.z ~= 0 then
		local direction = helper.deltaToDirection(delta.x, delta.z)
		turnTowardsDirection(direction)
		repeat digInFront() until execWithSlot(wrapt.place, CHEST_SLOT)
		dropIntoEntangled(wrapt.drop)
		wrapt.select(slots.get(CHEST_SLOT))
		digInFront()
		wrapt.select(slots.get(MISC_SLOT))
	else
		error("Something went really wrong")
	end
end

local function makeNewChestInPlace(chestData, diggingArea)
	local newPos = table.remove(chestData.reserved)
	if not newPos then
		error("Out of reserved chest spots")
		return false
	end
	chestData.placed = newPos
	
	local chestPosIndex = helper.getIndex(chestData.placed.x, chestData.placed.y, chestData.placed.z)
	chestData.reserved[chestPosIndex] = nil
	local blockAbove = {x = chestData.placed.x, y = chestData.placed.y+1, z = chestData.placed.z}
	if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_INSIDE_NONPRESERVED_AREA) then
		helper.printWarning("Could not pathfind to new chest location, trying again ignoring walls...")
		if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
			helper.printWarning("Fallback pathfinding failed")
			return false
		end
	end
	digBelow()
	while true do
		local success = execWithSlot(wrapt.placeDown, CHEST_SLOT)
		if success then
			break
		end
		local chestItemDetails = wrapt.getItemDetail(slots.get(CHEST_SLOT))
		if not chestItemDetails then
			helper.printWarning("Out of chests. Add chests to slot", slots.get(CHEST_SLOT))
		end
---@diagnostic disable-next-line: undefined-field
		os.sleep(RETRY_DELAY)
	end
	diggingArea[chestPosIndex].preserve = true
	return true
end

local function dropOffNormally(chestData, diggingArea)
	if not chestData.placed then
		makeNewChestInPlace(chestData, diggingArea)
	end

	local blockAbove = {x = chestData.placed.x, y = chestData.placed.y+1, z = chestData.placed.z}
	if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_INSIDE_NONPRESERVED_AREA) then
		helper.printWarning("Could not pathfind to chest location, trying again ignoring walls...")
		if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
			helper.printWarning("Fallback pathfinding failed too")
		end
	end
	repeat
		for i=slots.get(MISC_SLOT),16 do
			wrapt.select(i)
			if not wrapt.dropDown() and wrapt.getItemDetail(i) then
				makeNewChestInPlace(chestData, diggingArea)
				wrapt.dropDown()
			end
		end
	until not wrapt.getItemDetail(16)
	wrapt.select(slots.get(MISC_SLOT))
end

local function tryDropOffThings(chestData, diggingArea, entangledChest, force)
	if not wrapt.getItemDetail(16) and not force then
		return
	end

	if entangledChest then
		dropOffEntangledChest(diggingArea)
		return
	end

	dropOffNormally(chestData, diggingArea)
end



local function getFuelAndConsumeFromEntangled(suckFunction, dropFunction)
	local result = false
	wrapt.select(16)
	repeat
		repeat
			if not suckFunction(32) then
				helper.printWarning("Refuel chest is empty, retrying...")
				break
			end
			if not wrapt.refuel() then
				helper.printWarning("Refuel chest contains garbage, retrying...")
				while not dropFunction() do
					helper.printWarning("Could not return garbage back to the chest, retrying...")
---@diagnostic disable-next-line: undefined-field
					os.sleep(RETRY_DELAY)
				end
				break
			end
			result = wrapt.getFuelLevel() > REFUEL_THRESHOLD
		until result
		if not result then
---@diagnostic disable-next-line: undefined-field
			os.sleep(RETRY_DELAY)
		end
	until result
end

local function refuelEntangled(chestData, diggingArea, dropOffEntangled)
	tryDropOffThings(chestData, diggingArea, dropOffEntangled)
	
	local selectedOption = findSuitableEntangledChestPos(diggingArea)
	if not selectedOption then return end

	local curPosition = wrapt.getPosition()
	local delta = {x = selectedOption.x-curPosition.x, y = selectedOption.y-curPosition.y, z = selectedOption.z-curPosition.z}
	if delta.y < 0 then
		repeat digBelow() until execWithSlot(wrapt.placeDown, FUEL_CHEST_SLOT)
		getFuelAndConsumeFromEntangled(wrapt.suckDown, wrapt.dropDown)
		wrapt.select(slots.get(FUEL_CHEST_SLOT))
		digBelow()
		wrapt.select(slots.get(MISC_SLOT))
	elseif delta.y > 0 then
		repeat digAbove() until execWithSlot(wrapt.placeUp, FUEL_CHEST_SLOT)
		getFuelAndConsumeFromEntangled(wrapt.suckUp, wrapt.dropUp)
		wrapt.select(slots.get(FUEL_CHEST_SLOT))
		digAbove()
		wrapt.select(slots.get(MISC_SLOT))
	elseif delta.x ~= 0 or delta.y ~= 0 or delta.z ~= 0 then
		local direction = helper.deltaToDirection(delta.x, delta.z)
		turnTowardsDirection(direction)
		repeat digInFront() until execWithSlot(wrapt.place, FUEL_CHEST_SLOT)
		getFuelAndConsumeFromEntangled(wrapt.suck, wrapt.drop)
		wrapt.select(slots.get(FUEL_CHEST_SLOT))
		digInFront()
		wrapt.select(slots.get(MISC_SLOT))
	else
		error("Something went really wrong")
	end
end

local function refuelNormally()
	repeat
		local fuelData = wrapt.getItemDetail(slots.get(FUEL_SLOT))
		if not fuelData then
			sortInventory(true)
		end
		repeat
			local newFuelData = wrapt.getItemDetail(slots.get(FUEL_SLOT))
			if not newFuelData then
				helper.printWarning("Out of fuel. Put fuel in slot", slots.get(FUEL_SLOT))
	---@diagnostic disable-next-line: undefined-field
				os.sleep(RETRY_DELAY)
				helper.osYield()
			end
		until newFuelData
		execWithSlot(wrapt.refuel, FUEL_SLOT)
	until wrapt.getFuelLevel() > REFUEL_THRESHOLD
end

local function tryToRefuel(chestData, diggingArea, dropOffEntangledChest, refuelEntangledChest)
	if wrapt.getFuelLevel() < REFUEL_THRESHOLD then
		if refuelEntangledChest then 
			refuelEntangled(chestData, diggingArea, dropOffEntangledChest)
			return
		else
			refuelNormally()
			return
		end
	end
end



local function executeDigging(layers, diggingArea, chestData, config)
	local counter = 0
	for layerIndex, layer in ipairs(layers) do
		for blockIndex, block in ipairs(layer) do
			if counter % 5 == 0 or not wrapt.getItemDetail(slots.get(BLOCK_SLOT)) then
				sortInventory(not config.useFuelEntangledChest)
			end
			if counter % 5 == 0 then
				tryToRefuel(chestData, diggingArea, config.useEntangledChests, config.useFuelEntangledChest)
			end
			tryDropOffThings(chestData, diggingArea, config.useEntangledChests)
			if not diggingArea[helper.getIndex(block.x, block.y, block.z)].preserve then
				if not goToCoords(block, diggingArea, PATHFIND_INSIDE_NONPRESERVED_AREA) then
					helper.printWarning("Couldn't find a path to next block, trying again ingoring walls...")
					if not goToCoords(block, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
						helper.printWarning("Fallback pathfinding failed, skipping the block")
						break
					end
				end
				if block.adjacent then
					processAdjacent(diggingArea, config)
				elseif block.triple then
					processTriple(diggingArea)
				end
			end
			counter = counter + 1
		end
	end
	tryDropOffThings(chestData, diggingArea, config.useEntangledChests, true)
end

local function getValidatedRegion(config, default)
	ui.printInfo()
	ui.printProgramsInfo()
	while true do
		local shape = nil
		if default then
			shape = ui.parseProgram(config.defaultCommand)
			if not shape then
				helper.printError("defaultCommand is invalid")
				default = false
			end
		end

		if not default then
			shape = ui.promptForShape()
		end

		local genRegion = shape.shape.generate(table.unpack(shape.args))
		local validationResult = region.validateRegion(genRegion)
		if validationResult == SUCCESS then
			return genRegion
		end
		ui.showValidationError(validationResult)
	end
end

local function launchDigging(config, default)
	local diggingArea = getValidatedRegion(config, default)
	local layers = region.createLayersFromArea(diggingArea, config.layerSeparationAxis)
	local chestData = region.reserveChests(diggingArea)
	executeDigging(layers, diggingArea, chestData, config)
end


local function executeMineLoop(config)
	local cumDelta = {x = 0, y = 0, z = 0}
	local prevPos = nil
	while true do
		--create a region to dig
		local shape = ui.parseProgram(config.mineLoopCommand)
		local diggingArea = shape.shape.generate(table.unpack(shape.args))
		diggingArea = region.shiftRegion(diggingArea, cumDelta)
		local layers = region.createLayersFromArea(diggingArea, config.layerSeparationAxis)
		local chestData = region.reserveChests(diggingArea)

		--place chunk loader
		local chunkLoaderPos = table.remove(chestData.reserved)
		local chunkLoaderIndex = helper.getIndex(chunkLoaderPos.x, chunkLoaderPos.y, chunkLoaderPos.z)
		local blockAbove = {x = chunkLoaderPos.x, y = chunkLoaderPos.y+1, z = chunkLoaderPos.z}
		if prevPos then
			prevPos.preserve = true
			diggingArea[helper.getIndex(prevPos.x, prevPos.y, prevPos.z)] = prevPos
		end
		if not goToCoords(blockAbove, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
			helper.printError("Could not navigate to new chunk loader position, aborting...")
			return
		end
		repeat digBelow() until execWithSlot(wrapt.placeDown, CHUNK_LOADER_SLOT)
		diggingArea[chunkLoaderIndex].preserve = true

		--remove old chunk loader
		if prevPos then
			local prevBlockAbove = {x = prevPos.x, y = prevPos.y+1, z = prevPos.z}
			if not goToCoords(prevBlockAbove, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
				helper.printError("Could not navigate to previous chunkloader, aborting")
			end
			execWithSlot(digBelow, CHUNK_LOADER_SLOT)
			if not goToCoords(blockAbove, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
				helper.printError("Could not navigate back from old loader, aborting...")
				return
			end
		end

		--dig the region
		executeDigging(layers, diggingArea, chestData, config)

		prevPos = chunkLoaderPos
		cumDelta = {x = cumDelta.x + config.mineLoopOffset.x,y = cumDelta.y + config.mineLoopOffset.y,z = cumDelta.z + config.mineLoopOffset.z}
	end
end

local function launchMineLoop(config, autostart)
	helper.print("Verifying mineLoopCommand...")
	local shape = ui.parseProgram(config.mineLoopCommand)
	if not shape then
		helper.printError("mineLoopCommand is invalid")
		return
	end
	local areaToValidate = shape.shape.generate(table.unpack(shape.args))
	local validationResult = region.validateRegion(areaToValidate)
	if validationResult ~= SUCCESS then
		ui.showValidationError(validationResult)
		return
	end

	if not autostart then
		ui.printInfo()
		helper.print("Press Enter to start the loop")
		helper.read()
	end
	executeMineLoop(config)
end



local function main(...)
	local args = {...}
	local config = helper.readOnlyTable(cfg.processConfig())
	slots.assignSlots(config)

	local default = args[1] == "def"

	if config.mineLoop then
		launchMineLoop(config, default)
		return
	end
	
	launchDigging(config, default)
end

main(...)