--[[
    X Farming. Extends Minetest farming mod with new plants, crops and ice fishing.
    Copyright (C) 2024 SaKeL

    This library is free software; you can redistribute it and/or
    modify it under 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
--]]

local S = minetest.get_translator(minetest.get_current_modname())
local rand = PcgRandom(tonumber(tostring(os.time()):reverse():sub(1, 9)))

local function update_hive_infotext(pos)
    local meta = minetest.get_meta(pos)
    local data = minetest.deserialize(meta:get_string('x_farming'))

    if data then
        local text = 'Occupancy: ' .. data.occupancy .. ' / 3\n'
            .. 'Saturation: ' .. data.saturation .. ' / 5'
        meta:set_string('infotext', text)
    end
end

local function bee_particles(pos)
    minetest.add_particlespawner({
        amount = 20,
        time = 0.1,
        minpos = { x = pos.x - 0.25, y = pos.y, z = pos.z - 0.25 },
        maxpos = { x = pos.x + 0.25, y = pos.y, z = pos.z + 0.25 },
        minvel = { x = -0.5, y = -2, z = -0.5 },
        maxvel = { x = 0.5, y = -2, z = 0.5 },
        minacc = { x = -0.5, y = -2, z = -0.5 },
        maxacc = { x = 0.5, y = -2, z = 0.5 },
        minexptime = 0.5,
        maxexptime = 1,
        texture = 'x_farming_default_particle.png^[colorize:#FFD69C:255',
        collisiondetection = true
    })
end

local function update_bee_infotext(pos)
    local meta = minetest.get_meta(pos)
    local data = minetest.deserialize(meta:get_string('x_farming'))

    if data then
        meta:set_string('infotext', 'Hive position: ' .. data.pos_hive)
    end
end

-- how often node timers for plants will tick, +/- some random value
local function tick_hive(pos)
    minetest.get_node_timer(pos):start(math.random(15, 25))
end
-- how often a growth failure tick is retried (e.g. too dark)
local function tick_bee(pos)
    minetest.get_node_timer(pos):start(math.random(90, 150))
end

local function is_valid_hive_position(pos, params)
    if not pos then
        return false
    end

    local _params = params or {}
    local ommit_node_group_check = _params.ommit_node_group_check or false

    local hive_node = minetest.get_node(pos)

    if not hive_node then
        return false
    end

    if not ommit_node_group_check then
        if minetest.get_item_group(hive_node.name, 'bee_hive') == 0 then
            return false
        end
    end

    local meta_hive = minetest.get_meta(pos)
    local data_hive = minetest.deserialize(meta_hive:get_string('x_farming'))

    if not data_hive then
        return false
    end

    if not data_hive.occupancy then
        return false
    end

    if data_hive.occupancy >= 3 then
        return false
    end

    return true
end

local function get_valid_hive_position(pos_hive, pos_bee)
    local valid_pos = nil

    if pos_hive then
        valid_pos = vector.copy(pos_hive)
    end

    if is_valid_hive_position(valid_pos) then
        return valid_pos
    end

    valid_pos = nil

    -- Find neighboring bee hive position
    local hive_positions = minetest.find_nodes_in_area(
        vector.add(pos_bee, 5),
        vector.subtract(pos_bee, 5),
        { 'group:bee_hive' }
    )

    for _, p in ipairs(hive_positions) do
        if is_valid_hive_position(p, { ommit_node_group_check = true }) then
            valid_pos = p
            break
        end
    end

    return valid_pos
end

-- Hive
minetest.register_node('x_farming:bee_hive', {
    description = S('Bee Hive'),
    short_description = S('Bee Hive'),
    tiles = {
        'x_farming_bee_hive_top.png',
        'x_farming_bee_hive_bottom.png',
        'x_farming_bee_hive_side.png',
        'x_farming_bee_hive_side.png',
        'x_farming_bee_hive_side.png',
        'x_farming_bee_hive_front.png',
    },
    paramtype2 = 'facedir',
    groups = {
        -- MTG
        choppy = 2,
        oddly_breakable_by_hand = 1,
        bee_hive = 1,
        no_silktouch = 1,
        -- MCL
        handy = 1,
        axey = 1,
        building_block = 1,
        material_wood = 1,
        fire_encouragement = 5,
        fire_flammability = 5,
        -- ALL
        tree = 1,
        flammable = 2,
    },
    _mcl_blast_resistance = 2,
    _mcl_hardness = 2,
    sounds = x_farming.node_sound_wood_defaults(),
    on_timer = function(pos, elapsed)
        -- Hive data
        local meta_hive = minetest.get_meta(pos)
        local data_hive = minetest.deserialize(meta_hive:get_string('x_farming'))
        local node = minetest.get_node(pos)

        if data_hive.occupancy == 0 then
            return
        end

        local flower_positions = minetest.find_nodes_in_area_under_air(
            vector.add(pos, 5),
            vector.subtract(pos, 5),
            { 'group:flower' }
        )

        if not flower_positions then
            tick_hive(pos)
        end

        if flower_positions and #flower_positions > 0 then
            local random_pos = flower_positions[rand:next(1, #flower_positions)]
            local pos_bee = vector.new(random_pos.x, random_pos.y + 1, random_pos.z)
            local tod = minetest.get_timeofday()
            local is_day = false

            if tod > 0.2 and tod < 0.805 then
                is_day = true
            end

            if data_hive and data_hive.occupancy > 0 and is_day then
                local pos_hive_front = vector.subtract(vector.new(pos.x, pos.y + 0.5, pos.z), minetest.facedir_to_dir(node.param2))

                -- Send bee out
                data_hive.occupancy = data_hive.occupancy - 1

                minetest.swap_node(pos_bee, { name = 'x_farming:bee', param2 = rand:next(0, 3) })
                tick_bee(pos_bee)

                minetest.sound_play('x_farming_bee', {
                    pos = pos_bee,
                })

                bee_particles(pos_bee)
                bee_particles(pos_hive_front)

                -- Bee data
                local meta_bee = minetest.get_meta(pos_bee)
                local data_bee = {
                    pos_hive = vector.new(pos):to_string()
                }

                meta_bee:set_string('x_farming', minetest.serialize(data_bee))
                meta_hive:set_string('x_farming', minetest.serialize(data_hive))

                update_hive_infotext(pos)
                update_bee_infotext(pos_bee)
            end
        end

        if data_hive and data_hive.occupancy > 0 then
            tick_hive(pos)
        end
    end,
    on_construct = function(pos)
        local meta = minetest.get_meta(pos)
        local data = {
            occupancy = 0,
            saturation = 0
        }

        meta:set_string('x_farming', minetest.serialize(data))
        update_hive_infotext(pos)
    end,
    after_place_node = function(pos, placer, itemstack, pointed_thing)
        tick_hive(pos)
    end,
    on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
        local meta = minetest.get_meta(pos)
        local data = minetest.deserialize(meta:get_string('x_farming'))

        if not data then
            return itemstack
        end

        if data.occupancy == 3 then
            return itemstack
        end

        if minetest.get_item_group(itemstack:get_name(), 'bee') > 0 then
            data.occupancy = data.occupancy + 1
            meta:set_string('x_farming', minetest.serialize(data))
            update_hive_infotext(pos)
            itemstack:take_item()

            minetest.sound_play('x_farming_bee', {
                pos = pos,
            })
        end

        if data.occupancy > 0 and not minetest.get_node_timer(pos):is_started() then
            tick_hive(pos)
        end

        return itemstack
    end,
    after_dig_node = function(pos, oldnode, oldmetadata, digger)
        local data = minetest.deserialize(oldmetadata.fields.x_farming)
        local positions = minetest.find_nodes_in_area_under_air(
            vector.add(pos, 5),
            vector.subtract(pos, 5),
            { 'group:flower', 'group:flora' }
        )

        if positions and #positions > 0 and data.occupancy and data.occupancy > 0 then
            for i = 1, data.occupancy do
                local p = positions[i]

                if p then
                    local pos_bee = vector.new(p.x, p.y + 1, p.z)
                    minetest.swap_node(pos_bee, { name = 'x_farming:bee', param2 = rand:next(0, 3) })
                    tick_bee(pos_bee)
                    bee_particles(pos_bee)

                    if i == 1 then
                        minetest.sound_play('x_farming_bee', {
                            pos = pos_bee,
                        })
                    end
                end
            end
        end
    end,
})

-- Hive saturrated
minetest.register_node('x_farming:bee_hive_saturated', {
    description = S('Bee Hive'),
    short_description = S('Bee Hive'),
    tiles = {
        'x_farming_bee_hive_top.png',
        'x_farming_bee_hive_bottom.png',
        'x_farming_bee_hive_side.png^x_farming_bee_hive_saturated_overlay.png',
        'x_farming_bee_hive_side.png^x_farming_bee_hive_saturated_overlay.png',
        'x_farming_bee_hive_side.png^x_farming_bee_hive_saturated_overlay.png',
        'x_farming_bee_hive_front.png^x_farming_bee_hive_saturated_overlay.png',
    },
    paramtype2 = 'facedir',
    drop = 'x_farming:bee_hive',
    groups = {
        -- MTG
        choppy = 2,
        oddly_breakable_by_hand = 1,
        bee_hive = 1,
        no_silktouch = 1,
        not_in_creative_inventory = 1,
        -- MCL
        handy = 1,
        axey = 1,
        building_block = 1,
        material_wood = 1,
        fire_encouragement = 5,
        fire_flammability = 5,
        -- ALL
        tree = 1,
        flammable = 2,
    },
    _mcl_blast_resistance = 2,
    _mcl_hardness = 2,
    sounds = x_farming.node_sound_wood_defaults(),
    on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
        local stack_name = itemstack:get_name()
        local stack = itemstack
        local meta = minetest.get_meta(pos)
        local data = minetest.deserialize(meta:get_string('x_farming'))

        if stack_name == 'vessels:glass_bottle' or stack_name == 'x_farming:glass_bottle' then
            -- Fill bottle with honey and return it
            itemstack:take_item()

            local pos_hive_front = vector.subtract(vector.new(pos.x, pos.y + 0.5, pos.z), minetest.facedir_to_dir(node.param2))

            minetest.add_item(
                pos_hive_front,
                ItemStack({ name = 'x_farming:bottle_honey' })
            )

            minetest.swap_node(pos, { name = 'x_farming:bee_hive', param2 = node.param2 })

            minetest.sound_play('x_farming_bee', {
                pos = pos,
            })

            bee_particles(pos_hive_front)

            data.saturation = 0
            meta:set_string('x_farming', minetest.serialize(data))
            update_hive_infotext(pos)

            tick_hive(pos)
        elseif stack_name == 'x_farming:honeycomb_saw' then
            -- Add use to the tool and drop honeycomb
            itemstack:add_wear(65535 / 50)

            local pos_hive_front = vector.subtract(vector.new(pos.x, pos.y + 0.5, pos.z), minetest.facedir_to_dir(node.param2))

            minetest.add_item(
                pos_hive_front,
                ItemStack({ name = 'x_farming:honeycomb' })
            )

            minetest.swap_node(pos, { name = 'x_farming:bee_hive', param2 = node.param2 })

            minetest.sound_play('x_farming_bee', {
                pos = pos,
            })

            bee_particles(pos_hive_front)

            data.saturation = 0
            meta:set_string('x_farming', minetest.serialize(data))
            update_hive_infotext(pos)

            tick_hive(pos)
        end

        return stack
    end,
    after_dig_node = function(pos, oldnode, oldmetadata, digger)
        local data = minetest.deserialize(oldmetadata.fields.x_farming)
        local positions = minetest.find_nodes_in_area_under_air(
            vector.add(pos, 5),
            vector.subtract(pos, 5),
            { 'group:flower', 'group:flora' }
        )

        if positions and #positions > 0 and data.occupancy and data.occupancy > 0 then
            for i = 1, data.occupancy do
                local p = positions[i]

                if p then
                    local pos_bee = vector.new(p.x, p.y + 1, p.z)
                    minetest.swap_node(pos_bee, { name = 'x_farming:bee', param2 = rand:next(0, 3) })
                    tick_bee(pos_bee)
                    bee_particles(pos_bee)

                    if i == 1 then
                        minetest.sound_play('x_farming_bee', {
                            pos = pos,
                        })
                    end
                end
            end
        end
    end,
})

-- Bee
minetest.register_node('x_farming:bee', {
    description = S('Bee'),
    short_description = S('Bee'),
    drawtype = 'mesh',
    mesh = 'x_farming_bee.obj',
    tiles = {
        {
            name = 'x_farming_bee_mesh_animated.png',
            animation = {
                type = 'vertical_frames',
                aspect_w = 34,
                aspect_h = 30,
                length = 0.3
            },
            backface_culling = false
        },
    },
    use_texture_alpha = 'clip',
    paramtype = 'light',
    sunlight_propagates = true,
    paramtype2 = 'facedir',
    walkable = false,
    selection_box = {
        type = 'fixed',
        fixed = { -4 / 16, -4 / 16, -4 / 16, 4 / 16, 4 / 16, 4 / 16 }
    },
    groups = {
        -- MTG
        snappy = 2,
        cracky = 2,
        crumbly = 2,
        choppy = 2,
        fleshy = 10,
        oddly_breakable_by_hand = 1,
        disable_jump = 1,
        bee = 1,
        no_silktouch = 1,
        not_in_creative_inventory = 1,
        -- MCL
        handy = 1,
    },
    damage_per_second = 2,
    move_resistance = 7,
    _mcl_blast_resistance = 1,
    _mcl_hardness = 1,
    sounds = x_farming.node_sound_wood_defaults(),
    drop = {
        max_items = 1,
        items = {
            {
                --  1/20 chance
                items = { 'x_farming:honeycomb' },
                rarity = 10,
            }
        }
    },
    on_timer = function(pos, elapsed)
        -- Bee data
        local meta_bee = minetest.get_meta(pos)
        local data_bee = minetest.deserialize(meta_bee:get_string('x_farming')) or {}
        local pos_hive = get_valid_hive_position(data_bee.pos_hive and vector.from_string(data_bee.pos_hive) or nil, pos)

        if not pos_hive then
            -- Bee data not found, remove bee
            minetest.remove_node(pos)

            minetest.sound_play('x_farming_bee', {
                pos = pos,
            })

            bee_particles(pos)

            return
        end

        local meta_hive = minetest.get_meta(pos_hive)
        local data_hive = minetest.deserialize(meta_hive:get_string('x_farming'))
        local node_hive = minetest.get_node(pos_hive)

        -- Bee go home
        data_hive.occupancy = data_hive.occupancy + 1

        local flower_node = minetest.get_node(vector.new(pos.x, pos.y - 1, pos.z))

        if flower_node then
            if minetest.get_item_group(flower_node.name, 'flower') > 0 and data_hive.saturation < 5 then
                data_hive.saturation = data_hive.saturation + 1
            end

            if data_hive.saturation >= 5 then
                minetest.swap_node(pos_hive, { name = 'x_farming:bee_hive_saturated', param2 = node_hive.param2 })
            end
        end

        meta_hive:set_string('x_farming', minetest.serialize(data_hive))
        minetest.remove_node(pos)
        update_hive_infotext(pos_hive)

        minetest.sound_play('x_farming_bee', {
            pos = pos,
        })

        local pos_hive_front = vector.subtract(vector.new(pos_hive.x, pos_hive.y + 0.5, pos_hive.z), minetest.facedir_to_dir(node_hive.param2))

        bee_particles(pos)
        bee_particles(pos_hive_front)

        if not minetest.get_node_timer(pos_hive):is_started() then
            tick_hive(pos_hive)
        end
    end,
    on_punch = function(pos, node, puncher, pointed_thing)
        if not puncher then
            return
        end

        -- Hurt player when punching bee
        local armor_groups = puncher:get_armor_groups()
        local damage = 2

        if armor_groups.fleshy then
            damage = math.round((2 * 100) / armor_groups.fleshy)
        end

        puncher:punch(puncher, 1, {
            full_punch_interval = 1,
            damage_groups = { fleshy = damage },
        }, nil)

        minetest.sound_play('x_farming_bee', {
            pos = pos,
        })

        bee_particles(pos)
    end,
    on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
        local stack_name = itemstack:get_name()

        if stack_name == 'x_farming:jar_empty' then
            itemstack:take_item()
            minetest.remove_node(pos)
            minetest.add_item(vector.new(pos.x, pos.y + 0.3, pos.z), ItemStack({ name = 'x_farming:jar_with_bee' }))

            minetest.sound_play('x_farming_bee', {
                pos = pos,
            })

            bee_particles(pos)
        end

        return itemstack
    end
})

-- Spawn bees on flowers
minetest.register_globalstep(function(dtime)
    local abr = minetest.get_mapgen_setting('active_block_range') or 4
    local spawn_reduction = 0.5
    local spawn_rate = 0.5
    local spawnpos, liquidflag = x_farming.get_spawn_pos_abr(dtime, 3, abr * 16, spawn_rate, spawn_reduction)
    local tod = minetest.get_timeofday()
    local is_day = false

    if tod > 0.2 and tod < 0.805 then
        is_day = true
    end

    if spawnpos and not liquidflag and is_day then
        local bees = minetest.find_node_near(spawnpos, abr * 16 * 1.1, { 'group:bee' }) or {}

        if #bees > 1 then
            return
        end

        local flower_positions = minetest.find_nodes_in_area_under_air(
            vector.add(spawnpos, 5),
            vector.subtract(spawnpos, 5),
            { 'group:flower' }
        )

        if flower_positions and #flower_positions > 1 then
            local rand_pos = flower_positions[rand:next(1, #flower_positions)]
            local bee_pos = vector.new(rand_pos.x, rand_pos.y + 1, rand_pos.z)

            minetest.swap_node(bee_pos, { name = 'x_farming:bee', param2 = rand:next(0, 3) })
            tick_bee(spawnpos)

            minetest.log('action', '[x_farming] Added Bee at ' .. minetest.pos_to_string(bee_pos))
        end
    end
end)
