-------------
---- API ----
-------------
-- Ver 0.1 --

local random = math.random
local pi = math.pi
local abs = math.abs
local ceil = math.ceil

local vec_dist = vector.distance

local abr = minetest.get_mapgen_setting('active_block_range')

local function hitbox(self) return self.object:get_properties().collisionbox end

local walkable_nodes = {}

minetest.register_on_mods_loaded(function()
	for name in pairs(minetest.registered_nodes) do
		if name ~= "air" and name ~= "ignore" then
			if minetest.registered_nodes[name].walkable then
				table.insert(walkable_nodes,name)
			end
		end
	end
end)

function better_fauna.particle_spawner(pos, texture, type, min_pos, max_pos)
    type = type or "float"
    min_pos = min_pos or {
        x = pos.x - 2,
        y = pos.y - 2,
        z = pos.z - 2,
    }
    max_pos = max_pos or {
        x = pos.x + 2,
        y = pos.y + 2,
        z = pos.z + 2,
    }
	if type == "float" then
		minetest.add_particlespawner({
			amount = 16,
			time = 0.25,
			minpos = min_pos,
			maxpos = max_pos,
			minvel = {x = 0, y = 0.2, z = 0},
			maxvel = {x = 0, y = 0.25, z = 0},
			minexptime = 0.75,
			maxexptime = 1,
			minsize = 4,
			maxsize = 4,
			texture = texture,
			glow = 1,
		})
	elseif type == "splash" then
		minetest.add_particlespawner({
			amount = 6,
			time = 0.25,
			minpos = {x = pos.x - 7/16, y = pos.y + 0.6, z = pos.z - 7/16},
			maxpos = {x = pos.x + 7/16, y = pos.y + 0.6, z = pos.z + 7/16},
			minvel = vector.new(-1, 2, -1),
			maxvel = vector.new(1, 5, 1),
			minacc = vector.new(0, -9.81, 0),
			maxacc = vector.new(0, -9.81, 0),
			minsize = 2,
			maxsize = 4,
			collisiondetection = true,
			texture = texture,
		})
	end
end

---------------------------
-- Mob Control Functions --
---------------------------

local function can_fit(pos, width)
    local pos1 = vector.new(pos.x - width, pos.y, pos.z - width)
    local pos2 = vector.new(pos.x + width, pos.y, pos.z + width)
    for x = pos1.x, pos2.x do
        for y = pos1.y, pos2.y do
            for z = pos1.z, pos2.z do
                local p2 = vector.new(x, y, z)
                local node = minetest.get_node(p2)
                if minetest.registered_nodes[node.name].walkable then
                    local p3 = vector.new(p2.x, p2.y + 1, p2.z)
                    local node2 = minetest.get_node(p3)
                    if minetest.registered_nodes[node2.name].walkable then
                        return false
                    end
                end
            end
        end
    end
    return true
end

local function move_from_wall(pos, width)
    local pos1 = vector.new(pos.x - width, pos.y, pos.z - width)
    local pos2 = vector.new(pos.x + width, pos.y, pos.z + width)
    for x = pos1.x, pos2.x do
        for y = pos1.y, pos2.y do
            for z = pos1.z, pos2.z do
                local p2 = vector.new(x, y, z)
                if can_fit(p2, width) then
                    return p2
                end
            end
        end
    end
    return pos
end

function better_fauna.find_path(pos, tpos, width)

    local endpos = tpos

    if not minetest.registered_nodes[minetest.get_node(
        vector.new(endpos.x, endpos.y - 1, endpos.z))
        .name].walkable then
        local min = vector.subtract(endpos, 1)
        local max = vector.add(endpos, 1)

        local index_table = minetest.find_nodes_in_area_under_air( min, max, better_fauna.walkable_nodes)
        for _, i_pos in pairs(index_table) do
            if minetest.registered_nodes[minetest.get_node(i_pos)
                .name].walkable then
                endpos = vector.new(i_pos.x, i_pos.y + 1, i_pos.z)
                break
            end
        end
    end

    local path = minetest.find_path(pos, endpos, 32, 1, 1, "A*_noprefetch")

    if not path
    or #path < 2 then return end
	
    table.remove(path, 1)
    table.remove(path, #path)
	
    for i = #path, 1, -1 do
        if not path then return end
        if not can_fit(path[i], width + 1) then
            local clear = move_from_wall(path[i], width + 1)
            if clear and can_fit(clear, width) then
                path[i] = clear
            end
		end
        if #path > 3 then
            local pos1 = path[i - 2]
			local pos2 = path[i]
			-- Handle Diagonals
            if pos1
            and pos2
            and pos1.x ~= pos2.x
            and pos1.z ~= pos2.z then
				if minetest.line_of_sight(pos1, pos2) then
					local pos3 = vector.divide(vector.add(pos1, pos2), 2)
					if can_fit(pos, width) then
						table.remove(path, i - 1)
					end
                end
			end
			-- Reduce Straight Lines
			if pos1
            and pos2
            and pos1.x == pos2.x
			and pos1.z ~= pos2.z
			and pos1.y == pos2.y then
                if minetest.line_of_sight(pos1, pos2) then
					local pos3 = vector.divide(vector.add(pos1, pos2), 2)
					if can_fit(pos, width) then
						table.remove(path, i - 1)
					end
                end
            elseif pos1
			and pos2
			and pos1.x ~= pos2.x
			and pos1.z == pos2.z
			and pos1.y == pos2.y then
				if minetest.line_of_sight(pos1, pos2) then
					local pos3 = vector.divide(vector.add(pos1, pos2), 2)
					if can_fit(pos, width) then
						table.remove(path, i - 1)
					end
				end
			end
		end
    end

    if #path > 2 then
        if vector.distance(pos, path[2]) <= width + 1 then
            for i = 3, #path do
                path[i - 1] = path[i]
            end
        end
    end

    return path
end

function better_fauna.path_to_next_waypoint(self, tpos, speed_factor)
    speed_factor = speed_factor or 1
    local pos = self.object:get_pos()
	local path_data = better_fauna.find_path(pos, tpos, 1)
	if not path_data
	or #path_data < 2 then
        return true
    end
    local pos2 = path_data[2]
	if pos2 then
		local yaw = self.object:get_yaw()
        local tyaw = minetest.dir_to_yaw(vector.direction(pos, pos2))
        if abs(tyaw - yaw) > 0.1 then
            mobkit.lq_turn2pos(self, pos2)
        end
        mobkit.lq_dumbwalk(self, pos2, speed_factor)
        return true
    end
end

function better_fauna.feed_tame(self, clicker, feed_count, tame, breed)
	local item = clicker:get_wielded_item()
	local pos = self.object:get_pos()
	local mob_name = mob_core.get_name_proper(self.name)
	if mob_core.follow_holding(self, clicker) then
		if creative == false then
			item:take_item()
			clicker:set_wielded_item(item)
		end
		mobkit.heal(self, self.max_hp/feed_count)
		if self.hp >= self.max_hp then
			self.hp = self.max_hp
		end
        self.food = mobkit.remember(self, "food", self.food + 1)

        local minppos = vector.add(pos, hitbox(self)[4])
        local maxppos = vector.subtract(pos, hitbox(self)[4])
        local def = minetest.registered_items[item:get_name()]
        local texture = def.inventory_image
        if not texture or texture == "" then
            texture = def.wield_image
            if def.tiles then
                texture = def.tiles[1]
            end
        end
        texture = texture .. "^[resize:8x8" -- Crops image
        minetest.add_particlespawner({
            amount = 12*self.height,
            time = 0.1,
            minpos = minppos,
            maxpos = maxppos,
            minvel = {x=-1, y=1, z=-1},
            maxvel = {x=1, y=2, z=1},
            minacc = {x=0, y=-5, z=0},
            maxacc = {x=0, y=-9, z=0},
            minexptime = 1,
            maxexptime = 1,
            minsize = 2*self.height,
            maxsize = 3*self.height,
            collisiondetection = true,
            vertical = false,
            texture = texture,
        })
        if self.food >= feed_count then
            self.food = mobkit.remember(self, "food", 0)
            if tame and not self.tamed then
                mob_core.set_owner(self, clicker:get_player_name())
                minetest.chat_send_player(clicker:get_player_name(), mob_name.." has been tamed!")
                mobkit.clear_queue_high(self)
                paleotest.particle_spawner(pos, "mob_core_green_particle.png", "float", min, max)
            end
            if breed then
                if self.child then return false end
                if self.breeding then return false end
                if self.breeding_cooldown <= 0 then
                    self.breeding = true
                    local min = vector.subtract(pos, 0.5)
                    local max = vector.add(pos, 0.5)
                    better_fauna.particle_spawner(pos, "heart.png", "float", min, max)
                end
            end
        end
	end
	return false
end

------------------
-- HQ Functions --
------------------

function better_fauna.hq_sporadic_flee(self, prty, player)
    local tyaw = 0
    local init = true
	local timer = 1
	if not player then return true end
	local func = function(self)
		if not mobkit.is_alive(player) then return true end
		if not mobkit.is_alive(self) then return true end
		local pos = mobkit.get_stand_pos(self)
		local yaw = self.object:get_yaw()
		local tpos = vector.add(pos, vector.multiply(minetest.yaw_to_dir(yaw), 4))

		if init then
			tyaw = minetest.dir_to_yaw(vector.direction(pos, tpos))
		end
		if self._anim ~= "run" then
			mobkit.animate(self, "run")
		end

        if mobkit.is_queue_empty_low(self) then
            timer = timer - self.dtime
            if timer < 0 then
				tyaw = yaw - random(1.6, 3.2)
				timer = 1
            end
            if abs(tyaw - yaw) > 0.1 then
                mobkit.turn2yaw(self, tyaw)
            end
            mobkit.go_forward_horizontal(self, self.max_speed)
            if timer <= 0 then return true end
        end
	end
    mobkit.queue_high(self, func, prty)
end

function better_fauna.hq_follow_player(self, prty, player) -- Follow Player
	if not player then return end
    if not mob_core.follow_holding(self, player) then return end
    local func = function(self)
		if not mobkit.is_alive(player) then
            mobkit.clear_queue_high(self)
            return true
		end
		if mobkit.is_queue_empty_low(self) then
			local pos = mobkit.get_stand_pos(self)
			local tpos = player:get_pos()
			if mob_core.follow_holding(self, player) then
				self.status = mobkit.remember(self, "status", "following")
				local dist = vec_dist(pos, tpos)
				local yaw = self.object:get_yaw()
				local tyaw = minetest.dir_to_yaw(vector.direction(pos, tpos))
				if dist > self.view_range then
					self.status = mobkit.remember(self, "status", "")
					return true
				end
				better_fauna.path_to_next_waypoint(self, tpos, 0.85)
				if vec_dist(pos, tpos) < hitbox(self)[4] + 2 then
					mobkit.lq_idle(self, 0.1, "stand")
				end
			else
				self.status = mobkit.remember(self, "status", "")
				mobkit.lq_idle(self, 0.1, "stand")
				return true
			end
		end
    end
    mobkit.queue_high(self, func, prty)
end

function better_fauna.get_nearby_mate(self, name)
	for _,obj in ipairs(self.nearby_objects) do
        if mobkit.is_alive(obj)
        and not obj:is_player()
        and obj:get_luaentity().name == name
        and obj:get_luaentity().gender ~= self.gender
        and obj:get_luaentity().breeding then
            return obj
        end
	end
	return
end

function better_fauna.hq_breed(self, prty)
    local mate = better_fauna.get_nearby_mate(self, self.name)
    if not mate then return end
    local speed_factor = 0.5
    local func = function(self)
        if mobkit.is_queue_empty_low(self) then
            local pos = mobkit.get_stand_pos(self)
            local tpos = mate:get_pos()
            local dist = vec_dist(pos, tpos) - math.abs(hitbox(self)[4])
            if dist > 1.5 then
                speed_factor = 0.5
            else
                speed_factor = 0.1
            end
            mob_core.goto_next_waypoint(self, tpos, speed_factor)
            if dist < 1.75 then
                self.breeding_time = self.breeding_time + 1
            end
            if self.breeding_time >= 2
            or mate:get_luaentity().breeding_time >= 2 then
                if self.gender == "female" then
                    mob_core.spawn_child(pos, self.name)
                end
                self.breeding = false
                self.breeding_time = 0
                self.breeding_cooldown = 300
                mobkit.remember(self, "breeding", self.breeding)
                mobkit.remember(self, "breeding_time", self.breeding_time)
                mobkit.remember(self, "breeding_cooldown", self.breeding_cooldown)
                return true
            end
        end
    end
    mobkit.queue_high(self, func, prty)
end

function better_fauna.hq_fowl_breed(self, prty)
    local mate = better_fauna.get_nearby_mate(self, self.name)
    if not mate then return end
    local speed_factor = 0.5
    local func = function(self)
        if mobkit.is_queue_empty_low(self) then
            local pos = mobkit.get_stand_pos(self)
            local tpos = mate:get_pos()
            local dist = vec_dist(pos, tpos) - math.abs(hitbox(self)[4])
            if dist > 1.5 then
                speed_factor = 0.5
            else
                speed_factor = 0.1
            end
            mob_core.goto_next_waypoint(self, tpos, speed_factor)
            if dist < 1.75 then
                self.breeding_time = self.breeding_time + 1
            end
            if self.breeding_time >= 2
            or mate:get_luaentity().breeding_time >= 2 then
                if self.gender == "female" then
                    minetest.add_particlespawner({
                        amount = 6,
                        time = 0.25,
                        minpos = {x = pos.x - 7/16, y = pos.y - 5/16, z = pos.z - 7/16},
                        maxpos = {x = pos.x + 7/16, y = pos.y - 5/16, z = pos.z + 7/16},
                        minvel = vector.new(-1, 2, -1),
                        maxvel = vector.new(1, 5, 1),
                        minacc = vector.new(0, -9.81, 0),
                        maxacc = vector.new(0, -9.81, 0),
                        collisiondetection = true,
                        texture = "better_fauna_egg_fragment.png",
                    })
                    mob_core.spawn_child(pos, self.name)
                end
                self.breeding = false
                self.breeding_time = 0
                self.breeding_cooldown = 300
                mobkit.remember(self, "breeding", self.breeding)
                mobkit.remember(self, "breeding_time", self.breeding_time)
                mobkit.remember(self, "breeding_cooldown", self.breeding_cooldown)
                return true
            end
        end
    end
    mobkit.queue_high(self, func, prty)
end

function better_fauna.hq_eat(self, prty)
    local func = function(self)
        local pos = mobkit.get_stand_pos(self)
        local under = vector.new(pos.x, pos.y - 1, pos.z)
        for _, node in ipairs(self.consumable_nodes) do
            if node.name == minetest.get_node(under).name then
                minetest.set_node(under, {name = node.replacement})
                local def = minetest.registered_nodes[node.name]
                local texture = def.tiles[1]
                texture = texture .. "^[resize:8x8"
                minetest.add_particlespawner({
                    amount = 6,
                    time = 0.1,
                    minpos = vector.new(
                        pos.x - 0.5,
                        pos.y + 0.1,
                        pos.z - 0.5
                    ),
                    maxpos = vector.new(
                        pos.x + 0.5,
                        pos.y + 0.1,
                        pos.z + 0.5
                    ),
                    minvel = {x=-1, y=1, z=-1},
                    maxvel = {x=1, y=2, z=1},
                    minacc = {x=0, y=-5, z=0},
                    maxacc = {x=0, y=-9, z=0},
                    minexptime = 1,
                    maxexptime = 1,
                    minsize = 1,
                    maxsize = 2,
                    collisiondetection = true,
                    vertical = false,
                    texture = texture,
                })
                self.gotten = false
                mobkit.remember(self, "gotten", self.gotten)
                return true
            else
                return true
            end
        end
    end
    mobkit.queue_high(self, func, prty)
end

---------------------------------
-- Entity Definition Functions --
---------------------------------

function better_fauna.on_step(self, dtime, moveresult)
    mob_core.on_step(self, dtime, moveresult)
    if mobkit.timer(self, 1) then
        if self.breeding_cooldown > 0 then
            self.breeding_cooldown = self.breeding_cooldown - 1
        end
        mobkit.remember(self, "breeding_cooldown", self.breeding_cooldown)
    end
end

function better_fauna.on_activate(self, staticdata, dtime_s)
    mob_core.on_activate(self, staticdata, dtime_s)
    self.gotten = mobkit.recall(self, "gotten") or false
    self.attention_span = mobkit.recall(self, "attention_span") or 0
    self.breeding = mobkit.recall(self, "breeding") or false
    self.breeding_time = mobkit.recall(self, "breeding_time") or 0
    self.breeding_cooldown = mobkit.recall(self, "breeding_cooldown") or 0
end

-------------
-- Physics --
-------------

function better_fauna.lightweight_physics(self)
	local vel = self.object:get_velocity()
	if self.isonground and not self.isinliquid then
		self.object:set_velocity({x= vel.x> 0.2 and vel.x*mobkit.friction or 0,
								y=vel.y,
								z=vel.z > 0.2 and vel.z*mobkit.friction or 0})
	end
	if self.springiness and self.springiness > 0 then
		local vnew = vector.new(vel)
		
		if not self.collided then
			for _,k in ipairs({'y','z','x'}) do			
				if vel[k]==0 and abs(self.lastvelocity[k])> 0.1 then 
					vnew[k]=-self.lastvelocity[k]*self.springiness 
				end
			end
		end
		if not vector.equals(vel,vnew) then
			self.collided = true
		else
			if self.collided then
				vnew = vector.new(self.lastvelocity)
			end
			self.collided = false
		end
		
		self.object:set_velocity(vnew)
	end
	local surface = nil
	local surfnodename = nil
	local spos = mobkit.get_stand_pos(self)
	spos.y = spos.y+0.01
	local snodepos = mobkit.get_node_pos(spos)
	local surfnode = mobkit.nodeatpos(spos)
	while surfnode and surfnode.drawtype == 'liquid' do
		surfnodename = surfnode.name
		surface = snodepos.y+0.5
		if surface > spos.y+self.height then break end
		snodepos.y = snodepos.y+1
		surfnode = mobkit.nodeatpos(snodepos)
	end
	self.isinliquid = surfnodename
	if surface then
		local submergence = min(surface-spos.y,self.height)/self.height
		local buoyacc = mobkit.gravity*(self.buoyancy-submergence)
		mobkit.set_acceleration(self.object,
			{x=-vel.x*self.water_drag,y=buoyacc-vel.y*abs(vel.y)*0.4,z=-vel.z*self.water_drag})
	else
		self.object:set_acceleration({x=0,y=-2.8,z=0})
	end
end