defense.mobs = {}
local mobs = defense.mobs
mobs.gravity = -9.81
mobs.default_prototype = {
	-- minetest properties & defaults
	physical = true,
	collide_with_objects = true,
	makes_footstep_sound = true,
	visual = "mesh",
	automatic_face_movement_dir = true,
	stepheight = 0.6,
	-- custom properties
	id = 0, -- Automatically set
	smart_path = true, -- Use the pathfinder
	--for switching pathfinding on and off depending on line of line_of_sight
	--if negative (default) it's always on, if >= 0 it'll switch
	smart_path_for = -1,
	mass = 1,
	movement = "ground", -- "ground"/"air"
	move_speed = 1,
	jump_height = 1,
	armor = 0,
	attack_range = 1,
	attack_damage = 1,
	attack_interval = 1,

	current_animation = nil,
	current_animation_end = 0,
	destination = nil, -- position
	last_attack_time = 0,
	life_timer = 75,
	pause_timer = 0,
	timer = 0,

	-- cache
	cache_is_standing = nil,
	cache_find_nearest_player = nil,
}

local reg_nodes = minetest.registered_nodes

local function vec_zero() return {x=0, y=0, z=0} end

local function dump_table(tab)
	minetest.chat_send_all("{")
	for i, o in pairs(tab)
	do
		local to = type(o)
		if to == "string" or to == "number"
		then
			minetest.chat_send_all(""..i..": "..o)
		elseif to == "table"
		then
			minetest.chat_send_all(""..i..": ")
			dump_table(o)
		elseif to == "boolean"
		then
			if o
			then
				o = "true"
			else
				o = "false"
			end
			minetest.chat_send_all("".. i ..": "..o)
		end
	end
	minetest.chat_send_all("}")
end

function mobs.default_prototype:on_rightclick()
	--minetest.chat_send_all(""..self.smart_path or 4)
	--dump_table(self.smart_dest or {})
	--dump_table(self)
end


function mobs.default_prototype:on_activate(staticdata)
	self.object:set_armor_groups({fleshy = 100 - self.armor})
	if self.movement ~= "air" then
		self.object:setacceleration({x=0, y=mobs.gravity, z=0})
	end
	self.id = math.random(0, 100000)
end

function mobs.default_prototype:on_step(dtime)
	self.cache_is_standing = nil
	self.cache_find_nearest_player = nil
	
	
	
	
	if self.pause_timer <= 0 then
		if self.destination then
			self:move(dtime, self.destination)
			if vector.distance(self.object:get_pos(), self.destination) < 0.5 then
				self.destination = nil
			end
		else
			self:move(dtime, self.object:get_pos())
		end
	else
		self.pause_timer = self.pause_timer - dtime
	end

	if self.movement ~= "air" and not self:is_standing() then
		self:set_animation("fall", {"jump", "attack", "move_attack"})
	end

	-- Die when morning comes
	if not defense:is_dark() then
		local damage = self.object:get_hp() * math.random()
		if damage >= 0.5 then
			self:damage(math.ceil(damage))
		end
	end

	-- Remove when far enough and may not reach the player at all
	local nearest = self:find_nearest_player()
	if self.life_timer <= 0 then
		if nearest.distance > 12 then
			self.object:remove()
		end
	else
		self.life_timer = self.life_timer - dtime
	end

	-- Disable collision when far enough
	if self.collide_with_objects then
		if nearest.distance > 6 then
			self.collide_with_objects = false
			self.object:set_properties({collide_with_objects = self.collide_with_objects})
		end
	else
		if nearest.distance < 1.5 then
			self.collide_with_objects = true
			self.object:set_properties({collide_with_objects = self.collide_with_objects})
		end
	end

	self.timer = self.timer + dtime
end

function mobs.default_prototype:on_punch(puncher, time_from_last_punch, tool_capabilities, dir)
	-- Weapon wear code adapted from TenPlus1's mobs redo (https://github.com/tenplus1/mobs)
	if puncher then
		local weapon = puncher:get_wielded_item()
		if tool_capabilities then
			local wear = (0.01) * (self.armor / 100) * 65534 + 1
			weapon:add_wear(wear)
			puncher:set_wielded_item(weapon)
		end
	end

	dir.y = dir.y + 1
	local m = self.mass or 1
	local knockback = vector.multiply(vector.normalize(dir), 10 / (1 + m))
	self.object:setvelocity(vector.add(self.object:getvelocity(), knockback))
	self.pause_timer = 0.3

	if self.object:get_hp() <= 0 then
		self:die()
	end
end

function mobs.default_prototype:damage(amount)
	if self.object:get_hp() <= amount then
		self:die()
	else
		self.object:set_hp(self.object:get_hp() - amount)
	end
end

function mobs.default_prototype:attack(obj, dir)
	obj:punch(self.object, self.timer - self.last_attack_time,  {
		full_punch_interval=self.attack_interval,
		damage_groups = {fleshy=self.attack_damage}
	}, dir)
end

function mobs.default_prototype:move(dtime, destination)
	mobs.move_method[self.movement](self, dtime, destination)
end

-- Attack the player
function mobs.default_prototype:hunt()
	local nearest = self:find_nearest_player()

	if nearest.player then
		if nearest.distance <= self.attack_range then
			self:do_attack(nearest.player)
		end

		if nearest.distance > self.attack_range or nearest.distance < self.attack_range/2-1 then
			local pos = self.object:get_pos()	

			local smart_dest = nil
			if self.smart_path then --Do pathfinding
				if self.smart_path_for > 0
				then
					self.smart_path_for = self.smart_path_for - 1
				end
				
				--add a little smarts if needed
				if self.smart_path_for >= 0 --this way it stays on if it's negative
				then
					local eyepos = {x = pos.x, y = pos.y + 0.5, z = pos.z}
					local los, obstacle = minetest.line_of_sight(nearest.position, eyepos)
					if obstacle and vector.distance(eyepos, obstacle) < 10
					then
						self.smart_path_for = self.smart_path_for + 20
					end
				end
				if smart_path_for ~= 0
				then
					smart_dest = defense.pathfinder:get_waypoint(self.name, pos.x, pos.y, pos.z)
				end
			end

			if smart_dest then
				self.destination = smart_dest
			else
				local r = math.max(0, self.attack_range - 2)
				local dir = vector.aim(nearest.position, pos)
				self.destination = vector.add(nearest.position, vector.multiply(dir, r))
			end
			
		end
	end
end

function mobs.default_prototype:do_attack(obj)
	if self.last_attack_time + self.attack_interval < self.timer then
		local dir = vector.aim(self.object:get_pos(), obj:get_pos())
		self:attack(obj, dir)
		self.last_attack_time = self.timer

		if self.current_animation == "move" then
			self:set_animation("move_attack")
		else
			self:set_animation("attack")
		end
	end

	self.life_timer = math.min(300, self.life_timer + 60)
end

function mobs.default_prototype:jump(direction)
	if self:is_standing() then
		if direction then
			direction.y = 0.1
			direction = vector.normalize(direction)
		else
			direction = vec_zero()
		end

		local v = self.object:getvelocity()
		v.y = math.sqrt(2 * -mobs.gravity * (self.jump_height + 0.2))
		v.x = direction.x * self.jump_height
		v.z = direction.z * self.jump_height
		self.object:setvelocity(vector.add(self.object:getvelocity(), v))

		self:set_animation("jump")
	end
end

function mobs.default_prototype:die()
	self:on_death()
	self.object:remove()
end

function mobs.default_prototype:on_death()
end

function mobs.default_prototype:is_standing()
	if self.cache_is_standing ~= nil then
		return self.cache_is_standing
	end

	if self.movement == "air" then
		self.cache_is_standing = false
		return false
	end

	if self.object:getvelocity().y ~= 0 then
		self.cache_is_standing = false
		return false
	end

	-- Check the four bottom corners for collision
	local p = self.object:get_pos()
	p.y = p.y + self.collisionbox[2] - 0.25
	local corners = {
		vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[3]}),
		vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[6]}),
		vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[3]}),
		vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[6]}),
	}
	for _,c in ipairs(corners) do
		local node = minetest.get_node_or_nil(c)
		if not node or reg_nodes[node.name].walkable then
			self.cache_is_standing = true
			return true
		end
	end

	self.cache_is_standing = false
	return false
end

function mobs.default_prototype:set_animation(name, inhibit)
	if self.current_animation == name then
		return
	end
	if inhibit then
		for _,p in ipairs(inhibit) do
			if self.current_animation == p and self.timer < self.current_animation_end then
				return
			end
		end
	end

	local anim_prop = self.animation[name]
	if anim_prop then
		self.current_animation = name
		self.current_animation_end = self.timer + (anim_prop.b - anim_prop.a - 1) / anim_prop.rate
		self.object:set_animation({x=anim_prop.a, y=anim_prop.b}, anim_prop.rate, 0)
	end
end

function mobs.default_prototype:find_nearest_player()
	if self.cache_find_nearest_player ~= nil then
		return self.cache_find_nearest_player
	end

	local p = self.object:get_pos()
	local nearest_player = nil
	local nearest_pos = p
	local nearest_dist = math.huge
	for _,obj in ipairs(minetest.get_connected_players()) do
		local pos = obj:get_pos()
		pos.y = pos.y + 1
		local d = vector.distance(pos, p)
		if d < nearest_dist then
			nearest_player = obj
			nearest_pos = pos
			nearest_dist = d
		end
	end

	local ret = {player=nearest_player, position=nearest_pos, distance=nearest_dist}
	self.cache_find_nearest_player = ret
	return ret
end


-- Movement implementations for the default movement types
mobs.move_method = {}
function mobs.move_method:air(dtime, destination)
	local delta = vector.subtract(destination, self.object:get_pos())
	local dist = vector.length(delta)

	-- Add random variation
	if dist > 3 then
		local r_angle = (self.id/100000) * 2 * math.pi
		local r_radius = (self.id/100000) * (dist - 3)/3
		delta = vector.add(delta, {
			x=math.cos(r_angle)*r_radius,
			y=r_radius,
			z=math.sin(r_angle)*r_radius
		})
	end

	-- Compute speed and smoothing factor
	local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
	local t
	local v = self.object:getvelocity()
	if vector.length(v) < self.move_speed * 1.5 then
		t = math.pow(0.1, dtime)
	else
		t = math.pow(0.4, dtime)
		speed = speed * 0.9
	end

	-- Compute and set resulting velocity
	self.object:setvelocity(vector.add(
		vector.multiply(self.object:getvelocity(), t),
		vector.multiply(dist > 0 and vector.normalize(delta) or vec_zero(), speed * (1-t))
	))
	
	if speed > self.move_speed * 0.04 then
		self:set_animation("move", {"attack", "move_attack"})
	else
		self:set_animation("idle", {"attack", "move_attack"})
	end
end
function mobs.move_method:ground(dtime, destination)
	local delta = vector.subtract(destination, self.object:get_pos())
	delta.y = 0
	local dist = vector.length(delta)

	-- Add random variation
	if dist > 4 then
		local r_angle = (self.id/100000) * 2 * math.pi
		local r_radius = (dist - 4)/4
		delta = vector.add(delta, {
			x=math.cos(r_angle)*r_radius,
			y=0,
			z=math.sin(r_angle)*r_radius
	})
	end

	-- Compute speed and smoothing factor
	local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
	local t
	local v = self.object:getvelocity()
	if self:is_standing() and vector.length(v) < self.move_speed * 4 then
		t = math.pow(0.001, dtime)
	else
		t = math.pow(0.4, dtime)
		speed = speed * 0.9
	end

	-- Compute and set resulting velocity
	local dir = dist > 0 and vector.normalize(delta) or vec_zero()
	local v2 = vector.add(
		vector.multiply(v, t),
		vector.multiply(dir, speed * (1-t))
	)
	v2.y = v.y
	self.object:setvelocity(v2)

	
	local jump = nil
	local pos = self.object:get_pos()
	
	-- Check for jump
	if dist > 1 then
		--Jump over obstacles
		local p = vector.new(pos)
		p.y = p.y + self.collisionbox[2] + 0.5
		local sx = self.collisionbox[4] - self.collisionbox[1]
		local sz = self.collisionbox[6] - self.collisionbox[3]
		local r = math.sqrt(sx*sx + sz*sz)/2 + 0.5
		local fronts = {} --might be worth caching those
		do
			local xedge, zedge
			local offset = 0.2
			if math.sign(delta.x) < 0
			then
				xedge = self.collisionbox[1] - offset
			else
				xedge = self.collisionbox[4] + offset
			end
			if math.sign(delta.z) < 0
			then
				zedge = self.collisionbox[3] - offset
			else
				zedge = self.collisionbox[6] + offset
			end
			
			local index = 1
			--point to each node on the sides
			for i = 0, sz
			do
				fronts[index] = {x = self.collisionbox[1] + i, y = 0, z = zedge}
				index = index + 1
			end
			for i = 0, sx
			do
				fronts[index] = {x = xedge, y = 0, z = self.collisionbox[3] + i}
				index = index + 1
			end
			--also check the corner
			fronts[index] = {x = xedge, y = 0, z = zedge}
		end
		

		for _,f in ipairs(fronts) do
			local node = minetest.get_node_or_nil(vector.add(p, f))
			if not node or reg_nodes[node.name].walkable then
				jump = vector.aim(pos, destination)
				break
			end
		end
	elseif destination.y > pos.y + 1
	then
		jump = vector.aim(pos, destination)
	end


	if jump then
		self:jump(jump)
	elseif self:is_standing() then
		if speed > self.move_speed * 0.06 then
			self:set_animation("move", {"move_attack"})
		else
			self:set_animation("idle", {"attack", "move_attack"})
		end
	end
end

function mobs.register_mob(name, def)
	local prototype = {}
	--copy default prototype
	for k,v in pairs(mobs.default_prototype) do
		prototype[k] = v
	end
	--overwrite fields of default prototype that def uses
	for k,v in pairs(def) do
		prototype[k] = v
	end

	prototype.move = def.move or mobs.move_method[prototype.movement]

	-- Register for pathfinding
	if defense.pathfinder and prototype.smart_path then
		defense.pathfinder:register_class(name, {
			collisionbox = prototype.collisionbox,
			jump_height = math.floor(prototype.jump_height),
			path_check = def.pathfinder_check or defense.pathfinder.default_path_check[prototype.movement],
			cost_method = def.pathfinder_cost or defense.pathfinder.default_cost_method[prototype.movement],
		})
	end

	minetest.register_entity(name, prototype)
end
