-- If you don't understand some code, ****make a github issue about it****! Code should be understandable unlike in other similar item transport projects.

local function has_no_connections(connections)
    for i = 1, 6 do
        if connections[i] == 1 then return false end
    end
    return true
end

local function make_connection(connections, wallmounted_dir, remove_connection)
    local set_to = 1
    if remove_connection then set_to = 0 end
    connections[stube.wallmounted_to_connections_index[wallmounted_dir]] = set_to
end

---@return ({[1]:ivec, [2]:core.Node.set})[]
function stube.get_connect_tubes_to(pos, dir, pointed_thing, sneaking)
    pos = vector.copy(pos) -- guaranteed to be a vector

    local modifications = {} -- if just previewing

    -- Connect to our tube if it is pointing to it
    for idir = 0, 5 do
        local neighbor_dir_from_origin = core.wallmounted_to_dir(idir)
        local neighbor_pos = pos + neighbor_dir_from_origin
        local neighbor_node = stube.get_or_load_node(neighbor_pos)

        if core.get_item_group(neighbor_node.name, 'stube') == 1 then
            local split = stube.split_tube_name(neighbor_node.name)
            local pointing_to = (core.wallmounted_to_dir(split.dir) + neighbor_pos)

            if pointing_to == pos or has_no_connections(split.connections) then
                local connection_dir = core.dir_to_wallmounted(pos - neighbor_pos)

                if has_no_connections(split.connections) then split.dir = connection_dir end
                make_connection(split.connections, connection_dir)

                modifications[#modifications + 1] = { neighbor_pos, { name = stube.join_tube_name(split) } }
            end
        end
    end

    -- Switch the direction of the position under
    -- Makes making turns actually possible
    if pointed_thing then
        local node = stube.get_or_load_node(pointed_thing.under)

        if core.get_item_group(node.name, 'stube') == 1 then
            local split = stube.split_tube_name(node.name)
            split.dir = dir
            make_connection(split.connections, dir)
            modifications[#modifications + 1] = { pointed_thing.under, { name = stube.join_tube_name(split) } }
        end
    end

    -- make the tube that is in front of us, point to us
    -- This makes connecting tubes with multiple inputs possible, after they have already been placed
    if dir and not sneaking then
        local vdir = core.wallmounted_to_dir(dir)
        local front_pos = pos + vdir
        local front_node = stube.get_or_load_node(front_pos)

        if core.get_item_group(front_node.name, 'stube') == 1 then
            local split = stube.split_tube_name(front_node.name)

            make_connection(split.connections, split.dir)
            make_connection(split.connections, core.dir_to_wallmounted(pos - front_pos))
            modifications[#modifications + 1] = { front_pos, { name = stube.join_tube_name(split) } }
        end
    end
    return modifications
end

---@see stube.place_tube
---@return core.Node.get
function stube.get_placed_tube_node(tube_name, pos, tube_dir, pointed_thing, sneaking, just_preview)
    if not just_preview then
        local set_nodes = stube.get_connect_tubes_to(pos, tube_dir, pointed_thing, sneaking)
        for _, v in ipairs(set_nodes) do
            core.set_node(v[1], v[2])
        end
    end

    local connections = {
        -- X, Y, Z
        0,
        0,
        0,
        -- -X, -Y, -Z
        0,
        0,
        0,
    }

    --- Connect to any neighboring STubes that are pointing to us
    for neighbor_dir_number = 0, 5 do
        local neighbor_dir = core.wallmounted_to_dir(neighbor_dir_number)
        local neighbor_pos = vector.add(pos, neighbor_dir)
        local neighbor_node = stube.get_or_load_node(neighbor_pos)

        if core.get_item_group(neighbor_node.name, 'stube') == 1 then
            local tube_params = stube.get_tube_name_info(neighbor_node.name)
            if vector.equals(vector.add(core.wallmounted_to_dir(tube_params[1]), neighbor_pos), pos) then -- If the tube is pointing to ours
                make_connection(connections, neighbor_dir_number)
            end
        end
    end

    --- Automatically connect any receivers to our tube, unless we are sneaking
    if not sneaking then
        for i = 0, 5 do
            local neighbor_dir = core.wallmounted_to_dir(i)
            local neighbor_pos = vector.add(pos, neighbor_dir)
            local neighbor_node = stube.get_or_load_node(neighbor_pos)

            if stube.can_connect_to_receiver(neighbor_node, i, true) then make_connection(connections, i) end
        end
    else -- Only connect the receiver we are sneaking at
        local under_node = stube.get_or_load_node(pointed_thing.under)
        local under_dir = vector.subtract(pointed_thing.above, pointed_thing.under)
        local wallmounted_dir = core.dir_to_wallmounted(under_dir)

        if stube.can_connect_to_receiver(under_node, wallmounted_dir, true) then
            make_connection(connections, stube.opposite_wallmounted(wallmounted_dir))
        end
    end

    --- Count the connections
    local no_connections = true
    local amount_of_connections = 0
    for i = 1, 6 do
        if connections[i] == 1 then
            no_connections = false
            amount_of_connections = amount_of_connections + 1
        end
    end

    -- the square/no connections tube, only one exists
    if no_connections then tube_dir = 0 end

    -- if a tube is not straight, make a connection to its dir
    -- Basically, makes short tubes whenever possible
    -- and maintain a connection to things which interact with tubes, that is important

    local tube_pointing_to = vector.add(pos, core.wallmounted_to_dir(tube_dir))
    local tube_pointing_to_node = stube.get_or_load_node(tube_pointing_to)

    local straight_tube_index = (stube.wallmounted_to_connections_index[tube_dir] + 3) % 6
    if straight_tube_index == 0 then straight_tube_index = 6 end
    if
        not (
            ((amount_of_connections == 1 and connections[straight_tube_index] == 1) or amount_of_connections == 0) -- if the tube is straight or a box
            and not stube.can_connect_to_receiver(tube_pointing_to_node, tube_dir, false) -- and not pointing to anything important
        )
    then
        make_connection(connections, tube_dir)
    end

    -- if there is a tube in the front of us, point to it
    -- This makes connecting tubes with multiple inputs possible, after they have already been placed, but sometimes you may not want this behavior

    if not sneaking then
        local vdir = core.wallmounted_to_dir(tube_dir)
        local front_pos = pos + vdir
        local front_node = stube.get_or_load_node(front_pos)
        if core.get_item_group(front_node.name, 'stube') == 1 then make_connection(connections, tube_dir) end
    end
    return { name = stube.get_prefix_tube_name(tube_name) .. '_' .. tube_dir .. table.concat(connections) }
end

function stube.place_tube(name, pos, tube_dir, pointed_thing, sneaking)
    core.set_node(pos, stube.get_placed_tube_node(name, pos, tube_dir, pointed_thing, sneaking))
end

-- feels like counting in binary
local corners = {
    vector.new(1, 1, 1),
    vector.new(1, 1, -1),
    vector.new(1, -1, 1),
    vector.new(1, -1, -1),
    vector.new(-1, 1, 1),
    vector.new(-1, 1, -1),
    vector.new(-1, -1, 1),
    vector.new(-1, -1, -1),
}

-- When an stube is broken, or something near it was, this should get called
-- It cleans up garbage connections (e.g. connection to nowhere, that isn't needed)
function stube.update_placement(pos)
    -- All the cases of garbage connections:
    --   - When a connection is pointing to air/incompatible node
    --   - When a connection is pointing to a tube, which is not connected to it -> weird visual
    --   - When a tube could be a short tube instead

    -- So, this function will iterate over the neighboring tubes of this position
    pos = vector.copy(pos) -- ensure pos is a vector
    stube.update_placement_single(pos)
    for i = 0, 5 do
        stube.update_placement_single(pos + core.wallmounted_to_dir(i))
    end

    -- also for corner cases
    for i = 1, #corners do
        stube.update_placement_single(pos + corners[i])
    end
end

---@see stube.update
function stube.update_placement_single(pos)
    local node = stube.get_or_load_node(pos)
    if core.get_item_group(node.name, 'stube') ~= 1 then return end

    local split = stube.split_tube_name(node.name)

    for i = 1, 6 do
        local connection = split.connections[i]
        local connection_dir = stube.connections_to_wallmounted[i]
        local connection_dirv = core.wallmounted_to_dir(connection_dir)
        local connection_pos = pos + connection_dirv

        local connection_node = stube.get_or_load_node(connection_pos)

        local ig = core.get_item_group

        if connection == 1 then
            -- first case: pointing to air/incompatible node
            if
                not (
                    ig(connection_node.name, 'stube') == 1
                    or stube.is_receiver(connection_node) == true -- Not using stube.can_connect_to_receiver because that can be a bit expensive
                )
            then
                split.connections[i] = 0
            end

            -- second case: connection is pointing to a tube that is not connected to it (=> a weird/out of place connection)
            if ig(connection_node.name, 'stube') == 1 then
                local other_tube_split = stube.split_tube_name(connection_node.name)

                local other_connection_index = (i + 3) % 6
                if other_connection_index == 0 then other_connection_index = 6 end
                if other_tube_split.connections[other_connection_index] == 0 then split.connections[i] = 0 end

                -- another case: The tube wants nothing to do with us (manifests as arrows pointing away from eachother), should disconnect
                -- so all connected tubes should point to us, if not disconnect
                if
                    connection_dir ~= split.dir
                    and not vector.equals((core.wallmounted_to_dir(other_tube_split.dir) + connection_pos), pos)
                then
                    split.connections[i] = 0
                end
            end
        end
    end

    local amount_of_connections = 0
    for i = 1, 6 do
        if split.connections[i] == 1 then amount_of_connections = amount_of_connections + 1 end
    end

    local straight_tube_index = (stube.wallmounted_to_connections_index[split.dir] + 3) % 6 -- the index opposite to the dir, if that makes sense
    if straight_tube_index == 0 then straight_tube_index = 6 end

    -- case 2.5: "We could totally be connected to that tubedevice/routing block right now actually, we don't need to be a short tube"
    if amount_of_connections == 1 and split.connections[straight_tube_index] == 1 then
        local dir = core.wallmounted_to_dir(split.dir)
        local next_pos = pos + dir
        local next_node = stube.get_or_load_node(next_pos)
        if stube.can_connect_to_receiver(next_node, split.dir, false) then
            make_connection(split.connections, split.dir)
        end
    end

    -- third case: it could be a short tube instead, if not, check if there is a tube in our direction, and check if it is properly connected
    if
        not ((amount_of_connections == 1 and split.connections[straight_tube_index] == 1) or amount_of_connections == 0)
    then -- not a short tube
        make_connection(split.connections, split.dir)

        local next_pos = pos + core.wallmounted_to_dir(split.dir)
        local next_node = stube.get_or_load_node(next_pos)
        -- also make sure the neighboring tube is properly connected, this must not occur with short tubes
        if core.get_item_group(next_node.name, 'stube') == 1 then
            local next_tube_split = stube.split_tube_name(next_node.name)
            make_connection(next_tube_split.connections, core.dir_to_wallmounted(pos - next_pos))

            -- small complication: if next_node is a short tube, this could make an invalid tube, crashing the game.. ugh
            -- if short tubes were just not a thing, this mod's source code would be substantially smaller
            -- but the alternative is really ugly (as in, in-game looks, not source code) soo gotta deal with it

            local set_to = stube.join_tube_name(next_tube_split)
            if not core.registered_nodes[set_to] then -- okay this is the extremely lazy way, how did i not think of this earlier
                make_connection(next_tube_split.connections, next_tube_split.dir)
                set_to = stube.join_tube_name(next_tube_split)
            end

            core.set_node(next_pos, { name = set_to })
        end
    end

    if amount_of_connections == 0 then split.dir = 0 end

    core.set_node(pos, { name = stube.join_tube_name(split) })
end

function stube.tube_after_place(pos, placer, stack, pointed)
    if not placer then return end
    if not placer:is_valid() then return end
    if pointed.type ~= 'node' then return end

    local face = vector.subtract(pointed.above, pointed.under)
    local dir = core.dir_to_wallmounted(face)

    local sneaking = placer:get_player_control().sneak
    stube.place_tube(stack:get_name(), pos, dir, pointed, sneaking)
    stube.update_placement(pos)
end

-- Change the direction to the one we are pointing to
-- On sneak+punch
-- Also puts an item in a tube if sneak+punching
function stube.default_tube_punch(pos, node, puncher, pointed_thing)
    if not puncher then return end
    local name = puncher:get_player_name()
    if core.is_protected(pos, name) then
        -- core.record_protection_violation(pos, name)
        return
    end

    stube.update_placement(pos)
    if not puncher:get_player_control().sneak then return end

    local wielded_item, wielded_index = puncher:get_wielded_item(), puncher:get_wield_index()
    if wielded_item:is_empty() then
        local split = stube.split_tube_name(node.name)
        split.dir = core.dir_to_wallmounted(pointed_thing.above - pointed_thing.under)
        split.connections[stube.wallmounted_to_connections_index[split.dir]] = 1
        core.set_node(pos, { name = stube.join_tube_name(split) })
        stube.update_placement(pos)
        local set_nodes = stube.get_connect_tubes_to(pos, split.dir, pointed_thing, false)
        for _, v in ipairs(set_nodes) do
            core.set_node(v[1], v[2])
        end
    else
        local wielded_tubeitem = { stack = wielded_item }
        local empty_wielded_item = false
        for dir = 0, 6 do
            local item = stube.send_item(wielded_tubeitem, pos, pos, node, dir)
            if item == true then
                empty_wielded_item = true
                break
            end
        end

        if empty_wielded_item then
            puncher:set_wielded_item(ItemStack())
        else
            puncher:set_wielded_item(wielded_tubeitem.stack)
        end
    end
end

function stube.default_tube_rightclick(pos, node, clicker, itemstack, pointed_thing)
    core.after(0, function()
        if not clicker then return end
        local name = clicker:get_player_name()
        if core.is_protected(pos, name) then
            -- core.record_protection_violation(pos, name)
            return
        end

        local prefix = stube.get_prefix_tube_name(node.name)
        if not prefix then return end
        local all_states = stube.all_stubes[prefix]
        if not all_states then return end

        local player_inv = clicker:get_inventory()

        local state = all_states[core.hash_node_position(pos)]
        if not state then return end
        for i = 0, 6 do
            local connection = state.connections[i]
            if connection then
                local leftover = player_inv:add_item('main', connection.stack)
                if leftover:is_empty() then
                    local ent = connection.entity
                    state.connections[i] = nil
                    if ent then ent:remove() end
                end
                connection.stack = leftover
            end
        end
    end)
    return itemstack
end
