local S = core.get_translator("screwdriver")

screwdriver = {
	ROTATE_FACE = 1,
	ROTATE_AXIS = 2,
	rotate_simple = function(pos, node, player, mode, axis, amount, rotate_function)
		if mode ~= 1 then return false end
	end,
	disallow = false,
}

local get_pointed = dofile(core.get_modpath("screwdriver") .. DIR_DELIM .. "pointed.lua")

-- Functions to choose rotation based on pointed location
local AXISPAIR_DIR = {xy = 1, yz = 1, zx = 1; zy = -1, yx = -1, xz = -1}
local function push_edge(normal, point)
	local biggest = 0
	local biggest_axis
	local normal_axis
	-- Find the normal axis, and the axis of the with the
	-- greatest magnitude (other than the normal axis)
	for axis in pairs(point) do
		if normal[axis] ~= 0 then
			normal_axis = axis
		elseif math.abs(point[axis]) > biggest then
			biggest = math.abs(point[axis])
			biggest_axis = axis
		end
	end
	-- Find the third axis, which is the one to rotate around
	if normal_axis and biggest_axis then
		for axis in pairs(point) do
			if axis ~= normal_axis and axis ~= biggest_axis then
				-- Decide which direction to rotate (+ or -)
				return axis, AXISPAIR_DIR[normal_axis..biggest_axis] * math.sign(
					normal[normal_axis] * point[biggest_axis]
				)
			end
		end
	end
	return "y", 0
end
local function rotate_face(normal, _)
	-- Find the normal axis
	for axis, value in pairs(normal) do
		if value ~= 0 then
			return axis, math.sign(value)
		end
	end
	return "y", 0
end

-- Numbers taken from <https://forum.luanti.org/viewtopic.php?p=73195>
local facedir_cycles = {
	x = {{12,13,14,15}, {16,19,18,17}, { 0, 4,22, 8}, { 1, 5,23, 9}, { 2, 6,20,10}, { 3, 7,21,11}},
	y = {{ 0, 1, 2, 3}, {20,23,22,21}, { 4,13,10,19}, { 8,17, 6,15}, {12, 9,18, 7}, {16, 5,14,11}},
	z = {{ 4, 5, 6, 7}, { 8,11,10, 9}, { 0,16,20,12}, { 1,17,21,13}, { 2,18,22,14}, { 3,19,23,15}},
}
local wallmounted_cycles = {
	x = {{0, 4, 1, 5}},
	y = {{4, 2, 5, 3}, {0, 1}},
	z = {{0, 3, 1, 2}},
}
local function cycle_find(cycles, axis, param2, amount)
	for _, cycle in ipairs(cycles[axis]) do
		-- Find the current dir
		for i, d in ipairs(cycle) do
			if d == param2 then return cycle[1 + (i - 1 + amount) % #cycle] end
		end
	end
end

-- Functions to rotate a facedir/wallmounted value around an axis by a certain amount
local PARAM2TYPES = {
	-- Facedir: lower 5 bits used for direction, 0 - 23
	facedir = {
		rotate = function(param2, axis, amount)
			local facedir = param2 % 32
			local new_facedir = cycle_find(facedir_cycles, axis, facedir, amount) or facedir
			return param2 - facedir + new_facedir
		end,
		check_mode = function(old, new)
			if old - old % 4 == new - new % 4 then return screwdriver.ROTATE_FACE end
			return screwdriver.ROTATE_AXIS
		end,
	},
	-- Wallmounted: lower 3 bits used, 0 - 5
	wallmounted = {
		rotate = function(param2, axis, amount)
			local wallmounted = param2 % 8
			local new_wallmounted = cycle_find(wallmounted_cycles, axis, wallmounted, amount) or wallmounted
			return param2 - wallmounted + new_wallmounted
		end,
		check_mode = function(old, new)
			if (old <= 1) == (new <= 1) then return screwdriver.ROTATE_FACE end
			return screwdriver.ROTATE_AXIS
		end,
	},
	-- 4dir: lower 2 bits used, 0 - 3
	["4dir"] = {
		rotate = function(param2, axis, amount)
			if axis ~= "y" then return param2 end
			local dir = param2 % 4
			return param2 - dir + ((dir + amount) % 4)
		end,
		check_mode = function(old, new)
			return screwdriver.ROTATE_FACE
		end,
	},
}
PARAM2TYPES.colorfacedir = PARAM2TYPES.facedir
PARAM2TYPES.colorwallmounted = PARAM2TYPES.wallmounted
PARAM2TYPES.color4dir = PARAM2TYPES["4dir"]
-- TODO: maybe support degrotate?

local function rect(angle, radius)
	return math.cos(2*math.pi * angle) * radius, math.sin(2*math.pi * angle) * radius
end

local other_axes = {x = {"y","z"}, y = {"z","x"}, z = {"x","y"}}
-- Generate the screwdriver particle effects
local function particle_ring(pos, axis, direction)
	local axis2, axis3 = unpack(other_axes[axis])
	local particle_pos = vector.new()
	local particle_vel = vector.new()
	for i = 0, 0.999, 1/6 do
		particle_pos[axis3], particle_pos[axis2] = rect(i, 0.5 ^ 0.5)
		particle_vel[axis3], particle_vel[axis2] = rect(i - 1/4 * direction, 2)

		core.add_particle({
			pos = vector.add(pos, particle_pos),
			velocity = particle_vel,
			acceleration = vector.multiply(particle_pos, -7),
			expirationtime = 0.25,
			size = 2,
			texture = "screwdriver.png",
		})
	end
end

local corners = {
	vector.new(0.5, 0.5, 0.5), vector.new(0.5, 0.5, -0.5),
	vector.new(-0.5, 0.5, -0.5), vector.new(-0.5, 0.5, 0.5),
	vector.new(0.5, -0.5, 0.5), vector.new(0.5, -0.5, -0.5),
	vector.new(-0.5, -0.5, -0.5), vector.new(-0.5, -0.5, 0.5),
}
local function corner_particles(pos, player_name)
	for _, off in ipairs(corners) do
		core.add_particle({
			pos = pos + off,
			expirationtime = 1,
			size = 4,
			texture = "screwdriver_screw.png",
			glow = 14,
			playername = player_name
		})
	end
end

local SOUND_GROUPS = {"cracky", "crumbly", "dig_immediate", "metal", "choppy", "oddly_breakable_by_hand", "snappy"}
-- Decide what sound to make when rotating a node
local function get_dig_sound(def)
	if def.sounds and def.sounds.dig then
		return def.sounds.dig
	elseif not def.sound_dig or def.sound_dig == "__group" then
		local groups = def.groups
		for i, name in ipairs(SOUND_GROUPS) do
			if groups[name] and groups[name] > 0 then
				return "default_dig_"..name
			end
		end
	else
		return def.sound_dig
	end
end

-- IDEA: split this into 2 functions
-- 1: on_use parameters -> axis/amount/etc.
-- 2: param2/axis/amount/etc. -> new param2
function screwdriver.use(itemstack, player, pointed_thing, is_right_click, uses)
	if pointed_thing.type ~= "node" then return end
	local pos = pointed_thing.under

	-- Check protection
	local player_name = player:get_player_name()
	if core.is_protected(pos, player_name) then
		core.record_protection_violation(pos, player_name)
		return
	end

	-- Get node info
	local node = core.get_node_or_nil(pos)
	if not node then return end
	local def = core.registered_nodes[node.name]
	if not def then return end -- probably unnessesary

	if core.get_meta(pos):get_int("screwdriver:fixed") > 0 then return end

	local on_rotate = def.on_rotate
	if on_rotate == false then return end

	-- Choose rotation function based on paramtype2 (facedir/wallmounted)
	local param2type = PARAM2TYPES[def.paramtype2]
	if not param2type then return end

	-- Choose rotation axis/direction and param2 based on click type and pointed location
	local axis, amount
	local normal, point = get_pointed(player, pointed_thing)
	if not normal or vector.length(normal) == 0 then return end -- Raycast failed or player is inside selection box

	local control = player:get_player_control()
	if is_right_click then
		axis, amount = rotate_face(normal, point)
		-- This line intentionally left blank.
	else
		axis, amount = push_edge(normal, point)
		if control.sneak then amount = -amount end
	end
	local new_param2 = param2type.rotate(node.param2, axis, amount)
	if not new_param2 then return end

	-- Calculate particle position
	local particle_offset = vector.new()
	particle_offset[axis] = point[axis]--math.sign(normal[axis]) * 0.5

	-- Handle node's on_rotate function
	local handled
	if type(on_rotate) == "function" then
		local result = on_rotate(
			vector.new(pos),
			table.copy(node),
			player,
			param2type.check_mode(node.param2, new_param2),
			new_param2,
			-- New:
			axis, -- "x", "y", or "z"
			amount, -- 90 degrees = 1, etc.
			param2type.rotate -- function(node.param2, axis, amount) -> new_param2
		)
		if result == false then
			return
		elseif result == true then
			handled = true
		end
	end

	-- Draw particles
	particle_ring(vector.add(pos, particle_offset), axis, amount)
	-- Sound
	local sound = get_dig_sound(def)
	if sound then
		core.sound_play(sound, {
			pos = pos,
			gain = 0.25,
			max_hear_distance = 32,
		})
	end

	-- Replace node
	if not handled then
		if new_param2 == node.param2 then return end -- no rotation was done
		node.param2 = new_param2
		core.swap_node(pos, node)
	end
	core.check_for_falling(pos)
	if def._after_rotate then def._after_rotate(pos) end

	-- Apply wear if not in creative mode
	if not core.is_creative_enabled(player_name) then
		itemstack:add_wear_by_uses(uses)
		return itemstack
	end
end
-- Backwards compatibility wrapper
function screwdriver.handler(itemstack, user, pointed_thing, mode, uses)
	local is_rightclick = mode == screwdriver.ROTATE_AXIS
	return screwdriver.use(itemstack, user, pointed_thing, is_rightclick, uses)
end

local function fix_handler(itemstack, player, pointed_thing)
	if pointed_thing.type ~= "node" then return end
	local pos = pointed_thing.under

	-- Check protection
	local name = player:get_player_name()
	if core.is_protected(pos, name) then
		core.record_protection_violation(pos, name)
		return
	end

	-- Get node info
	local node = core.get_node_or_nil(pos)
	if not node then return end
	local def = core.registered_nodes[node.name]
	if not def then return end

	local inv = player:get_inventory()
	local meta = core.get_meta(pos)
	if meta:get_int("screwdriver:fixed") > 0 then
		meta:set_int("screwdriver:fixed", 0)
		core.sound_play("screwdriver_unfix", {
			pos = pos, gain = 1.5, pitch = 1 + math.random(), max_hear_distance = 8
		}, true)
		if not (core.is_creative_enabled(name) and inv:contains_item("main", "screwdriver:screw")) then
			inv:add_item("main", "screwdriver:screw")
		end
		return true
	end

	-- Check if player has the required screw
	if not inv:contains_item("main", "screwdriver:screw") then
		core.chat_send_player(name, S("You need screws to fix nodes in place!"))
		return
	end

	meta:set_int("screwdriver:fixed", 1)
	core.sound_play("screwdriver_fix", {
		pos = pos, gain = 1.5, pitch = math.max(0.8, 1 - math.random()), max_hear_distance = 8
	}, true)
	corner_particles(pos, name)
	if not core.is_creative_enabled(name) then
		inv:remove_item("main", "screwdriver:screw")
	end
	return true
end

-- Display corner screw particles when pointing at a fixed node while wielding a screwdriver
local timer = 0
core.register_globalstep(function(dtime)
	timer = timer + dtime
	if timer < 0.5 then return end
	timer = 0
	for _, player in ipairs(core.get_connected_players()) do
		local wield = player:get_wielded_item():get_name()
		if core.get_item_group(wield, "screwdriver") ~= 0 then
			local ppos = player:get_pos()
			local epos = ppos + vector.new(0, 1.5, 0)
			local dir = player:get_look_dir()
			for pointed_thing in core.raycast(epos, epos + (dir * 10), false, false) do
				local pos = pointed_thing.under
				if core.get_meta(pos):get_int("screwdriver:fixed") > 0 then
					corner_particles(pos, player:get_player_name())
				end
			end
		end
	end
end)

function screwdriver.fix(itemstack, player, pointed_thing, uses)
	if fix_handler(itemstack, player, pointed_thing) then
		if not core.is_creative_enabled(player) then
			itemstack:add_wear_by_uses(uses)
		end
	end
	return itemstack
end

core.register_craftitem("screwdriver:screw", {
	description = S("Screw"),
	inventory_image = "screwdriver_screw.png",
	groups = {screw = 1}
})

core.register_craft({
	output = "screwdriver:screw 6",
	recipe = {
		{"default:iron_ingot"},
	}
})

-- Screwdriver
core.register_tool("screwdriver:screwdriver", {
	description = S("Screwdriver"),
	_tt_help = S("Punch pushes edge, Place rotates face") .. "\n" ..
		S("Sneak + Place fixes the node in place (requires screws)") .. "\n" ..
		S("Point at a node to see if it's fixed"),
	inventory_image = "screwdriver.png",
	groups = {screwdriver = 1, tool = 1},
	on_use = function(itemstack, player, pointed_thing)
		return screwdriver.use(itemstack, player, pointed_thing, false, 200)
	end,
	on_place = function(itemstack, player, pointed_thing)
		local ctrl = player:get_player_control()
		if ctrl.sneak then
			return screwdriver.fix(itemstack, player, pointed_thing, 600)
		end
		return screwdriver.use(itemstack, player, pointed_thing, true, 200)
	end,
})

core.register_craft({
	output = "screwdriver:screwdriver",
	recipe = {
		{"default:steel_ingot"},
		{"group:stick"}
	}
})

core.register_alias("screwdriver:screwdriver1", "screwdriver:screwdriver")
core.register_alias("screwdriver:screwdriver2", "screwdriver:screwdriver")
core.register_alias("screwdriver:screwdriver3", "screwdriver:screwdriver")
core.register_alias("screwdriver:screwdriver4", "screwdriver:screwdriver")
