-- Settings
blacklistSlotStart = 1
blacklistSlotEnd = 5
digSlotsStart = 6
digSlotsEnd = 14

originNoVeinRadius = 4
originNoAttackRadius = 6
maxVeinX = 0
maxVeinZ = 0

minorTunnelsPerSide = 6
spaceBetweenMinorTunnels = 3
minorTunnelLengthPerSide = 30
verticalLevels = 3
spaceBetweenVerticalLevels = 2

chestSlot = 16
chunkLoaderSlot = 15
enderChestMode = 0
chunkLoaderChunkRadius = 1

twoHighPassages = 0
refuelFromInvIfBelowFuelLevel = 10000

-- Constants
DIR_POS_X = 0
DIR_POS_Z = 1
DIR_NEG_X = 2
DIR_NEG_Z = 3

-- Global initializations
currentX = 0
currentY = 0
currentZ = 0
currentDirection = DIR_POS_X

-- GENERIC FUNCTIONS

function selectEmptySlot(rangeStart, rangeEnd)
	local i
	for i = rangeStart,rangeEnd do
		if turtle.getItemCount(i) == 0 then
			turtle.select(i)
			return true
		end
	end
	return false
end

function checkFuel(minFuel)
	if turtle.getFuelLevel() < minFuel then
		local needFuel = minFuel - turtle.getFuelLevel()
		if selectEmptySlot(1, 16) then
			print("Place at least " .. needFuel .. " units of fuel in selected slot")
			io.read()
			turtle.refuel()
			return checkFuel(minFuel)
		else
			turtle.select(16)
			print("Place at least " .. needFuel .. " units of fuel in selected slot and save items in there")
			io.read()
			turtle.refuel()
			print("Replace items that were in the slot")
			io.read()
			return checkFuel(minFuel)
		end
	else
		return true
	end
end

function goForward()
	checkFuel(1)
	if turtle.forward() then
		if currentDirection == DIR_POS_X then
			currentX = currentX + 1
		elseif currentDirection == DIR_POS_Z then
			currentZ = currentZ + 1
		elseif currentDirection == DIR_NEG_X then
			currentX = currentX - 1
		elseif currentDirection == DIR_NEG_Z then
			currentZ = currentZ - 1
		end
		return true
	else
		return false
	end
end

function goBack()
	checkFuel(1)
	if turtle.back() then
		if currentDirection == DIR_POS_X then
			currentX = currentX - 1
		elseif currentDirection == DIR_POS_Z then
			currentZ = currentZ - 1
		elseif currentDirection == DIR_NEG_X then
			currentX = currentX + 1
		elseif currentDirection == DIR_NEG_Z then
			currentZ = currentZ + 1
		end
		return true
	else
		return false
	end
end

function goUp()
	if turtle.up() then
		currentY = currentY + 1
		return true
	else
		return false
	end
end

function goDown()
	if turtle.down() then
		currentY = currentY - 1
		return true
	else
		return false
	end
end

function turnRight()
	turtle.turnRight()
	if currentDirection == DIR_POS_X then
		currentDirection = DIR_POS_Z
	elseif currentDirection == DIR_POS_Z then
		currentDirection = DIR_NEG_X
	elseif currentDirection == DIR_NEG_X then
		currentDirection = DIR_NEG_Z
	elseif currentDirection == DIR_NEG_Z then
		currentDirection = DIR_POS_X
	end
end

function turnLeft()
	turtle.turnLeft()
	if currentDirection == DIR_POS_X then
		currentDirection = DIR_NEG_Z
	elseif currentDirection == DIR_NEG_Z then
		currentDirection = DIR_NEG_X
	elseif currentDirection == DIR_NEG_X then
		currentDirection = DIR_POS_Z
	elseif currentDirection == DIR_POS_Z then
		currentDirection = DIR_POS_X
	end
end


-- Select the first slot in a given range that has items in it, or prompt if none found
function selectUsedSlot(rangeStart, rangeEnd, itemString)
	local i
	for i = rangeStart,rangeEnd do
		if turtle.getItemCount(i) > 0 then
			turtle.select(i)
			return
		end
	end
	print("Place item " .. itemString .. " in slots " .. rangeStart .. " through " .. rangeEnd)
	io.read()
	selectUsedSlot(rangeStart, rangeEnd, itemString)
end

function getSlotRangeSpace(rangeStart, rangeEnd)
	local i
	local ctr = 0
	for i = rangeStart,rangeEnd do
		if turtle.getItemCount(i) == 0 then
			ctr = ctr + 1
		end
	end
	return ctr
end

function changeDirection(toDir)
	if
		(currentDirection == DIR_POS_X and toDir == DIR_NEG_X)
		or (currentDirection == DIR_NEG_X and toDir == DIR_POS_X)
		or (currentDirection == DIR_POS_Z and toDir == DIR_NEG_Z)
		or (currentDirection == DIR_NEG_Z and toDir == DIR_POS_Z)
	then
		turnLeft()
		turnLeft()
	elseif
		(currentDirection == DIR_POS_X and toDir == DIR_POS_Z)
		or (currentDirection == DIR_POS_Z and toDir == DIR_NEG_X)
		or (currentDirection == DIR_NEG_X and toDir == DIR_NEG_Z)
		or (currentDirection == DIR_NEG_Z and toDir == DIR_POS_X)
	then
		turnRight()
	elseif
		(currentDirection == DIR_POS_X and toDir == DIR_NEG_Z)
		or (currentDirection == DIR_NEG_Z and toDir == DIR_NEG_X)
		or (currentDirection == DIR_NEG_X and toDir == DIR_POS_Z)
		or (currentDirection == DIR_POS_Z and toDir == DIR_POS_X)
	then
		turnLeft()
	end
end

function getOriginDistance()
	return math.abs(currentX) + math.abs(currentY) + math.abs(currentZ)
end

function checkGoForward()
	if goForward() then
		return false
	end
	while not goForward() do
		if (not turtle.dig()) and getOriginDistance() > originNoAttackRadius then turtle.attack() end
	end
	return true
end

function checkGoUp()
	if goUp() then
		return false
	end
	while not goUp() do
		if (not turtle.digUp()) and getOriginDistance() > originNoAttackRadius then turtle.attackUp() end
	end
	return true
end

function checkGoDown()
	if goDown() then
		return false
	end
	while not goDown() do
		if (not turtle.digDown()) and getOriginDistance() > originNoAttackRadius then turtle.attackDown() end
	end
	return true
end

digUpAfterEveryMoveToStep = 0

function moveTo(x, y, z, dir)
	checkFuel(math.abs(x - currentX) + math.abs(y - currentY) + math.abs(z - currentZ))
	while currentX < x do
		changeDirection(DIR_POS_X)
		checkGoForward()
		if digUpAfterEveryMoveToStep == 1 then turtle.digUp() end
	end
	while currentX > x do
		changeDirection(DIR_NEG_X)
		checkGoForward()
		if digUpAfterEveryMoveToStep == 1 then turtle.digUp() end
	end
	while currentZ < z do
		changeDirection(DIR_POS_Z)
		checkGoForward()
		if digUpAfterEveryMoveToStep == 1 then turtle.digUp() end
	end
	while currentZ > z do
		changeDirection(DIR_NEG_Z)
		checkGoForward()
		if digUpAfterEveryMoveToStep == 1 then turtle.digUp() end
	end
	while currentY < y do
		checkGoUp()
	end
	while currentY > y do
		checkGoDown()
	end
	changeDirection(dir)
end

function packCurLocState()
	local curLoc = { x=currentX, y=currentY, z=currentZ, dir=currentDirection }
	return curLoc
end

function goPackedLocState(state)
	moveTo(state["x"], state["y"], state["z"], state["dir"])
end

-- MINER FUNCTIONS

-- dir is "up", "down", or "forward".  Returns "empty", "blacklist", or "good"
-- TODO: check if within origin safe zone
function compareBlacklist(dir)
	if dir == "up" then
		if not turtle.detectUp() then
			return "empty"
		end
	elseif dir == "down" then
		if not turtle.detectDown() then
			return "empty"
		end
	elseif dir == "forward" then
		if not turtle.detect() then
			return "empty"
		end
	end
	local i
	for i = blacklistSlotStart,blacklistSlotEnd do
		if turtle.getItemCount(i) > 0 then
			turtle.select(i)
			if dir == "up" and turtle.compareUp() then
				return "blacklist"
			elseif dir == "down" and turtle.compareDown() then
				return "blacklist"
			elseif dir == "forward" and turtle.compare() then
				return "blacklist"
			end
		end
	end
	return "good"
end

function isForwardInVeinRange()
	local x = currentX
	local z = currentZ
	if currentDirection == DIR_POS_X then x = x + 1
	elseif currentDirection == DIR_NEG_X then x = x - 1
	elseif currentDirection == DIR_POS_Z then z = z + 1
	elseif currentDirection == DIR_NEG_Z then z = z - 1
	end
	if maxVeinX > 0 and math.abs(x) > maxVeinX then
		return false
	end
	if maxVeinZ > 0 and math.abs(z) > maxVeinZ then
		return false
	end
	return true
end

function checkMineVein()
	if math.abs(currentX) < originNoVeinRadius and math.abs(currentY) < originNoVeinRadius and math.abs(currentZ) < originNoVeinRadius then
		return "blacklist"
	end
	turnLeft()
	if isForwardInVeinRange() and compareBlacklist("forward") == "good" and turtle.dig() and goForward() then
		checkMineVein()
		goBack()
	end
	turnRight()
	if isForwardInVeinRange() and compareBlacklist("forward") == "good" and turtle.dig() and goForward() then
		checkMineVein()
		goBack()
	end
	if compareBlacklist("up") == "good" and turtle.digUp() and goUp() then
		checkMineVein()
		goDown()
	end
	if compareBlacklist("down") == "good" and turtle.digDown() and goDown() then
		checkMineVein()
		goUp()
	end
	turnRight()
	if isForwardInVeinRange() and compareBlacklist("forward") == "good" and turtle.dig() and goForward() then
		checkMineVein()
		goBack()
	end
	turnLeft()
end

-- Returns to 0, 0, 0, DIR_POS_X through (hopefully) existing tunnels
function goHome()
	moveTo(0, currentY, currentZ, currentDirection)
	moveTo(0, currentY, 0, DIR_POS_X)
	moveTo(0, 0, 0, DIR_POS_X)
end

function goToPoint(x, y, z)
	if currentY ~= y then
		moveTo(0, currentY, currentZ, currentDirection)
		moveTo(0, currentY, 0, currentDirection)
		moveTo(0, y, 0, currentDirection)
	end
	if currentZ ~= z then
		moveTo(0, y, currentZ, currentDirection)
		moveTo(0, y, z, currentDirection)
	end
	if currentX ~= x then
		moveTo(x, y, z, currentDirection)
	end
end

function isSelectedSlotInBlacklist()
	local i
	for i = blacklistSlotStart,blacklistSlotEnd do
		if turtle.getItemCount(i) > 0 and turtle.compareTo(i) then
			return true
		end
	end
	return false
end

function checkRefuelFromInventory()
	local i
	if turtle.getFuelLevel() < refuelFromInvIfBelowFuelLevel then
		for i = digSlotsStart,digSlotsEnd do
			if turtle.getItemCount(i) > 0 then
				turtle.select(i)
				turtle.refuel()
			end
		end
	end
end

-- Returns number of free slots after pruning
function pruneInventory()
	checkRefuelFromInventory()
	local i
	local freeSlots = 0
	for i = digSlotsStart,digSlotsEnd do
		if turtle.getItemCount(i) > 0 then
			turtle.select(i)
			if isSelectedSlotInBlacklist() then
				turtle.dropUp()
				freeSlots = freeSlots + 1
			end
		else
			freeSlots = freeSlots + 1
		end
	end
	return freeSlots
end

function getEstFuelToHome()
	return math.abs(currentX) + math.abs(currentY) + math.abs(currentZ)
end

function dropDigSlots()
	local i
	for i = digSlotsStart,digSlotsEnd do
		while turtle.getItemCount(i) > 0 do
			turtle.select(i)
			while not turtle.drop() do
				print("Out of space to deposit items.  Free up space.")
				os.sleep(1)
			end
		end
	end
end

function goHomeForFuel(requireFuel)
	local returnX = currentX
	local returnY = currentY
	local returnZ = currentZ
	local returnDir = currentDirection
	local estReturnFuel = getEstFuelToHome()
	goHome()
	checkFuel(requireFuel + estReturnFuel + 1)
	goToPoint(returnX, returnY, returnZ)
	changeDirection(returnDir)
end

function goHomeForDump()
	local returnX = currentX
	local returnY = currentY
	local returnZ = currentZ
	local returnDir = currentDirection
	local estReturnFuel = getEstFuelToHome()
	goHome()
	checkFuel(estReturnFuel + 32)
	changeDirection(DIR_POS_X)
	pruneInventory()
	dropDigSlots()
	goToPoint(returnX, returnY, returnZ)
	changeDirection(returnDir)
end

function goHomeForFinish()
	goHome()
	changeDirection(DIR_POS_X)
	pruneInventory()
	dropDigSlots()
end

function mineStep(stepDirection)
	local returnX = currentX
	local returnY = currentY
	local returnZ = currentZ
	local returnDir = currentDirection
	-- Make sure there's enough fuel to get home
	local requireFuel = getEstFuelToHome() + 128
	if turtle.getFuelLevel() < requireFuel then
		goHomeForFuel(requireFuel * 2 + 128)
	end
	-- Make sure there's enough free inventory space
	if getSlotRangeSpace(digSlotsStart, digSlotsEnd) < 2 then
		local newFreeSlots = pruneInventory()
		if newFreeSlots < 3 then
			goHomeForDump()
		end
	end
	-- Mine the block and move
	turtle.select(digSlotsStart)
	local dugBlock = false
	if stepDirection == "forward" then
		dugBlock = checkGoForward()
	elseif stepDirection == "up" then
		dugBlock = checkGoUp()
	elseif stepDirection == "down" then
		dugBlock = checkGoDown()
	end
	-- Check for veins
	-- TODO: Maybe check for veins sometimes even if a block is not dug?
	if dugBlock then
		checkMineVein()
	end
	-- Dig upwards for human followers
	if twoHighPassages == 1 then
		while turtle.detectUp() do turtle.digUp() end
	end
end

function mineTo(x, y, z, dir)
	print("Mining to " .. x .. ", " .. y .. ", " .. z)
	if x == nil then print("x is nil") end
	if currentX == nil then print("currentX is nil") end
	if y == nil then print("y is nil") end
	if currentY == nil then print("currentY is nil") end
	if z == nil then print("z is nil") end
	if currentZ == nil then print("currentZ is nil") end
	checkFuel(math.abs(x - currentX) + math.abs(y - currentY) + math.abs(z - currentZ))
	while currentX < x do
		changeDirection(DIR_POS_X)
		mineStep("forward")
	end
	while currentX > x do
		changeDirection(DIR_NEG_X)
		mineStep("forward")
	end
	while currentZ < z do
		changeDirection(DIR_POS_Z)
		mineStep("forward")
	end
	while currentZ > z do
		changeDirection(DIR_NEG_Z)
		mineStep("forward")
	end
	while currentY < y do
		mineStep("up")
	end
	while currentY > y do
		mineStep("down")
	end
	changeDirection(dir)
end

function mineToTargetPoint(x, y, z)
	-- Check for worst case estimated fuel consumption (if we have to go through home to get to target, then go to target, then go back home, possibly with ore veins along the way)
	local requireFuel = getEstFuelToHome() + (math.abs(x) + math.abs(y) + math.abs(z)) * 3
	if turtle.getFuelLevel() < requireFuel then
		goHomeForFuel(requireFuel)
	end
	if currentY ~= y then
		-- These tunnels should already be mined
		moveTo(0, currentY, currentZ, currentDirection)
		moveTo(0, currentY, 0, currentDirection)
		-- This tunnel might not be mined
		mineTo(0, y, 0, currentDirection)
	end
	if currentZ ~= z then
		-- Should already be mined
		moveTo(0, y, currentZ, currentDirection)
		-- Might not be mined
		mineTo(0, y, z, currentDirection)
	end
	if currentX ~= x then
		mineTo(x, y, z, currentDirection)
	end
end

-- Function to call for main mining
function mineMain()
	local nextVertLevel = 0
	local nextMinorTunnel = -1
	while 1 do
		local y = nextVertLevel * (spaceBetweenVerticalLevels + 1)
		local z
		if nextMinorTunnel < 0 then
			z = (nextMinorTunnel + 1) * (spaceBetweenMinorTunnels + 1) - originNoVeinRadius
		else
			z = (nextMinorTunnel - 1) * (spaceBetweenMinorTunnels + 1) + originNoVeinRadius
		end
		mineToTargetPoint(minorTunnelLengthPerSide, y, z)
		mineToTargetPoint(0 - minorTunnelLengthPerSide, y, z)
		if nextMinorTunnel < 0 then
			nextMinorTunnel = nextMinorTunnel - 1
			if nextMinorTunnel < 0 - minorTunnelsPerSide then
				nextMinorTunnel = 1
			end
		else
			nextMinorTunnel = nextMinorTunnel + 1
			if nextMinorTunnel > minorTunnelsPerSide then
				nextMinorTunnel = -1
				nextVertLevel = nextVertLevel + 1
				if nextVertLevel >= verticalLevels then
					break
				end
			end
		end
	end
	goHomeForFinish()
end

function goToChunkCenter(x, z)
	local chunkX = math.floor(x / 16)
	local chunkZ = math.floor(z / 16)
	local cornerX = chunkX * 16
	local cornerZ = chunkZ * 16
	local centerX = cornerX + 8
	local centerZ = cornerZ + 8
	local moveX = centerX - x
	local moveZ = centerZ - z
	print("Moving to chunk center " .. centerX .. "," .. centerZ .. " of chunk " .. chunkX .. "," .. chunkZ .. " (offset " .. moveX .. "," .. moveZ .. ")")
	moveTo(moveX, currentY, moveZ, currentDirection)
end

function tryPlace(placeSlot)
	turtle.select(placeSlot)
	while not turtle.place() do
		turtle.select(digSlotsStart)
		turtle.dig()
		turtle.select(placeSlot)
	end
end

function setupAndMineArea()
	-- Recalibrate coordinate system
	currentX = 0
	currentY = 0
	currentZ = 0
	currentDirection = DIR_POS_X
	-- Place chest in front
	turtle.select(chestSlot)
	if enderChestMode == 1 then
		if turtle.getItemCount(chestSlot) < 1 then
			print("No ender chest found.  Place in slot " .. chestSlot)
			io.read()
		end
		checkGoForward()
		turtle.digUp()
		turnLeft()
		turnLeft()
		checkGoForward()
		turnLeft()
		turnLeft()
		tryPlace(chestSlot)
	else
		if turtle.getItemCount(chestSlot) < 3 then
			print("Need at least 3 chests in slot " .. chestSlot)
			io.read()
		end
		checkGoForward()
		turtle.digUp()
		turnRight()
		checkGoForward()
		turtle.digUp()
		turnRight()
		checkGoForward()
		turnLeft()
		turnLeft()
		tryPlace(chestSlot)
		turnLeft()
		checkGoForward()
		turnRight()
		tryPlace(chestSlot)
	end
	pruneInventory()
	dropDigSlots()
	-- Mine
	mineMain()
end

function chunkLoaderGoNextArea()
	-- If in ender chest mode, mine up the ender chest
	if enderChestMode == 1 then
		turtle.select(chestSlot)
		if turtle.getItemCount(chestSlot) > 0 then
			if not turtle.drop() then turtle.dropUp() end
		end
		turtle.dig()
	end
	turtle.select(digSlotsStart)
	-- Go to location of first intermediate chunk loader
	local i
	local curDistToMove
	local baseDistToMove
	baseDistToMove = chunkLoaderChunkRadius * 16 + 4
	curDistToMove = baseDistToMove
	turnRight()
	for i = 1,curDistToMove do
		checkGoForward()
		turtle.digUp()
	end
	-- Place it
	turnLeft()
	turtle.dig()
	turtle.select(chunkLoaderSlot)
	local hasExtraLoaders = true
	if turtle.getItemCount(chunkLoaderSlot) < 1 then
		print("Insert chunk loaders in slot " .. chunkLoaderSlot)
		io.read()
	end
	if turtle.getItemCount(chunkLoaderSlot) < 2 then
		hasExtraLoaders = false
	end
	tryPlace(chunkLoaderSlot)
	-- Go back and fetch the old chunk loader
	turtle.select(digSlotsStart)
	turnLeft()
	for i = 1,curDistToMove do
		checkGoForward()
		turtle.digUp()
	end
	turnLeft()
	turtle.select(chunkLoaderSlot)
	if (not hasExtraLoaders) and turtle.getItemCount(chunkLoaderSlot) > 0 then turtle.dropUp() end
	turtle.dig()
	-- Go to the location of the second intermediate chunk loader
	turtle.select(digSlotsStart)
	turnLeft()
	curDistToMove = baseDistToMove + 16
	for i = 1,curDistToMove do
		checkGoForward()
		turtle.digUp()
	end
	-- Place it
	turnLeft()
	turtle.dig()
	turtle.select(chunkLoaderSlot)
	tryPlace(chunkLoaderSlot)
	-- Go back and fetch the first intermediate chunk loader
	turtle.select(digSlotsStart)
	turnLeft()
	for i = 1,16 do
		checkGoForward()
		turtle.digUp()
	end
	-- Mine it
	turnRight()
	turtle.select(chunkLoaderSlot)
	if (not hasExtraLoaders) and turtle.getItemCount(chunkLoaderSlot) > 0 then turtle.dropUp() end
	turtle.dig()
	-- Go to the final location
	turnRight()
	curDistToMove = (chunkLoaderChunkRadius * 2 + 1) * 16 - baseDistToMove
	turtle.select(digSlotsStart)
	for i = 1,curDistToMove do
		checkGoForward()
		turtle.digUp()
	end
	-- Place the chunk loader
	turnRight()
	turtle.dig()
	turtle.select(chunkLoaderSlot)
	tryPlace(chunkLoaderSlot)
	-- Go back and fetch the second intermedate chunk loader
	turnRight()
	turtle.select(digSlotsStart)
	curDistToMove = curDistToMove - 16
	for i = 1,curDistToMove do
		checkGoForward()
		turtle.digUp()
	end
	-- Mine it
	turnRight()
	turtle.select(chunkLoaderSlot)
	if (not hasExtraLoaders) and turtle.getItemCount(chunkLoaderSlot) > 0 then turtle.dropUp() end
	turtle.dig()
	-- Go to final location
	turnRight()
	for i = 1,curDistToMove do
		checkGoForward()
		turtle.digUp()
	end
	turnLeft()
end

function chunkLoaderInitSettings()
	originNoVeinRadius = 4
	maxVeinX = chunkLoaderChunkRadius * 16 + 6
	maxVeinZ = chunkLoaderChunkRadius * 16 + 6
	minorTunnelsPerSide = math.floor((chunkLoaderChunkRadius * 16 + 6 - originNoVeinRadius - 2) / (spaceBetweenMinorTunnels + 1))
	minorTunnelLengthPerSide = chunkLoaderChunkRadius * 16 + 6
end

function chunkLoaderModeMineMain()
	chunkLoaderInitSettings()
	turnRight()
	turnRight()
	turtle.select(digSlotsStart)
	turtle.dig()
	tryPlace(chunkLoaderSlot)
	turnLeft()
	turnLeft()
	while 1 do
		print("Mining area ...")
		setupAndMineArea()
		print("Moving to next area ...")
		chunkLoaderGoNextArea()
	end
end

function chunkLoaderModeMain()
	print("Ensure the turtle is facing positive X.  If not, move it and restart.")
	print("Enter the turtle's X coordinate")
	local curWorldX = tonumber(io.read())
	print("Enter the turtle's Z coordinate")
	local curWorldZ = tonumber(io.read())
	print("Place stone, dirt, gravel, cobble, marble in slots 1-5, place 2 chunk loaders in slot 15, place ender chest or chests in slot 16")
	io.read()
	print("Moving to chunk center ...")
	digUpAfterEveryMoveToStep = 1
	goToChunkCenter(curWorldX, curWorldZ)
	digUpAfterEveryMoveToStep = 0
	print("Going.")
	chunkLoaderModeMineMain()
end

chunkLoaderModeMain()

-- mineMain()

