---@type string|boolean|nil
local unload_switching_stations = core.settings:get('sbz_switching_station_unload')
if unload_switching_stations == nil or unload_switching_stations == 'default' then
    unload_switching_stations = not core.is_singleplayer()
elseif unload_switching_stations == 'always' then
    unload_switching_stations = true
else
    unload_switching_stations = false
end

local all_switching_stations = {} -- h(pos) = true|nil

local touched_nodes = {}

---@type function
local hash = minetest.hash_node_position
local h = hash
---@type function
local unhash = minetest.get_position_from_hash
---@type table
local node_defs = minetest.registered_nodes
---@type function
local IG = minetest.get_item_group

sbz_api.pos2network = {}
sbz_api.networks = {}

local pos2network = sbz_api.pos2network
local networks = sbz_api.networks

local max_net_id = 0
local function get_next_network_id()
    max_net_id = max_net_id + 1
    return max_net_id
end

local function get_network(pos)
    local hpos = h(pos)
    if hpos and pos2network[hpos] then
        local net = networks[pos2network[hpos]]
        if net then
            if net.dirty or os.difftime(net.lagstamp, os.time()) > 12 then
                networks[pos2network[hpos]] = nil
                net = nil
            end
            return net
        end
    end
end

---@param start_pos vector
---@param seen table|nil
function sbz_api.assemble_network(start_pos, seen, parent_net_id)
    local t0 = sbz_api.clock_ms()
    local by_connector = parent_net_id
    seen = seen or {}

    local net_id, network
    if not by_connector then
        net_id = get_next_network_id()

        networks[net_id] = {
            generators = {},
            machines = {},
            switching_stations = {},
            batteries = {},
            connectors = {},
            subticking_machines = {},
            --            dirty = false
            timer_subtick = math.random(-25, 0) / 10,
        }
        network = networks[net_id]
    else
        net_id = parent_net_id
        network = {
            generators = {},
            machines = {},
            switching_stations = {},
            batteries = {},
            connectors = {},
            subticking_machines = {},
            dirty = false,
        }
    end

    local generators = network.generators
    local machines = network.machines
    local switching_stations = network.switching_stations
    local batteries = network.batteries

    setmetatable(seen, {
        __index = function(t, k)
            return rawget(t, h(k))
        end,
        __newindex = function(t, k, v)
            rawset(t, h(k), v)
        end,
    })

    local queue = Queue.new()
    -- I ask: DO NOT CHANGE THIS TO A STACK in the quest of optimizing
    -- It is there because with a breadth-first search it is easy to guess which machines execute in what order
    -- With a depth first search??? not so much...
    -- ================================================
    -- Okay, on second thought: logic gates are useless
    -- do whatever you want, just make sure you somehow make logic gates work or just deprecate them

    queue:enqueue { start_pos, vector.zero() }
    seen[start_pos] = true
    pos2network[h(start_pos)] = net_id

    sbz_api.vm_begin()

    -- inspired somewhat by https://github.com/minetest-mods/digilines/blob/7d4895d5d4a093041e3e6c8a8676f3b99bb477b7/internal.lua#L99

    while not queue:is_empty() do
        local current_pos, dir = unpack(queue:dequeue())
        local nn = (sbz_api.get_or_load_node(current_pos) or {}).name -- node name

        local is_conducting = IG(nn, 'pipe_conducts') > 0
        local is_generator = IG(nn, 'sbz_generator') > 0
        local is_machine = IG(nn, 'sbz_machine') > 0
        local is_battery = IG(nn, 'sbz_battery') > 0
        local is_connector = IG(nn, 'sbz_connector') > 0
        local is_subticking = IG(nn, 'sbz_machine_subticking') > 0
        local is_teleporter = IG(nn, 'sbz_power_teleporter') > 0

        if nn == 'sbz_power:switching_station' then
            if by_connector then
                touched_nodes[hash(current_pos)] = os.time()
            elseif h(current_pos) ~= h(start_pos) then
                switching_stations[#switching_stations + 1] = current_pos
            end
        elseif is_battery then
            batteries[#batteries + 1] = { current_pos, nn, dir }
        elseif is_generator then
            generators[#generators + 1] = { current_pos, nn, dir }
        elseif is_machine then
            machines[#machines + 1] = { current_pos, nn, dir }
        elseif is_connector then
            local def = core.registered_nodes[nn]
            local node = sbz_api.get_or_load_node(current_pos)

            if def.can_assemble(current_pos, node, dir, network, seen, parent_net_id) then
                pos2network[h(current_pos)] = net_id
                minetest.registered_nodes[nn].assemble(current_pos, node, dir, network, seen, net_id)
            else
                seen[current_pos] = false
            end
        elseif is_teleporter then
            local def = core.registered_nodes[nn]
            local node = sbz_api.get_or_load_node(current_pos)

            local new_pos = def._sbz_power_teleport(current_pos)
            if new_pos and not seen[new_pos] then
                queue:enqueue { new_pos, vector.zero() }
                seen[new_pos] = true
            end
        end

        if is_subticking then network.subticking_machines[#network.subticking_machines + 1] = { current_pos, nn } end

        if is_conducting then
            if net_id then pos2network[h(current_pos)] = net_id end
            iterate_around_pos(current_pos, function(pos, dir) -- FIXME: NOT PERFORMANT.
                if not seen[pos] then
                    seen[pos] = true
                    queue:enqueue { pos, dir }
                end
            end)
        end
    end

    sbz_api.vm_abort()

    network.lag = sbz_api.clock_ms() - t0
    network.lagstamp = os.time()
    if not by_connector then
        return net_id
    else
        return network
    end
end

function sbz_api.switching_station_tick(start_pos)
    if touched_nodes[hash(start_pos)] and os.time() - touched_nodes[hash(start_pos)] < 1 then
        minetest.get_meta(start_pos):set_string('infotext', 'Inactive (connected to another network)')
        return
    end
    sbz_api.vm_begin()

    local meta = minetest.get_meta(start_pos)
    local network_before = get_network(start_pos)

    if network_before ~= nil then
        -- ah batteries
        local excess = (network_before.supply - network_before.battery_supply_only) - network_before.demand
        local supply = network_before.supply
        local demand = network_before.demand
        for k, v in ipairs(network_before.batteries) do
            if excess == 0 then break end
            local position = v[1]
            local node = v[2]
            local dir = v[3]
            local max = v[4]
            local meta = minetest.get_meta(position)
            local def = node_defs[node]
            local current = 0
            if def.get_power then
                current = def.get_power(position, node, meta, supply, demand, v[3])
            else
                current = meta:get_int 'power'
            end

            if max then -- if not max then battery has not been ran
                local set_power = def.set_power
                    or function(pos, node, meta, current_power, supplied_power, dir)
                        meta:set_int('power', supplied_power)
                    end
                if excess > 0 then -- charging
                    local power_add = max - current
                    if power_add > excess then power_add = excess end
                    excess = excess - power_add
                    set_power(position, node, meta, current, current + power_add, dir)
                elseif excess < 0 then -- discharging
                    local power_remove = current
                    if power_remove > -excess then power_remove = -excess end
                    excess = excess + power_remove
                    set_power(position, node, meta, current, current - power_remove, dir)
                end
            end
        end

        for k, v in ipairs(network_before.batteries) do
            local position = v[1]
            local node = v[2]
            local dir = v[3]
            node_defs[node].action(position, node, minetest.get_meta(position), supply, demand, dir)
        end

        local network_size = #network_before.generators + #network_before.machines + #network_before.batteries

        meta:set_string(
            'infotext',
            string.format(
                'Supply: %s\nDemand: %s\nBattery capacity: %s\nLag: %sms\nNetwork Size: %s',
                sbz_api.format_power(network_before.supply - network_before.battery_supply_only),
                sbz_api.format_power(network_before.demand),
                sbz_api.format_power(network_before.battery_supply_only, network_before.battery_max),
                math.floor(network_before.lag),
                network_size
            )
        )
    end

    local net_id = pos2network[h(start_pos)]
    local network = networks[net_id]

    if network and network.dirty then
        networks[net_id] = nil
        network = nil
    end
    if network == nil then
        net_id = sbz_api.assemble_network(start_pos)
        network = networks[net_id]
    end

    local t0 = sbz_api.clock_ms()

    local generators = network.generators
    local machines = network.machines
    local switching_stations = network.switching_stations
    local batteries = network.batteries

    local profiler = {}

    network.switching_station_pos = start_pos

    local supply = 0
    local demand = 0
    local battery_max = 0

    if #switching_stations > 0 then
        local pos = vector.copy(start_pos)
        local range = vector.new(5, 5, 5)
        minetest.add_particlespawner {
            amount = 500,
            time = 0.3,
            texture = 'error_particle.png',
            glow = 14,
            pos = pos,
            radius = 0.1,
            acc = { min = -range, max = range },
            vel = { min = -range, max = range },
            drag = { x = 0.5, y = 0.5, z = 0.5 },
        }
        --        networks[net_id] = nil
        minetest.remove_node(start_pos)
        return false
    end

    local ids = {}
    for k, v in ipairs(batteries) do
        local position = v[1]
        local node = v[2]
        local meta = minetest.get_meta(position)
        local d = node_defs[node]
        local id_good = true
        if d.get_battery_id then
            local id = d.get_battery_id(position, meta)
            if ids[id] then
                touched_nodes[hash(position)] = os.time()
                id_good = false
            end
            ids[id] = true
        end
        if meta:get_int 'force_off' ~= 1 and id_good then
            local profiler_t0 = sbz_api.clock_ms()
            touched_nodes[hash(position)] = os.time()

            v[4] = d.battery_max
            if d.get_battery_max then v[4] = d.get_battery_max(position, meta) end
            local battery_power = 0
            if d.get_power then
                battery_power = d.get_power(position, node, meta, supply, demand, v[3])
            else
                battery_power = meta:get_int 'power'
            end

            battery_max = battery_max + v[4]
            supply = supply + battery_power
            profiler[node] = profiler[node] or {}
            --            profiler[node].generated = ...
            profiler[node].lag = (profiler[node].lag or 0) + (sbz_api.clock_ms() - profiler_t0)
            profiler[node].count = (profiler[node].count or 0) + 1
        end
    end

    network.battery_supply_only = supply -- copy

    for k, v in ipairs(generators) do
        local position = v[1]
        local node = v[2]
        if minetest.get_meta(v[1]):get_int 'force_off' ~= 1 then
            touched_nodes[hash(position)] = os.time()

            local profiler_t0 = sbz_api.clock_ms()
            local action_result = node_defs[node].action(position, node, minetest.get_meta(position), supply, demand)
            assert(action_result, 'You need to return something in the action function... fauly node: ' .. node)
            supply = supply + action_result

            profiler[node] = profiler[node] or {}
            profiler[node].generated = (profiler[node].generated or 0) + action_result
            profiler[node].lag = (profiler[node].lag or 0) + (sbz_api.clock_ms() - profiler_t0)
            profiler[node].count = (profiler[node].count or 0) + 1
        end
    end

    for k, v in ipairs(machines) do
        local position = v[1]
        local node = v[2]

        if minetest.get_meta(v[1]):get_int 'force_off' ~= 1 then
            touched_nodes[hash(position)] = os.time()

            local profiler_t0 = sbz_api.clock_ms()

            local action_result = node_defs[node].action(position, node, minetest.get_meta(position), supply, demand)
            assert(action_result, 'You need to return something in the action function... fauly node: ' .. node)
            if action_result >= 0 then
                demand = demand + action_result
            else
                supply = supply - action_result
            end

            profiler[node] = profiler[node] or {}
            profiler[node].generated = (profiler[node].generated or 0) - action_result
            profiler[node].lag = (profiler[node].lag or 0) + (sbz_api.clock_ms() - profiler_t0)
            profiler[node].count = (profiler[node].count or 0) + 1
        end
    end

    local t1 = sbz_api.clock_ms()
    local lag = t1 - t0
    if network.lagstamp == os.time() then
        network.lag = network.lag + lag
    else
        network.lag = lag
    end

    network.lagstamp = os.time()
    network.supply = supply
    network.demand = demand
    network.battery_max = battery_max
    network.profiler = profiler
    return true
end

function sbz_api.switching_station_sub_tick(start_pos)
    local t0 = sbz_api.clock_ms()

    local network = get_network(start_pos)
    if network == nil then return end
    if network.dirty then networks[pos2network[h(start_pos)]] = nil end

    local machines = network.subticking_machines or {}
    local supply = network.supply
    local demand = network.demand
    local profiler = network.profiler or {}
    for k, v in ipairs(machines) do
        local position = v[1]
        local node = v[2]
        touched_nodes[hash(position)] = os.time()
        local profiler_t0 = sbz_api.clock_ms()
        local action_result =
            node_defs[node].action_subtick(position, node, minetest.get_meta(position), supply, demand)
        demand = demand + action_result
        profiler[node] = profiler[node] or {}
        profiler[node].generated = (profiler[node].generated or 0) + action_result
        profiler[node].lag = (profiler[node].lag or 0) + (sbz_api.clock_ms() - profiler_t0)
        profiler[node].count = (profiler[node].count or 0) + 1
    end

    local t1 = sbz_api.clock_ms()
    network.lag = network.lag + (t1 - t0)

    network.demand = demand
    network.profiler = profiler
    return true
end

local function profiler_formspec(pos, username)
    local net = get_network(pos)
    if not net then return end
    if net.dirty then return end
    if not net.profiler then return end
    core.chat_send_player(username, '[Switching Station] Network ID: ' .. dump(sbz_api.pos2network[h(pos)])) -- use: detect if the network has changed
    local fs = [[
formspec_version[7]
size[10,11]
tablecolumns[text,align=left,width=12;text,align=center,padding=4;text,align=center;text,align=center]
table[0,0;10,10;machines;Type,Amount,Lag,Power,%s;1]
button_exit[0,10;10,1;exit;Exit]
]]
    local table_text = {}

    for k, v in pairs(net.profiler) do
        table_text[#table_text + 1] = k
        table_text[#table_text + 1] = v.count
        table_text[#table_text + 1] = v.lag and (math.floor(v.lag) .. 'ms') or '-'
        table_text[#table_text + 1] = v.generated or '-'
    end
    fs = string.format(fs, table.concat(table_text, ','))
    core.show_formspec(username, 'sbz_power:switching_station_profiler', fs)
end

minetest.register_node('sbz_power:switching_station', {
    description = 'Switching Station',
    tiles = { 'switching_station.png' },
    groups = { matter = 1, cracky = 1, pipe_connects = 1, pipe_conducts = 1 },
    light_source = 3,

    on_construct = function(pos)
        local meta = minetest.get_meta(pos)
        meta:set_string('infotext', 'Loading....')
    end,
    on_rightclick = function(pos, node, clicker, stack, pointed)
        profiler_formspec(pos, clicker:get_player_name())
    end,
})

minetest.register_craft {
    output = 'sbz_power:switching_station',
    recipe = {
        { '', 'sbz_resources:matter_plate', '' },
        { 'sbz_resources:matter_plate', 'sbz_resources:matter_plate', 'sbz_resources:matter_plate' },
        { '', 'sbz_resources:matter_plate', '' },
    },
}

minetest.register_abm {
    label = 'Machine activation - switching stations',
    nodenames = { 'sbz_power:switching_station' },
    interval = 1,
    chance = 1,
    catch_up = false,
    action = function(pos)
        local poshash = hash(pos)
        if not all_switching_stations[poshash] then all_switching_stations[hash(pos)] = true end
    end,
}

local timeout_limit = 3 -- seconds

minetest.register_abm {
    label = 'Machine timeout check',
    nodenames = { 'group:sbz_machine' },
    interval = timeout_limit,
    chance = 1,
    action = function(pos, node)
        if
            not touched_nodes[hash(pos)]
            or (os.time() - touched_nodes[hash(pos)] >= timeout_limit)
                and core.get_item_group(node.name, 'network_always_found') ~= 1
        then
            minetest.get_meta(pos):set_string('infotext', 'No network found')
            if node_defs[node.name].stateful then sbz_api.turn_off(pos) end
            if node_defs[node.name].on_timeout then node_defs[node.name].on_timeout(pos, node) end
        end
    end,
}

sbz_api.power_tick = 1
sbz_api.power_subtick = 0.25

local has_monitoring = core.get_modpath 'monitoring'
local switching_station_count
if has_monitoring then
    switching_station_count =
        monitoring.gauge('sbz_switching_station_count', 'Number of active switching stations', { autoflush = true })
end

sbz_api.switching_station_globalstep = function(dtime)
    if not sbz_api.enable_switching_station_globalstep then return end
    local getnode = not unload_switching_stations and sbz_api.get_node_force or minetest.get_node
    -- local getnode = minetest.get_node
    local swcount = 0

    for k, _ in pairs(all_switching_stations) do
        swcount = swcount + 1
        local pos = unhash(k)
        if getnode(pos).name ~= 'sbz_power:switching_station' then
            all_switching_stations[k] = nil
        else
            local network = get_network(pos)
            if network then
                network.timer_subtick = (network.timer_subtick or 0) + dtime
                if network.timer_subtick >= sbz_api.power_subtick then
                    network.timer_subtick = 0
                    network.timer_tick = (network.timer_tick or 0) + sbz_api.power_subtick
                    sbz_api.switching_station_sub_tick(pos)
                end
                if (network.timer_tick or 0) >= sbz_api.power_tick then
                    network.timer_tick = 0
                    sbz_api.switching_station_tick(pos)
                end
            else
                sbz_api.switching_station_tick(pos)
            end
        end
    end
    if switching_station_count then switching_station_count.set(swcount) end
end

minetest.register_globalstep(sbz_api.switching_station_globalstep)

if has_monitoring then
    local switching_station_lag = monitoring.counter('sbz_switching_station_lag', 'Switching station lag')
    sbz_api.switching_station_globalstep = switching_station_lag.wraptime(sbz_api.switching_station_globalstep)
end

mesecon.register_on_mvps_move(function(moved_nodes)
    for i = 1, #moved_nodes do
        local moved_node = moved_nodes[i]
        local oldpos = moved_node.oldpos
        local pos = moved_node.pos
        local node = moved_node.node

        if node.name == 'sbz_power:switching_station' then
            all_switching_stations[hash(oldpos)] = nil
            all_switching_stations[hash(pos)] = true
        elseif touched_nodes[hash(oldpos)] then
            touched_nodes[hash(pos)] = touched_nodes[hash(oldpos)]
            touched_nodes[hash(oldpos)] = nil
        end
    end
end)

minetest.register_chatcommand('toggle_power', {
    description = 'Toggles if switching stations are enabled or not',
    params = '<yes/no>',

    privs = { ['server'] = true },
    func = function(name, param)
        if core.is_yes(param) then
            sbz_api.enable_switching_station_globalstep = true
            core.chat_send_player(name, 'Enabled switching stations')
        else
            sbz_api.enable_switching_station_globalstep = false
            core.chat_send_player(name, 'Temporarily disabled switching stations.')
        end
    end,
})

-- MISC api related to sbz_api.assemble_network
---@param radius number|nil
function sbz_api.get_power_from_batteries(start_pos, radius)
    radius = radius or math.huge
    local radius_vect = vector.new(radius, radius, radius)
    local bat_power = 0
    local network = get_network(start_pos)
    if network and not network.dirty then
        for k, v in pairs(network.batteries) do
            local pos = v[1]
            if vector.in_area(pos, -radius_vect, radius_vect) then
                local meta = core.get_meta(pos)
                local name = v[2]
                local def = node_defs[name]
                local battery_power = 0
                if core.get_item_group(name, 'limited_battery') == 0 then
                    if def.get_power then
                        battery_power = def.get_power(pos, name, meta, 0, 0, v[3])
                    else
                        battery_power = meta:get_int 'power'
                    end
                    bat_power = bat_power + battery_power
                end
            end
        end
    end
    return bat_power
end

---@param radius number|nil
function sbz_api.drain_power_from_batteries(start_pos, power, radius)
    local network = get_network(start_pos)
    radius = radius or math.huge
    local radius_vect = vector.new(radius, radius, radius)
    if network and not network.dirty then
        for k, v in pairs(network.batteries) do
            local pos = v[1]
            local meta = core.get_meta(pos)
            local name = v[2]
            local def = node_defs[name]
            if vector.in_area(pos, -radius_vect, radius_vect) then
                if core.get_item_group(name, 'limited_battery') == 0 then
                    local set_power = def.set_power
                        or function(pos, node, meta, current_power, supplied_power, dir)
                            meta:set_int('power', supplied_power)
                        end

                    local battery_power = 0
                    if def.get_power then
                        battery_power = def.get_power(pos, name, meta, 0, 0, v[3])
                    else
                        battery_power = meta:get_int 'power'
                    end
                    local taken_away = math.min(power, battery_power)
                    set_power(pos, name, meta, battery_power, battery_power - taken_away, v[3])
                    power = power - taken_away
                    if power <= 0 then break end
                end
            end
        end
    end
end

core.register_on_mods_loaded(function()
    for name, def in pairs(core.registered_nodes) do
        if IG(name, 'pipe_connects') > 0 or IG(name, 'pipe_conducts') > 0 then
            local og_construct = def.on_construct
            local og_destruct = def.on_destruct
            local og_on_rotate = def.on_rotate
            core.override_item(name, {
                on_construct = function(pos)
                    iterate_around_pos(pos, function(ipos, dir)
                        if get_network(ipos) then get_network(ipos).dirty = true end
                    end)

                    if og_construct then return og_construct(pos) end
                end,
                on_destruct = function(pos)
                    iterate_around_pos(pos, function(ipos, dir)
                        if get_network(ipos) then get_network(ipos).dirty = true end
                    end)

                    if og_destruct then return og_destruct(pos) end
                end,
                on_rotate = function(pos, ...)
                    iterate_around_pos(pos, function(ipos, dir)
                        if get_network(ipos) then get_network(ipos).dirty = true end
                    end)
                    if og_on_rotate then return og_on_rotate(pos, ...) end
                end,
            })
        end
    end
end)

sbz_api.get_switching_station_network = get_network

-- debug

sbz_api.make_network_visible = function(p1, p2, net)
    p1, p2 = vector.sort(p1, p2)
    local va = VoxelArea(p1, p2)
    local network = networks[net]
    for i in va:iterp(p1, p2) do
        local p = va:position(i)
        if pos2network[core.hash_node_position(p)] == net then core.add_entity(p, 'sbz_base:debug_entity') end
    end
end

core.register_chatcommand('teleport_to_laggiest_switching_station', {
    description = 'Teleports to the laggiest switching station, useful to diagnose issues with skyblock zero on multiplayer.',
    privs = { ['server'] = true },
    func = function(name, param)
        local pos = nil
        local max_lag = -math.huge
        for k, v in pairs(networks) do
            local lag = v.lag
            if lag > max_lag then
                max_lag = lag
                pos = v.switching_station_pos
            end
        end
        if pos == nil then return false, 'Could not find a switching station like that' end
        local player = core.get_player_by_name(name)
        player:set_pos(pos)
        return true, 'Done'
    end,
})
