--[[
Research N' Duplication
Copyright (C) 2020 Noodlemire

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
--]]

--This function registers a weapon able to shoot projectiles
function projectile.register_weapon(name, def)
	--either create a groups table for the definition, or use the provided one
	def.groups = def.groups or {}
	--Every projectile weapon belongs to the projectile_weapon group.
	def.groups.projectile_weapon = 1

	--Charge time defaults to 1 second
	def.charge_time = def.charge_time or 1
	--The weapon's damage multiplier defaults to 1.
	def.damage = def.damage or 1
	--The weapon's speed multiplier defaults to 1.
	def.speed = def.speed or 1

	--A function that determines when a weapon can fire. If none is provided in a definition, a weapon can always fire.
	--Note that it does not prevent the need for ammunition.
	def.can_fire = def.can_fire or function() return true end

	--If this weapons has to be charged...
	if def.charge then
		--Define a function to reset the weapon's sprite and delete the player's charge data.
		local uncharge = function(wep, user, cancelled)
			--If nothing was fired and the weapon defines an on_cancel function, call it.
			if cancelled and def.on_cancel then
				def.on_cancel(wep, user)
			end

			if projectile.charge_levels[user:get_player_name()] and projectile.charge_levels[user:get_player_name()].sound then
				minetest.sound_stop(projectile.charge_levels[user:get_player_name()].sound)
			end

			--Delete charge data.
			projectile.charge_levels[user:get_player_name()] = nil

			--Change the name of the stack to transform it back to its uncharged form.
			wep:set_name(name)

			return wep
		end

		--A function that begins a new charge, or fires a shot if the player is charging.
		local charge = function(wep, user)
			--If it is allowed to fire...
			if def.can_fire(wep, user) then
				local pname = user:get_player_name()

				--If there is no charge data yet...
				if not projectile.charge_levels[pname] then
					local inv = user:get_inventory()

					--Look for ammo in the player's inventory, starting from the first slot.
					for i = 1, inv:get_size("main") do
						--Get the itemstack of the current slot.
						local ammo = inv:get_stack("main", i)

						--If the stack is there, and it's registered as ammo that this weapon can use...
						if not ammo:is_empty() and projectile.registered_projectiles[def.rw_category][ammo:get_name()] then
							--Create new charge data. Store the inventory slot of the weapon, and start the charge at 0
							projectile.charge_levels[pname] = {slot = user:get_wield_index(), charge = 0}

							--As feedback for the charge beginning, change the weapon's sprite to show it loaded.
							--I originally wanted the item to be shown loaded with specific ammo, but it doesn't seem to be possible.
							wep = ItemStack({name = name.."_2", wear = wep:get_wear()})

							--If a callback is defined to do something when a charge begins, call it now.
							if def.on_charge_begin then
								def.on_charge_begin(wep, user)
							end

							--Once ammo is found, the search can be stopped.
							break
						end
					end

					--If no ammo was found, a charge won't start at all. No dry-firing allowed.

				--Otherwise, if there is charge data...
				else
					--If a callback is defined to do something right before firing, call it now.
					if def.on_fire then
						def.on_fire(wep, user)
					end

					--Shoot out the projectile
					projectile.shoot(wep, user, projectile.charge_levels[pname].charge)

					--If a callback is defined to do something right after firing, call it now.
					if def.after_fire then
						def.after_fire(wep, user)
					end

					--Then, end the charge
					uncharge(wep, user, false)
				end
			end

			return wep
		end

		--Right-click to start a charge. Right-click again to fire.
		def.on_place = charge
		def.on_secondary_use = charge
		--Left-click to cancel a charge without firing.
		def.on_use = uncharge

		--Start the creating of the partially and fully charged versions of this item, first by copying the definition.
		local def2 = table.copy(def)
		local def3 = table.copy(def)

		--The partially and fully-charged versions have specific inventory images
		def2.inventory_image = def.inventory_image_2
		def3.inventory_image = def.inventory_image_3
		--The projectile_weapon group rating increases with charge level
		def2.groups.projectile_weapon = 2
		def3.groups.projectile_weapon = 3
		--Partially charged weapons cannot be grabbed from the creative inventory.
		def2.groups.not_in_creative_inventory = 1
		def3.groups.not_in_creative_inventory = 1

		--Weapons that need charging can only be fired before fully charging if def.fire_while_charging is set to true.
		if def.charge and not def.fire_while_charging then
			def2.on_place = nil
			def2.on_secondary_use = nil
		end

		--Some versions store the names of different versions, for convenience.
		--The partially-charged version stores the name of the fully charged version, to be used when transitioning to the fully charged version.
		def2.full_charge_name = name.."_3"
		--Full and partial charge versions can both be cancelled, so they remember the name of the uncharged version
		def2.no_charge_name = name
		def3.no_charge_name = name

		--Finally, register the partially and fully charged projectile weapons.
		minetest.register_tool(name.."_2", def2)
		minetest.register_tool(name.."_3", def3)
	else
		--Otherwise, right-click simply shoots the projectile.
		def.on_place = function(wep, user)
			--If it is allowed to fire...
			if def.can_fire(wep, user) then
				--Shoot every time on_place is called.
				projectile.shoot(wep, user)
			end
		end

		def.on_secondary_use = def.on_place
	end

	--Finally, register the projectile weapon here.
	--This is the only thing that happens regardless of if the weapon has to charge or not.
	minetest.register_tool(name, def)
end

--Register a projectile that can be fired by a weapon.
--Note that it also has to define what kind of weapon can fire it, and the item version of itself.
function projectile.register_projectile(name, usable_by, ammo, def)
	--First, check that a table exists for that particular weapon category. If not, make it.
	projectile.registered_projectiles[usable_by] = projectile.registered_projectiles[usable_by] or {}
	--Then, add this projectile to said table.
	projectile.registered_projectiles[usable_by][ammo] = name

	--Default initial properties for the projectile
	--Including the table itself, if it wasn't already created.
	def.initial_properties = def.initial_properties or {}
	--The projectile is always physical. It has to hit stuff, after all.
	def.initial_properties.physical = true
	--The projectile also definitely has to be able to hit other entities.
	def.initial_properties.collide_with_objects = true
	--By default, the projectile's hitbox is half a block in size.
	def.initial_properties.collisionbox = def.initial_properties.collisionbox or  {-.25, 0, -.25, .25, .5, .25}
	--The projectile can't be hit by players.
	def.initial_properties.pointable = false
	--By default, the projectile is a flat image, provided by the "image" field.
	def.initial_properties.visual = def.initial_properties.visual or "sprite"
	def.initial_properties.textures = def.initial_properties.textures or {def.image}
	--By default, the projectile's visual size is also half size.
	def.initial_properties.visual_size = def.initial_properties.visual_size or {x = 0.5, y = 0.5, z = 0.5}
	--The projectile won't be saved if it becomes unloaded.
	def.initial_properties.static_save = false

	--collide_self allows projectiles fired by the same person to strike each other. This is true by default.
	if def.collide_self == nil then
		def.collide_self = true
	end

	--"visible" can be used as a shorthand to set itself in initial_properties.
	if def.visible == false then
		def.initial_properties.is_visible = false
	end

	--During each of this entity's steps...
	def.on_step = function(self, dtime, info)
		--Let projectiles define their own on_step if they need to
		if self._on_step then
			self._on_step(self, dtime, info)
		end

		--A little shorthand
		local selfo = self.object
		--By default, assume nothing was hit this step.
		local hit = false

		--selfo:get_pos() is used to know if remove() was called in the _on_step function.
		if not selfo:get_pos() then
			return
		end

		--For each collision that was found...
		for k, c in pairs(info.collisions) do
			--If it's a node, don't do anything more than acknowledging that something was hit.
			if c.type == "node" then
				hit = true

				--Get the definition of the node that was hit...
				local ndef = minetest.registered_nodes[minetest.get_node(c.node_pos).name]
				--If the definition exists and defines a sound for being dug...
				if ndef and ndef.sounds and ndef.sounds.dug then
					--Play that sound
					minetest.sound_play(ndef.sounds.dug, {gain = 1.0, pos = c.node_pos}, true)
				end

			--If it's an object...
			else
				--As long as that object isn't the player who fired this projectile and the target isn't already dead...
				--And the target isn't a projectile owned by the same player when collide_self is disabled...
				--And the target isn't in the same party as this projectile's owner...
				if not (c.object:is_player() and self.owner == c.object:get_player_name()) and c.object:get_hp() > 0
						and not (self.collide_self == false and not c.object:is_player() and self.owner == c.object:get_luaentity().owner) 
						and not projectile.in_same_party(self, c.object) then
					--Acknowledge the hit
					hit = true
					--Deal damage to the target.
					c.object:punch(selfo, 1, {full_punch_interval = 1, damage_groups = {fleshy = def.damage * self.level * self.damage}}, vector.normalize(selfo:get_velocity()))
				else
					--Otherwise, pass by the object as best as possible. 
					selfo:set_velocity(self.oldvel)
				end
			end
		end

		--If this projectile hit something...
		if hit then
			--Grant the entity an on_impact function that it can define
			if self.on_impact then
				self:on_impact(info.collisions)
			end

			if node_damage and minetest.settings:get_bool("placeable_impacts_damage_nodes") then
				for _, c in pairs(info.collisions) do
					if c.type == "node" then
						node_damage.damage(c.node_pos)
						break
					end
				end
			end

			--Make the projectile destroy itself.
			selfo:remove()
		end
	end

	--Finally, register the entity.
	minetest.register_entity(name, def)
end

--A function that creates and launches a function out of a player's side when they use a projectile weapon.
function projectile.shoot(wep, user, level)
	--Some useful shorthands
	local pname = user:get_player_name()
	local inv = user:get_inventory()
	local def = wep:get_definition()

	--A projectile isn't spawned directly inside a player, and it doesn't come from the center of the screen.
	--It does start directly in front of the player...
	local pos = user:get_look_dir()
	--But then it's shifted to the right of the player, where it looks like the weapon is held.
	pos = vector.rotate(pos, {x=0 , y = -math.pi / 4, z=0})
	--Then it's shifted up by the player's face.
	pos.y = 1
	--The user's actual position is added last, to make rotating easier.
	pos = vector.add(pos, user:get_pos())

	--Charge level depends on how long the player waited before firing. 1 = 100% charge.
	level = math.min(level / def.charge_time, 1)

	--Look through each inventory slot...
	for i = 1, inv:get_size("main") do
		--Get the stack itself
		local ammo = inv:get_stack("main", i)
		--Get the name of the entity that will be created by this ammo type.
		local ammo_entity = projectile.registered_projectiles[def.rw_category][ammo:get_name()]

		--If there is an item stack, and it's registered as an ammo type that this weapon can use...
		if not ammo:is_empty() and ammo_entity then
			local adef = minetest.registered_entities[ammo_entity]

			--Fire an amount of projectiles at once according to the ammo's defined "count".
			for n = 1, (adef.count or 1) do
				--Create the projectile entity at the determined position
				local projectile = minetest.add_entity(pos, ammo_entity)
				--A shorthand of the luaentity version of the projectile, where data can easily be stored
				local luapro = projectile:get_luaentity()

				--Set velocity according to the direction it was fired. Speed is determined by the weapon, ammo, and how long the weapon was charged.
				projectile:set_velocity(vector.multiply(user:get_look_dir(), luapro.speed * level * def.speed))
				--An acceleration of -9.81y is how gravity is applied.
				projectile:set_acceleration({x=0, y=-9.81, z=0})

				--If the ammo defines a spread, randomly rotate the direction of velocity by that given radius.
				if adef.spread then
					local rx = (math.random() * adef.spread * 2 - adef.spread) * math.pi / 180
					local ry = (math.random() * adef.spread * 2 - adef.spread) * math.pi / 180

					projectile:set_velocity(vector.rotate(projectile:get_velocity(), {x = rx, y = ry, z = 0}))
				end

				--Store level for later, to determine impact damage
				luapro.level = level
				--Also store the projectile's damage itself.
				luapro.damage = def.damage
				--The player's name is stored to prevent hitting yourself
				--And by "hitting yourself" I mean accidentally being hit by the arrow just by firing it at a somewhat low angle, the moment it spawns.
				luapro.owner = pname
				--Store the initial velocity for passing by objects when needed.
				luapro.oldvel = projectile:get_velocity()
			end

			--If the player isn't in creative mode, some weapon durability and ammo is consumed.
			if not minetest.is_creative_enabled(pname) then
				ammo:take_item(1)
				inv:set_stack("main", i, ammo)

				wep:add_wear(65536 / (def.durability or 100))
			end

			--Once the ammo is found, the search is stopped.
			break
		end
	end

	return wep
end

--A helper function to know when the party of a projectile's owner and target is the same.
function projectile.in_same_party(projectile, target)
	--Automatically return false if:
	--The parties mod isn't included.
	--The target isn't a player.
	--The projectile's target and/or owner isn't in a party/
	if not parties or not target:is_player() or not parties.is_player_in_party(projectile.owner) or not parties.is_player_in_party(target:get_player_name()) then 
		return false 
	end

	--Return true only if the projectile's owner and target and under the same party leadership.
	return parties.get_party_leader(projectile.owner) == parties.get_party_leader(target:get_player_name())
end

--A helper functions for arrows in general, as they rotate themselves according to how they move.
function projectile.autorotate_arrow(self)
	--Shorthand for velocity
	local v = self.object:get_velocity()
	--Set calculate rotation according to velocity
	local rot = vector.dir_to_rotation(v)

	--Define a timer for itself. Based on how fast its currently moving, and how far the timer has progressed,
	--this makes it seem to spin through the air, with the tip still always pointing forward.
	self.timer = (self.timer or 0) + (v.x + v.y + v.z) / 30
	rot.z = rot.z + self.timer

	--Apply the calculated rotation.
	self.object:set_rotation(rot)
end

--A can_fire function for flintlock weapons that enforces a gunpowder requirement, in addition to the usual ammo needs
function projectile.needs_gunpowder(wep, user)
	--Automatically return true if creative mode is enabled or the weapon is already firing.
	if minetest.is_creative_enabled(user:get_player_name()) or projectile.charge_levels[user:get_player_name()] then
		return true
	end

	--Shorthand to get the user's inventory
	local inv = user:get_inventory()

	--For each slot in the user's inventory...
	for i = 1, inv:get_size("main") do
		--Get the current stack at this index.
		local stack = inv:get_stack("main", i)

		--If this stack contains gunpowder...
		if stack:get_name() == "tnt:gunpowder" then
			--Consume some.
			stack:take_item()
			--Update the inventory.
			inv:set_stack("main", i, stack)

			--Allow firing.
			return true
		end
	end

	--If no gunpowder could be consumed, disallow firing.
	return false
end

--A flintlock's can_fire function consumes gunpowder before letting the weapon charge.
--So, if that charge was cancelled, gunpowder should be returned.
function projectile.return_gunpowder(wep, user)
	--In creative mode, nothing gets taken, so nothing needs to be returned.
	if not minetest.is_creative_enabled(user:get_player_name()) then
		--Try adding the gunpowder back into the inventory. Store the leftover stack.
		local leftover = user:get_inventory():add_item("main", ItemStack({name = "tnt:gunpowder"}))

		--If the gunpowder couldn't be added due to the inventory being full...
		if not leftover:is_empty() then
			--Drop it on the ground instead.
			minetest.add_item(user:get_pos(), leftover)
		end
	end
end