-----------
-- Lasso --
-----------

local abs = math.abs
local asin = math.asin
local atan2 = math.atan2
local cos = math.cos
local deg = math.deg
local sin = math.sin

local function diff(a, b) -- Get difference between 2 angles
	return atan2(sin(b - a), cos(b - a))
end

local vec_add, vec_dir, vec_dist, vec_len = vector.add, vector.direction, vector.distance, vector.length
local dir2yaw, dir2rot = minetest.dir_to_yaw, vector.dir_to_rotation

-- Entities --

local using_lasso = {}

minetest.register_entity("animalia:lasso_entity", {
	visual = "mesh",
	mesh = "animalia_lasso_entity.b3d",
	textures = {"animalia_lasso_entity.png"},
	pointable = false,
	on_activate = function(self)
		self.object:set_armor_groups({immortal = 1})
	end,
	_scale = 1,
	on_step = function(self, dtime)
		local pos, parent = self.object:get_pos(), (self.object:get_attach() or self._attached)
		local pointed_ent = self._point_to and self._point_to:get_luaentity()
		local point_to = self._point_to and self._point_to:get_pos()
		if not pos or not parent or not point_to then self.object:remove() return end
		if type(parent) == "string" then
			parent = minetest.get_player_by_name(parent)
			local tgt_pos = parent:get_pos()
			local dist = vec_dist(pos, tgt_pos)
			if dist > 0.5 then
				self.object:set_pos(tgt_pos)
			else
				self.object:move_to(tgt_pos)
			end
			self.object:set_rotation(dir2rot(vec_dir(tgt_pos, point_to)))
		elseif parent.x then
			point_to.y = point_to.y + pointed_ent.height * 0.5
			self.object:set_rotation(dir2rot(vec_dir(pos, point_to)))
		else
			self.object:remove()
			return
		end
		local size = vec_dist(pos, point_to)
		if abs(size - self._scale) > 0.1 then
			self.object:set_properties({
				visual_size = {x = 1, y = 1, z = size}
			})
			self._scale = size
		end
	end
})

local function remove_from_fence(self)
	local pos = self.object:get_pos()
	local mob = self._mob and self._mob:get_luaentity()
	if not mob then
		self.object:remove()
		return
	end
	mob._lassod_to = nil
	mob:forget("_lassod_to")
	mob._lasso_ent:remove()
	local dirs = {
		{x = 0.5, y = 0, z = 0},
		{x = -0.5, y = 0, z = 0},
		{x = 0, y = 0.5, z = 0},
		{x = 0, y = -0.5, z = 0},
		{x = 0, y = 0, z = 0.5},
		{x = 0, y = 0, z = -0.5}
	}
	for i = 1, 6 do
		local i_pos = vec_add(pos, dirs[i])
		if not creatura.get_node_def(i_pos).walkable then
			minetest.add_item(i_pos, "animalia:lasso")
			break
		end
	end
	self.object:remove()
end

minetest.register_entity("animalia:tied_lasso_entity", {
	collisionbox = {-0.25,-0.25,-0.25, 0.25,0.25,0.25},
	visual = "cube",
	visual_size = {x = 0.3, y = 0.3},
	mesh = "model",
	textures = {
		"animalia_tied_lasso_entity.png",
		"animalia_tied_lasso_entity.png",
		"animalia_tied_lasso_entity.png",
		"animalia_tied_lasso_entity.png",
		"animalia_tied_lasso_entity.png",
		"animalia_tied_lasso_entity.png",
	},
	on_activate = function(self)
		self.object:set_armor_groups({immortal = 1})
	end,
	on_step = function(self)
		local mob = self._mob and self._mob:get_luaentity()
		if not mob then remove_from_fence(self) return end
	end,
	on_rightclick = remove_from_fence,
	on_punch = remove_from_fence
})

-- API --

local function add_lasso(self, origin)
	local pos = self.object:get_pos()
	if not pos then return end
	local object = minetest.add_entity(pos, "animalia:lasso_entity")
	local ent = object and object:get_luaentity()
	if not ent then return end
	-- Attachment point of entity
	ent._attached = origin
	if type(origin) == "string" then
		local player = minetest.get_player_by_name(origin)
		--object:set_attach(player)
	else
		object:set_pos(origin)
	end
	self._lassod_to = origin
	ent._point_to = self.object
	self:memorize("_lassod_to", origin)
	return object
end

local function get_rope_velocity(pos1, pos2, dist)
	local force = dist / 10
	local vel = vector.new((pos2.x - pos1.x) * force, ((pos2.y - pos1.y) / (24 + dist)), (pos2.z - pos1.z) * force)
	return vel
end

function animalia.initialize_lasso(self)
	self._lassod_to = self:recall("_lassod_to") or self:recall("lasso_origin")
	if self._lassod_to then
		local origin = self._lassod_to
		if type(origin) == "table"
		and minetest.get_item_group(minetest.get_node(origin).name, "fence") > 0 then
			local object = minetest.add_entity(origin, "animalia:tied_lasso_entity")
			object:get_luaentity()._mob = self.object
			self._lasso_ent = add_lasso(self, origin)
		elseif type(origin) == "string" then
			self._lassod_to = origin
			self._lasso_ent = add_lasso(self, origin)
		else
			self:forget("_lassod_to")
		end
	end
end

function animalia.update_lasso_effects(self)
	local pos = self.object:get_pos()
	if not creatura.is_alive(self) then return end
	if self._lassod_to then
		local lasso = self._lassod_to
		self._lasso_ent = self._lasso_ent or add_lasso(self, lasso)
		if type(lasso) == "string" then
			using_lasso[lasso] = self
			local name = lasso
			lasso = minetest.get_player_by_name(lasso)
			if lasso:get_wielded_item():get_name() ~= "animalia:lasso" then
				using_lasso[name] = nil
				self._lasso_ent:remove()
				self._lasso_ent = nil
				self._lassod_to = nil
				self:forget("_lassod_to")
				return
			end
			local lasso_pos = lasso:get_pos()
			local dist = vec_dist(pos, lasso_pos)
			local vel = self.object:get_velocity()
			if not vel or dist < 8 and self.touching_ground then return end
			if vec_len(vel) < 8 then
				self.object:add_velocity(get_rope_velocity(pos, lasso_pos, dist))
			end
			return
		elseif type(lasso) == "table" then
			local dist = vec_dist(pos, lasso)
			local vel = self.object:get_velocity()
			if not vel or dist < 8 and self.touching_ground then return end
			if vec_len(vel) < 8 then
				self.object:add_velocity(get_rope_velocity(pos, lasso, dist))
			end
			return
		end
	end
	if self._lasso_ent then
		self._lasso_ent:remove()
		self._lasso_ent = nil
		self._lassod_to = nil
		self:forget("_lassod_to")
	end
end

-- Item

minetest.register_craftitem("animalia:lasso", {
	description = "Lasso",
	inventory_image = "animalia_lasso.png",
	on_secondary_use = function(_, placer, pointed)
		local ent = pointed.ref and pointed.ref:get_luaentity()
		if ent
		and (ent.name:match("^animalia:")
		or ent.name:match("^monstrum:")) then
			if not ent.catch_with_lasso then return end
			local name = placer:get_player_name()
			if not ent._lassod_to
			and not using_lasso[name] then
				using_lasso[name] = ent
				ent._lassod_to = name
				ent:memorize("_lassod_to", name)
			elseif ent._lassod_to
			and ent._lassod_to == name then
				using_lasso[name] = nil
				ent._lassod_to = nil
				ent:forget("_lassod_to")
			end
		end
	end,
	on_place = function(itemstack, placer, pointed_thing)
		if pointed_thing.type == "node" then
			local pos = minetest.get_pointed_thing_position(pointed_thing)
			if minetest.get_item_group(minetest.get_node(pos).name, "fence") > 0 then
				local name = placer:get_player_name()
				local ent = using_lasso[name]
				if ent
				and ent._lassod_to
				and ent._lassod_to == name then
					using_lasso[name] = nil
					ent._lasso_ent:set_detach()
					ent._lasso_ent:set_pos(pos)
					ent._lasso_ent:get_luaentity()._attached = pos
					ent._lassod_to = pos
					ent:memorize("_lassod_to", pos)
					local fence_obj = minetest.add_entity(pos, "animalia:tied_lasso_entity")
					fence_obj:get_luaentity()._mob = ent.object
					fence_obj:get_luaentity()._lasso_obj = ent._lasso_ent
					itemstack:take_item(1)
				end
			end
		end
		return itemstack
	end
})