---------
-- API --
---------

animalia.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(animalia.walkable_nodes, name)
            end
        end
    end
end)

-- Math --

local pi = math.pi
local random = math.random
local abs = math.abs
local deg = math.deg

-- Vector Math --

local vec_dir = vector.direction
local vec_add = vector.add
local vec_sub = vector.subtract
local vec_multi = vector.multiply
local vec_divide = vector.divide
local vec_len = vector.length

local dir2yaw = minetest.dir_to_yaw
local yaw2dir = minetest.yaw_to_dir

--------------
-- Settings --
--------------

local creative = minetest.settings:get_bool("creative_mode")

---------------------
-- Local Utilities --
---------------------

function animalia.correct_name(str)
    if str then
        if str:match(":") then str = str:split(":")[2] end
        return (string.gsub(" " .. str, "%W%l", string.upper):sub(2):gsub("_", " "))
    end
end

local correct_name = animalia.correct_name

----------------------
-- Global Utilities --
----------------------

function animalia.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 = {x = -1, y = 2, z = -1},
			maxvel = {x = 1, y = 5, z = 1},
			minacc = {x = 0, y = -9.81, z = 0},
			maxacc = {x = 0, y = -9.81, z = 0},
			minsize = 2,
			maxsize = 4,
			collisiondetection = true,
			texture = texture,
		})
	end
end

function animalia.get_average_pos(vectors)
    local sum = {x = 0, y = 0, z = 0}
    for _, vec in pairs(vectors) do sum = vec_add(sum, vec) end
    return vec_divide(sum, #vectors)
end

----------------------
-- Entity Utilities --
----------------------

function animalia.get_group_positions(name, pos, radius)
    local objects = minetest.get_objects_in_area(vec_sub(pos, radius), vec_add(pos, radius))
    local group = {}
    for i = 1, #objects do
        local object = objects[i]
        if object
        and object:get_luaentity()
        and object:get_luaentity().name == name then
            table.insert(group, object:get_pos())
        end
    end
    return group
end

function animalia.get_group(self)
    local pos = self.object:get_pos()
    local radius = self.tracking_range
    local objects = minetest.get_objects_in_area(vec_sub(pos, radius), vec_add(pos, radius))
    local group = {}
    for i = 1, #objects do
        local object = objects[i]
        if object
        and object ~= self.object
        and object:get_luaentity()
        and object:get_luaentity().name == self.name then
            table.insert(group, object)
        end
    end
    return group
end


function animalia.get_nearby_mate(self, name)
    local objects = minetest.get_objects_inside_radius(self:get_center_pos(), self.tracking_range)
	for _, object in ipairs(objects) do
        if creatura.is_alive(object)
        and not object:is_player()
        and object:get_luaentity().name == name
        and object:get_luaentity().gender ~= self.gender
        and object:get_luaentity().breeding then
            return object
        end
	end
end

-------------------
-- Mob Functions --
-------------------

function animalia.initialize_api(self)
    self.gender = self:recall("gender") or nil
    if not self.gender then
        local genders = {"male", "female"}
        self.gender = self:memorize("gender", genders[random(2)])
    end
    self.food = self:recall("food") or 0
    self.gotten = self:recall("gotten") or false
    self.breeding = false
    self.breeding_cooldown = self:recall("breeding_cooldown") or 0
    if self.growth_scale then
        self:memorize("growth_scale", self.growth_scale) -- This is for spawning children
    end
    self.growth_scale = self:recall("growth_scale") or 1
    self:set_scale(self.growth_scale)
    if self.growth_scale < 0.8
    and self.child_textures then
        if not self.texture_no then
            self.texture_no = random(#self.child_textures)
        end
        self:set_texture(self.texture_no, self.child_textures)
        return
    elseif self.growth_scale > 0.7
    and self.male_textures
    and self.female_textures then
        if not self.texture_no then
            self.texture_no = random(#self[self.gender .. "_textures"])
        end
        self:set_texture(self.texture_no, self[self.gender .. "_textures"])
        return
    end
end

function animalia.step_timers(self)
    self.breeding_cooldown = self.breeding_cooldown - self.dtime
    if self.breeding
    and self.breeding_cooldown <= 30 then
        self.breeding = false
    end
    self:memorize("breeding_cooldown", self.breeding_cooldown)
end

function animalia.do_growth(self, interval)
    if self.growth_scale
    and self.growth_scale < 0.9 then
        if self:timer(interval) then
            self.growth_scale = self.growth_scale + 0.1
            self:set_scale(self.growth_scale)
            if self.growth_scale < 0.8
            and self.child_textures then
                local tex_no = self.texture_no
                if not self.child_textures[tex_no] then
                    tex_no = 1
                end
                self:set_texture(tex_no, self.child_textures)
            elseif self.growth_scale == 0.8 then
                if self.male_textures
                and self.female_textures then
                    if #self.child_textures == 1 then
                        self.texture_no = random(#self[self.gender .. "_textures"])
                    end
                    self:set_texture(self.texture_no, self[self.gender .. "_textures"])
                else
                    if #self.child_textures == 1 then
                        self.texture_no = random(#self.textures)
                    end
                    self:set_texture(self.texture_no, self.textures)
                end
            end
            self:memorize("growth_scale", self.growth_scale)
        end
    end
end

-----------------------
-- Dynamic Animation --
-----------------------

local function clamp_bone_rot(n) -- Fixes issues with bones jittering when yaw clamps
    if n < -180 then
        n = n + 360
    elseif n > 180 then
        n = n - 360
    end
    if n < -60 then
        n = -60
    elseif n > 60 then
        n = 60
    end
    return n
end

local function interp_bone_rot(a, b, w) -- Smoothens bone movement
    if abs(a - b) > deg(pi) then
        if a < b then
            return ((a + (b - a) * w) + (deg(pi) * 2))
        elseif a > b then
            return ((a + (b - a) * w) - (deg(pi) * 2))
        end
    end
    return a + (b - a) * w
end

function animalia.move_head(self, tyaw, pitch)
    local data = self.head_data
    local _, rot = self.object:get_bone_position(data.bone or "Head.CTRL")
    local yaw = self.object:get_yaw()
    local look_yaw = clamp_bone_rot(deg(yaw - tyaw))
    local look_pitch = 0
    if pitch then
        look_pitch = clamp_bone_rot(deg(pitch))
    end
    if tyaw ~= yaw then
        look_yaw = look_yaw * 0.66
    end
    yaw = interp_bone_rot(rot.z, look_yaw, 0.1)
    local ptch = interp_bone_rot(rot.x, look_pitch + data.pitch_correction, 0.1)
    self.object:set_bone_position(data.bone or "Head.CTRL", data.offset, {x = ptch, y = yaw, z = yaw})
end

function animalia.head_tracking(self)
    if not self.head_data then return end
    local yaw = self.object:get_yaw()
    local pos = self.object:get_pos()
    local v = vec_add(pos, vec_multi(yaw2dir(yaw), self.head_data.pivot_h))
    pos.x = v.x
    pos.y = pos.y + self.head_data.pivot_v
    pos.z = v.z
    --[[minetest.add_particle({
        pos = pos,
        velocity = {x=0, y=0, z=0},
        acceleration = {x=0, y=0, z=0},
        expirationtime = 0.1,
        size = 8,
        collisiondetection = false,
        vertical = false,
        texture = "mob_core_green_particle.png",
        playername = "singleplayer"
    })]]
    local vel = self.object:get_velocity()
    if abs(yaw - self.last_yaw) < 0.1 then
        animalia.move_head(self, yaw)
    else
        animalia.move_head(self, self._tyaw)
    end
    if not self.head_tracking
    and self:timer(3)
    and random(4) < 2 then
        local objects = minetest.get_objects_inside_radius(pos, 6)
        for _, object in ipairs(objects) do
            if object:is_player() then
                self.head_tracking = object
                break
            end
        end
    else
        if not creatura.is_valid(self.head_tracking) then
            self.head_tracking = nil
            self.head_tracking_turn = nil
            return
        end
        local ppos = self.head_tracking:get_pos()
        ppos.y = ppos.y + 1.4
        local dir = vec_dir(pos, ppos)
        local tyaw = dir2yaw(dir)
        if self:timer(1)
        and abs(yaw - tyaw) > 1
        and abs(yaw - tyaw) < 5.3
        and self.head_tracking_turn then
            self.head_tracking = nil
            self.head_tracking_turn = nil
            dir.y = 0
            return
        elseif not self.head_tracking_turn then
            self.head_tracking_turn = tyaw
        end
        if self.head_tracking_turn
        and self._anim == "stand" then
            self:turn_to(self.head_tracking_turn, 2)
        end
        animalia.move_head(self, tyaw, dir.y)
    end
end

-----------------------
-- World Interaction --
-----------------------

function animalia.random_drop_item(item, chance)
    if random(chance) < 2 then
        local object = minetest.add_item(ItemStack(item))
        object:add_velocity({
            x = random(-2, 2),
            y = 1.5,
            z = random(-2, 2)
        })
    end
end

function animalia.protect_from_despawn(self)
    self._despawn = self:memorize("_despawn", false)
    self.despawn_after = self:memorize("despawn_after", false)
end

------------------------
-- Player Interaction --
------------------------

function animalia.feed(self, player, tame, breed)
    local item, item_name = self:follow_wielded_item(player)
    if item_name then
        if not creative then
            item:take_item()
            player:set_wielded_item(item)
        end
        if self.hp < self.max_health then
            self:heal(self.max_health / 5)
        end
        self.food = self.food + 1
        if self.food >= 5 then
            local pos = self:get_center_pos()
            local minp = vec_sub(pos, 1)
            local maxp = vec_add(pos, 1)
            self.food = 0
            local follow = self.follow
            if type(follow) == "table" then
                follow = follow[1]
            end
            if tame
            and not self.owner
            and (follow == item_name) then
                self.owner = self:memorize("owner", player:get_player_name())
                local name = correct_name(self.name)
                minetest.chat_send_player(player:get_player_name(), name .. " has been tamed!")
                if self.logic then
                    self:clear_task()
                end
                animalia.particle_spawner(pos, "creatura_particle_green.png", "float", minp, maxp)
                if not animalia.pets[self.owner][self.object] then
                    table.insert(animalia.pets[self.owner], self.object)
                end
            end
            if breed then
                if self.breeding then return false end
                if self.breeding_cooldown <= 0 then
                    self.breeding = true
                    self.breeding_cooldown = 60
                    animalia.particle_spawner(pos, "heart.png", "float", minp, maxp)
                end
            end
        end
        animalia.protect_from_despawn(self)
        return true
    end
    return false
end

local animate_player = {}

if minetest.get_modpath("default")
and minetest.get_modpath("player_api") then
    animate_player = player_api.set_animation
elseif minetest.get_modpath("mcl_player") then
    animate_player = mcl_player.set_animation
end

function animalia.mount(self, player, params)
    if not creatura.is_alive(player) then
        return
    end
    if (player:get_attach()
    and player:get_attach() == self.object)
    or not params then
        player:set_detach()
        player:set_properties({
            visual_size = {
                x = 1,
                y = 1
            }
        })
        player:set_eye_offset()
        if player_api then
            animate_player(player, "stand", 30)
            if player_api.player_attached then
                player_api.player_attached[player:get_player_name()] = false
            end
        end
        self.rider = nil
        return
    end
    if player_api then
        player_api.player_attached[player:get_player_name()] = true
    end
	minetest.after(0.2, function()
		if player
        and player:is_player()
        and player_api then
			animate_player(player, "sit", 30)
		end
	end)    self.rider = player
    local mob_size = self.object:get_properties().visual_size
    local player_size = player:get_properties().visual_size
    player:set_attach(self.object, "Torso", params.pos, params.rot)
    player:set_properties({
        visual_size = {
            x = player_size.x / mob_size.x,
            y = player_size.y / mob_size.y
        }
    })
    player:set_eye_offset({x = 0, y = 15, z = 0}, {x = 0, y = 15, z = 15})
end

-------------
-- Sensors --
-------------

function animalia.find_collision(self, dir)
    local pos = self.object:get_pos()
    local pos2 = vec_add(pos, vec_multi(dir, 16))
    local ray = minetest.raycast(pos, pos2, false, false)
    for pointed_thing in ray do
        if pointed_thing.type == "node" then
            return pointed_thing.under
        end
    end
    return nil
end

----------
-- Misc --
----------

function animalia.alias_mob(old_mob, new_mob)
    minetest.register_entity(":" .. old_mob, {
        on_activate = function(self)
            local pos = self.object:get_pos()
            minetest.add_entity(pos, new_mob)
            self.object:remove()
        end,
    })
end

--------------
-- Spawning --
--------------

animalia.registered_biome_groups = {}

function animalia.register_biome_group(name, def)
    animalia.registered_biome_groups[name] = def
    animalia.registered_biome_groups[name].biomes = {}
end

local function assign_biome_group(name)
    local def = minetest.registered_biomes[name]
    local turf = def.node_top
    local heat = def.heat_point or 0
    local humidity = def.humidity_point or 50
    local y_min = def.y_min
    local y_max = def.y_max
    for group, params in pairs(animalia.registered_biome_groups) do -- k, v in pairs
        if name:find(params.name_kw or "")
        and turf and turf:find(params.turf_kw or "")
        and heat >= params.min_heat
        and heat <= params.max_heat
        and humidity >= params.min_humidity
        and humidity <= params.max_humidity
        and (not params.min_height or y_min >= params.min_height)
        and (not params.max_height or y_max <= params.max_height) then
            table.insert(animalia.registered_biome_groups[group].biomes, name)
        end
    end
end

minetest.register_on_mods_loaded(function()
	for name in pairs(minetest.registered_biomes) do
        assign_biome_group(name)
	end
end)

local spawn_biomes = {
    ["animalia:bat"] = "cave",
    ["animalia:bird"] = "temperate",
    ["animalia:cat"] = "urban",
    ["animalia:chicken"] = "tropical",
    ["animalia:cow"] = "grassland",
    ["animalia:tropical_fish"] = "ocean",
    ["animalia:frog"] = "swamp",
    ["animalia:horse"] = "grassland",
    ["animalia:pig"] = "temperate",
    ["animalia:reindeer"] = "boreal",
    ["animalia:sheep"] = "grassland",
    ["animalia:turkey"] = "boreal",
    ["animalia:wolf"] = "boreal",
}

animalia.register_biome_group("temperate", {
    name_kw = "",
    turf_kw = "grass",
    min_heat = 45,
    max_heat = 70,
    min_humidity = 0,
    max_humidity = 50
})

animalia.register_biome_group("urban", {
    name_kw = "",
    turf_kw = "grass",
    min_heat = 0,
    max_heat = 100,
    min_humidity = 0,
    max_humidity = 100
})

animalia.register_biome_group("grassland", {
    name_kw = "",
    turf_kw = "grass",
    min_heat = 45,
    max_heat = 90,
    min_humidity = 0,
    max_humidity = 80
})

animalia.register_biome_group("boreal", {
    name_kw = "",
    turf_kw = "litter",
    min_heat = 10,
    max_heat = 55,
    min_humidity = 0,
    max_humidity = 80
})

animalia.register_biome_group("ocean", {
    name_kw = "ocean",
    turf_kw = "",
    min_heat = 0,
    max_heat = 100,
    min_humidity = 0,
    max_humidity = 100,
    max_height = 0
})

animalia.register_biome_group("swamp", {
    name_kw = "",
    turf_kw = "",
    min_heat = 55,
    max_heat = 90,
    min_humidity = 55,
    max_humidity = 90,
    max_height = 10,
    min_height = -20
})

animalia.register_biome_group("tropical", {
    name_kw = "",
    turf_kw = "litter",
    min_heat = 70,
    max_heat = 90,
    min_humidity = 65,
    max_humidity = 90
})

animalia.register_biome_group("cave", {
    name_kw = "under",
    turf_kw = "",
    min_heat = 0,
    max_heat = 100,
    min_humidity = 0,
    max_humidity = 100,
    max_height = 5
})

animalia.register_biome_group("common", {
    name_kw = "",
    turf_kw = "",
    min_heat = 25,
    max_heat = 75,
    min_humidity = 20,
    max_humidity = 80,
    min_height = 1
})

---------------
-- Libri API --
---------------

local function contains_item(inventory, item)
    return inventory and inventory:contains_item("main", ItemStack(item))
end

function animalia.get_libri(inventory)
    local list = inventory:get_list("main")
    for i = 1, inventory:get_size("main") do
        local stack = list[i]
        if stack:get_name()
        and stack:get_name() == "animalia:libri_animalia" then
            return stack, i
        end
    end
end

local get_libri = animalia.get_libri

function animalia.add_libri_page(self, player, page)
    local inv = minetest.get_inventory({type = "player", name = player:get_player_name()})
    if contains_item(inv, "animalia:libri_animalia") then
        local libri, list_i = get_libri(inv)
        local pages = minetest.deserialize(libri:get_meta():get_string("pages")) or {}
        if #pages > 0 then
            local add_page = true
            for i = 1, #pages do
                if pages[i].name == page.name then
                    add_page = false
                    break
                end
            end
            if add_page then
                table.insert(pages, page)
                libri:get_meta():set_string("pages", minetest.serialize(pages))
                inv:set_stack("main", list_i, libri)
                return true
            end
        else
            table.insert(pages, page)
            libri:get_meta():set_string("pages", minetest.serialize(pages))
            inv:set_stack("main", list_i, libri)
            return true
        end
    end
end

function animalia.get_item_list(list, offset_x, offset_y) -- Creates a visual list of items for Libri formspecs
    local size = 1 / #list
    if size < 0.45 then size = 0.45 end
    local spacing = 0.3
    local total_scale = size + spacing
    local max_horiz = 3
    local max_verti = 6
    local form = {}
    for i = 1, #list do
        local vert_multi = math.floor((i - 1) / max_horiz)
        local horz_multi = (total_scale * max_horiz) * vert_multi
        table.insert(form, "item_image[" .. offset_x + ((total_scale * i) - horz_multi) .. "," .. offset_y + (total_scale * vert_multi ).. ";" .. size .. "," .. size .. ";" .. list[i] .. "]")
    end
    return table.concat(form, "")
end

-- Libri should list: Spawn Biomes, Drops, Food, Taming Method, Catchability, and Lassoability


local function get_inventory_cube(name)
    local def = minetest.registered_nodes[name]
    local tiles
    if name:find(".png") then
        tiles = {
            name,
            name,
            name
        }
    elseif def then
        tiles = table.copy(def.tiles) or table.copy(def.textures)
    else
        return
    end
    if not tiles
    or type(tiles) ~= "table"
    or #tiles < 1 then
        return
    end
    for i = 1, #tiles do
        if type(tiles[i]) == "table" then
            tiles[i] = tiles[i].name
        end
    end
    local cube
    if #tiles < 3 then
        cube = minetest.inventorycube(tiles[1], tiles[1], tiles[1])
    else
        cube = minetest.inventorycube(tiles[1], tiles[3], tiles[3])
    end
    return cube
end

local function get_textures(name)
    local def = minetest.registered_entities[name]
    local textures = def.textures
    if not textures then
        if #def.female_textures < 2 then
            textures = {def.female_textures[1], def.male_textures[1]}
        else
            textures = {}
            local num = #def.female_textures
            for i = 1, num do
                if num + #def.male_textures < 7 then
                    textures = {unpack(def.male_textures), unpack(def.female_textures)}
                else
                    if i < num * 0.5 then
                        table.insert(textures, def.female_textures[i])
                    else
                        table.insert(textures, def.male_textures[i])
                    end
                end
            end
        end
    end
    return textures
end

local animalia_libri_info = {}

local libri_animal_info = {
    ["animalia:bat"] = {
        invcube = "default:stone",
        info = {
            domestication = {
                "While they can't be truly",
                "domesticated, Bats will begin ",
                "to trust you if you feed them ",
                "often. A Bat that trusts you will ",
                "not flee when you walk near it.",
                "This is useful as it allows ",
                "Players to keep them around ",
                "to harvest their guano, which ",
                "can be used as a powerful ",
                "fertilizer."
            },
            behavior = {
                "Bats are mostly harmless, and ",
                "can be found hanging from ",
                "trees and cliff ceilings during ",
                "the day. The only harm they ",
                "can cause it to property, with ",
                "guano accumulating ",
                "underneath them while they ",
                "rest. Being social creatures, it's ",
                "not uncommon to see a few ",
                "hanging from ceilings together ",
                "or swarming, which often ",
                "occurs at evening or when a ",
                "Player approaches."
            }
        }
    },
    ["animalia:bird"] = {
        info = {
            domestication = {
                "Cannot be tamed.",
            },
            behavior = {
                "Song Birds are found across ",
                "various biomes, except for ",
                "biomes too inhospitable like ",
                "deserts or tundras. They fly in ",
                "flocks that vary in size from 4 ",
                "or 5 individuals to large flocks ",
                "exceeding a dozen individuals. Their calls vary between ",
                "species, making it easy to tell ",
                "what kind of birds are around."
            }
        }
    },
    ["animalia:cat"] = {
        info = {
            domestication = {
                "Unlike Wolves and Horses," ,
                "which are almost immediately ",
                "trusting upon being tamed, ",
                "Cats will remain untrusting ",
                "until you gain their trust. To do ",
                "so, you must feed and play ",
                "with it often. As trust builds ",
                "the cat will become more ",
                "comfortable in your presence, ",
                "and will be more receptive to ",
                "commands.",
            },
            behavior = {
                "Cats are very annoying ",
                "animals, to the point that ",
                "some may even call them a ",
                "pest. Their behavior in the ",
                "wild is somehow more tame ",
                "than their domesticated ",
                "behavior. They find immense ",
                "joy in running front of their ",
                "owner and even destroying ",
                "glass vessels. Despite this, ",
                "they are an incredibly popular ",
                "pet, especially for those who ",
                "don't often leave their home. ",
                "Like Wolves, a tamed Cat will ",
                "follow commands, but only if it ",
                "highly trusts it's owner."
            }
        }
    },
    ["animalia:chicken"] = {
        info = {
            domestication = {
                "Chickens are very valuable as a ",
                "livestock. They're a good ",
                "source of meat, but also lay ",
                "eggs. This, paired with their ",
                "small size, makes them great ",
                "for farming with limited space."
            },
            behavior = {
                "Chickens, or Jungle Fowl, are ",
                "most often found in groups. ",
                "They exhibit gender ",
                "dimorphism to a high degree, ",
                "with males having large tail ",
                "feathers. In the wild, they ",
                "dwell jungle floors, picking up ",
                "seeds and insects."
            }
        }
    },
    ["animalia:cow"] = {
        info = {
            domestication = {
                "Cows are commonplace on ",
                "farms because of their many ",
                "uses. They can be slaughtered ",
                "for beef and leather, and ",
                "females can be milked. Beef is ",
                "one of the most valuable ",
                "meats because of how much ",
                "satiation it provides, and ",
                "leather is valuable for crafting ",
                "various items."
            },
            behavior = {
                "Cows are always found in ",
                "groups of 3+ individuals. ",
                "Despite being capable of ",
                "inflicting damage, they will ",
                "always choose to flee, even ",
                "when in a large group. They ",
                "exhibit gender dimorphism, ",
                "with females having udders on ",
                "their belly."
            },
        }
    },
    ["animalia:frog"] = {
        info = {
            domestication = {
                "Cannot be tamed.",
            },
            behavior = {
                "Frogs are small creatures ",
                "almost exclusively found near ",
                "bodies of water. They will flee ",
                "to nearby water when a Player ",
                "approaches. They have quite ",
                "an affinity for water, moving ",
                "faster while in it and only ",
                "being able to breed when ",
                "submerged. They come to land ",
                "to search for food, which they ",
                "catch with their long tongue."
            },
        }
    },
    ["animalia:horse"] = {
        info = {
            domestication = {
                "Horses are one of the most ",
                "valuable animals to ",
                "domesticate because of their ",
                "ability carry Players and ",
                "maintain speed. They can ",
                "make traversing the world far ",
                "faster and easier, but aren't ",
                "easy to tame. To tame one, ",
                "you must keep your line of ",
                "sight lined up with the Horses ",
                "for a varying period of time. ",
                "This process is difficult but ",
                "well worth it."
            },
            behavior = {
                "Horses live in large groups, ",
                "wandering open grasslands. ",
                "They have a number of colors ",
                "and patterns, which are passed ",
                "down to their offspring, as ",
                "well as varying jumping and ",
                "running abilities."
            },
        }
    },
    ["animalia:reindeer"] = {
        info = {
            domestication = {
                "Cannot be tamed.",
            },
            behavior = {
                "Reindeer are found in large ",
                "groups in cold regions. They ",
                "stick tightly togther and move ",
                "in coordinated directions, even ",
                "while fleeing. They're also a ",
                "common food source for those ",
                "lost in taigas and tundras."
            }
        }
    },
    ["animalia:pig"] = {
        info = {
            domestication = {
                "Pigs are not quite as versatile ",
                "as other livestock like Cows or ",
                "Chickens, with their only ",
                "valuable resource being pork. ",
                "But they have a distinct ",
                "advantage by being able to ",
                "have more offspring at once ",
                "than Cows while also being ",
                "smaller."
            },
            behavior = {
                "Pigs in the wild can be very ",
                "destructive of ecosystems if ",
                "not controlled. Their ability to ",
                "reproduce quickly means ",
                "keeping populations under ",
                "control can be an issue. They ",
                "are known to destroy farmland ",
                "and will go as far as destroying ",
                "fences to do so."
            },
        }
    },
    ["animalia:sheep"] = {
        info = {
            domestication = {
                "Sheep are one of the most ",
                "useful animals to domesticate. ",
                "Their wool is a great resource ",
                "for crafting and building, and is ",
                "entirely renewable. Their wool ",
                "can also be dyed, though there ",
                "is little use for this."
            },
            behavior = {
                "Sheep are well known for ",
                "living in large groups. In the ",
                "wild these groups range from 4 ",
                "to 8 individuals, larger than ",
                "most other animals."
            }
        }
    },
    ["animalia:tropical_fish"] = {
        special_models = {
            [3] = "animalia_angelfish.b3d"
        },
        info = {
            domestication = {
                "Cannot be tamed."
            },
            behavior = {
                "All varieties of Tropical Fish ",
                "can be found in schools around ",
                "reefs. While they don't ",
                "provide food or any resources, ",
                "they are a beautiful sight to ",
                "see while traversing oceans."
            },
        }
    },
    ["animalia:turkey"] = {
        info = {
            domestication = {
                "Even though Turkeys take up ",
                "more space than Chickens, ",
                "they also produce more meat, ",
                "at the cost of laying less eggs. ",
                "This makes them a good option ",
                "for those who don't want to ",
                "build a farm large enough to ",
                "support Cows or other large ",
                "livestock but also don't need ",
                "many eggs."
            },
            behavior = {
                "Turkeys are similar ",
                "behaviorally to Chickens, but ",
                "spawn in colder biomes and ",
                "are slightly larger. They exhibit ",
                "gender dimorphism, with ",
                "males having a large fan of ",
                "feathers on their tail."
            }
        }
    },
    ["animalia:wolf"] = {
        info = {
            domestication = {
                "Their intelligence allows them ",
                "not only to form tight bonds ",
                "with players, but to also obey ",
                "orders. Once ordered to attack ",
                "a target, they will pursue it and ",
                "attack relentlessly, even if ",
                "death certain."
            },
            behavior = {
                "Wolves are found in packs of ",
                "up to 3. They hunt down Sheep ",
                "as a group and can quickly ",
                "overwhelm their target with ",
                "numbers. They're also ",
                "remarkebly intelligent, and ",
                "will remember players who ",
                "have harmed them and will ",
                "attack them on sight."
            }
        }
    }
}

-- Libri Utilities --

local function offset_info_text(offset_x, offset_y, tbl)
    local info_text = {}
    for i = 1, #tbl do
        local str = tbl[i]
        local center_offset = 0
        if string.len(str) < 30 then
            center_offset = (30 - string.len(str)) * 0.05
        end
        table.insert(info_text, "label[" .. offset_x + center_offset .. "," .. offset_y + i * 0.25 .. ";" .. minetest.colorize("#383329", tbl[i] .. "\n") .. "]")
    end
    return table.concat(info_text, "")
end

local function get_libri_page(mob_name, player_name)
    local def = minetest.registered_entities[mob_name]
    local animal_info = libri_animal_info[mob_name]
    -- Get Inventory Cube and Mob Texture
    local biome_group = spawn_biomes[mob_name]
    local spawn_biome = animalia.registered_biome_groups[biome_group].biomes[animalia_libri_info[player_name].biome_idx] or "grassland"
    local invcube
    if not minetest.registered_biomes[spawn_biome]
    or not minetest.registered_biomes[spawn_biome].node_top then
        invcube = get_inventory_cube("unknown_node.png")
    else
        invcube = get_inventory_cube(animal_info.invcube or minetest.registered_biomes[spawn_biome].node_top)
    end
    local texture = get_textures(mob_name)[animalia_libri_info[player_name].texture_idx]
    local mesh = def.mesh
    if libri_animal_info[mob_name].special_models
    and libri_animal_info[mob_name].special_models[animalia_libri_info[player_name].texture_idx] then
        mesh = libri_animal_info[mob_name].special_models[animalia_libri_info[player_name].texture_idx]
    end
    -- Create Formspec
    local form = {
        -- Background
        "formspec_version[3]",
        "size[16,10]",
        "background[-0.7,-0.5;17.5,11.5;animalia_libri_bg_v2.png]",
        "image[-0.7,-0.5;17.5,11.5;animalia_libri_info_fg.png]",
        -- Mesh
        "model[1.5,1.5;5,5;libri_mesh;" .. mesh .. ";" .. texture .. ";-30,225;false;false;0,0;0]",
        -- Spawn Biome Group
        "image[0.825,8.15;1,1;" .. invcube .. "]",
        "tooltip[0.825,8.15;1,1;" .. correct_name(spawn_biome) .. "]",
        -- Health
        "image[2.535,8.15;1,1;animalia_libri_health_fg.png]",
        "label[3.25,9;x" .. def.max_health / 2 .. "]",
        -- Net
        "item_image[4.25,8.15;1,1;animalia:lasso]",
        "image[4.75,8.75;0.5,0.5;animalia_libri_true_icon.png]",
        -- Lasso
        "item_image[6,8.15;1,1;animalia:net]",
        "image[6.5,8.75;0.5,0.5;animalia_libri_true_icon.png]",
        -- Labels
        "label[9.5,7.25;" .. minetest.colorize("#383329", "Drops:") .. "]",
        "label[14,7.25;" .. minetest.colorize("#383329", "Eats:") .. "]",
        -- Info Text
        "label[9.25,1.5;" .. minetest.colorize("#000000", "Domestication:") .. "]",
        "label[13.5,1.5;" .. minetest.colorize("#000000", "Behavior:") .. "]",
    }
    -- Mob Info
    if libri_animal_info[mob_name] then
        if libri_animal_info[mob_name].info.domestication then
            table.insert(form, offset_info_text(8.5, 2, libri_animal_info[mob_name].info.domestication))
        end
        if libri_animal_info[mob_name].info.behavior then
            table.insert(form, offset_info_text(12.5, 2, libri_animal_info[mob_name].info.behavior))
        end
    end
    if def.follow then
        table.insert(form, animalia.get_item_list(def.follow, 12.35, 8.05))
    end
    if def.drops then
        local drops = {}
        for i = 1, #def.drops do
            table.insert(drops, def.drops[i].name)
        end
        table.insert(form, animalia.get_item_list(drops, 8, 8.05))
    end
    return table.concat(form, "")
end

local function update_libri(player_name, mob_name)
    if not animalia_libri_info[player_name]
    or animalia_libri_info[player_name].name ~= mob_name then
        return
    end
    local texture_idx = animalia_libri_info[player_name].texture_idx or 1
    local biome_idx = animalia_libri_info[player_name].biome_idx or 1
    if texture_idx >= #get_textures(mob_name) then
        texture_idx = 1
    else
        texture_idx = texture_idx + 1
    end
    local spawn_biomes = animalia.registered_biome_groups[spawn_biomes[mob_name]].biomes
    if biome_idx >= #spawn_biomes then
        biome_idx = 1
    else
        biome_idx = biome_idx + 1
    end
    animalia_libri_info[player_name] = {
        texture_idx = texture_idx,
        biome_idx = biome_idx,
        name = mob_name
    }
    minetest.show_formspec(player_name, "animalia:libri_" .. string.split(mob_name, ":")[2], get_libri_page(mob_name, player_name))
    minetest.after(4, function()
        update_libri(player_name, mob_name)
    end)
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
    local player_name = player:get_player_name()
	if formname == "animalia:libri_main" then
        animalia_libri_info[player_name] = {}
        for i = 1, #animalia.animals do
            local name = string.split(animalia.animals[i], ":")[2]
            if fields["pg_" .. name] then
                -- Get data for mob and biome visuals
                animalia_libri_info[player_name] = {
                    texture_idx = 1,
                    biome_idx = 1,
                    name = animalia.animals[i]
                }
                update_libri(player_name, animalia.animals[i])
                break
            end
        end
        if fields["btn_next"] then
            local pages = animalia.libri_pages[player_name]
            if pages
            and #pages > 1 then
                animalia.show_libri_main_form(player, pages, 2)
            end
        end
	end
    if formname:match("^animalia:libri_") then
        if fields.quit or fields.key_enter then
            animalia_libri_info[player_name] = nil
        end
    end
end)