--[[
    Let the player craft Mob Spawners. Mobs are spawning randomly in a short intervals, giving the option of creating mob farms and grinders.
    Copyright (C) 2016 - 2023 SaKeL <juraj.vajda@gmail.com>

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

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

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to juraj.vajda@gmail.com
--]]

-- main tables
spawners_mobs = {
    ENABLED_MODS = {},
    MOBS_PROPS = {}
}
spawners_mobs.mob_tables = {}
local max_obj_per_mapblock = tonumber(minetest.settings:get('max_objects_per_block'))
local enable_particles = minetest.settings:get_bool('enable_particles')
local tick_max = 30
local tick_short_max = 20

-- check if mods exists and build tables
for k, mob_mod in ipairs(spawners_mobs.ENABLED_MODS) do
    local modpath = minetest.get_modpath(mob_mod)
    -- list of mobs and their info
    if (modpath) then
        for j, mob in ipairs(spawners_mobs.MOBS_PROPS[mob_mod]) do
            local mob_egg

            -- disabled extra check for mobs redo due to incompatibility with Lua 5.1, this method is available from Lua 5.2
            -- if mob_mod == 'mobs' and not (mobs.mod == 'redo') then goto continue end

            table.insert(spawners_mobs.mob_tables,
                {
                    name = mob.name,
                    mod_prefix = mob_mod,
                    egg_name_custom = mob.egg_name_custom,
                    dummy_size = mob.dummy_size,
                    dummy_offset = mob.dummy_offset,
                    dummy_mesh = mob.dummy_mesh,
                    dummy_texture = mob.dummy_texture,
                    night_only = mob.night_only,
                    sound_custom = mob.sound_custom
                }
            )
            -- use custom egg or create a default egg
            if mob.egg_name_custom ~= '' then
                mob_egg = mob.egg_name_custom
            else
                mob_egg = mob_mod .. ':' .. mob.name
            end

            -- recipes
            minetest.register_craft({
                output = 'spawners_mobs:' .. mob_mod .. '_' .. mob.name .. '_spawner',
                recipe = {
                    { 'default:diamondblock', 'fire:flint_and_steel', 'default:diamondblock' },
                    { 'xpanes:bar_flat', mob_egg, 'xpanes:bar_flat' },
                    { 'default:diamondblock', 'xpanes:bar_flat', 'default:diamondblock' },
                }
            })

            -- Lua > 5.1
            -- ::continue::
        end
    end
end

--
-- Particles
--
function spawners_mobs.cloud_booom(pos)
    if not enable_particles then return end

    minetest.add_particlespawner({
        amount = 5,
        time = 2,
        minpos = vector.subtract({ x = pos.x - 0.3, y = pos.y, z = pos.z - 0.3 }, 0.3),
        maxpos = vector.add({ x= pos.x + 0.3, y = pos.y, z = pos.z + 0.3 }, 0.3),
        minvel = { x = 0.1, y = 0.1, z = 0.1 },
        maxvel = { x = 0.2, y = 0.2, z = 0.2 },
        minacc = vector.new({ x = -0.1, y = 0.3, z = -0.1 }),
        maxacc = vector.new({ x = 0.1, y = 0.6, z = 0.1 }),
        minexptime = 2,
        maxexptime = 3,
        minsize = 16,
        maxsize = 24,
        texture = 'spawners_mobs_smoke_particle_2.png',
        animation = {
            type = 'vertical_frames',
            -- Width of a frame in pixels
            aspect_w = 16,
            -- Height of a frame in pixels
            aspect_h = 16,
            -- Full loop length
            length = 2.0,
        },
    })
end

function spawners_mobs.add_flame_effects(pos)
    if not enable_particles then return end

    return minetest.add_particlespawner({
        amount = 6,
        time = 0,
        minpos = vector.subtract({ x = pos.x - 0.001, y = pos.y - 0.001, z = pos.z - 0.001 }, 0.5),
        maxpos = vector.add({ x = pos.x + 0.001, y = pos.y + 0.001, z = pos.z + 0.001 }, 0.5),
        minvel = { x = -0.1, y = -0.1, z = -0.1 },
        maxvel = { x = 0.1, y = 0.1, z = 0.1 },
        minacc = vector.new(),
        maxacc = vector.new(),
        minexptime = 1,
        maxexptime = 5,
        minsize = 0.5,
        maxsize = 2.5,
        texture = 'spawners_mobs_flame_particle_2.png',
    })
end

function spawners_mobs.add_smoke_effects(pos)
    if not enable_particles then return end

    return minetest.add_particlespawner({
        amount = 1,
        time = 0,
        minpos = vector.subtract({ x = pos.x - 0.001, y = pos.y - 0.001, z = pos.z - 0.001 }, 0.5),
        maxpos = vector.add({ x = pos.x + 0.001, y = pos.y + 0.001, z = pos.z + 0.001 }, 0.5),
        minvel = { x = -0.5, y = 0.5, z = -0.5 },
        maxvel = { x = 0.5, y = 1.5, z = 0.5 },
        minacc = vector.new({ x = -0.1, y = 0.1, z = -0.1 }),
        maxacc = vector.new({ x = 0.1, y = 0.3, z = 0.1 }),
        minexptime = 0.5,
        maxexptime = 2,
        minsize = 0.5,
        maxsize = 2,
        texture = 'spawners_mobs_smoke_particle.png^[transform' .. math.random(0, 3),
    })
end

--
-- Timers
--
-- how often node timers for spawners will tick, +/- some random value
function spawners_mobs.tick(pos)
    local meta = minetest.get_meta(pos)
    local tick_counter = meta:get_int('tick')
    local owner = meta:get_string('owner')
    local privs = minetest.get_player_privs(owner);

    -- not for admin
    if not privs.privs then
        tick_counter = tick_counter + 1
        meta:set_int('tick', tick_counter)
    end

    -- print('tick_counter: ' .. tick_counter .. ' at ' .. minetest.pos_to_string(pos))

    -- rusty spawner
    if tick_counter >= tick_max then
        spawners_mobs.set_status(pos, 'rusty')
        return
    end

    minetest.get_node_timer(pos):start(math.random(72, 193))
    -- minetest.get_node_timer(pos):start(math.random(20, 30))
end

-- how often a spawn failure tick is retried (e.g. too dark)
function spawners_mobs.tick_short(pos)
    local meta = minetest.get_meta(pos)
    local tick_short_counter = meta:get_int('tick_short')

    if tick_short_counter >= tick_short_max then
        spawners_mobs.tick(pos)
        return
    else
        tick_short_counter = tick_short_counter + 1
        meta:set_int('tick_short', tick_short_counter)
        -- print('tick_short_counter: ' .. tick_short_counter .. ' at ' .. minetest.pos_to_string(pos))
    end
    minetest.get_node_timer(pos):start(math.random(20, 60))
    -- minetest.get_node_timer(pos):start(math.random(10, 20))
end

--
-- Core Functions
--
-- start spawning mobs
function spawners_mobs.start_spawning(spawn_area_random_pos, mob_name, mod_prefix, sound_custom)
    if not (spawn_area_random_pos or mob_name) then
        return
    end

    local sound_name = mod_prefix .. '_' .. mob_name
    -- use custom sounds
    if sound_custom ~= '' then
        sound_name = sound_custom
    end

    -- use random colors for sheeps
    if mob_name == 'sheep_white' then
        local sheep_colors = { 'black', 'blue', 'brown', 'cyan', 'dark_green', 'dark_grey', 'green', 'grey', 'magenta', 'orange', 'pink', 'red', 'violet', 'white', 'yellow' }
        mob_name = 'sheep_' .. sheep_colors[math.random(#sheep_colors)]
    end

    for i = 1, #spawn_area_random_pos do
        -- spawn a bit more above the block - prevent spawning inside the block
        spawn_area_random_pos[i].y = spawn_area_random_pos[i].y + 0.5

        spawners_mobs.cloud_booom(spawn_area_random_pos[i])

        minetest.after(1, function()
            -- minetest.set_node(spawn_area_random_pos[i], {name = 'default:apple' })
            local obj = minetest.add_entity(spawn_area_random_pos[i], mod_prefix .. ':' .. mob_name)
            if obj then
                if sound_name then
                    minetest.sound_play(sound_name, {
                        pos = spawn_area_random_pos[i],
                        max_hear_distance = 16,
                        gain = 0.5
                    })
                end
            end
        end)
    end
end

function spawners_mobs.on_timer(pos, elapsed)
    local meta = minetest.get_meta(pos)
    local idx = meta:get_int('idx') or nil
    local mob_table = spawners_mobs.mob_tables[idx] or false

    if not mob_table then return end

    local posmin = { x = pos.x - 3, y = pos.y - 1, z = pos.z - 3 }
    local posmax = { x = pos.x + 4, y = pos.y + 1, z = pos.z + 4 }
    local player_near = false
    local entities_near = 0
    local entities_max = 6
    local node_light_min = 13

    local owner = meta:get_string('owner') or ''
    local mod_prefix = mob_table.mod_prefix
    local mob_name = mob_table.name
    local sound_custom = mob_table.sound_custom
    local night_only = mob_table.night_only
    local max_objects = max_obj_per_mapblock / 4

    -- check spawner light
    local node_light = minetest.get_node_light(pos)

    -- dark
    if (not node_light or node_light < node_light_min) and not night_only then
        -- print('Too dark for mob ( ' .. mob_name .. ' ) to spawn. Waiting for day .. .')
        spawners_mobs.set_status(pos, 'waiting')

        -- set infotext
        meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nToo dark for mob to spawn. Waiting for day .. .')
        spawners_mobs.tick_short(pos)
        return

    elseif node_light >= node_light_min and night_only then
        -- light
        -- print('Too much light for mob ( ' .. mob_name .. ' ) to spawn. Waiting for night .. .')
        spawners_mobs.set_status(pos, 'waiting')

        -- set infotext
        meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nToo much light for mob to spawn. Waiting for night .. .')
        spawners_mobs.tick_short(pos)
        return
    end

    -- positions where mobs can spawn
    local spawn_area_pos = minetest.find_nodes_in_area(posmin, posmax, 'air')

    -- check if there is enough place to spawn mob
    if #spawn_area_pos < 1 then
        spawners_mobs.set_status(pos, 'waiting')

        -- set infotext
        meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nNot enough place to spawn mob. Find more space!')
        spawners_mobs.tick(pos)
        return
    end

    -- spawn 2 mobs on 2 different positions by chance
    local how_many = math.random(2)
    local spawn_area_random_pos = {}

    -- get random spawn position from spawn area
    for i = 1, how_many do
        while #spawn_area_random_pos < how_many and #spawn_area_pos > 0 do

            local random_pos = spawn_area_pos[math.random(#spawn_area_pos)]
            local random_pos_above = minetest.get_node({ x = random_pos.x, y = random_pos.y + 1, z = random_pos.z }).name

            if random_pos_above == 'air' and not minetest.is_protected(random_pos, owner) then
                table.insert(spawn_area_random_pos, random_pos)
                -- print('spawn_area_random_pos: ' .. #spawn_area_random_pos)
            else
                table.remove(spawn_area_pos, i)
                -- print('spawn_area_pos: ' .. #spawn_area_pos)
            end

        end
    end

    -- print(dump(spawn_area_random_pos))

    -- check if there is still enough place to spawn mob
    if #spawn_area_random_pos < 1 then
        spawners_mobs.set_status(pos, 'waiting')

        -- set infotext
        meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nNot enough place to spawn mob. Searching for new location .. .')
        spawners_mobs.tick_short(pos)
        return
    end

    -- area where player and entity count will be detected
    local activation_area = minetest.get_objects_inside_radius(pos, 16)

    -- prevent object clutter on the map
    if #activation_area > max_objects then
        spawners_mobs.set_status(pos, 'waiting')

        -- set infotext
        meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nToo many objects in the area (' .. #activation_area .. '/' .. max_objects .. '), clean-up dropped objects first!')
        spawners_mobs.tick_short(pos)
        return
    end

    for k, object in ipairs(activation_area) do
        -- find player inside activation area
        if object:is_player() then
            player_near = true
            -- print('found player: ' .. object:get_player_name())
        end

        -- find entities inside activation area
        if not object:is_player()
            and object:get_luaentity()
            and object:get_luaentity().name ~= '__builtin:item'
        then
            local tmp_mob_name = string.split(object:get_luaentity().name, ':')[2]

            if tmp_mob_name ~= nil then
                -- sheeps have colors in names
                if string.find(tmp_mob_name, 'sheep') and string.find(mob_name, 'sheep') and not string.find(tmp_mob_name, 'dummy') then
                    -- print('found entity: ' .. tmp_mob_name)
                    entities_near = entities_near + 1

                elseif tmp_mob_name == mob_name then
                    -- print('found entity: ' .. tmp_mob_name)
                    entities_near = entities_near + 1
                end
            else
                minetest.log('warning', '[spawners_mobs] tmp_mob_name was nil, luaentity name was: ' .. object:get_luaentity().name .. ' at: ' .. minetest.pos_to_string(object:get_pos()))
            end
        end

        -- stop looping when met all conditions
        if entities_near >= entities_max and player_near then
            -- print('max entities reached ' .. entities_max .. ' and player_near found, breaking .. ')
            break
        end
    end

    -- don't do anything and try again later when player not near or max entities reached
    if entities_near >= entities_max or not player_near then
        spawners_mobs.set_status(pos, 'waiting')

        -- sheeps have color in the name
        -- local name = mob_name
        -- if string.find(mob_name, 'sheep') then
        --     name = 'sheep'
        -- end

        meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nmax mobs reached: ' .. entities_near .. '/' .. entities_max) -- or player not near
        spawners_mobs.tick_short(pos)
        return
    end

    -- start spawning
    spawners_mobs.start_spawning(spawn_area_random_pos, mob_name, mod_prefix, sound_custom)

    spawners_mobs.set_status(pos, 'active')
    meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nspawner is active reached: ' .. entities_near .. '/' .. entities_max)

    meta:set_int('tick', 0)
    meta:set_int('tick_short', 0)

    spawners_mobs.tick(pos)
end

--
-- Status Manager
--
function spawners_mobs.set_status(pos, set_status)
    local meta = minetest.get_meta(pos)
    local idx = meta:get_int('idx')
    local mob_table = spawners_mobs.mob_tables[idx] or false

    if not mob_table then return end

    local mod_prefix = mob_table.mod_prefix
    local mob_name = mob_table.name
    local offset = mob_table.dummy_offset

    -- get meta
    local owner = meta:get_string('owner')
    local meta_status = meta:get_string('status')
    local id_flame = meta:get_int('id_flame')
    local id_smoke = meta:get_int('id_smoke')

    --
    -- active
    --
    if set_status == 'active' then
        -- remove particles and add them again - keeps particles after server restart
        -- delete particles
        if id_flame ~= -1 and id_smoke ~= -1 then
            -- print('#1 delete id_flame: ' .. id_flame .. ' at ' .. minetest.pos_to_string(pos))
            -- print('#1 delete id_smoke: ' .. id_smoke .. ' at ' .. minetest.pos_to_string(pos))
            minetest.delete_particlespawner(id_flame)
            minetest.delete_particlespawner(id_smoke)
            meta:set_int('id_flame', -1)
            meta:set_int('id_smoke', -1)
        end

        -- add particles
        id_flame = spawners_mobs.add_flame_effects(pos)
        id_smoke = spawners_mobs.add_smoke_effects(pos)
        meta:set_int('id_flame', id_flame)
        meta:set_int('id_smoke', id_smoke)
        -- print('#1 add id_flame: ' .. id_flame .. ' at ' .. minetest.pos_to_string(pos))
        -- print('#1 add id_smoke: ' .. id_smoke .. ' at ' .. minetest.pos_to_string(pos))

        if meta_status ~= set_status then
            -- add dummy entity
            minetest.add_entity({ x = pos.x, y = pos.y + offset, z = pos.z },'spawners_mobs:dummy_' .. mod_prefix .. '_' .. mob_name)

            meta:set_string('status', 'active')

            minetest.swap_node(pos, { name = 'spawners_mobs:' .. mod_prefix .. '_' .. mob_name .. '_spawner' })
        end
    elseif set_status == 'waiting' and meta_status ~= set_status then
        --
        -- waiting
        --

        -- delete particles
        if id_flame ~= -1 and id_smoke ~= -1 then
            -- print('#2 delete id_flame: ' .. id_flame .. ' at ' .. minetest.pos_to_string(pos))
            -- print('#2 delete id_smoke: ' .. id_smoke .. ' at ' .. minetest.pos_to_string(pos))
            minetest.delete_particlespawner(id_flame)
            minetest.delete_particlespawner(id_smoke)
            meta:set_int('id_flame', -1)
            meta:set_int('id_smoke', -1)
        end

        -- remove dummy
        local objs = minetest.get_objects_inside_radius(pos, 0.5)
        if objs then
            for _, obj in ipairs(objs) do
                if obj and obj:get_luaentity() and obj:get_luaentity().name == 'spawners_mobs:dummy_' .. mod_prefix .. '_' .. mob_name then
                    obj:remove()
                end
            end
        end

        meta:set_string('status', 'waiting')

        minetest.swap_node(pos, { name = 'spawners_mobs:' .. mod_prefix .. '_' .. mob_name .. '_spawner_waiting' })
    elseif set_status == 'rusty' and meta_status ~= set_status then
        --
        -- rusty
        --

        -- delete particles
        if id_flame ~= -1 and id_smoke ~= -1 then
            -- print('#3 delete id_flame: ' .. id_flame .. ' at ' .. minetest.pos_to_string(pos))
            -- print('#3 delete id_smoke: ' .. id_smoke .. ' at ' .. minetest.pos_to_string(pos))
            minetest.delete_particlespawner(id_flame)
            minetest.delete_particlespawner(id_smoke)
            meta:set_int('id_flame', -1)
            meta:set_int('id_smoke', -1)
        end

        -- remove dummy
        local objs = minetest.get_objects_inside_radius(pos, 0.5)
        if objs then
            for _, obj in ipairs(objs) do
                if obj and obj:get_luaentity() and obj:get_luaentity().name == 'spawners_mobs:dummy_' .. mod_prefix .. '_' .. mob_name then
                    obj:remove()
                end
            end
        end

        meta:set_string('status', 'rusty')

        minetest.swap_node(pos, { name = 'spawners_mobs:' .. mod_prefix .. '_' .. mob_name .. '_spawner_rusty' })

        -- set infotext
        meta:set_string('infotext', mob_name .. ' spawner\nowner: ' .. owner .. '\nSpawner was searching for too long and got rusted! Dig up the spawner and place it again.')
        return
    end
end
