-- these tables are getting a little ridiculous...
local player_selections = {}
local pull_cooldowns = {}
local sticky_pulling = {}
local player_hover_distances = {}
local player_selections_last_pos = {}
local player_selections_velocities = {}
local player_gravity_gun_entities = {}
local player_last_held_item = {}

local block_air_drag = 1
local block_ground_drag = 5

local block_gravity = -1
local sticky_pull_time = 4

local drop_power_multiplier = 0.2

local pull_cooldown = 20
local pull_distance = 25
local pull_multiplier = 10

local hover_distance = 4
local max_hover_distance = 5
local min_hover_distance = 1.5
local hover_increment = 0.075

local launch_power = 40
local grab_distance = 6

core.register_on_shutdown(function(player)
	-- player_selections[player] = {}
end)

core.register_on_leaveplayer(function(player, timed_out)
	local selection = player_selections[player]
	if selection then
		unselect_object(player, selection.obj)
	end
end)

local function rad_to_deg(rad)
	return (rad / math.pi) * 180
end

local function process_selection(player, obj, dtime)
	dtime = dtime or 1
	local player_rotation = player:get_look_vertical()
	local player_look = player:get_look_dir()
	local player_pos = vector.offset(player:get_pos(), 0, 1.5, 0)
	local pos = vector.add(player_pos, vector.multiply(player_look, player_hover_distances[player]))
	local controls = player:get_player_control()

	if controls.sneak and controls.RMB then
		player_hover_distances[player] = math.max(player_hover_distances[player] - hover_increment, min_hover_distance)
	end

	if controls.sneak and controls.LMB then
		player_hover_distances[player] = math.min(player_hover_distances[player] + hover_increment, max_hover_distance)
	end

	local raycast = core.raycast(player_pos, pos, false, false)
	local next = raycast:next()
	local prev_next
	while next do
		prev_next = next
		next = raycast:next()
	end

	local hover_offset
	if prev_next then
		hover_offset = vector.distance(player_pos, vector.subtract(prev_next.intersection_point, player_look))
	else
		hover_offset = player_hover_distances[player]
	end

	local new_pos = vector.add(player_pos, vector.multiply(player_look, hover_offset))
	if not player_selections_last_pos[player] then
		-- on first selection step
		player_selections_last_pos[player] = {now = new_pos, prev = nil}
	else
		-- all the other steps
		local last_pos = player_selections_last_pos[player].now
		if not vector.equals(last_pos, new_pos) then
			player_selections_last_pos[player] = {now = new_pos, prev = last_pos}
			player_selections_velocities[player] = vector.subtract(player_selections_last_pos[player].now, player_selections_last_pos[player].prev) / dtime
		end
	end

	obj:set_attach(
		player,
		"",
		{x = 0, y = hover_offset * 10 * math.sin(-player_rotation) + 15, z = hover_offset * 10 * math.cos(-player_rotation)},
		{x = rad_to_deg(player_rotation), y = 0, z = 0},
		true
	)
end

local function select_object(player, obj)
	local selection = player_selections[player]
	if selection and selection.obj and not selection.obj:get_pos() then
		if selection.obj and not selection.obj:get_pos() then
			player_selections[player] = nil
		else
			return
		end
	end
	player_selections[player] = {
		obj = obj,
	}
	player_hover_distances[player] = hover_distance

	process_selection(player, obj)
end

local function unselect_object(player, obj)
	pull_cooldowns[player] = pull_cooldown
	obj:set_detach()

	local player_look = player:get_look_dir()
	local player_pos = vector.offset(player:get_pos(), 0, 1.5, 0)
	local pos = vector.add(player_pos, vector.multiply(player_look, player_hover_distances[player]))
	obj:set_pos(pos)

	if player_selections_velocities[player] then
		obj:add_velocity(player_selections_velocities[player] * drop_power_multiplier)
		player_selections_velocities[player] = nil
	end


	player_selections[player] = nil
end

local function pull_object(player, obj, no_sticky)
	if not pull_cooldowns[player] or pull_cooldowns[player] < 0 then
		local player_pos = vector.offset(player:get_pos(), 0, 1.5, 0)
		local player_look = player:get_look_dir()
		local distance = vector.distance(obj:get_pos(), player_pos)
		if distance <= grab_distance then
			select_object(player, obj)
			return
		end
		obj:add_velocity(vector.multiply(player_look, -1 * (pull_multiplier / (distance / 2))))

		if not no_sticky then
			sticky_pulling[player] = {time = sticky_pull_time, obj = obj}
		end
	end
end

local function is_node_solid(def)
	return def.walkable
end

local function make_block_entity(pos)
	local node = core.get_node(pos)
	core.set_node(pos, {name = "air"})

	local obj = core.add_entity(pos, "gravity_gun_reloaded:block")

	if not obj then
		return
	end

	obj:set_properties({node = node})

	return obj
end

core.register_globalstep(function(dtime)
	for _, player in pairs(core.get_connected_players()) do
		local wielded_item_name = player:get_wielded_item():get_name()
		local wielded_item_def = core.registered_nodes[wielded_item_name]
		local selection = player_selections[player]
		local is_pulling = false
		if wielded_item_def and wielded_item_def._gravity_gun_reloaded_entity and (player_last_held_item[player] == nil or player_last_held_item[player] == wielded_item_name) then

			player_last_held_item[player] = wielded_item_name
			if not player_gravity_gun_entities[player] then
				local gravity_gun_model = core.add_entity(vector.new(0, 0, 0), wielded_item_def._gravity_gun_reloaded_entity)
				player_gravity_gun_entities[player] = gravity_gun_model
			end

			local eye_level = player:get_properties().eye_height
			local gun_model = player_gravity_gun_entities[player]
			local player_look_vertical = player:get_look_vertical()
			local gun_angle = (-math.pi / 4) - player_look_vertical -- offset by 45 degrees
			gun_model:set_attach(
				player,
				"",
				{x = 6, y = (eye_level * 10) - 3 + math.sin(gun_angle) * 3, z = 3 + math.cos(gun_angle) * 3},
				{x = rad_to_deg(player_look_vertical * 0.9), y = 0, z = 0},
				true)

			if selection and selection.obj then
				process_selection(player, selection.obj, dtime)
			else
				local controls = player:get_player_control()
				if controls.RMB then
					local player_pos = vector.offset(player:get_pos(), 0, 1.5, 0)
					local player_look = player:get_look_dir()

					local raycast = core.raycast(player_pos, vector.add(player_pos, vector.multiply(player_look, pull_distance)), true)
					local first_result
					for pointed_thing in raycast do
						if pointed_thing.type == "object" and not pointed_thing.ref:is_player() then
							first_result = pointed_thing
							break
						elseif pointed_thing.type == "node" and is_node_solid(core.registered_nodes[core.get_node(pointed_thing.under).name]) then
							break
						end
					end

					if first_result then
						pull_object(player, first_result.ref)
						is_pulling = true
					end
				end
			end
		else
			if player_gravity_gun_entities[player] then
				player_gravity_gun_entities[player]:remove()
				player_gravity_gun_entities[player] = nil
			end

			player_last_held_item[player] = nil

			if selection and selection.obj then
				unselect_object(player, selection.obj)
			end
		end

		if not is_pulling and sticky_pulling[player] then
			pull_object(player, sticky_pulling[player].obj, true)
			sticky_pulling[player].time = sticky_pulling[player].time - 1

			if sticky_pulling[player].time == 0 then
				sticky_pulling[player] = nil
			end
		end

		if pull_cooldowns[player] then
			pull_cooldowns[player] = pull_cooldowns[player] - 1
		end
	end
end)

core.register_node("gravity_gun_reloaded:uber_gravity_gun",
{
	description = "Uber gravity gun",
	drawtype = "mesh",
	mesh = "uber-gravity-gun.glb",
	tiles = {"UBER.png", "UBER.png", "UBER.png", "UBER.png", "UBER.png", "UBER.png"},
	stack_max = 1,
	node_placement_prediction = "",
	range = grab_distance,
	wield_image = "blank.png",
	_gravity_gun_reloaded_entity = "gravity_gun_reloaded:uber_gravity_gun_model",
	on_secondary_use = function(itemstack, player, pointed_thing)
		local controls = player:get_player_control()
		local selection = player_selections[player]

		if selection then
			if not controls.sneak then
				if selection and selection.obj then
					unselect_object(player, selection.obj)
				else
					select_object(player, pointed_thing.ref)
				end
			end
		end
	end,
	on_use = function(itemstack, player, pointed_thing)
		local selection = player_selections[player]
		local controls = player:get_player_control()
		local player_look = player:get_look_dir()

		local function push_away()
			if pointed_thing.type == "object" then
				pointed_thing.ref:set_velocity(player_look * launch_power)
			elseif pointed_thing.type == "node" then
				-- local success, obj = core.spawn_falling_node(pointed_thing.under)
				local obj = make_block_entity(pointed_thing.under)
				obj:set_velocity(player_look * launch_power)
			end
		end

		if selection and selection.obj then
			if not controls.sneak then
				unselect_object(player, selection.obj)
				selection.obj:set_velocity(player_look * launch_power)
			end
		else
			if controls.sneak then
				-- to do: flip the controls
			else
				push_away()
			end
		end
	end,
	on_place = function(itemstack, player, pointed_thing)
		if pointed_thing.type == "node" and is_node_solid(core.registered_nodes[core.get_node(pointed_thing.under).name]) then
			-- local success, obj = core.spawn_falling_node(pointed_thing.under)
			local obj = make_block_entity(pointed_thing.under)
			select_object(player, obj)
		end
	end,
})

core.register_node("gravity_gun_reloaded:gravity_gun",
{
	description = "Gravity gun",
	drawtype = "mesh",
	mesh = "regular-gravity-gun.glb",
	tiles = {"REGULAR.png", "REGULAR.png", "REGULAR.png", "REGULAR.png", "REGULAR.png", "REGULAR.png"},
	stack_max = 1,
	wield_image = "blank.png",
	node_placement_prediction = "",
	range = grab_distance,
	_gravity_gun_reloaded_entity = "gravity_gun_reloaded:gravity_gun_model",
	on_secondary_use = function(itemstack, player, pointed_thing)
		local controls = player:get_player_control()
		local selection = player_selections[player]

		if selection then
			if not controls.sneak then
				if selection and selection.obj then
					unselect_object(player, selection.obj)
				else
					select_object(player, pointed_thing.ref)
				end
			end
		end
	end,
	on_use = function(itemstack, player, pointed_thing)
		local selection = player_selections[player]
		local controls = player:get_player_control()
		local player_look = player:get_look_dir()

		local function push_away()
			if pointed_thing.type == "object" then
				pointed_thing.ref:set_velocity(player_look * launch_power)
			end
		end

		if selection and selection.obj then
			if not controls.sneak then
				unselect_object(player, selection.obj)
				selection.obj:set_velocity(player_look * launch_power)
			end
		else
			if controls.sneak then
				-- to do: flip the controls
			else
				push_away()
			end
		end
	end,
	on_place = function()
		-- this is so stupid
	end
})

local function get_block_hardness(def)
	return def._mcl_hardness or 2
end

local function hit_object(vel, block_entity, obj)
	local entity = obj:get_luaentity()
	if mcl_core and (not entity or entity.name ~= "gravity_gun_reloaded:block") then
		local hardness = get_block_hardness(core.registered_nodes[block_entity:get_properties().node.name])
		mcl_util.deal_damage(obj, vector.length(vel) * hardness, { type = "generic", direct = block_entity })

		if hardness <= 0.5 then
			block_entity:remove()
		end
	else
		obj:punch(obj, 9999, {damage_groups = {fleshy = 4 * vector.length(vel) / 5}}, vector.normalize(vel))
	end
end

local _5_degrees = 5 / 180 * math.pi
local _85_degrees = 5 / 180 * math.pi

core.register_entity("gravity_gun_reloaded:block", {
	initial_properties = {
		hp_max = 99999,
		physical = true,
		visual = "node",
		node = {name = "air"},
		static_save = true,
	},
	_selected = true,
	on_activate = function(self, staticdata, dtime_s)
		local node = core.deserialize(staticdata)
		self.object:set_properties({node = node})
		self.object:set_armor_groups({fleshy = 100})
	end,
	get_staticdata = function(self)
		return core.serialize(self.object:get_properties().node)
	end,
	on_detach = function(self, parent)
		if parent:is_player() then
			self.object:set_yaw(parent:get_look_horizontal())
		end
	end,
	on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
		self.object:add_velocity(dir * math.max(damage, 1))
	end,
	on_step = function(self, dtime, moveresult)
		local obj = self.object
		local nodename = obj:get_properties().node.name

		if obj:get_attach() then
			return
		end

		local velocity = vector.multiply(
			vector.offset(obj:get_velocity(), 0, block_gravity, 0),
			1 - (block_air_drag * dtime * (moveresult.touching_ground and block_ground_drag / (core.get_item_group(nodename, "slippery") + 1 or 1) or 1))
		)
		obj:set_velocity(velocity)

		if moveresult.collides then
			for _, v in pairs(moveresult.collisions) do
				if v.type == "object" then
					hit_object(v.old_velocity, self.object, v.object)
				end
			end
		end

		if moveresult.touching_ground and vector.length(velocity) < 0.2 then
			local rounded = vector.round(obj:get_pos())
			local distance_from_rounded = vector.distance(rounded, obj:get_pos())
			local yaw = obj:get_yaw() % (math.pi / 2)
			if distance_from_rounded < 0.25 and (yaw < _5_degrees or yaw > _85_degrees) then
				core.set_node(rounded, obj:get_properties().node)
				obj:remove()
			end
		end
	end
})

core.register_entity("gravity_gun_reloaded:gravity_gun_model", {
	initial_properties = {
		physical = false,
		visual = "mesh",
		mesh = "regular-gravity-gun.glb",
		backface_culling = false,
		collisionbox = {0, 0, 0, 0, 0, 0},
		pointable = false,
		textures = {"REGULAR.png"},
		static_save = false,
	},
	on_step = function(self, dtime, moveresult)
		if not self.object:get_attach() then self.object:remove() end
	end
})

core.register_entity("gravity_gun_reloaded:uber_gravity_gun_model", {
	initial_properties = {
		physical = false,
		visual = "mesh",
		mesh = "uber-gravity-gun.glb",
		backface_culling = false,
		collisionbox = {0, 0, 0, 0, 0, 0},
		pointable = false,
		textures = {"UBER.png"},
		static_save = false,
	},
	on_step = function(self, dtime, moveresult)
		if not self.object:get_attach() then self.object:remove() end
	end
})
