local Position = require("lib.position")
local Direction = require("lib.direction")
local Stack = require("lib.stack")

---@alias TurtleControllerConfig {fuel_direction: Direction, fuel_name: string, refuel_amount: number, refuel_safety_margin: number, storage_direction: Direction}

---@class TurtleController
---@field config TurtleControllerConfig
---@field position Position
---@field last_positions Stack
---@field mine_forward boolean
---@field mine_above boolean
---@field mine_below boolean
local TurtleController = {}
TurtleController.__index = TurtleController

---@return TurtleController
function TurtleController:Create()
	local t = {}
	setmetatable(t, TurtleController)

	-----------------------------------------------------------------------------------------------
	-- Fields
	-----------------------------------------------------------------------------------------------

	t.config = {
		fuel_direction = Direction.EAST,
		fuel_name = turtle.getItemDetail(1) == nil and "" or turtle.getItemDetail(1).name,
		refuel_amount = 500,
		refuel_safety_margin = 160,
		storage_direction = Direction.WEST,
	}
	t.position = Position:Empty()
	t.last_positions = Stack:Create()
	t.mine_forward = false
	t.mine_above = false
	t.mine_below = false

	return t
end

-----------------------------------------------------------------------------------------------
-- Movement Methods
-----------------------------------------------------------------------------------------------

---Positive numbers turn clockwise, negative numbers turn counterclockwise
---@param number_of_turns number
function TurtleController:TurnRelative(number_of_turns)
	if number_of_turns % 4 == 0 then
		return
	end

	-- Turn turtle
	local turns = number_of_turns % 4
	if turns == 3 then
		-- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, -1))

		-- If we're rotating by 3, we could turn faster by rotating by 1 in the opposite direction
		turtle.turnLeft()
	else
		-- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, turns))

		for _ = 1, turns do
			turtle.turnRight()
		end
	end

	-- Update current_position
	self.position.dir = (self.position.dir + number_of_turns) % 4
end

---@param direction Direction
function TurtleController:TurnToDirection(direction)
	if self.position.dir == direction then
		return
	end

	self:TurnRelative(direction - self.position.dir)
end

---Move forward by a number of blocks depending on the current rotation
---@param number_of_blocks number
---@param skip_unstocking boolean | nil
---@param skip_refueling boolean | nil
function TurtleController:MoveForward(number_of_blocks, skip_unstocking, skip_refueling)
	skip_unstocking = skip_unstocking or false
	skip_refueling = skip_refueling or false
	if number_of_blocks == 0 then
		return
	end

	-- print(("Turtle is moving forward by %d blocks..."):format(number_of_blocks))

	for _ = 1, math.abs(number_of_blocks) do
		if not skip_refueling then
			self:RefuelIfEmpty()
		end
		if not skip_unstocking then
			self:UnstockIfFull()
		end

		-- Mine/Move
		if self.mine_forward then
			while not turtle.forward() do
				if self:TestForBlock("minecraft:chest") then
					error("Won't mine because a chest is in the way!")
				end
				turtle.dig()
			end
		elseif not turtle.forward() then
			error("Turtle failed to move forward!")
		end
		if self.mine_above then
			if self:TestForBlock("minecraft:chest", true, false) then
				error("Won't mine because a chest is in the way!")
			end
			turtle.digUp()
		end
		if self.mine_below then
			if self:TestForBlock("minecraft:chest", false, true) then
				error("Won't mine because a chest is in the way!")
			end
			turtle.digDown()
		end

		-- Update current position
		if self.position.dir == Direction.NORTH then
			self.position.z = self.position.z + 1
		elseif self.position.dir == Direction.SOUTH then
			self.position.z = self.position.z - 1
		elseif self.position.dir == Direction.EAST then
			self.position.x = self.position.x + 1
		elseif self.position.dir == Direction.WEST then
			self.position.x = self.position.x - 1
		end
	end
end

---Positive numbers move up, negative numbers move down.
---@param number_of_blocks number
---@param skip_unstocking boolean | nil
---@param skip_refueling boolean | nil
function TurtleController:MoveVertical(number_of_blocks, skip_unstocking, skip_refueling)
	skip_unstocking = skip_unstocking or false
	skip_refueling = skip_refueling or false
	if number_of_blocks == 0 then
		return
	end

	-- print(("Turtle is moving vertically by %d blocks..."):format(number_of_blocks))

	local move_function = number_of_blocks > 0 and turtle.up or turtle.down
	local mine_function = number_of_blocks > 0 and turtle.digUp or turtle.digDown
	local mine_enabled = number_of_blocks > 0 and self.mine_above or self.mine_below

	for _ = 1, math.abs(number_of_blocks) do
		if not skip_refueling then
			self:RefuelIfEmpty()
		end
		if not skip_unstocking then
			self:UnstockIfFull()
		end

		-- Mine/Move
		if mine_enabled then
			while not move_function() do
				if self:TestForBlock("minecraft:chest", number_of_blocks > 0, number_of_blocks < 0) then
					error("Won't mine because a chest is in the way!")
				end
				mine_function()
			end
		elseif not move_function() then
			error("Turtle failed to move vertically!")
		end

		-- Update current position
		if number_of_blocks > 0 then
			self.position.y = self.position.y + 1
		elseif number_of_blocks < 0 then
			self.position.y = self.position.y - 1
		end
	end
end

---Stores the current position on the stack so we can return using TurtleController:MoveBack()
function TurtleController:StorePosition()
	self.last_positions:Push(Position:Copy(self.position))
end

---Move to an absolute position. Stores the current position on the stack so we can return using TurtleController:MoveBack()
---@param x number The EAST/WEST axis (grows from WEST to EAST)
---@param y number The UP/DOWN axis (grows from DOWN to UP)
---@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH)
---@param dir Direction
---@param skip_unstocking boolean | nil
---@param skip_refueling boolean | nil
function TurtleController:MoveToPosition(x, y, z, dir, skip_unstocking, skip_refueling)
	-- print(("Turtle is moving to (%d, %d, %d)..."):format(x, y, z))

	-- Store the current position on the stack, so we can return using TurtleController:MoveBack()
	self.last_positions:Push(Position:Copy(self.position))

	-- Store mining config so we can restore it later
	local mine_forward = self.mine_forward
	local mine_above = self.mine_above
	local mine_below = self.mine_below

	-- EAST/WEST axis (do first to not interfere with chests)
	self:EnableMiningForward()
	self:DisableMiningAbove()
	self:DisableMiningBelow()
	if self.position.x > x then
		self:TurnToDirection(Direction.WEST)
	elseif self.position.x < x then
		self:TurnToDirection(Direction.EAST)
	end
	self:MoveForward(math.abs(x - self.position.x), skip_unstocking, skip_refueling)

	-- NORTH/SOUTH axis
	if self.position.z > z then
		self:TurnToDirection(Direction.SOUTH)
	elseif self.position.z < z then
		self:TurnToDirection(Direction.NORTH)
	end
	self:MoveForward(math.abs(z - self.position.z), skip_unstocking, skip_refueling)

	-- UP/DOWN axis
	self:DisableMiningForward()
	if y < self.position.y then
		self:EnableMiningBelow()
	elseif y > self.position.y then
		self:EnableMiningAbove()
	end
	self:MoveVertical(y - self.position.y, skip_unstocking, skip_refueling)

	-- Direction
	self:TurnToDirection(dir or Direction.NORTH)

	-- Restore mining config
	if mine_forward then
		self:EnableMiningForward()
	else
		self:DisableMiningForward()
	end
	if mine_above then
		self:EnableMiningAbove()
	else
		self:DisableMiningAbove()
	end
	if mine_below then
		self:EnableMiningBelow()
	else
		self:DisableMiningBelow()
	end

	-- Sanity check
	if not (self.position.x == x and self.position.y == y and self.position.z == z and self.position.dir == dir) then
		error("TurtleController:MoveToPosition failed to move to target position!")
	end
end

---Move by a vector. Stores the current position on the stack so we can return using TurtleController:MoveBack()
---@param x number The EAST/WEST axis (grows from WEST to EAST)
---@param y number The UP/DOWN axis (grows from DOWN to UP)
---@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH)
---@param dir Direction
---@param skip_unstocking boolean | nil
---@param skip_refueling boolean | nil
function TurtleController:MoveByVector(x, y, z, dir, skip_unstocking, skip_refueling)
	self:MoveToPosition(
		self.position.x + x,
		self.position.y + y,
		self.position.z + z,
		dir,
		skip_unstocking,
		skip_refueling
	)
end

---Move to the previously stored position and pop this position from the stack.
---@param skip_unstocking boolean | nil
---@param skip_refueling boolean | nil
function TurtleController:MoveBack(skip_unstocking, skip_refueling)
	local last_position = self.last_positions:Pop()

	if last_position == nil then
		error("Failed to obtain last_position to move back!")
	else
		self:MoveToPosition(
			last_position.x,
			last_position.y,
			last_position.z,
			last_position.dir,
			skip_unstocking,
			skip_refueling
		)

		-- Pop the stack because MoveToPosition pushes our current position
		self.last_positions:Pop()
	end
end

-----------------------------------------------------------------------------------------------
-- Testing Methods
-----------------------------------------------------------------------------------------------

---@param block_name string
---@param above boolean | nil
---@param below boolean | nil
---@return boolean
function TurtleController:TestForBlock(block_name, above, below)
	above = above or false
	below = below or false
	if above and below then
		error("Can only test for blocks in one direction!")
	end

	local inspect_function = turtle.inspect
	if above then
		inspect_function = turtle.inspectUp
	elseif below then
		inspect_function = turtle.inspectDown
	end

	local has_block, block_data = inspect_function()

	if not has_block then
		return false
	end

	return block_data.name == block_name
end

-----------------------------------------------------------------------------------------------
-- Inventory Methods
-----------------------------------------------------------------------------------------------

---@return boolean
function TurtleController:HasFuel()
	local level = turtle.getFuelLevel()
	local distance_home = self.position.x + self.position.y + self.position.z

	return level > distance_home + self.config.refuel_safety_margin
end

---@return boolean
function TurtleController:HasInventorySpace()
	for slot = 1, 16 do
		if turtle.getItemDetail(slot) == nil then
			return true
		end
	end

	return false
end

---@param slot number
---@param count number
---@return boolean
function TurtleController:SuckItemFromChest(slot, count)
	if not self:TestForBlock("minecraft:chest") then
		error("Can't suck item: no chest!")
	end

	local previous_slot = turtle.getSelectedSlot()

	turtle.select(slot or 1)
	local sucked = turtle.suck(count)

	turtle.select(previous_slot)

	return sucked
end

---@param slot number
---@param count number
---@return boolean
function TurtleController:DropItemIntoChest(slot, count)
	if not self:TestForBlock("minecraft:chest") then
		error("Can't drop item: no chest!")
	end

	local previous_slot = turtle.getSelectedSlot()

	turtle.select(slot or 1)
	local dropped = turtle.drop(count)

	turtle.select(previous_slot)

	return dropped
end

function TurtleController:DropInventoryIntoChest()
	if not self:TestForBlock("minecraft:chest") then
		error("Can't drop item: no chest!")
	end

	print("Dropping inventory into chest...")

	for slot = 1, 16 do
		turtle.select(slot)
		turtle.drop()
	end

	turtle.select(1)
end

---@param skip_inventory_check boolean | nil
function TurtleController:UnstockIfFull(skip_inventory_check)
	skip_inventory_check = skip_inventory_check or false
	if not skip_inventory_check and self:HasInventorySpace() then
		return
	end
	if skip_inventory_check then
		print(("HasInventorySpace() returned %s"):format(self:HasInventorySpace()))
	end

	print("Turtle is unstocking...")

	self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true)
	self:DropInventoryIntoChest()
	self:RefuelIfEmpty()

	self:TurnToDirection(Direction.NORTH)
	self:MoveForward(1)
	self:MoveBack(true, true)
end

---@param skip_fuel_check boolean | nil
function TurtleController:RefuelIfEmpty(skip_fuel_check)
	skip_fuel_check = skip_fuel_check or false
	if not skip_fuel_check and self:HasFuel() then
		return
	end
	if skip_fuel_check then
		print(("HasFuel() returned %s"):format(self:HasFuel()))
	end

	print("Turtle is refueling...")

	-- Clear our inventory into the storage chest
	self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true)
	self:DropInventoryIntoChest()

	-- Prepare refueling
	self:TurnToDirection(self.config.fuel_direction)
	turtle.select(1)

	-- Include the distance to the last position when refueling
	-- to keep the amount of work done between refuelings constant
	local target_fuel_level = self.config.refuel_amount
	local last_position = self.last_positions:Peek()
	if last_position == nil then
		error("Failed to obtain last_position while refueling!")
	else
		target_fuel_level = target_fuel_level + last_position.x + last_position.y + last_position.z
	end

	-- Refuel until we hit the refuel_amount
	local before_level = turtle.getFuelLevel()
	repeat
		if not self:SuckItemFromChest(1, 1) then
			error("Failed to suck fuel out of fuel chest!")
		end
		turtle.refuel()
	until turtle.getFuelLevel() >= target_fuel_level
	local after_level = turtle.getFuelLevel()

	self:TurnToDirection(Direction.NORTH)
	self:MoveForward(1)
	self:MoveBack(true, true)

	print(
		("Refuelled %d units, current level is %d (old level was %d)"):format(
			after_level - before_level,
			after_level,
			before_level
		)
	)
end

-----------------------------------------------------------------------------------------------
-- Management Methods
-----------------------------------------------------------------------------------------------

function TurtleController:Configure()
	local config_complete = false

	while not config_complete do
		-- Check for valid fuel
		while self.config.fuel_name == "" do
			print("Please put valid fuel into the first slot and press Enter!")
			local _ = io.read()
			self.config.fuel_name = turtle.getItemDetail(1) == nil and "" or turtle.getItemDetail(1).name
		end

		print("A chest with fuel has to be placed beside the turtle's starting position. Is this chest...")
		print(("...right of the turtle (enter %d)?"):format(Direction.EAST))
		print(("...behind the turtle (enter %d)?"):format(Direction.SOUTH))
		print(("...left of the turtle (enter %d)?"):format(Direction.WEST))
		print("By default, the chest is assumed on the left.")
		self.config.fuel_direction = tonumber(io.read()) or Direction.WEST

		print("A chest to store mined blocks has to be placed beside the turtle's starting position. Is this chest...")
		print(("...right of the turtle (enter %d)?"):format(Direction.EAST))
		print(("...behind the turtle (enter %d)?"):format(Direction.SOUTH))
		print(("...left of the turtle (enter %d)?"):format(Direction.WEST))
		print("By default, the chest is assumed on the right.")
		self.config.storage_direction = tonumber(io.read()) or Direction.EAST

		print("Configuration complete!")
		print(
			('Turtle will consume "%s" for fuel. Put the desired fuel into the first slot to change.'):format(
				self.config.fuel_name
			)
		)
		print("Do you want to accept the configuration (enter 1, otherwise 0)?")
		config_complete = tonumber(io.read()) == 1
	end
end

function TurtleController:EnableMiningForward()
	self.mine_forward = true
end

function TurtleController:DisableMiningForward()
	self.mine_forward = false
end

function TurtleController:EnableMiningAbove()
	self.mine_above = true
end

function TurtleController:DisableMiningAbove()
	self.mine_above = false
end

function TurtleController:EnableMiningBelow()
	self.mine_below = true
end

function TurtleController:DisableMiningBelow()
	self.mine_below = false
end

return TurtleController
