--[[
    X Farming. Extends Luanti farming mod with new plants, crops and ice fishing.
    Copyright (C) 2025 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())

-- main class
x_farming = {
    hunger_ng = minetest.get_modpath('hunger_ng'),
    hbhunger = minetest.get_modpath('hbhunger') and minetest.global_exists('hbhunger'),
    vessels = minetest.get_modpath('vessels'),
    bucket = minetest.get_modpath('bucket'),
    colors = {
        brown = '#DEB887',
        red = '#FF8080',
        green = '#98E698'
    },
    x_bonemeal = {
        tree_defs = {
            ['x_farming:christmas_tree_sapling'] = {
                -- christmas tree
                name = 'x_farming:christmas_tree_sapling',
                chance = 2,
                grow_tree = function(pos)
                    if not x_farming.x_bonemeal.is_on_soil(pos) then
                        return false
                    end

                    x_farming.grow_christmas_tree(pos)

                    return true
                end
            },
            ['x_farming:kiwi_sapling'] = {
                -- Kiwi Tree
                name = 'x_farming:kiwi_sapling',
                chance = 2,
                grow_tree = function(pos)
                    if not x_farming.x_bonemeal.is_on_soil(pos) then
                        return false
                    end

                    x_farming.grow_kiwi_tree(pos)

                    return true
                end
            },
            ['x_farming:large_cactus_with_fruit_seedling'] = {
                -- Cactus Seedling
                name = 'x_farming:large_cactus_with_fruit_seedling',
                chance = 2,
                grow_tree = function(pos)
                    if not x_farming.x_bonemeal.is_on_sand(pos) then
                        return false
                    end

                    x_farming.grow_large_cactus(pos)

                    return true
                end
            },
            ['x_farming:jungle_with_cocoa_sapling'] = {
                -- Jungle Tree with Cocoa
                name = 'x_farming:jungle_with_cocoa_sapling',
                chance = 2,
                grow_tree = function(pos)
                    if not x_farming.x_bonemeal.is_on_soil(pos) then
                        return false
                    end

                    x_farming.grow_jungle_tree(pos)

                    return true
                end
            },
            ['x_farming:pine_nut_sapling'] = {
                -- Pine Nut Tree
                name = 'x_farming:pine_nut_sapling',
                chance = 2,
                grow_tree = function(pos)
                    if not x_farming.x_bonemeal.is_on_soil(pos) then
                        return false
                    end

                    x_farming.grow_pine_nut_tree(pos)

                    return true
                end
            },
        }
    },
    allowed_crate_items = {},
    allowed_bag_items = {},
    registered_crates = {},
    lbm_nodenames_crates = {},
    registered_plants = {},
    mcl = {},
    candle_colors = {
        black = {
            name = S('Black'),
            hex = '#2B2B2B',
            mcl_groups = { basecolor_black = 1, excolor_black = 1, unicolor_black = 1 }
        },
        dark_grey = {
            name = S('Dark Grey'),
            hex = '#4E4E4E',
            mcl_groups = { basecolor_grey = 1, excolor_darkgrey = 1, unicolor_darkgrey = 1 }
        },
        grey = {
            name = S('Grey'),
            hex = '#A5A5A5',
            mcl_groups = { basecolor_grey = 1, excolor_grey = 1, unicolor_grey = 1 }
        },
        red = {
            name = S('Red'),
            hex = '#AB5C4A',
            mcl_groups = { basecolor_red = 1, excolor_red = 1, unicolor_red = 1 }
        },
        violet = {
            name = S('Violet'),
            hex = '#595287',
            mcl_groups = { basecolor_magenta = 1, excolor_violet = 1, unicolor_violet = 1 }
        },
        magenta = {
            name = S('Magenta'),
            hex = '#A25B5D',
            mcl_groups = { basecolor_magenta = 1, excolor_red_violet = 1, unicolor_red_violet = 1 }
        },
        pink = {
            name = S('Pink'),
            hex = '#FFA6A6',
            mcl_groups = { basecolor_red = 1, excolor_red = 1, unicolor_light_red = 1 }
        },
        dark_green = {
            name = S('Dark Green'),
            hex = '#556E48',
            mcl_groups = { basecolor_green = 1, excolor_green = 1, unicolor_dark_green = 1 }
        },
        green = {
            name = S('Green'),
            hex = '#779154',
            mcl_groups = { basecolor_green = 1, excolor_green = 1, unicolor_green = 1 }
        },
        cyan = {
            name = S('Cyan'),
            hex = '#4E7683',
            mcl_groups = { basecolor_cyan = 1, excolor_cyan = 1, unicolor_cyan = 1 }
        },
        blue = {
            name = S('Blue'),
            hex = '#4B6696',
            mcl_groups = { basecolor_blue = 1, excolor_blue = 1, unicolor_blue = 1 }
        },
        light_blue = {
            name = S('Light Blue'),
            hex = '#648CB4',
            craft_dye = 'group:dye,color_light_blue',
            mcl_groups = { basecolor_blue = 1, excolor_blue = 1, unicolor_light_blue = 1 }
        },
        orange = {
            name = S('Orange'),
            hex = '#A86A4D',
            mcl_groups = { basecolor_orange = 1, excolor_orange = 1, unicolor_orange = 1 }
        },
        yellow = {
            name = S('Yellow'),
            hex = '#BD8D39',
            mcl_groups = { basecolor_yellow = 1, excolor_yellow = 1, unicolor_yellow = 1 }
        },
        brown = {
            name = S('Brown'),
            hex = '#684E45',
            mcl_groups = { basecolor_brown = 1, excolor_orange = 1, unicolor_dark_orange = 1 }
        }
    },
}

function x_farming.node_sound_grass_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_grass_footstep', gain = 0.4 }
    table.dig = table.dig or { name = 'x_farming_grass_hit', gain = 1.2 }
    table.dug = table.dug or { name = 'x_farming_dirt_hit', gain = 1.0 }
    table.place = table.place or { name = 'x_farming_dirt_hit', gain = 1.0 }
    return table
end

function x_farming.node_sound_leaves_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_leaves_footstep', gain = 0.1 }
    table.dig = table.dig or { name = 'x_farming_leaves_hit', gain = 0.25 }
    table.dug = table.dug or { name = 'x_farming_leaves_dug', gain = 0.5 }
    table.place = table.place or { name = 'x_farming_leaves_place', gain = 0.4 }
    return table
end

function x_farming.node_sound_wood_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_wood_footstep', gain = 0.15 }
    table.dig = table.dig or { name = 'x_farming_wood_hit', gain = 0.5 }
    table.dug = table.dug or { name = 'x_farming_wood_place', gain = 0.1 }
    table.place = table.place or { name = 'x_farming_wood_place', gain = 0.15 }
    return table
end

function x_farming.node_sound_sand_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_sand_footstep', gain = 0.1 }
    table.dig = table.dig or { name = 'x_farming_sand_hit', gain = 0.5 }
    table.dug = table.dug or { name = 'x_farming_sand_dug', gain = 0.1 }
    table.place = table.place or { name = 'x_farming_sand_place', gain = 0.15 }
    return table
end

function x_farming.node_sound_thin_glass_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_thin_glass_footstep', gain = 0.3 }
    table.dig = table.dig or { name = 'x_farming_thin_glass_footstep', gain = 0.5 }
    table.dug = table.dug or { name = 'x_farming_break_thin_glass', gain = 1.0 }
    table.place = table.place or { name = 'x_farming_glass_place', gain = 0.2 }
    return table
end

function x_farming.node_sound_dirt_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_dirt_footstep', gain = 0.15 }
    table.dig = table.dig or { name = 'x_farming_dirt_hit', gain = 0.4 }
    table.dug = table.dug or { name = 'x_farming_dirt_hit', gain = 1.0 }
    table.place = table.place or { name = 'x_farming_dirt_hit', gain = 1.0 }
    return table
end

function x_farming.node_sound_ice_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_ice_footstep', gain = 0.2 }
    table.dig = table.dig or { name = 'x_farming_ice_hit', gain = 0.4 }
    table.dug = table.dug or { name = 'x_farming_ice_hit', gain = 1.0 }
    table.place = table.place or { name = 'x_farming_ice_hit', gain = 1.0 }
    return table
end

function x_farming.node_sound_stone_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_stone_footstep', gain = 0.2 }
    table.dig = table.dig or { name = 'x_farming_stone_hit', gain = 1.0 }
    table.dug = table.dug or { name = 'x_farming_stone_dug', gain = 0.6 }
    table.place = table.place or { name = 'x_farming_stone_place', gain = 1.0 }
    return table
end

function x_farming.node_sound_slime_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_slime_footstep', gain = 0.2 }
    table.dig = table.dig or { name = 'x_farming_slime_dig', gain = 1.0 }
    table.dug = table.dug or { name = 'x_farming_slime_dug', gain = 0.3 }
    table.place = table.place or { name = 'x_farming_slime_footstep', gain = 1.0 }
    return table
end

function x_farming.node_sound_rope_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_rope_footstep', gain = 0.05 }
    table.dig = table.dig or { name = 'x_farming_rope_hit', gain = 0.7 }
    table.dug = table.dug or { name = 'x_farming_rope_dug', gain = 0.2 }
    table.place = table.place or { name = 'x_farming_rope_hit', gain = 0.8 }
    return table
end

function x_farming.node_sound_pillow_defaults(table)
    table = table or {}
    table.footstep = table.footstep or { name = 'x_farming_pillow_footstep', gain = 0.1 }
    table.dig = table.dig or { name = 'x_farming_pillow_hit', gain = 0.15 }
    table.dug = table.dug or { name = 'x_farming_pillow_dug', gain = 0.2 }
    table.place = table.place or { name = 'x_farming_pillow_footstep', gain = 0.25 }
    return table
end

---how often node timers for plants will tick, +/- some random value
function x_farming.tick_block(pos)
    minetest.get_node_timer(pos):start(math.random(498, 1287))
end

---how often a growth failure tick is retried (e.g. too dark)
function x_farming.tick_block_short(pos)
    minetest.get_node_timer(pos):start(math.random(332, 858))
end

-- how often node timers for plants will tick, +/- some random value
function x_farming.tick(pos)
    minetest.get_node_timer(pos):start(math.random(166, 286))
end
-- how often a growth failure tick is retried (e.g. too dark)
function x_farming.tick_again(pos)
    minetest.get_node_timer(pos):start(math.random(40, 80))
end

---just shorthand for minetest metadata handling
function x_farming.meta_get_str(key, pos)
    local meta = minetest.get_meta(pos)
    return meta:get_string(key)
end

---just shorthand for minetest metadata handling
function x_farming.meta_set_str(key, value, pos)
    local meta = minetest.get_meta(pos)
    meta:set_string(key, value)
end

---merge two indexed tables
function x_farming.mergeTables(t1, t2)
    local _t1 = { unpack(t1) }
    local _t2 = { unpack(t2) }

    for k, v in ipairs(_t2) do
        table.insert(_t1, v)
    end

    return _t1
end

---Change an entire string to Title Case (i.e. capitalise the first letter of each word)
function x_farming.tchelper(first, rest)
    return first:upper() .. rest:lower()
end

--- Hoes - copy from MTG

x_farming.hoe_on_use = function(itemstack, user, pointed_thing, uses)
    local pt = pointed_thing
    -- check if pointing at a node
    if not pt then
        return
    end
    if pt.type ~= 'node' then
        return
    end

    local under = minetest.get_node(pt.under)
    local p = { x = pt.under.x, y = pt.under.y + 1, z = pt.under.z }
    local above = minetest.get_node(p)

    -- return if any of the nodes is not registered
    if not minetest.registered_nodes[under.name] then
        return
    end
    if not minetest.registered_nodes[above.name] then
        return
    end

    -- check if the node above the pointed thing is air
    if above.name ~= 'air' then
        return
    end

    -- check if pointing at soil
    if minetest.get_item_group(under.name, 'soil') ~= 1 then
        return
    end

    -- check if (wet) soil defined
    local regN = minetest.registered_nodes
    if regN[under.name].soil == nil or regN[under.name].soil.wet == nil or regN[under.name].soil.dry == nil then
        return
    end

    local player_name = user and user:get_player_name() or ''

    if minetest.is_protected(pt.under, player_name) then
        minetest.record_protection_violation(pt.under, player_name)
        return
    end

    if minetest.is_protected(pt.above, player_name) then
        minetest.record_protection_violation(pt.above, player_name)
        return
    end

    -- turn the node into soil and play sound
    minetest.set_node(pt.under, { name = regN[under.name].soil.dry })
    minetest.sound_play('x_farming_dirt_hit', {
        pos = pt.under,
        gain = 0.3,
    }, true)

    if not minetest.is_creative_enabled(player_name) then
        -- wear tool
        local wdef = itemstack:get_definition()
        itemstack:add_wear_by_uses(uses)
        -- tool break sound
        if itemstack:get_count() == 0 and wdef.sound and wdef.sound.breaks then
            minetest.sound_play(wdef.sound.breaks, {pos = pt.above,
                gain = 0.5}, true)
        end
    end
    return itemstack
end

-- Register new hoes
x_farming.register_hoe = function(name, def)
    local _def = table.copy(def)
    -- Check for : prefix (register new hoes in your mod's namespace)
    if name:sub(1, 1) ~= ':' then
        name = ':' .. name
    end
    -- Check def table
    if _def.description == nil then
        _def.description = S('Hoe')
    end
    if _def.inventory_image == nil then
        _def.inventory_image = 'x_farming_unknown_item.png'
    end
    if _def.max_uses == nil then
        _def.max_uses = 30
    end

    if minetest.get_modpath('farming') then
        _def.on_use = function(itemstack, user, pointed_thing)
            return x_farming.hoe_on_use(itemstack, user, pointed_thing, _def.max_uses)
        end
    end

    if minetest.get_modpath('mcl_farming') then
        _def.on_place = x_farming.mcl.hoe_on_place_function(_def.max_uses)
    end

    -- MCL
    _def.groups = _def.groups
    _def.sound = { breaks = 'x_farming_tool_breaks' }
    _def.wield_scale = _def.wield_scale or { x = 1, y = 1, z = 1 }

    -- Register the tool
    minetest.register_tool(name, _def)
    -- Register its recipe
    if _def.recipe then
        minetest.register_craft({
            output = name:sub(2),
            recipe = _def.recipe
        })
    elseif _def.material then
        minetest.register_craft({
            output = name:sub(2),
            recipe = {
                { _def.material, 'group:stick' },
                { _def.material, 'group:stick' },
                { '', 'group:stick' }
            }
        })
    end
end

--
-- Log API / helpers - copy from MTG
--

local log_non_player_actions = minetest.settings:get_bool('log_non_player_actions', false)

local is_pos = function(v)
    return type(v) == 'table' and
        type(v.x) == 'number' and type(v.y) == 'number' and type(v.z) == 'number'
end

function x_farming.log_player_action(player, ...)
    local msg = player:get_player_name()
    if player.is_fake_player or not player:is_player() then
        if not log_non_player_actions then
            return
        end
        msg = msg .. '(' .. (type(player.is_fake_player) == 'string'
            and player.is_fake_player or '*') .. ')'
    end
    for _, v in ipairs({ ... }) do
        -- translate pos
        local part = is_pos(v) and minetest.pos_to_string(v) or v
        -- no leading spaces before punctuation marks
        msg = msg .. (string.match(part, '^[;,.]') and '' or ' ') .. part
    end
    minetest.log('action', msg)
end

function x_farming.set_inventory_action_loggers(def, name)
    def.on_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
        x_farming.log_player_action(player, 'moves stuff in', name, 'at', pos)
    end
    def.on_metadata_inventory_put = function(pos, listname, index, stack, player)
        x_farming.log_player_action(player, 'moves', stack:get_name(), 'to', name, 'at', pos)
    end
    def.on_metadata_inventory_take = function(pos, listname, index, stack, player)
        x_farming.log_player_action(player, 'takes', stack:get_name(), 'from', name, 'at', pos)
    end
end

--
-- Sapling 'on place' function to check protection of node and resulting tree volume
--

function x_farming.sapling_on_place(itemstack, placer, pointed_thing, sapling_name, minp_relative, maxp_relative, interval)
    -- Position of sapling
    local pos = pointed_thing.under
    local node = minetest.get_node_or_nil(pos)
    local pdef = node and minetest.registered_nodes[node.name]

    if not node then
        return itemstack
    end

    if pdef and pdef.on_rightclick and
        not (placer and placer:is_player() and
        placer:get_player_control().sneak)
    then
        return pdef.on_rightclick(pos, node, placer, itemstack, pointed_thing)
    end

    if not pdef or not pdef.buildable_to then
        pos = pointed_thing.above
        node = minetest.get_node_or_nil(pos)
        pdef = node and minetest.registered_nodes[node.name]
        if not pdef or not pdef.buildable_to then
            return itemstack
        end
    end

    local player_name = placer and placer:get_player_name() or ''

    -- Check sapling position for protection
    if minetest.is_protected(pos, player_name) then
        minetest.record_protection_violation(pos, player_name)
        return itemstack
    end

    -- Check tree volume for protection
    if minetest.is_area_protected(
            vector.add(pos, minp_relative),
            vector.add(pos, maxp_relative),
            player_name,
            interval) then
        minetest.record_protection_violation(pos, player_name)
        -- Print extra information to explain
        -- minetest.chat_send_player(player_name,
        -- itemstack:get_definition().description .. ' will intersect protection ' ..
        -- 'on growth')
        minetest.chat_send_player(
            player_name,
            S('@1 will intersect protection on growth.', itemstack:get_definition().description)
        )
        return itemstack
    end

    x_farming.log_player_action(placer, 'places node', sapling_name, 'at', pos)

    local take_item = not minetest.is_creative_enabled(player_name)
    local newnode = { name = sapling_name }
    local ndef = minetest.registered_nodes[sapling_name]
    minetest.set_node(pos, newnode)

    -- Run callback
    if ndef and ndef.after_place_node then
        -- Deepcopy place_to and pointed_thing because callback can modify it
        if ndef.after_place_node(table.copy(pos), placer, itemstack, table.copy(pointed_thing)) then
            take_item = false
        end
    end

    -- Run script hook
    for _, callback in ipairs(minetest.registered_on_placenodes) do
        -- Deepcopy pos, node and pointed_thing because callback can modify them
        if callback(table.copy(pos), table.copy(newnode),
                placer, table.copy(node or {}),
                itemstack, table.copy(pointed_thing)) then
            take_item = false
        end
    end

    if take_item then
        itemstack:take_item()
    end

    return itemstack
end

--
-- Leafdecay
--

-- Prevent decay of placed leaves

function x_farming.after_place_leaves(pos, placer, itemstack, pointed_thing)
    if placer and placer:is_player() then
        local node = minetest.get_node(pos)
        node.param2 = 1
        minetest.set_node(pos, node)
    end
end

-- Leafdecay
local function leafdecay_after_destruct(pos, oldnode, def)
    for _, v in pairs(minetest.find_nodes_in_area(vector.subtract(pos, def.radius), vector.add(pos, def.radius), def.leaves)) do
        local node = minetest.get_node(v)
        local timer = minetest.get_node_timer(v)
        if (node.param2 ~= 1 or minetest.get_item_group('cocoa') == 0) and not timer:is_started() then
            timer:start(math.random(20, 120) / 10)
        end
    end
end

local movement_gravity = tonumber(minetest.settings:get('movement_gravity')) or 9.81

local function leafdecay_on_timer(pos, def)
    if minetest.find_node_near(pos, def.radius, def.trunks) then
        return false
    end

    local node = minetest.get_node(pos)
    local drops = minetest.get_node_drops(node.name)
    for _, item in ipairs(drops) do
        local is_leaf
        for _, v in pairs(def.leaves) do
            if v == item then
                is_leaf = true
            end
        end

        if minetest.get_item_group(item, 'leafdecay_drop') ~= 0 or not is_leaf then
            minetest.add_item({
                x = pos.x - 0.5 + math.random(),
                y = pos.y - 0.5 + math.random(),
                z = pos.z - 0.5 + math.random(),
            }, item)
        end
    end

    minetest.remove_node(pos)
    minetest.check_for_falling(pos)

    -- spawn a few particles for the removed node
    minetest.add_particlespawner({
        amount = 8,
        time = 0.001,
        minpos = vector.subtract(pos, { x = 0.5, y = 0.5, z = 0.5 }),
        maxpos = vector.add(pos, { x = 0.5, y = 0.5, z = 0.5 }),
        minvel = vector.new(-0.5, -1, -0.5),
        maxvel = vector.new(0.5, 0, 0.5),
        minacc = vector.new(0, -movement_gravity, 0),
        maxacc = vector.new(0, -movement_gravity, 0),
        minsize = 0,
        maxsize = 0,
        node = node,
    })
end

function x_farming.register_leafdecay(def)
    assert(def.leaves)
    assert(def.trunks)
    assert(def.radius)

    for _, v in pairs(def.trunks) do
        minetest.override_item(v, {
            after_destruct = function(pos, oldnode)
                leafdecay_after_destruct(pos, oldnode, def)
            end,
        })
    end

    for _, v in pairs(def.leaves) do
        minetest.override_item(v, {
            on_timer = function(pos)
                leafdecay_on_timer(pos, def)
            end,
        })
    end
end

-- Seed placement - copy from MTG
function x_farming.place_seed(itemstack, placer, pointed_thing, plantname)
    local pt = pointed_thing
    -- check if pointing at a node
    if not pt then
        return itemstack
    end
    if pt.type ~= 'node' then
        return itemstack
    end

    local under = minetest.get_node(pt.under)
    local above = minetest.get_node(pt.above)

    local player_name = placer and placer:get_player_name() or ''

    if minetest.is_protected(pt.under, player_name) then
        minetest.record_protection_violation(pt.under, player_name)
        return itemstack
    end
    if minetest.is_protected(pt.above, player_name) then
        minetest.record_protection_violation(pt.above, player_name)
        return itemstack
    end

    -- return if any of the nodes is not registered
    if not minetest.registered_nodes[under.name] then
        return itemstack
    end
    if not minetest.registered_nodes[above.name] then
        return itemstack
    end

    -- check if pointing at the top of the node
    if pt.above.y ~= pt.under.y + 1 then
        return itemstack
    end

    -- check if you can replace the node above the pointed node
    if not minetest.registered_nodes[above.name].buildable_to then
        return itemstack
    end

    -- check if pointing at soil
    if minetest.get_item_group(under.name, 'soil') < 2 then
        return itemstack
    end

    -- add the node and remove 1 item from the itemstack
    x_farming.log_player_action(placer, 'places node', plantname, 'at', pt.above)
    minetest.add_node(pt.above, { name = plantname, param2 = 1 })
    x_farming.tick(pt.above)
    if not minetest.is_creative_enabled(player_name) then
        itemstack:take_item()
    end

    return itemstack
end

x_farming.grow_plant = function(pos, elapsed)
    local node = minetest.get_node(pos)
    local name = node.name
    local def = minetest.registered_nodes[name]

    if not def.next_plant then
        -- disable timer for fully grown plant
        return
    end

    -- grow seed
    if minetest.get_item_group(node.name, 'seed') and def.fertility then
        local soil_node = minetest.get_node_or_nil({ x = pos.x, y = pos.y - 1, z = pos.z })
        if not soil_node then
            x_farming.tick_again(pos)
            return
        end
        -- omitted is a check for light, we assume seeds can germinate in the dark.
        for _, v in pairs(def.fertility) do
            if minetest.get_item_group(soil_node.name, v) ~= 0 or string.find(soil_node.name, 'mcl_farming:soil') then
                local placenode = { name = def.next_plant }
                if def.place_param2 then
                    placenode.param2 = def.place_param2
                end
                minetest.swap_node(pos, placenode)
                if minetest.registered_nodes[def.next_plant].next_plant then
                    x_farming.tick(pos)
                    return
                end
            end
        end

        return
    end

    -- check if on wet soil
    local below = minetest.get_node({ x = pos.x, y = pos.y - 1, z = pos.z })
    if minetest.get_item_group(below.name, 'soil') < 3 then
        x_farming.tick_again(pos)
        return
    end

    -- check light
    local light = minetest.get_node_light(pos)
    if not light or light < def.minlight or light > def.maxlight then
        x_farming.tick_again(pos)
        return
    end

    -- grow
    local placenode = { name = def.next_plant }
    if def.place_param2 then
        placenode.param2 = def.place_param2
    end
    minetest.swap_node(pos, placenode)

    -- new timer needed?
    if minetest.registered_nodes[def.next_plant].next_plant then
        x_farming.tick(pos)
    end
    return
end

-- Register plants
x_farming.register_plant = function(name, def)
    local mname = name:split(':')[1]
    local pname = name:split(':')[2]

    -- Check def table
    if not def.description then
        def.description = S('Seed')
    end
    if not def.harvest_description then
        def.harvest_description = pname:gsub('^%l', string.upper)
    end
    if not def.inventory_image then
        def.inventory_image = 'unknown_item.png'
    end
    if not def.steps then
        return nil
    end
    if not def.minlight then
        def.minlight = 1
    end
    if not def.maxlight then
        def.maxlight = 14
    end
    if not def.fertility then
        def.fertility = {}
    end

    x_farming.registered_plants[pname] = def

    -- Register seed
    local lbm_nodes = { mname .. ':seed_' .. pname }
    local g = {
        -- MTG
        seed = 1,
        snappy = 3,
        attached_node = 1,
        flammable = 2,
        -- MCL
        handy = 1,
        shearsy = 1,
        deco_block = 1,
        dig_by_water = 1,
        destroy_by_lava_flow = 1,
        dig_by_piston = 1
    }

    for k, v in pairs(def.fertility) do
        g[v] = 1
    end

    if def.groups then
        for group, value in pairs(def.groups) do
            g[group] = value
        end
    end

    minetest.register_node(':' .. mname .. ':seed_' .. pname, {
        description = def.description,
        tiles = def.tiles or { def.inventory_image },
        inventory_image = def.inventory_image,
        wield_image = def.inventory_image,
        drawtype = def.drawtype or 'signlike',
        groups = g,
        paramtype = 'light',
        paramtype2 = def.paramtype2 or 'wallmounted',
        place_param2 = def.place_param2 or nil, -- this isn't actually used for placement
        walkable = false,
        sunlight_propagates = true,
        selection_box = def.selection_box or {
            type = 'fixed',
            fixed = { -0.5, -0.5, -0.5, 0.5, -5 / 16, 0.5 },
        },
        fertility = def.fertility,
        sounds = x_farming.node_sound_grass_defaults(),
        special_tiles = def.special_tiles and { { name = def.special_tiles, tileable_vertical = true } } or nil,
        visual_scale = def.visual_scale or 1,
        node_dig_prediction = def.node_dig_prediction or '',
        node_placement_prediction = def.node_placement_prediction or nil,

        on_place = def.on_place or function(itemstack, placer, pointed_thing)
            local under = pointed_thing.under
            local node = minetest.get_node(under)
            local udef = minetest.registered_nodes[node.name]
            if udef and udef.on_rightclick and
                not (placer and placer:is_player() and
                placer:get_player_control().sneak)
            then
                return udef.on_rightclick(under, node, placer, itemstack, pointed_thing) or itemstack
            end

            return x_farming.place_seed(itemstack, placer, pointed_thing, mname .. ':seed_' .. pname)
        end,
        after_destruct = def.after_destruct or nil,
        next_plant = mname .. ':' .. pname .. '_1',
        on_timer = def.on_timer or x_farming.grow_plant,
        minlight = def.minlight,
        maxlight = def.maxlight,
        _mcl_blast_resistance = 0,
        _mcl_hardness = 0,
    })

    -- Register harvest
    minetest.register_craftitem(':' .. mname .. ':' .. pname, {
        description = def.harvest_description,
        inventory_image = mname .. '_' .. pname .. '.png',
        groups = def.groups or { flammable = 2 },
    })

    -- Register growing steps
    for i = 1, def.steps do
        local base_rarity = 1
        if def.steps ~= 1 then
            base_rarity = 8 - (i - 1) * 7 / (def.steps - 1)
        end
        local drop = {
            items = {
                {items = { mname .. ':' .. pname }, rarity = base_rarity },
                {items = { mname .. ':' .. pname }, rarity = base_rarity * 2 },
                {items = { mname .. ':seed_' .. pname }, rarity = base_rarity },
                {items = { mname .. ':seed_' .. pname }, rarity = base_rarity * 2 },
            }
        }
        local nodegroups = {
            -- MTG
            snappy = 3,
            flammable = 2,
            plant = 1,
            not_in_creative_inventory = 1,
            attached_node = 1,
            -- MCL
            handy = 1,
            shearsy = 1,
            deco_block = 1,
            dig_by_water = 1,
            destroy_by_lava_flow = 1,
            dig_by_piston = 1
        }
        nodegroups[pname] = i

        if def.groups then
            for group, value in pairs(def.groups) do
                nodegroups[group] = value
            end
        end

        local next_plant = nil

        if i < def.steps then
            next_plant = mname .. ':' .. pname .. '_' .. (i + 1)
            lbm_nodes[#lbm_nodes + 1] = mname .. ':' .. pname .. '_' .. i
        end

        local _buildable_to = true

        if def.buildable_to ~= nil then
            _buildable_to = def.buildable_to
        end

        minetest.register_node(':' .. mname .. ':' .. pname .. '_' .. i, {
            drawtype = def.drawtype or 'plantlike',
            waving = 1,
            tiles = def.tiles or { mname .. '_' .. pname .. '_' .. i .. '.png' },
            special_tiles = def.special_tiles and { { name = mname .. '_' .. pname .. '_' .. i .. '.png', tileable_vertical = true } } or nil,
            paramtype = 'light',
            paramtype2 = def.paramtype2 or nil,
            place_param2 = def.place_param2 or nil,
            visual_scale = def.visual_scale or 1,
            node_dig_prediction = def.node_dig_prediction or '',
            node_placement_prediction = def.node_placement_prediction or nil,
            walkable = false,
            buildable_to = _buildable_to,
            drop = drop,
            selection_box = def['selection_box_' .. i] and def['selection_box_' .. i] or {
                type = 'fixed',
                fixed = { -0.5, -0.5, -0.5, 0.5, -5 / 16, 0.5 },
            },
            groups = nodegroups,
            sounds = x_farming.node_sound_leaves_defaults(),
            next_plant = next_plant,
            on_timer = def.on_timer or x_farming.grow_plant,
            minlight = def.minlight,
            maxlight = def.maxlight,
            _mcl_blast_resistance = 0,
            _mcl_hardness = 0,
            after_destruct = def.after_destruct or nil,
        })
    end

    -- replacement LBM for pre-nodetimer plants
    minetest.register_lbm({
        name = ':' .. mname .. ':start_nodetimer_' .. pname,
        nodenames = lbm_nodes,
        action = function(pos, node)
            x_farming.tick_again(pos)
        end,
    })

    -- Return
    local r = {
        seed = mname .. ':seed_' .. pname,
        harvest = mname .. ':' .. pname
    }
    return r
end

---grow blocks next to the plant
function x_farming.grow_block(pos, elapsed)
    local node = minetest.get_node(pos)
    local random_pos = false
    local spawn_positions = {}
    local right_pos = { x = pos.x + 1, y = pos.y, z = pos.z }
    local front_pos = { x = pos.x, y = pos.y, z = pos.z + 1 }
    local left_pos = { x = pos.x - 1, y = pos.y, z = pos.z }
    local back_pos = { x = pos.x, y = pos.y, z = pos.z - 1 }
    local right = minetest.get_node(right_pos)
    local front = minetest.get_node(front_pos)
    local left = minetest.get_node(left_pos)
    local back = minetest.get_node(back_pos)
    local def = minetest.registered_nodes[node.name]

    local children = {}

    ---look for fruits around the stem
    if (right.name == def.next_plant) then
        children.right = right_pos
    end
    if (front.name == def.next_plant) then
        children.front = front_pos
    end
    if (left.name == def.next_plant) then
        children.left = left_pos
    end
    if (back.name == def.next_plant) then
        children.back = back_pos
    end

    ---check if the fruit belongs to this stem
    for side, child_pos in pairs(children) do

        local parent_pos_from_child = x_farming.meta_get_str('parent', child_pos)

        ---disable timer for fully grown plant - fruit for this stem already exists
        if minetest.pos_to_string(pos) == parent_pos_from_child then
            return
        end
    end

    ---make sure that at least one side of the plant has space to put fruit
    if right.name == 'air' then
        table.insert(spawn_positions, right_pos)
    end
    if front.name == 'air' then
        table.insert(spawn_positions, front_pos)
    end
    if left.name == 'air' then
        table.insert(spawn_positions, left_pos)
    end
    if back.name == 'air' then
        table.insert(spawn_positions, back_pos)
    end

    ---plant is closed from all sides
    if #spawn_positions < 1 then
        x_farming.tick_block_short(pos)
        return
    else
        ---pick random from the open sides
        local pick_random

        if #spawn_positions == 1 then
            pick_random = #spawn_positions
        else
            pick_random = math.random(1, #spawn_positions)
        end

        for k, v in pairs(spawn_positions) do
            if k == pick_random then
                random_pos = v
            end
        end
    end

    ---check light
    local light = minetest.get_node_light(pos)
    if not light or light < 13 or light > 14 then
        x_farming.tick_block_short(pos)
        return
    end

    ---spawn block
    if random_pos then
        minetest.set_node(random_pos, { name = def.next_plant })
        x_farming.meta_set_str('parent', minetest.pos_to_string(pos), random_pos)
    end
    return
end

function x_farming.grow_kiwi_tree(pos)
    local path = minetest.get_modpath('x_farming') ..
        '/schematics/x_farming_kiwi_tree_from_sapling.mts'
    minetest.place_schematic({ x = pos.x - 2, y = pos.y, z = pos.z - 2 },
        path, 'random', nil, false)
end

-- 'can grow' function - copy from MTG

function x_farming.can_grow(pos)
    local node_under = minetest.get_node_or_nil({ x = pos.x, y = pos.y - 1, z = pos.z })
    if not node_under then
        return false
    end

    if minetest.get_item_group(node_under.name, 'soil') == 0 then
        return false
    end

    local light_level = minetest.get_node_light(pos)

    if not light_level or light_level < 13 then
        return false
    end

    return true
end

---Grow sapling

function x_farming.grow_sapling(pos)
    if not x_farming.can_grow(pos) then
        ---try again 5 min later
        minetest.get_node_timer(pos):start(300)
        return
    end

    local node = minetest.get_node(pos)
    if node.name == 'x_farming:kiwi_sapling' then
        minetest.log('action', 'A sapling grows into a tree at ' ..
            minetest.pos_to_string(pos))
        x_farming.grow_kiwi_tree(pos)
    end
end

---Grow Large Cactus

function x_farming.grow_large_cactus(pos)
    local path = minetest.get_modpath('x_farming') ..
        '/schematics/x_farming_large_cactus_from_seedling.mts'
    minetest.place_schematic({ x = pos.x, y = pos.y, z = pos.z },
        path, 'random', nil, false, 'place_center_x, place_center_z')
end

---Grow Jungle Tree

function x_farming.grow_jungle_tree(pos)
    local path = minetest.get_modpath('x_farming') ..
        '/schematics/x_farming_jungle_tree_with_cocoa_from_sapling.mts'
    minetest.place_schematic({ x = pos.x - 2, y = pos.y - 1, z = pos.z - 2 },
        path, nil, nil, false)
end

---Pine Nut Tree

function x_farming.grow_pine_nut_tree(pos)
    local path = minetest.get_modpath('x_farming') ..
        '/schematics/x_farming_pine_nut_tree_from_sapling.mts'
    minetest.place_schematic({ x = pos.x - 2, y = pos.y, z = pos.z - 2 },
        path, '0', nil, false)
end

---Christmas Tree

function x_farming.grow_christmas_tree(pos)
    local path
    if math.random() > 0.5 then
        path = minetest.get_modpath('x_farming') .. '/schematics/x_farming_christmas_tree_large.mts'
        minetest.place_schematic({ x = pos.x - 2, y = pos.y, z = pos.z - 2 }, path, '0', nil, false)
    else
        path = minetest.get_modpath('x_farming') .. '/schematics/x_farming_christmas_tree.mts'
        minetest.place_schematic({ x = pos.x - 1, y = pos.y, z = pos.z - 1 }, path, '0', nil, false)
    end
end

----
--- Crates and Bags
----

function x_farming.tick_crates(pos)
    minetest.get_node_timer(pos):start(math.random(332, 572))
end

function x_farming.tick_again_crates(pos)
    minetest.get_node_timer(pos):start(math.random(80, 160))
end

function x_farming.get_crate_or_bag_formspec(pos, label_copy)
    local spos = pos.x .. ',' .. pos.y .. ',' .. pos.z
    local hotbar_bg = ''
    local list_bg = ''

    for i = 0, 7, 1 do
        hotbar_bg = hotbar_bg .. 'image[' .. 0 + i .. ', ' .. 4.85 .. ';1,1;x_farming_crate_ui_bg_hb_slot.png]'
    end

    for row = 0, 2, 1 do
        for i = 0, 7, 1 do
            list_bg = list_bg .. 'image[' .. 0 + i .. ',' .. 6.08 + row .. ';1,1;x_farming_crate_ui_bg_slot.png]'
        end
    end

    local formspec = {
        'size[8,9]',
        'style_type[label;textcolor=#FFFFFF]',
        'background[5,5;1,1;x_farming_crate_ui_bg.png;true]',
        'list[nodemeta:', spos, ';main;0,0.3;8,4;]',
        'list[current_player;main;0,4.85;8,1;]',
        'list[current_player;main;0,6.08;8,3;8]',
        'listring[nodemeta:', spos, ';main]',
        'listring[current_player;main]',
        'label[2,0;' .. minetest.formspec_escape(label_copy) .. ']',
        list_bg,
        hotbar_bg,
        'image[0,0.3;1,1;x_farming_crate_ui_bg_hb_slot.png]'
    }

    formspec = table.concat(formspec, '')

    return formspec
end

---Crate
function x_farming.register_crate(name, def)
    local _def = table.copy(def) or {}

    _def._custom = _def._custom or {}

    _def.name = 'x_farming:' .. name
    _def.description = def.description or name
    _def.short_description = def.short_description or def.description
    _def.drawtype = 'mesh'
    _def.paramtype = 'light'
    _def.paramtype2 = 'facedir'
    _def.mesh = 'x_farming_crate.obj'
    _def.tiles = def.tiles
    _def.use_texture_alpha = 'clip'
    _def.sounds = def.sounds or x_farming.node_sound_wood_defaults()
    _def.is_ground_content = false
    _def.groups = def.groups or {
        -- MTG
        choppy = 2,
        oddly_breakable_by_hand = 2,
        -- MCL
        handy = 1,
        material_wood = 1,
        deco_block = 1,
        fire_encouragement = 3,
        fire_flammability = 4,
        -- ALL
        not_in_creative_inventory = 1,
        flammable = 2
    }
    _def.stack_max = def.stack_max or 1
    _def.mod_origin = 'x_farming'
    -- MCL
    _def._mcl_hardness = 0.6
    _def._mcl_blast_resistance = 0.6

    if _def._custom.crate_item then
        x_farming.allowed_crate_items[_def._custom.crate_item] = true
    end

    _def.on_construct = function(pos)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        meta:set_string('infotext', _def.short_description)
        meta:set_string('owner', '')
        inv:set_size('main', 1)
    end

    _def.after_place_node = function(pos, placer, itemstack, pointed_thing)
        local meta = minetest.get_meta(pos)
        local meta_st = itemstack:get_meta()
        local crate_inv = minetest.deserialize(meta_st:get_string('crate_inv'))
        local inv = meta:get_inventory()

        if crate_inv then
            inv:add_item('main', ItemStack(crate_inv))
        end

        local node = minetest.get_node(pos)

        meta:set_string('owner', placer:get_player_name() or '')

        if not inv:is_empty('main') then
            local inv_stack = inv:get_stack('main', 1)

            meta:set_string('infotext', _def.short_description
                .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')\n'
                .. inv_stack:get_description() .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())
        else
            local swap_node = minetest.registered_nodes['x_farming:crate_empty']
            if swap_node and inv:is_empty('main') and node.name ~= swap_node.name then
                minetest.swap_node(pos, { name = swap_node.name, param2 = node.param2 })
                meta:set_string('infotext', swap_node.short_description
                    .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')')
            end
        end
    end

    _def.on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
        local p_name = clicker:get_player_name()
        if minetest.is_protected(pos, p_name) then
            return itemstack
        end

        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local inv_stack = inv:get_stack('main', 1)
        local label_copy = _def.short_description .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner')
            .. ')\n' .. inv_stack:get_description()
        minetest.show_formspec(p_name, _def.name, x_farming.get_crate_or_bag_formspec(pos, label_copy))
        minetest.sound_play('x_farming_wood_hit', { gain = 0.3, pos = pos, max_hear_distance = 10 }, true)
    end

    _def.on_blast = function(pos, intensity)
        if minetest.is_protected(pos, '') then
            return
        end

        local drops = {}
        local inv = minetest.get_meta(pos):get_inventory()
        local n = #drops

        for i = 1, inv:get_size('main') do
            local stack = inv:get_stack('main', i)
            if stack:get_count() > 0 then
                drops[n + 1] = stack:to_table()
                n = n + 1
            end
        end

        drops[#drops + 1] = name
        minetest.remove_node(pos)
        return drops
    end

    _def.can_dig = function(pos, player)
        return not minetest.is_protected(pos, player:get_player_name())
    end

    _def.preserve_metadata = function(pos, oldnode, oldmeta, drops)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local stack = drops[1]
        local meta_drop = stack:get_meta()

        if not inv:is_empty('main') then
            local inv_stack = inv:get_stack('main', 1)

            meta_drop:set_string('crate_inv', minetest.serialize(inv_stack:to_table()))
            meta_drop:set_string('description', stack:get_description() .. '\n' .. inv_stack:get_description()
                .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())

            return
        end
    end

    _def.allow_metadata_inventory_put = function(pos, listname, index, stack, player)
        local st_name = stack:get_name()

        if minetest.is_protected(pos, player:get_player_name())
            or (
                not x_farming.allowed_crate_items[st_name]
                and minetest.get_item_group(st_name, 'fish') == 0
            )
        then
            return 0
        end

        return stack:get_count()
    end

    _def.allow_metadata_inventory_take = function(pos, listname, index, stack, player)
        local st_name = stack:get_name()

        if minetest.is_protected(pos, player:get_player_name())
            or (
                not x_farming.allowed_crate_items[st_name]
                and minetest.get_item_group(st_name, 'fish') == 0
            )
        then
            return 0
        end

        return stack:get_count()
    end

    _def.on_metadata_inventory_put = function(pos, listname, index, stack, player)
        local stack_name = stack:get_name()

        if not stack_name or stack_name == '' then
            return
        end

        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local node = minetest.get_node(pos)
        local inv_stack = inv:get_stack('main', 1)
        local split_name = stack_name:split(':')

        if minetest.get_item_group(stack_name, 'fish') ~= 0 then
            split_name = { 'x_farming', 'fish' }
        end

        if stack_name == 'x_farming:cotton' then
            split_name = { 'x_farming', 'cotton2' }
        end

        local swap_node = minetest.registered_nodes['x_farming:crate_' .. split_name[2] .. '_3']

        if not swap_node then
            return
        end

        if not inv:is_empty(listname) and node.name ~= swap_node.name then
            local p_name = player:get_player_name()

            minetest.swap_node(pos, { name = swap_node.name, param2 = node.param2 })

            local label_copy = swap_node.short_description .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')\n'
                .. inv_stack:get_description()

            minetest.show_formspec(p_name, _def.name, x_farming.get_crate_or_bag_formspec(pos, label_copy))
        end

        meta:set_string('infotext', swap_node.short_description .. ' (' .. S('owned by')
            .. ' ' .. meta:get_string('owner') .. ')\n'
            .. inv_stack:get_description() .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())
    end

    _def.on_metadata_inventory_take = function(pos, listname, index, stack, player)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local inv_stack = inv:get_stack('main', 1)
        local node = minetest.get_node(pos)

        if inv:is_empty(listname) then
            local p_name = player:get_player_name()
            local swap_node = minetest.registered_nodes['x_farming:crate_empty']

            if swap_node then
                minetest.swap_node(pos, { name = swap_node.name, param2 = node.param2 })
                meta:set_string('infotext', swap_node.short_description
                    .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')')

                local label_copy = swap_node.short_description .. ' (' .. S('owned by') .. ' '
                    .. meta:get_string('owner') .. ')\n' .. inv_stack:get_description()

                minetest.show_formspec(p_name, _def.name, x_farming.get_crate_or_bag_formspec(pos, label_copy))
            end
        else
            local node_def = minetest.registered_nodes[node.name]

            if node_def then
                meta:set_string('infotext', node_def.short_description
                    .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')\n'
                    .. inv_stack:get_description() .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())
            end
        end
    end

    _def.on_timer = function(pos, elapsed)
        local pos_above = { x = pos.x, y = pos.y + 1, z = pos.z }
        local node_above = minetest.get_node(pos_above)

        if not node_above then
            x_farming.tick_again_crates(pos)
            return
        end

        if node_above.name ~= 'air' then
            x_farming.tick_again_crates(pos)
            return
        end

        local rand1 = math.random(1, 2) / 10

        minetest.add_particlespawner({
            amount = 60,
            time = 15,
            minpos = { x = pos_above.x - 0.1, y = pos_above.y - 0.3, z = pos_above.z - 0.1 },
            maxpos = { x = pos_above.x + 0.1, y = pos_above.y + 0.4, z = pos_above.z + 0.1 },
            minvel = { x = rand1 * -1, y = rand1 * -1, z = rand1 * -1 },
            maxvel = { x = rand1, y = rand1, z = rand1 },
            minacc = { x = rand1 * -1, y = rand1 * -1, z = rand1 * -1 },
            maxacc = { x = rand1, y = rand1, z = rand1 },
            minexptime = 1,
            maxexptime = 1.5,
            minsize = 0.1,
            maxsize = 0.3,
            texture = 'x_farming_fly.png',
            collisiondetection = true,
            object_collision = true
        })

        x_farming.tick_crates(pos)
    end

    x_farming.registered_crates[_def.name] = _def

    if _def.name ~= 'x_farming:crate_empty' then
        table.insert(x_farming.lbm_nodenames_crates, _def.name)
    end

    minetest.register_node(_def.name, _def)
end

---Bag
function x_farming.register_bag(name, def)
    local _def = table.copy(def) or {}

    _def._custom = _def._custom or {}

    _def.name = 'x_farming:' .. name
    _def.description = def.description or name
    _def.short_description = def.short_description or def.description
    _def.drawtype = 'mesh'
    _def.paramtype = 'light'
    _def.paramtype2 = 'facedir'
    _def.mesh = 'x_farming_bag.obj'
    _def.tiles = def.tiles
    _def.use_texture_alpha = 'clip'
    _def.sounds = def.sounds or x_farming.node_sound_sand_defaults()
    _def.is_ground_content = false
    _def.groups = def.groups or {
        -- MTG
        choppy = 2,
        oddly_breakable_by_hand = 2,
        -- MCL
        handy = 1,
        building_block = 1,
        deco_block = 1,
        -- ALL
        not_in_creative_inventory = 1,
        flammable = 2
    }
    -- MCL
    _def._mcl_hardness = 0.6
    _def._mcl_blast_resistance = 0.6
    _def.stack_max = def.stack_max or 1
    _def.mod_origin = 'x_farming'

    if _def._custom.bag_item then
        x_farming.allowed_bag_items[_def._custom.bag_item] = true
    end

    _def.on_construct = function(pos)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        meta:set_string('infotext', _def.short_description)
        meta:set_string('owner', '')
        inv:set_size('main', 1)
    end

    _def.after_place_node = function(pos, placer, itemstack, pointed_thing)
        local meta = minetest.get_meta(pos)
        local meta_st = itemstack:get_meta()
        local bag_inv = minetest.deserialize(meta_st:get_string('bag_inv'))
        local inv = meta:get_inventory()

        if bag_inv then
            inv:add_item('main', ItemStack(bag_inv))
        end

        local node = minetest.get_node(pos)

        meta:set_string('owner', placer:get_player_name() or '')

        if not inv:is_empty('main') then
            local inv_stack = inv:get_stack('main', 1)

            meta:set_string('infotext', _def.short_description .. ' (' .. S('owned by') .. ' '
                .. meta:get_string('owner') .. ')\n' .. inv_stack:get_description()
                .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())
        else
            local swap_node = minetest.registered_nodes['x_farming:bag_empty']
            if swap_node and inv:is_empty('main') and node.name ~= swap_node.name then
                minetest.swap_node(pos, { name = swap_node.name, param2 = node.param2 })
                meta:set_string('infotext', swap_node.short_description
                    .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')')
            end
        end
    end

    _def.on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
        local p_name = clicker:get_player_name()

        if minetest.is_protected(pos, p_name) then
            return itemstack
        end

        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local inv_stack = inv:get_stack('main', 1)
        local label_copy = _def.short_description .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')\n'
            .. inv_stack:get_description()
        minetest.show_formspec(p_name, _def.name, x_farming.get_crate_or_bag_formspec(pos, label_copy))
        minetest.sound_play('x_farming_sand_footstep', { gain = 0.3, pos = pos, max_hear_distance = 10 }, true)
    end

    _def.on_blast = function(pos, intensity)
        if minetest.is_protected(pos, '') then
            return
        end

        local drops = {}
        local inv = minetest.get_meta(pos):get_inventory()
        local n = #drops

        for i = 1, inv:get_size('main') do
            local stack = inv:get_stack('main', i)
            if stack:get_count() > 0 then
                drops[n + 1] = stack:to_table()
                n = n + 1
            end
        end

        drops[#drops + 1] = name
        minetest.remove_node(pos)
        return drops
    end

    _def.can_dig = function(pos, player)
        return not minetest.is_protected(pos, player:get_player_name())
    end

    _def.preserve_metadata = function(pos, oldnode, oldmeta, drops)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local stack = drops[1]
        local meta_drop = stack:get_meta()

        if not inv:is_empty('main') then
            local inv_stack = inv:get_stack('main', 1)

            meta_drop:set_string('bag_inv', minetest.serialize(inv_stack:to_table()))
            meta_drop:set_string('description', stack:get_description() .. '\n'
                .. inv_stack:get_description() .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())

            return
        end
    end

    _def.allow_metadata_inventory_put = function(pos, listname, index, stack, player)
        if minetest.is_protected(pos, player:get_player_name()) or not x_farming.allowed_bag_items[stack:get_name()] then
            return 0
        end

        return stack:get_count()
    end

    _def.allow_metadata_inventory_take = function(pos, listname, index, stack, player)
        if minetest.is_protected(pos, player:get_player_name()) or not x_farming.allowed_bag_items[stack:get_name()] then
            return 0
        end

        return stack:get_count()
    end

    _def.on_metadata_inventory_put = function(pos, listname, index, stack, player)
        local stack_name = stack:get_name()

        if not stack_name or stack_name == '' then
            return
        end

        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local split_name = stack_name:split(':')
        local node = minetest.get_node(pos)
        local swap_node = minetest.registered_nodes['x_farming:bag_' .. split_name[2]]
        local inv_stack = inv:get_stack('main', 1)

        if not swap_node then
            return
        end

        if not inv:is_empty(listname) and node.name ~= swap_node.name then
            local p_name = player:get_player_name()

            minetest.swap_node(pos, { name = swap_node.name, param2 = node.param2 })

            local label_copy = swap_node.short_description .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')\n'
                .. inv_stack:get_description()

            minetest.show_formspec(p_name, _def.name, x_farming.get_crate_or_bag_formspec(pos, label_copy))
        end

        meta:set_string('infotext', swap_node.short_description .. ' (' .. S('owned by')
            .. ' ' .. meta:get_string('owner') .. ')\n'
            .. inv_stack:get_description() .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())
    end

    _def.on_metadata_inventory_take = function(pos, listname, index, stack, player)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        local inv_stack = inv:get_stack('main', 1)
        local node = minetest.get_node(pos)

        if inv:is_empty(listname) then
            local p_name = player:get_player_name()
            local swap_node = minetest.registered_nodes['x_farming:bag_empty']

            if swap_node then
                minetest.swap_node(pos, { name = swap_node.name, param2 = node.param2 })
                meta:set_string('infotext', swap_node.short_description
                    .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')')

                local label_copy = swap_node.short_description
                    .. ' (' .. S('owned by') .. ' ' .. meta:get_string('owner') .. ')\n' .. inv_stack:get_description()

                minetest.show_formspec(p_name, _def.name, x_farming.get_crate_or_bag_formspec(pos, label_copy))
            end
        else
            local node_def = minetest.registered_nodes[node.name]

            if node_def then
                meta:set_string('infotext', node_def.short_description .. ' (' .. S('owned by') .. ' '
                    .. meta:get_string('owner') .. ')\n' .. inv_stack:get_description()
                    .. '\n' .. S('Quantity') .. ': ' .. inv_stack:get_count())
            end
        end
    end

    minetest.register_node(_def.name, _def)
end

--
---Bonemeal
--

------------
---Main API for x_bonemeal Mod
---@author Juraj Vajda
---@license GNU LGPL 2.1
----

---Get creative mode setting from minetest.conf
local creative_mod_cache = minetest.settings:get_bool('creative_mode')

---Check if creating mode is enabled or player has creative privs
---@param name string name
---@return boolean
function x_farming.x_bonemeal.is_creative(name)
    return creative_mod_cache or minetest.check_player_privs(name, { creative = true })
end

---Check if node has a soil below its self
---@param under Vector of position
---@return boolean
function x_farming.x_bonemeal.is_on_soil(under)
    local below = minetest.get_node_or_nil({ x = under.x, y = under.y - 1, z = under.z })

    if not below then
        return false
    end

    if minetest.get_item_group(below.name, 'soil') == 0 and below.name ~= 'mcl_farming:soil_wet' then
        return false
    end

    return true
end

---Check if node has a sand below its self
---@param under Vector of position
---@return boolean
function x_farming.x_bonemeal.is_on_sand(under)
    local below = minetest.get_node_or_nil({ x = under.x, y = under.y - 1, z = under.z })

    if not below then
        return false
    end

    if minetest.get_item_group(below.name, 'sand') == 0
        and minetest.get_item_group(below.name, 'everness_sand') == 0
    then
        return false
    end

    return true
end

---Growth steps for farming plants, there is no way of getting them dynamically,
--- so they are defined in the local table variable
local farming_steps = {
    ['farming:wheat'] = 8,
    ['farming:cotton'] = 8,
    ['x_farming:coffee'] = 5,
    ['x_farming:corn'] = 10,
    ['x_farming:obsidian_wart'] = 6,
    ['x_farming:melon'] = 8,
    ['x_farming:pumpkin'] = 8,
    ['x_farming:carrot'] = 8,
    ['x_farming:potato'] = 8,
    ['x_farming:beetroot'] = 8,
    ['x_farming:strawberry'] = 4,
    ['x_farming:stevia'] = 8,
    ['x_farming:soybean'] = 7,
    ['x_farming:salt'] = 7,
    ['x_farming:barley'] = 8,
    ['x_farming:cotton'] = 8,
}

---Particle and sound effect after the bone meal is successfully used
---@param pos Vector containing position
function x_farming.x_bonemeal.particle_effect(pos)
    minetest.sound_play('x_farming_x_bonemeal_grow', {
        pos = pos,
        gain = 0.5,
    })

    minetest.add_particlespawner({
        amount = 6,
        time = 3,
        minpos = { x = pos.x - 0.4, y = pos.y - 0.4, z = pos.z - 0.4 },
        maxpos = { x = pos.x + 0.4, y = pos.y, z = pos.z + 0.4 },
        minvel = { x = 0, y = 0, z = 0 },
        maxvel = { x = 0, y = 0.1, z = 0 },
        minacc = vector.new({ x = 0, y = 0, z = 0 }),
        maxacc = vector.new({ x = 0, y = 0.1, z = 0 }),
        minexptime = 2,
        maxexptime = 3,
        minsize = 1,
        maxsize = 3,
        texture = 'x_farming_x_bonemeal_particles.png',
        animation = {
            type = 'vertical_frames',
            aspect_w = 8,
            aspect_h = 8,
            length = 3,
        },
    })
end

function x_farming.x_bonemeal.tableContains(table, value)
    local found = false

    if not table or type(table) ~= 'table' then
        return found
    end

    for k, v in ipairs(table) do
        if v == value then
            found = true
            break
        end
    end

    return found
end

function x_farming.x_bonemeal.groupContains(groups, fertility, value)
    local found = false

    if not groups or type(groups) ~= 'table' then
        return found
    end

    if groups[fertility] and groups[fertility] == value then
        found = true
    end

    return found
end

---Handle growth of decorations based on biome
---@param itemstack ItemStack
---@param user ObjectRef | nil
---@param pointed_thing PointedThingDef
---@return { ['success']: boolean, ['itemstack']: ItemStack }
function x_farming.x_bonemeal.grow_grass_and_flowers(itemstack, user, pointed_thing)
    local result = {
        success = false,
        itemstack = itemstack
    }
    local node = minetest.get_node(pointed_thing.under)

    if not node then
        return result
    end

    local pos0 = vector.subtract(pointed_thing.under, 3)
    local pos1 = vector.add(pointed_thing.under, 3)
    local biome_data = minetest.get_biome_data(pointed_thing.under)

    if not biome_data then
        return result
    end

    local biome_name = minetest.get_biome_name(biome_data.biome)

    if not biome_name then
        return result
    end

    local random_number = math.random(2, 6)
    local registered_decorations_filtered = {}
    ---@type ItemStack | nil
    local returned_itemstack
    local node_def = minetest.registered_nodes[node.name]
    local below_water = false
    local floats_on_water = false
    local node_in_decor = false
    local positions_dirty
    local positions = {}
    local decor_place_on = {}
    -- print('biome_name', biome_name)

    ---check 1 node below pointed node (floats on water)
    local test_node = minetest.get_node({
        x = pointed_thing.under.x,
        y = pointed_thing.under.y - 1,
        z = pointed_thing.under.z
    })
    local test_node_def = minetest.registered_nodes[test_node.name]

    if test_node_def
        and test_node_def.liquidtype == 'source'
        and minetest.get_item_group(test_node_def.name, 'water') > 0
    then
        floats_on_water = true
    end

    ---check 2 nodes above pointed nodes (below water)
    local water_nodes_above = 0
    for i = 1, 2 do
        local test_node2 = minetest.get_node({
            x = pointed_thing.under.x,
            y = pointed_thing.under.y + i,
            z = pointed_thing.under.z
        })
        local test_node_def2 = minetest.registered_nodes[test_node2.name]

        if test_node_def2
            and test_node_def2.liquidtype == 'source'
            and minetest.get_item_group(test_node_def2.name, 'water') > 0
        then
            water_nodes_above = water_nodes_above + 1
        end
    end

    if water_nodes_above == 2 then
        below_water = true
    end

    if below_water then
        positions_dirty = minetest.find_nodes_in_area(pos0, pos1, node.name)
    elseif floats_on_water then
        positions_dirty = minetest.find_nodes_in_area(pos0, pos1, 'air')
    else
        positions_dirty = minetest.find_nodes_in_area_under_air(pos0, pos1, node.name)
    end

    ---find suitable decorations
    for _, v in pairs(minetest.registered_decorations) do
        ---only for 'simple' decoration types
        if v.deco_type == 'simple' then
            ---filter based on biome name in `biomes` table and node name in `place_on` table
            if x_farming.x_bonemeal.tableContains(v.biomes, biome_name) then
                table.insert(registered_decorations_filtered, v)
            end
        end

        ---clicked node is in decoration
        local _decoration = v.decoration

        if type(v.decoration) == 'string' then
            _decoration = { v.decoration }
        end

        if x_farming.x_bonemeal.tableContains(_decoration, node.name) then
            node_in_decor = true
        end

        ---all nodes on which decoration can be placed on
        ---indexed by name
        if not decor_place_on[v.place_on] then
            if type(v.place_on) == 'string' then
                decor_place_on[v.place_on] = true
            elseif type(v.place_on) == 'table' then
                for _, v2 in ipairs(v.place_on) do
                    decor_place_on[v2] = true
                end
            end
        end
    end

    ---find suitable positions
    for j, pos_value in ipairs(positions_dirty) do
        local node_at_pos = minetest.get_node(pos_value)

        if below_water then
            ---below water
            local water_nodes_above2 = 0

            ---check if 2 nodes above are water
            for i = 1, 2 do
                local test_node3 = minetest.get_node({ x = pos_value.x, y = pos_value.y + i, z = pos_value.z })
                local test_node_def3 = minetest.registered_nodes[test_node3.name]

                if test_node_def3
                    and test_node_def3.liquidtype == 'source'
                    and minetest.get_item_group(test_node_def3.name, 'water') > 0
                then
                    water_nodes_above2 = water_nodes_above2 + 1
                end
            end

            if water_nodes_above2 == 2 and decor_place_on[test_node.name] then
                table.insert(positions, pos_value)
            end
        else
            ---above water (not on water)
            if decor_place_on[node_at_pos.name] then
                table.insert(positions, pos_value)
            end
        end
    end

    ---find suitable positions (float on water)
    if floats_on_water then
        for _, pos_value in ipairs(positions_dirty) do
            local node_at_pos_below = minetest.get_node({ x = pos_value.x, y = pos_value.y - 1, z = pos_value.z })
            local test_node_def4 = minetest.registered_nodes[node_at_pos_below.name]

            if test_node_def4
                and test_node_def4.liquidtype == 'source'
                and minetest.get_item_group(test_node_def4.name, 'water') > 0
            then
                table.insert(positions, pos_value)
            end
        end
    end

    local returned_itemstack_success = 0

    ---place decorations on random positions
    if #positions > 0 and #registered_decorations_filtered > 0 then
        for i = 1, random_number do
            local idx = math.random(1, #positions)
            local random_pos = positions[idx]
            local random_decor = registered_decorations_filtered[math.random(1, #registered_decorations_filtered)]
            local random_decor_item = random_decor.decoration

            if floats_on_water and node_in_decor then
                random_decor_item = node.name
            elseif type(random_decor.decoration) == 'table' then
                random_decor_item = random_decor.decoration[math.random(1, #random_decor.decoration)]
            end

            local random_decor_item_def = minetest.registered_nodes[random_decor_item]

            if random_pos ~= nil and random_decor_item_def.drawtype ~= 'airlike' then
                if random_decor_item_def.on_place ~= nil and node_def and not node_def.on_rightclick then
                    ---on_place
                    local pt = {
                        type = 'node',
                        above = {
                            x = random_pos.x,
                            y = random_pos.y + 1,
                            z = random_pos.z
                        },
                        under = {
                            x = random_pos.x,
                            y = random_pos.y,
                            z = random_pos.z
                        }
                    }

                    if floats_on_water then
                        pt.above.y = random_pos.y
                        pt.under.y = random_pos.y - 1
                    end

                    returned_itemstack = random_decor_item_def.on_place(ItemStack(random_decor_item), user, pt)

                    if returned_itemstack and returned_itemstack:is_empty() then
                        returned_itemstack_success = returned_itemstack_success + 1
                        x_farming.x_bonemeal.particle_effect(pt.above)
                    end
                elseif random_decor_item_def ~= nil then
                    ---everything else
                    local pos_y = 1

                    if random_decor.place_offset_y ~= nil then
                        pos_y = random_decor.place_offset_y
                    end

                    x_farming.x_bonemeal.particle_effect(random_pos)
                    minetest.set_node({
                        x = random_pos.x,
                        y = random_pos.y + pos_y,
                        z = random_pos.z
                    },
                    { name = random_decor_item })
                end

                table.remove(positions, idx)
            else
                return result
            end
        end
    else
        return result
    end

    ---take item
    if user and returned_itemstack_success > 0
        and not x_farming.x_bonemeal.is_creative(user:get_player_name())
    then
        itemstack:take_item()
    end

    result.success = true
    result.itemstack = itemstack
    return result
end

---Handle farming and farming addons plants.
---Needed to copy this function from minetest_game and modify it in order to ommit some checks (e.g. light..)
---@param itemstack ItemStack
---@param user ObjectRef | nil
---@param pointed_thing PointedThingDef
---@return { ['success']: boolean, ['itemstack']: ItemStack }
function x_farming.x_bonemeal.grow_farming(itemstack, user, pointed_thing)
    local result = {
        success = false,
        itemstack = itemstack
    }
    local pos_under = pointed_thing.under
    local replace_node_name = minetest.get_node(pos_under).name
    local ndef = minetest.registered_nodes[replace_node_name]
    local take_item = false

    if not ndef.next_plant
        or ndef.next_plant == 'x_farming:pumpkin_fruit'
        or ndef.next_plant == 'x_farming:melon_fruit'
    then
        return result
    end

    local pos0 = vector.subtract(pointed_thing.under, 3)
    local pos1 = vector.add(pointed_thing.under, 3)
    local positions = minetest.find_nodes_in_area(pos0, pos1, { 'group:plant', 'group:seed' })

    for i, pos in ipairs(positions) do
        local isFertile = false
        replace_node_name = minetest.get_node(pos).name

        ---check if on wet soil
        local below = minetest.get_node({ x = pos.x, y = pos.y - 1, z = pos.z })
        local below_def = minetest.registered_nodes[below.name]

        if minetest.get_item_group(below.name, 'soil') == 3 or below.name == 'mcl_farming:soil_wet' then
            local current_step = tonumber(string.reverse(string.reverse(replace_node_name):split('_')[1]))
            local max_step = farming_steps[replace_node_name:gsub('_%d+', '', 1)]

            ---check if seed
            ---farming:seed_wheat
            local mod_plant = replace_node_name:split(':')
            ---seed_wheat
            local seed_plant = mod_plant[2]:split('_')
            local seed_name = replace_node_name

            if seed_plant[1] == 'seed' then
                current_step = 0
                if replace_node_name == 'x_farming:seed_obsidian_wart' then
                    replace_node_name = mod_plant[1] .. ':' .. seed_plant[2] .. '_' .. seed_plant[3]
                else
                    replace_node_name = mod_plant[1] .. ':' .. seed_plant[2]
                end
                max_step = farming_steps[replace_node_name]
                replace_node_name = replace_node_name .. '_' .. current_step
            else
                if string.find(replace_node_name, 'obsidian_wart') then
                    seed_name = mod_plant[1] .. ':seed_' .. seed_plant[1] .. '_' .. seed_plant[2]
                else
                    seed_name = mod_plant[1] .. ':seed_' .. seed_plant[1]
                end
            end

            ---search for fertility (again after checking soil)
            local seed_def = minetest.registered_nodes[seed_name]

            if seed_def and below_def then
                if below_def.groups then
                    for _, v in ipairs(seed_def.fertility) do
                        if not isFertile then
                            isFertile = x_farming.x_bonemeal.groupContains(below_def.groups, v, 1) or below.name == 'mcl_farming:soil_wet'
                        end
                    end
                end
            end

            if current_step ~= nil and max_step ~= nil and current_step ~= max_step and isFertile then
                local available_steps = max_step - current_step
                local new_step = max_step - available_steps + math.random(available_steps)
                local new_plant = replace_node_name:gsub('_%d+', '_' .. new_step, 1)
                take_item = true

                local placenode_def = minetest.registered_nodes[new_plant]

                local placenode = { name = new_plant }
                if placenode_def and placenode_def.place_param2 then
                    placenode.param2 = placenode_def.place_param2
                end
                x_farming.x_bonemeal.particle_effect(pos)
                minetest.swap_node(pos, placenode)
            end
        end
    end

    ---take item if not in creative
    if user and not x_farming.x_bonemeal.is_creative(user:get_player_name()) and take_item then
        itemstack:take_item()
    end

    return {
        success = true,
        itemstack = itemstack
    }
end

---XBonemeal on_use
---@param self table x_farming.x_bonemeal
---@param itemstack ItemStack
---@param user ObjectRef | nil
---@param pointed_thing any
---@return { ['success']: boolean, ['itemstack']: ItemStack }
function x_farming.x_bonemeal.on_use(self, itemstack, user, pointed_thing)
    local result = {
        success = false,
        itemstack = itemstack
    }

    if not user then
        return result
    end

    local under = pointed_thing.under

    if not under then
        return result
    end
    if pointed_thing.type ~= 'node' then
        return result
    end
    if minetest.is_protected(under, user:get_player_name()) then
        return result
    end

    local node = minetest.get_node(under)

    if not node then
        return result
    end
    if node.name == 'ignore' then
        return result
    end

    local mod = node.name:split(':')[1]

    if (mod == 'farming' or mod == 'x_farming')
        and not string.find(node.name, '_sapling')
        and not string.find(node.name, '_seedling')
    then
        --
        -- Farming
        --
        return self.grow_farming(itemstack, user, pointed_thing)
    elseif self.tree_defs[node.name] then
        --
        -- Default (Trees, Bushes, Papyrus)
        --
        local def = self.tree_defs[node.name]
        local chance = math.random(1, def.chance)

        if chance == 1 then
            local success = def.grow_tree(under)

            if not success then
                return result
            end

            self.particle_effect({ x = under.x, y = under.y + 1, z = under.z })
        end

        -- take item if not in creative
        if not self.is_creative(user:get_player_name()) then
            itemstack:take_item()
        end

        return {
            success = true,
            itemstack = itemstack
        }
    else
        return self.grow_grass_and_flowers(itemstack, user, pointed_thing)
    end
end

--- API for registering tree growing from saplings using bonemeal
function x_farming.x_bonemeal.register_tree_defs(self, defs)
    if not defs or type(defs) ~= 'table' then
        minetest.log('warning', '[x_farming][x_bonemeal] Missing or incorrect definition: \n' .. dump(defs))
    end

    for _, value in ipairs(defs) do
        local def = table.copy(value)
        if not self.tree_defs[def.name] then
            self.tree_defs[def.name] = value
        end
    end
end

-- TheTermos (MIT)
-- vec components can be omitted e.g. vec={y=1}
function x_farming.pos_shift(pos, vec)
    vec.x = vec.x or 0
    vec.y = vec.y or 0
    vec.z = vec.z or 0

    return {
        x = pos.x + vec.x,
        y = pos.y + vec.y,
        z = pos.z + vec.z
    }
end

-- TheTermos (MIT)
function x_farming.get_node_pos(pos)
    return {
        x = math.floor(pos.x + 0.5),
        y = math.floor(pos.y + 0.5),
        z = math.floor(pos.z + 0.5),
    }
end

-- TheTermos (MIT)
function x_farming.nodeatpos(pos)
    local node = minetest.get_node_or_nil(pos)
    if node then
        return minetest.registered_nodes[node.name]
    end
end

-- TheTermos (MIT)
function x_farming.get_node_height(pos)
    local npos = x_farming.get_node_pos(pos)
    local node = x_farming.nodeatpos(npos)
    if node == nil then
        return nil
    end

    if node.walkable then
        if node.drawtype == 'nodebox' then
            if node.node_box and node.node_box.type == 'fixed' then
                if type(node.node_box.fixed[1]) == 'number' then
                    return npos.y + node.node_box.fixed[5], 0, false
                elseif type(node.node_box.fixed[1]) == 'table' then
                    return npos.y + node.node_box.fixed[1][5], 0, false
                else
                    -- todo handle table of boxes
                    return npos.y + 0.5, 1, false
                end
            elseif node.node_box and node.node_box.type == 'leveled' then
                return minetest.get_node_level(pos) / 64 - 0.5 + x_farming.get_node_pos(pos).y, 0, false
            else
                -- the unforeseen
                return npos.y + 0.5, 1, false
            end
        else
            -- full node
            return npos.y + 0.5, 1, false
        end
    else
        local liquidflag = false
        if node.drawtype == 'liquid' then liquidflag = true end
        return npos.y - 0.5, -1, liquidflag
    end
end

-- TheTermos (MIT)
-- get_terrain_height
-- steps(optional) number of recursion steps; default=3
-- dir(optional) is 1=up, -1=down, 0=both; default=0
-- liquidflag(forbidden) never provide this parameter.
-- dir is 1=up, -1=down, 0=both
function x_farming.get_terrain_height(pos, steps, dir, liquidflag)
    steps = steps or 3
    dir = dir or 0

    local h, f, l = x_farming.get_node_height(pos)
    if h == nil then
        return nil
    end
    if l then
        liquidflag = true
    end

    if f == 0 then
        return h, liquidflag
    end

    if dir == 0 or dir == f then
        steps = steps - 1
        if steps <= 0 then
            return nil
        end

        return x_farming.get_terrain_height(x_farming.pos_shift(pos, { y = f }), steps, f, liquidflag)
    else
        return h, liquidflag
    end
end

-- TheTermos (MIT)
-- modified by SaKeL
function x_farming.get_spawn_pos_abr(dtime, intrvl, radius, chance, reduction)
    dtime = math.min(dtime, 0.1)
    local players = minetest.get_connected_players()
    intrvl = 1 / intrvl

    if math.random() < dtime * (intrvl * #players) then
        -- choose random player
        local player = players[math.random(#players)]
        local vel = player:get_velocity()
        local spd = vector.length(vel)
        chance = (1 - chance) * 1 / (spd * 0.75 + 1)

        local yaw
        if spd > 1 then
            -- spawn in the front arc
            yaw = minetest.dir_to_yaw(vel) + math.random() * 0.35 - 0.75
        else
            -- random yaw
            yaw = math.random() * math.pi * 2 - math.pi
        end

        local pos = player:get_pos()
        local dir = vector.multiply(minetest.yaw_to_dir(yaw), radius)
        local pos2 = vector.add(pos, dir)

        pos2.y = pos2.y - 5

        local height, liquidflag = x_farming.get_terrain_height(pos2, 32)
        if height then
            local objs = minetest.find_node_near(pos, radius * 1.1, { 'group:bee' }) or {}

            -- count mobs in abrange
            for _, obj in ipairs(objs) do
                chance = chance + (1 - chance) * reduction
            end

            if chance < math.random() then
                pos2.y = height
                objs = minetest.get_objects_inside_radius(pos2, radius * 0.95)

                -- do not spawn if another player around
                for _, obj in ipairs(objs) do
                    if obj:is_player() then
                        return
                    end
                end

                return pos2, liquidflag
            end
        end
    end
end

function x_farming.on_flood_candle(pos, oldnode, newnode)
    local drops = minetest.get_node_drops(oldnode)

    for _, item_name in ipairs(drops) do
        minetest.add_item(pos, ItemStack(item_name))
    end

    -- Play flame-extinguish sound if liquid is not an 'igniter'
    local nodedef = minetest.registered_items[newnode.name]

    if not (nodedef and nodedef.groups
        and nodedef.groups.igniter and nodedef.groups.igniter > 0)
        and minetest.get_item_group(oldnode.name, 'candle_on') > 0
    then
        minetest.sound_play(
            'x_farming_extinguish_candle',
            { pos = pos, max_hear_distance = 16, gain = 0.07 },
            true
        )
    end

    -- Remove the torch node
    return false
end

-- Feasts
function x_farming.register_feast(name, def)
    local g = {
        -- MTG
        choppy = 3,
        oddly_breakable_by_hand = 3,
        compost = 100,
        no_silktouch = 1,
        -- MCL
        handy = 1,
        shearsy = 1,
        deco_block = 1,
        non_mycelium_plant = 1,
        fire_encouragement = 60,
        fire_flammability = 100,
        dig_by_water = 1,
        destroy_by_lava_flow = 1,
        compostability = 100,
        food = 2,
        eatable = 1,
        -- ALL
        flammable = 2,
        attached_node = 1,
    }

    -- merge groups from `def`
    if def.groups then
        for group, value in pairs(def.groups) do
            g[group] = value
        end
    end

    local _def = {
        description = def.description,
        short_description = def.short_description or def.description,
        drawtype = 'mesh',
        mesh = def.mesh,
        use_texture_alpha = def.use_texture_alpha or 'clip',
        inventory_image = def.inventory_image or ('x_farming_' .. name .. '_item.png'),
        wield_image = def.wield_image or ('x_farming_' .. name .. '_item.png'),
        wield_scale = { x = 2, y = 2, z = 1 },
        paramtype = 'light',
        paramtype2 = '4dir',
        is_ground_content = false,
        walkable = false,
        selection_box = def.selection_box,
        groups = g,
        _mcl_blast_resistance = 0,
        _mcl_hardness = 0,
        sounds = def.sounds or x_farming.node_sound_wood_defaults(),
        sunlight_propagates = true,
    }

    for i = 1, def.steps do
        local d = table.copy(_def)

        d._next_step = i + 1
        d.tiles = {
            { name = 'x_farming_' .. name .. '_mesh.png', backface_culling = def.tiles_backface_culling or false },
            { name = 'x_farming_' .. name .. '_mesh_' .. i .. '.png', backface_culling = def.tiles_backface_culling or false },
        }

        if i ~= 1 then
            d.groups['not_in_creative_inventory'] = 1
        end

        -- last (no more food) step
        if i == def.steps then
            d.drop = def.last_drop or 'x_farming:bowl'
        else
            d.drop = {
                max_items = def.steps - i,
                items = {
                    {
                        rarity = 1,
                        items = {
                            'x_farming:bowl_' .. name
                        }
                    },
                    {
                        rarity = 1,
                        items = {
                            'x_farming:bowl_' .. name
                        }
                    },
                    {
                        rarity = 1,
                        items = {
                            'x_farming:bowl_' .. name
                        }
                    },
                    {
                        rarity = 1,
                        items = {
                            'x_farming:bowl_' .. name
                        }
                    },
                }
            }
        end

        d.on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
            local n = minetest.registered_nodes[node.name]
            local p_name = clicker:get_player_name()
            local inv = clicker:get_inventory()
            local stack_name = itemstack:get_name()

            if not n then
                return itemstack
            end

            -- last (no more food) step
            if n._next_step > def.steps then
                minetest.chat_send_player(p_name, S('There is no more food left!'))
                return itemstack
            end

            if stack_name == 'x_farming:bowl' then
                minetest.swap_node(pos, { name = 'x_farming:' .. name .. '_' .. n._next_step, param2 = node.param2 })

                if not minetest.is_creative_enabled(p_name) then
                    itemstack:take_item()
                end

                minetest.sound_play('x_farming_wooden_bowl', {
                    pos = pos,
                    gain = 0.4,
                    max_hear_distance = 16
                })

                local stack_bowl = ItemStack({ name = 'x_farming:bowl_' .. name })

                if inv and inv:room_for_item('main', stack_bowl) then
                    inv:add_item('main', stack_bowl)
                else
                    -- drop on the ground
                    minetest.add_item(clicker:get_pos(), stack_bowl)
                end
            else
                minetest.chat_send_player(p_name, S('You need to hold empty bowl if you want to take portion of the food!'))
            end

            return itemstack
        end

        -- Node
        minetest.register_node('x_farming:' .. name .. '_' .. i, d)

        -- Craftitem definition
        local craftitem_def = {
            description = def.short_description .. ' ' .. S('Bowl') .. '\n' .. S('Compost chance') .. ': 100%\n'
            .. minetest.colorize(x_farming.colors.brown, S('Hunger') .. ': 8'),
            inventory_image = 'x_farming_bowl_' .. name .. '.png',
            wield_image = 'x_farming_bowl_' .. name .. '.png',
            groups = {
                -- MTG
                -- X Farming
                compost = 100,
                -- MCL
                food = 3,
                eatable = 10,
                compostability = 100,
            },
            -- MCL
            _mcl_saturation = 12.0,
        }

        if minetest.get_modpath('farming') then
            craftitem_def.on_use = minetest.item_eat(8)
        end

        if minetest.get_modpath('mcl_farming') then
            craftitem_def.on_place = minetest.item_eat(8)
            craftitem_def.on_secondary_use = minetest.item_eat(8)
        end

        -- Craftitem
        minetest.register_craftitem('x_farming:bowl_' .. name, craftitem_def)
    end
end

-- Pies

function x_farming.register_pie(name, def)
    local g = {
        -- MTG
        choppy = 3,
        oddly_breakable_by_hand = 3,
        compost = 100,
        no_silktouch = 1,
        -- MCL
        handy = 1,
        shearsy = 1,
        deco_block = 1,
        non_mycelium_plant = 1,
        fire_encouragement = 60,
        fire_flammability = 100,
        dig_by_water = 1,
        destroy_by_lava_flow = 1,
        compostability = 100,
        food = 2,
        eatable = 1,
        -- ALL
        flammable = 2,
        attached_node = 1,
    }

    -- merge groups from `def`
    if def.groups then
        for group, value in pairs(def.groups) do
            g[group] = value
        end
    end

    local _def = {
        description = def.description,
        short_description = def.short_description or def.description,
        drawtype = 'mesh',
        mesh = def.mesh,
        use_texture_alpha = def.use_texture_alpha or 'clip',
        inventory_image = def.inventory_image or ('x_farming_' .. name .. '_item.png'),
        wield_image = def.wield_image or ('x_farming_' .. name .. '_item.png'),
        wield_scale = { x = 2, y = 2, z = 1 },
        paramtype = 'light',
        paramtype2 = '4dir',
        is_ground_content = false,
        walkable = false,
        selection_box = {
            type = 'fixed',
            fixed = { -7 / 16, -8 / 16, -7 / 16, 7 / 16, -3 / 16, 7 / 16 }
        },
        groups = g,
        _mcl_blast_resistance = 0,
        _mcl_hardness = 0,
        sounds = def.sounds or x_farming.node_sound_wood_defaults(),
        sunlight_propagates = true,
        item_eat = 6
    }

    for i = 1, def.steps do
        local d = table.copy(_def)

        d._next_step = i + 1
        d.tiles = {
            { name = 'x_farming_' .. name .. '_mesh_' .. i .. '.png', backface_culling = def.tiles_backface_culling or false },
        }

        if i ~= 1 then
            d.groups['not_in_creative_inventory'] = 1
        end

        d.drop = {
            max_items = def.steps - i + 1,
            items = {
                {
                    rarity = 1,
                    items = {
                        'x_farming:slice_' .. name
                    }
                },
                {
                    rarity = 1,
                    items = {
                        'x_farming:slice_' .. name
                    }
                },
                {
                    rarity = 1,
                    items = {
                        'x_farming:slice_' .. name
                    }
                },
                {
                    rarity = 1,
                    items = {
                        'x_farming:slice_' .. name
                    }
                },
            }
        }

        d.on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
            local n = minetest.registered_nodes[node.name]

            if not n then
                return itemstack
            end

            if i < def.steps then
                minetest.swap_node(pos, { name = 'x_farming:' .. name .. '_' .. n._next_step, param2 = node.param2 })
            else
                minetest.remove_node(pos)
                minetest.check_for_falling(pos)
            end

            local sound_name = 'x_farming_wooden_bowl'

            if minetest.get_modpath('hunger_ng') then
                hunger_ng.alter_hunger(clicker:get_player_name(), _def.item_eat)
                sound_name = 'hunger_ng_eat'
            elseif minetest.get_modpath('hbhunger') then
                hbhunger.eat(_def.item_eat, nil, ItemStack({ name = 'x_farming:' .. name .. '_1' }), clicker, pointed_thing)
                sound_name = nil
            elseif minetest.get_modpath('stamina') and minetest.global_exists('stamina') then
                -- extra check for global variable since there are some mods called "stamina" without registering global "stamina" namespace
                -- @see https://content.minetest.net/threads/6791/
                stamina.change_saturation(clicker, _def.item_eat)
                sound_name = 'stamina_eat'
            elseif minetest.get_modpath('mcl_hunger') then
                local h = mcl_hunger.get_hunger(clicker)
                mcl_hunger.set_hunger(clicker, math.min(h + _def.item_eat, 20))
                sound_name = 'mcl_hunger_bite'
            else
                minetest.item_eat(_def.item_eat)
            end

            if sound_name then
                minetest.sound_play(sound_name, { pos = pos, gain = 0.7, max_hear_distance = 5 }, true)
            end

            return itemstack
        end

        -- Node
        minetest.register_node('x_farming:' .. name .. '_' .. i, d)

        local craftitem_def = {
            description = def.short_description .. ' ' .. S('Slice') .. '\n' .. S('Compost chance') .. ': 100%\n'
            .. minetest.colorize(x_farming.colors.brown, S('Hunger') .. ': ' .. _def.item_eat),
            inventory_image = 'x_farming_slice_' .. name .. '.png',
            wield_image = 'x_farming_slice_' .. name .. '.png',
            groups = {
                -- MTG
                -- X Farming
                compost = 100,
                -- MCL
                food = 3,
                eatable = 10,
                compostability = 100,
            },
            -- MCL
            _mcl_saturation = 10.0,
        }

        if minetest.get_modpath('farming') then
            craftitem_def.on_use = minetest.item_eat(_def.item_eat)
        end

        if minetest.get_modpath('mcl_farming') then
            craftitem_def.on_place = minetest.item_eat(_def.item_eat)
            craftitem_def.on_secondary_use = minetest.item_eat(_def.item_eat)
        end

        -- Craftitem
        minetest.register_craftitem('x_farming:slice_' .. name, craftitem_def)
    end
end

--
-- MCL
--

function x_farming.mcl.create_soil(pos, inv)
    if pos == nil then
        return false
    end
    local node = minetest.get_node(pos)
    local name = node.name
    local above = minetest.get_node({ x = pos.x, y = pos.y + 1, z = pos.z })
    if minetest.get_item_group(name, 'cultivatable') == 2 then
        if above.name == 'air' then
            node.name = 'mcl_farming:soil'
            minetest.set_node(pos, node)
            minetest.sound_play('x_farming_dirt_hit', { pos = pos, gain = 0.5 }, true)
            return true
        end
    elseif minetest.get_item_group(name, 'cultivatable') == 1 then
        if above.name == 'air' then
            node.name = 'mcl_core:dirt'
            minetest.set_node(pos, node)
            minetest.sound_play('x_farming_dirt_hit', { pos = pos, gain = 0.6 }, true)
            return true
        end
    end
    return false
end

function x_farming.mcl.hoe_on_place_function(uses)
    return function(itemstack, user, pointed_thing)
        -- Call on_rightclick if the pointed node defines it
        local node = minetest.get_node(pointed_thing.under)

        -- Custom: add support for MTG definition farming
        local regN = minetest.registered_nodes
        if regN[node.name].soil ~= nil
            and regN[node.name].soil.wet ~= nil
            and regN[node.name].soil.dry ~= nil
        then
            return x_farming.hoe_on_use(itemstack, user, pointed_thing, uses)
        end

        if user and not user:get_player_control().sneak then
            if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
                return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, user, itemstack) or itemstack
            end
        end

        if minetest.is_protected(pointed_thing.under, user:get_player_name()) then
            minetest.record_protection_violation(pointed_thing.under, user:get_player_name())
            return itemstack
        end

        if x_farming.mcl.create_soil(pointed_thing.under, user:get_inventory()) then
            if not minetest.is_creative_enabled(user:get_player_name()) then
                itemstack:add_wear_by_uses(uses)
            end
            return itemstack
        end
    end
end
