pr_villians.registered_henchmen = {}

pr_villians.henchmen = {}
pr_villians.henchmen.registered_callbacks = {
    on_activate = {},
    on_spawn = {},
    on_attack = {},
    on_punch = {}
}

function pr_villians.register_henchman(name, def)
    local modname = pr_villians.split_string(name, ":")[1]
    local itemname = pr_villians.split_string(name, ":")[2]

    -- defaults
    def.hp_max = def.hp_max or 100
    def.hp = def.hp or 100
    def.damage = def.damage or 1
    def.water_damage = def.water_damage or 1
    def.lifetime = def.lifetime or 120
    def.damageinterval = def.damageinterval or 0
    def.weight = def.weight or 155
    def.movingspeed = def.movingspeed or 2
    def.swimmingspeed = def.swimmingspeed or def.movingspeed
    def.max_free_fall = def.max_free_fall or 4
    def.benefit = def.benefit or 1
    def.killed_particle = def.killed_particle or "henchmen_killed_particle.png"

    -- bool defaults
    if def.attacks_players == nil then
        def.attacks_players = true
    end
    if def.attacks_own_kind == nil then
        def.attacks_own_kind = false
    end
    if def.description_is_plural == nil then
        def.description_is_plural = false
    end
    if def.benefits_on_attack == nil then
        def.benefits_on_attack = false
    end
    if def.can_swim == nil then
        def.can_swim = true
    end
    if def.teleports_around_player == nil then
        def.teleports_around_player = false
    end

    def.name = name
    pr_villians.henchmen[name] = def

    if def.on_activate ~= nil then
        table.insert(pr_villians.henchmen.registered_callbacks.on_activate, def.on_activate)
    end

    if def.on_spawn ~= nil then
        table.insert(pr_villians.henchmen.registered_callbacks.on_spawn, def.on_spawn)
    end

    if def.on_attack ~= nil then
        table.insert(pr_villians.henchmen.registered_callbacks.on_attack, def.on_attack)
    end

    if def.on_punch ~= nil then
        table.insert(pr_villians.henchmen.registered_callbacks.on_punch, def.on_punch)
    end

    minetest.register_entity(name, {
        -- entity properties

        hp_max = def.hp_max,
        collisionbox = {-0.35, -0.01, -0.35, 0.35, 1.8, 0.35},
        physical = true,
        visual = "mesh",
        mesh = "character.b3d", -- the model & texture is used from the player_ap mod
        textures = {def.texture}, -- so you can borrow resurses from other mods by adding them in the mod.conf --> depends 
        -- (before it was called depends.txt, now it is outdated) 

        -- you can declare variables here too, will be able in "self"

        animation = {
            stand = {
                x = 0,
                y = 79,
                speed = 30,
                loop = false
            },
            walk = {
                x = 168,
                y = 187,
                speed = 30
            },
            run = {
                x = 168,
                y = 187,
                speed = 40
            },
            attack = {
                x = 200,
                y = 219,
                speed = 30
            }
        },
        type = "npc",
        team = "default",
        time = 0,
        hp = def.hp,
        weight = def.weight,
        damage = def.damage,
        water_damage = def.water_damage,
        id = 0,
        movingspeed = def.movingspeed,
        swimmingspeed = def.swimmingspeed,
        damageinterval = def.damageinterval,
        damageinterval_time = 0,
        lifetime = def.lifetime,
        can_swim = def.can_swim,
        is_swimming = false,
        attacks_players = def.attacks_players,
        attacks_own_kind = def.attacks_own_kind,
        benefits_on_attack = def.benefits_on_attack,
        benefit = def.benefit,
        teleports_around_player = def.teleports_around_player,
        killed_particle = def.killed_particle,

        -- called while the entity is activate and deactivated, the string will be saved in the entity
        -- i like to do a "storage" variable so everyting in it will be automacly saved, simple

        get_staticdata = function(self)
            self.storage.hp = self.hp
            return minetest.serialize(self.storage)
        end,

        on_activate = function(self, staticdata)
            pcall(function()
                self.storage = minetest.deserialize(staticdata) or {} -- load storage or a new
                self.hp = self.storage.hp
                self.object:set_velocity({
                    x = 0,
                    y = -1,
                    z = 0
                })
                self.object:set_acceleration({
                    x = 0,
                    y = -10,
                    z = 0
                }) -- set the entity gravity.
                self.id = math.random(1, 9999) -- so the mob can determine difference from other mobs
                self.falling_to_punch = false
                self.fall_distance = 0
                self.kickback = false
                self.tap_time = 0
                self.tap_interval = math.random(2, 15)
                anim(self, "stand")

                for k, v in pairs(pr_villians.henchmen.registered_callbacks.on_activate) do
                    v(self, staticdata)
                end
            end)
        end,

        on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
            local en = puncher:get_luaentity()
            local dmg = 0
            self.last_punched_by = puncher or self.last_punched_by
            dmg = tool_capabilities.damage_groups.fleshy or 1

            if puncher:is_player() then -- adds wear to the tool, advanced stuff
                if self.attacks_players then
                    self.target = puncher
                end
                local look_dir = puncher:get_look_dir()
                if not self.kickback then
                    stand(self)
                end
                self.kickback = true
                self.object:add_velocity({
                    x = look_dir.x * (self.movingspeed * 2),
                    y = look_dir.y * (self.movingspeed * 2),
                    z = look_dir.z * (self.movingspeed * 2)
                })
                local item = minetest.registered_tools[puncher:get_wielded_item():get_name()]
                if item and item.tool_capabilities and item.tool_capabilities.damage_groups then
                    local d = tool_capabilities.damage_groups.fleshy
                    if d and d > 0 then
                        local tool = puncher:get_wielded_item()
                        tool:add_wear(math.ceil(self.hp_max / (dmg * dmg)))
                        puncher:set_wielded_item(tool)
                    end
                end
                for k, v in pairs(pr_villians.henchmen.registered_callbacks.on_punch) do
                    v(self, puncher, time_from_last_punch, tool_capabilities, dir, "player")
                end

            end
            self.hp = self.hp - dmg
            self.object:set_properties({
                nametag = self.hp .. " / " .. self.hp_max,
                nametag_color = "#00ff00"
            })
            if self.hp <= 0 then
                kill(self)
            end

            return self
        end,

        on_step = function(self, dtime)
            if self.object ~= nil then
                pcall(function()
                    if self.object == nil then
                        return
                    end

                    -- updating the mob each 0.5s is enough and makes it not lag
                    self.time = self.time + dtime
                    self.lifetime = self.lifetime - dtime
                    self.damageinterval_time = self.damageinterval_time + dtime
                    if self.lifetime <= 0 then -- removing the mob after a time, or there will be too many
                        self.object:remove()
                        return
                    elseif self.target and self.time < 0.1 or not self.target and self.time < 0.5 then
                        return self
                    end
                    self.time = 0

                    if self.object == nil then
                        return
                    end

                    local pos = self.object:get_pos()

                    -- kill the mob if it is in water and cannot swim
                    if not self.can_swim then
                        if pr_villians.check_if_any_group(self.object:get_pos(), {"water"}) then
                            self.object:punch(self.object, 1, {
                                full_punch_interval = 1,
                                damage_groups = {
                                    fleshy = self.water_damage
                                }
                            })
                            for k, v in pairs(pr_villians.henchmen.registered_callbacks.on_punch) do
                                v(self, puncher, 1, {
                                    full_punch_interval = 1,
                                    damage_groups = {
                                        fleshy = self.damage
                                    }
                                }, nil, "drowning")
                            end
                        end
                    end

                    if self.object == nil then
                        return
                    end

                    -- swimming in water
                    local swim_pos = self.object:get_pos()
                    local swim_pos_above = {
                        x = swim_pos.x,
                        y = swim_pos.y + 1,
                        z = swim_pos.z
                    }
                    if pr_villians.check_if_any_group(self.object:get_pos(), {"water"}) then
                        self.is_swimming = true
                        if self.can_swim then
                            local vel = self.object:get_velocity() or {
                                x = 0,
                                y = 0,
                                z = 0
                            }
                            if not pr_villians.check_if_any_group(swim_pos_above, {"water"}) then
                                self.object:set_acceleration({
                                    x = 0,
                                    y = 0,
                                    z = 0
                                })
                                self.object:add_velocity({
                                    x = 0,
                                    y = 0 - vel.y,
                                    z = 0
                                })
                            else
                                self.object:set_acceleration({
                                    x = 0,
                                    y = 4,
                                    z = 0
                                })
                                self.object:add_velocity({
                                    x = 0,
                                    y = 0 - vel.y,
                                    z = 0
                                })
                            end
                        else
                            self.object:set_acceleration({
                                x = 0,
                                y = -2,
                                z = 0
                            })
                        end
                    else
                        self.is_swimming = false
                        self.object:set_acceleration({
                            x = 0,
                            y = -10,
                            z = 0
                        }) -- set back to normal gravity.
                    end

                    if self.object == nil then
                        return
                    end

                    -- rnd walking
                    if self.target == nil then
                        local r = math.random(1, 4)
                        local v = self.object:get_velocity() or {
                            x = 0,
                            y = 0,
                            z = 0
                        }
                        if r == 1 then -- walk randomly
                            self.object:set_yaw(math.random(0, 6.28))
                            walk(self)
                        elseif r == 2 then
                            stand(self)
                        else
                            walk(self)
                        end
                    end

                    if self.object == nil then
                        return
                    end

                    -- punch after free fall
                    self.max_free_fall = self.max_free_fall or 4
                    local line_of_sight = minetest.line_of_sight(self.object:get_pos(), {
                        x = self.object:get_pos().x,
                        y = self.object:get_pos().y - self.max_free_fall,
                        z = self.object:get_pos().z
                    })
                    if line_of_sight == true then
                        local top_pos = self.object:get_pos()
                        local raycast = Raycast(top_pos, {
                            x = top_pos.x,
                            y = top_pos.y - (self.max_free_fall + 100),
                            z = top_pos.z
                        })
                        local bottom_pos = get_node_pos_raycast(raycast)
                        self.fall_distance = vector.distance(top_pos, bottom_pos)
                        self.falling_to_punch = true
                    end

                    if self.object == nil then
                        return
                    end

                    if self.falling_to_punch then
                        local node_under = {
                            x = self.object:get_pos().x,
                            y = self.object:get_pos().y - 1,
                            z = self.object:get_pos().z
                        }
                        if not pr_villians.check_if_any_group(node_under, {"water"}) then
                            local velocity = self.object:get_velocity() or {
                                x = 0,
                                y = 0,
                                z = 0
                            }
                            if velocity.y > -0.2 and velocity.y < 0.2 then
                                local damage = (self.weight * 0.047) * self.fall_distance
                                self.object:punch(self.object, 1, {
                                    full_punch_interval = 1,
                                    damage_groups = {
                                        fleshy = damage
                                    }
                                })
                                self.falling_to_punch = false
                            end
                        end
                    end

                    -- look for targets
                    if self.target == nil then
                        local rnd_target
                        for _, ob in pairs(minetest.get_objects_inside_radius(pos, 10)) do
                            local en = ob:get_luaentity() -- players do not have this property
                            local obp = ob:get_pos()

                            if is_enemy(self, ob) then
                                if (en == nil or en and en.id ~= self.id) and visible(self, obp) and
                                    viewfield(self, obp) then
                                    rnd_target = ob
                                    if math.random(1, 3) == 1 then -- choosing random targets
                                        break
                                    end
                                end
                            end
                        end
                        self.target = rnd_target
                    end

                    if self.object == nil then
                        return
                    end

                    -- attack target
                    if self.target then
                        local tarp = self.target:get_pos()
                        if tarp == nil or visible(self, tarp) == false or viewfield(self, tarp) == false and self.target ~=
                            self.last_punched_by then -- sometimes the object is gone but not the object
                            self.target = nil
                            return
                        end

                        lookat(self, tarp)

                        if self.kickback == true then
                            if not slow_velocity(self) then
                                return
                            end
                            self.kickback = false
                        end

                        walk(self, 2)
                        self.lifetime = 120 -- resets lifetime

                        if vector.distance(pos, tarp) <= 3 then
                            anim(self, "attack")
                            if self.damageinterval_time >= self.damageinterval then
                                if self.target:is_player() then
                                    if core.setting_getbool("enable_damage") then
                                        self.target:punch(self.object, 1, {
                                            full_punch_interval = 1,
                                            damage_groups = {
                                                fleshy = self.damage
                                            }
                                        })
                                    end
                                else
                                    self.target:punch(self.object, 1, {
                                        full_punch_interval = 1,
                                        damage_groups = {
                                            fleshy = self.damage
                                        }
                                    })
                                end
                                if self.benefits_on_attack then
                                    self.hp = self.hp + def.benefit
                                    if self.hp > self.hp_max then
                                        self.hp = self.hp_max
                                    end
                                    self.object:set_properties({
                                        nametag = self.hp .. " / " .. self.hp_max,
                                        nametag_color = "#00ff00"
                                    })
                                end
                                for k, v in pairs(pr_villians.henchmen.registered_callbacks.on_attack) do
                                    v(self, 1, {
                                        full_punch_interval = 1,
                                        damage_groups = {
                                            fleshy = self.damage
                                        }
                                    })
                                end
                            end
                            if self.target:get_hp() <= 0 then
                                self.target = nil
                            end
                        end
                    end

                    if self.object == nil then
                        return
                    end

                    -- teleport around player
                    if self.teleports_around_player then
                        self.tap_time = self.tap_time + dtime
                        if self.tap_time > self.tap_interval then
                            if self.target then
                                self.object:set_pos(find_pos_near(self.target:get_pos()))
                            end
                            self.tap_interval = math.random(2, 15)
                            self.tap_time = 0
                        end
                    end

                    -- reset damage interval time
                    if self.damageinterval_time >= self.damageinterval then
                        self.damageinterval_time = 0
                    end

                    if self.object == nil then
                        return
                    end

                    if self.can_swim then
                        if pr_villians.check_if_any_group({
                            x = pos.x,
                            y = pos.y - 1,
                            z = pos.z
                        }, {"cracky", "soil", "stone", "crumbly", "wood"}) and
                            not pr_villians.check_if_any_group(posinfront(self, 1), {"water"}) and
                            not pr_villians.check_if_any_group(posinfront(self, 2), {"water"}) then
                            jump(self)
                        end
                    else
                        if pr_villians.check_if_any_group({
                            x = pos.x,
                            y = pos.y - 1,
                            z = pos.z
                        }, {"cracky", "soil", "stone", "crumbly", "wood"}) then
                            jump(self)
                        end
                    end
                end)
            end
        end
    })

    local spawner = def.spawner
    spawner.henchman = name
    spawner.spawn_amount = def.spawn_amount or 1

    pr_villians.register_spawner(spawner.name, spawner)
end

function pr_villians.register_spawner(name, def)
    local on_place = def.on_place
    minetest.register_craftitem(name, {
        description = def.description,
        inventory_image = def.inventory_image,
        on_place = function(itemstack, user, pointed_thing)
            if pointed_thing.type == "node" then
                local p = pointed_thing.above
                pr_villians.spawn(def.henchman, {
                    x = p.x,
                    y = p.y + 1,
                    z = p.z
                }, def.spawn_amount)
                if not core.setting_getbool("creative_mode") then
                    itemstack:take_item()
                end
            end
            if on_place ~= nil then
                on_place(itemstack, user, pointed_thing)
            end
            return itemstack
        end
    })
end

function pr_villians.spawn(name, pos, amount)
    for i = 1, amount do
        minetest.add_entity(pos, name):set_yaw(math.random(0, 6.28))
    end

    for i, v in ipairs(pr_villians.henchmen.registered_callbacks.on_spawn) do
        v(name, pos, amount)
    end
end

function walk(self)
    if math.random(1, 3) > 1 then -- stands at 1
        local v = self.object:get_velocity() or {
            x = 0,
            y = 0,
            z = 0
        }
        self.object:set_velocity({
            x = math.random(-1, 1),
            y = v.y,
            z = math.random(-1, 1)
        })
    end
end

-- check if the anim exists, use it if it's not same as the last
function anim(self, type)
    if self.visual ~= "mesh" or type == self.anim or not self.animation then
        return
    end
    local a = self.animation[type]
    if not a then
        return
    end
    self.object:set_animation({
        x = a.x,
        y = a.y
    }, a.speed, 0, a.loop)
    self.anim = type
end

function jump(self)
    local v = self.object:get_velocity() or {
        x = 0,
        y = 0,
        z = 0
    }
    if v.y == 0 then -- dont jump in air
        self.object:set_velocity({
            x = v.x,
            y = 5.5,
            z = v.z
        })
    end
end

function stand(self)
    local v = self.object:get_velocity() or {
        x = 0,
        y = 0,
        z = 0
    }
    self.object:set_velocity({
        x = 0,
        y = v.y, -- keep falling
        z = 0
    })
    anim(self, "stand")
end

function walk(self, speed)
    speed = speed and (self.movingspeed * speed) or self.movingspeed -- choosing movingspeed as default if speed is nil

    local yaw = get_entity_yaw(self.object)
    local v = self.object:get_velocity() or {
        x = 0,
        y = 0,
        z = 0
    }
    local x = (math.sin(yaw) * -1) * speed
    local z = (math.cos(yaw) * 1) * speed

    if self.is_swimming then
        if self.swimmingspeed == nil then
            x = (x / 5) * 3
            z = (z / 5) * 3
        else
            x = (math.sin(yaw) * -1) * self.swimmingspeed
            z = (math.cos(yaw) * 1) * self.swimmingspeed
        end
    end

    if not self.can_swim then
        if not pr_villians.check_if_any_group(posinfront(self, 1), {"water"}) then
            self.object:set_velocity({
                x = x or 0,
                y = v.y or 0,
                z = z or 0
            })
        else
            stand(self)
        end
    else
        self.object:set_velocity({
            x = x,
            y = v.y,
            z = z
        })
    end

    if speed > self.movingspeed then
        anim(self, "run")
    else
        anim(self, "walk")
    end
end

function lookat(self, pos2)
    local pos1 = self.object:get_pos() or {
        x = 0,
        y = 0,
        z = 0
    }
    local vec = {
        x = pos1.x - pos2.x,
        y = pos1.y - pos2.y,
        z = pos1.z - pos2.z
    }
    local yaw = math.atan(vec.z / vec.x) - math.pi / 2
    if pos1.x >= pos2.x then
        yaw = yaw + math.pi
    end
    self.object:set_yaw(yaw)
end

function is_enemy(self, obj)
    if obj:is_player() then
        return self.attacks_players
    elseif pr_villians.henchmen[obj:get_luaentity().name] == nil then
        return true
    end

    if obj:get_luaentity().attacks_own_kind == true then
        return true
    end

    return false
end

function kill(self)
    local pos = self.object:get_pos() or {
        x = 0,
        y = 0,
        z = 0
    }
    self.object:remove()
    minetest.add_particlespawner({
        amount = 5,
        time = 0.2,
        minpos = vector.subtract(pos, 1 / 2),
        maxpos = vector.add(pos, 1 / 2),
        minvel = {
            x = -1,
            y = -1,
            z = -1
        },
        maxvel = {
            x = 1,
            y = 1,
            z = 1
        },
        minacc = vector.new(),
        maxacc = vector.new(),
        minexptime = 1,
        maxexptime = 1,
        minsize = 7,
        maxsize = 15,
        texture = self.killed_particle
    })
end

function visible(self, pos2) -- checking if someting is blocking the mobs view
    local pos1 = self.object:get_pos() or {
        x = 0,
        y = 0,
        z = 0
    }
    local v = {
        x = pos1.x - pos2.x,
        y = pos1.y - pos2.y - 1,
        z = pos1.z - pos2.z
    }
    v.y = v.y - 1
    local amount = (v.x ^ 2 + v.y ^ 2 + v.z ^ 2) ^ 0.5
    local d = vector.distance(pos1, pos2)
    v.x = (v.x / amount) * -1
    v.y = (v.y / amount) * -1
    v.z = (v.z / amount) * -1
    for i = 1, d, 1 do
        local node = minetest.registered_nodes[minetest.get_node({
            x = pos1.x + (v.x * i),
            y = pos1.y + (v.y * i),
            z = pos1.z + (v.z * i)
        }).name]
        if node and node.walkable then
            return false
        end
    end
    return true
end

function viewfield(self, p2) -- if target is in the view field
    local ob1 = self.object
    local ob1_yaw = get_entity_yaw(ob1)
    local p1 = ob1:get_pos() or {
        x = 0,
        y = 0,
        z = 0
    }
    local a = vector.normalize(vector.subtract(p2, p1))
    local yaw = math.floor(ob1_yaw * 100) / 100
    local b = {
        x = math.sin(yaw) * -1,
        y = 0,
        z = math.cos(yaw) * 1
    }
    local deg = math.acos((a.x * b.x) + (a.y * b.y) + (a.z * b.z)) * (180 / math.pi)
    return not (deg < 0 or deg > 50)
end

function get_entity_yaw(obj)
    if obj == nil then
        return 1
    end
    if obj:get_yaw() == nil then
        return 1
    end
    return obj:get_yaw()
end

function posinfront(self, distance)
    local yaw = get_entity_yaw(self.object)
    local p = self.object:get_pos() or {
        x = 0,
        y = 0,
        z = 0
    }
    distance = distance or 1
    local x = math.sin(yaw) * -distance
    local z = math.cos(yaw) * distance
    return vector.new(p.x + x, p.y - 1, p.z + z)
end

function slow_velocity(self)
    local vel = self.object:get_velocity() or {
        x = 0,
        y = 0,
        z = 0
    }
    local msp = self.movingspeed + (self.movingspeed * 0.5)
    local msn = (0 - self.movingspeed) - (self.movingspeed * 0.5)
    if vel.x < msp and vel.x > msn and vel.z < msp and vel.z > msn then
        return true
    end
    if self.object ~= nil then
        self.object:add_velocity({
            x = 0 - (vel.x * 0.1),
            y = 0,
            z = 0 - (vel.z * 0.1)
        })
    end
    return false
end

function get_node_pos_raycast(raycast)
    for pt in raycast do
        if pt.type == "node" then
            return pt.under
        end
    end
    return {
        x = 0,
        y = 0,
        z = 0
    }
end

function find_pos_near(pos)
    local max_dist = 8
    local max_x = pos.x + (max_dist / 2)
    local min_x = pos.x - (max_dist / 2)
    local max_z = pos.z + (max_dist / 2)
    local min_z = pos.z - (max_dist / 2)

    local x = math.random(min_x, max_x)
    local y = pos.y + 1
    local z = math.random(min_z, max_z)

    return {
        x = x,
        y = y,
        z = z
    }
end
