local pt_pos          = core.get_pointed_thing_position
local get_node        = core.get_node
local dig_node        = core.dig_node
local yaw_to_dir      = core.yaw_to_dir
local place_node      = core.place_node
local remove_node     = core.remove_node
local player_privs    = core.check_player_privs
local settings        = core.settings
local message         = core.chat_send_player
local sound_play      = core.sound_play
local player_creative = core.is_creative_enabled


local S = trowel.translator
local max_distance = trowel.max_distance
local single_node_wear =  trowel.single_node_wear


-- Directions for get_nodes()
local directions = {
    X = { { x = 1, z = 0 }, { x = -1, z =  0 } },
    Z = { { x = 0, z = 1 }, { x =  0, z = -1 } },
}


-- Check node relevancy
--
-- When a node at the given position has the same ID as the given reference
-- node, and has nothing above it, it is seen as relevant.
--
-- @param string id Reference node ID
-- @param table pos Positional table of the node to check
-- @return mixed    A positional table if the node is relevant, otherwise nil
local relevant = function (id, pos)
    local this_one = get_node(pos).name
    local above_id = get_node({x = pos.x, y = pos.y+1, z = pos.z}).name
    if this_one == id and above_id == 'air' then return pos end
end


-- Get relevant nodes
--
-- With the given node position as reference, scan the given axis for the
-- given node ID and check the nodes relevancy.
--
-- On the first not relevant node, this function breaks the scan loop and
-- returns the found nodes up to that point.
--
-- @param table  pos  Reference node position
-- @param string axis Single letter axis identifier to scan
-- @id    string id   Reference node ID
--
-- @return table The relevant nodes
local get_nodes = function (pos, axis, id)
    local nodes = {}

    if relevant(id, pos) then
        table.insert(nodes, pos)

        for _, dir in ipairs(directions[axis]) do
            local counter = 0
            while counter < max_distance do
                counter = counter + 1
                local check_pos = {
                    x = pos.x + dir.x * counter,
                    y = pos.y,
                    z = pos.z + dir.z * counter
                }
                local found = relevant(id, check_pos)
                if not found then break end
                table.insert(nodes, found)
            end
        end
    end

    return nodes
end


-- Safely get creative mode
--
-- There are multiple indicators of whether a player is in creative mode. With
-- this function all of them are checked (`core.is_creative_enabled`, the
-- `creative` privilege, and the server running with the creative flag set).
--
-- @param sting name The name of the player to check for
--
-- @return boolean If creative mode is enabled at least in one place
local get_creative_mode = function (name)
    local c_player = player_creative(name)
    local c_priv = player_privs(name, {creative = true })
    local c_mode = settings:get_bool('creative_mode')
    return c_player or c_priv or c_mode
end


-- Modify a wall
--
-- A wall is defined by nodes of the same ID in a row where no other nodes are
-- placed above those nodes. A wall ends with a node of a different type, a
-- node of same type with another node of any ID above it, or after the defined
-- maximum distance `trowel_max_distance`, which by default is 10.
--
-- Only straight walls in X or Z direction are checked. The scan for relevant
-- nodes starts in X direction and then a second scan in Z direction.
--
-- The longer of those two walls is modified. If both walls are of same length,
-- the wall in X direction takes precedence.
--
-- A wall can be modified in positive (`1`) or in negative direction (`-1`).
-- The `pointed_thing` is used as reference for both the ID and starting point
-- for that wall.
--
-- @param ObjectRef player        The player object
-- @param table     pointed_thing A “pointed_thing” for the reference position
-- @param number    direction     In what direction the wall should be risen
--
-- @return nil Indicator for the Luanti API to do nothing
trowel.modify_wall = function (player, pointed_thing, direction)
    if not player:is_player() or pointed_thing.type=='nothing' then return end

    local name = player:get_player_name()
    local inv = player:get_inventory()
    local main = inv:get_list('main')

    local pos = pt_pos(pointed_thing)
    local node = get_node(pos)
    local id = node.name

    local sounds = (core.registered_items[id] or {}).sounds or {}
    local action = ''
    local creative = get_creative_mode(name)

    -- Collect relevant nodes
    local nodes = get_nodes(pos, 'X', id)
    local z_nodes = get_nodes(pos, 'Z', id)
    if #nodes < #z_nodes then nodes = z_nodes end
    if #nodes == 0 then message(name, S('Invalid starting position!')) end

    -- Alter wall depending on direction
    for _, node_pos in pairs(nodes) do
        if direction == -1 then
            action = 'dug'
            if creative then
                remove_node(node_pos)
            else
                dig_node(node_pos, player)
            end
        elseif direction == 1 then
            node_pos.y = node_pos.y + 1
            if creative then
                action = 'place'
                place_node(node_pos, node, player)
            elseif inv:contains_item('main', id) then
                action = 'place'
                inv:remove_item('main', id)
                place_node(node_pos, node, player)
            else
                message(name, S('Not enough nodes!'))
                return
            end
        end
    end

    -- Play sound after action
    if action == 'place' then
        sound_play(sounds.place or "default_place_node", { pos = pos })
    elseif action == 'dug' then
        sound_play(sounds.dug or "default_dig", { pos = pos })
    end

    -- When not in creative mode, add wear and return the modified ItemStack
    --
    -- Wear is calculated by the base wear value per node multiplied with the
    -- amount of nodes that were modified. Wear is the same for removing or
    -- placing the node.
    --
    -- If for some reason no nodes were changed and none of the returns were
    -- triggered, the nodes table is empty and then this part will run but
    -- returns an unaltered ItemStack (single node wear multiplied by 0).
    if not creative then
        local stack = player:get_wielded_item()
        stack:add_wear(single_node_wear * #nodes)
        return stack
    end

    -- Always return nothing, when no other return was triggered!
    return
end
