
aom_playerapi.physics = {}

---@param c table collision_box table
---@param o table | nil vector for offset
---@param j number | nil top of colbox overflow
---@param z number | nil bottom of colbox underflow (prevent non-floor)
---@return table array of vector positions
function aom_playerapi.physics.collision_box_to_vertex_list(c, o, z, j)
    if o == nil then o = vector.new(0,0,0) end
    local list = {}
    z = z or 0.1
    j = j or 0.01
    -- back left
    table.insert(list, vector.new(c[1]-z+o.x, c[5]+z+o.y, c[3]-z+o.z))
    table.insert(list, vector.new(c[1]+j+o.x, c[2]-z+o.y, c[3]+j+o.z))
    -- front left
    table.insert(list, vector.new(c[1]-z+o.x, c[5]+z+o.y, c[6]+z+o.z))
    table.insert(list, vector.new(c[1]+j+o.x, c[2]-z+o.y, c[6]-j+o.z))
    -- front right
    table.insert(list, vector.new(c[4]+z+o.x, c[5]+z+o.y, c[6]+z+o.z))
    table.insert(list, vector.new(c[4]-j+o.x, c[2]-z+o.y, c[6]-j+o.z))
    -- back right
    table.insert(list, vector.new(c[4]+z+o.x, c[5]+z+o.y, c[3]-z+o.z))
    table.insert(list, vector.new(c[4]-j+o.x, c[2]-z+o.y, c[3]+j+o.z))
    return list
end




aom_playerapi._on_player_touch_nodes = {}
minetest.register_on_mods_loaded(function()
    for nodename, def in pairs(minetest.registered_nodes) do
        if def._on_player_touch or def._on_player_standing_on then
            aom_playerapi._on_player_touch_nodes[nodename] = def
        end
    end
end)

function aom_playerapi.physics.raycast_collisionbox(player, vert_list)
    local pi = aom_playerapi.check_player(player)
    pi.vert = (pi.vert or 0) % (#vert_list/2) + 1
    local vi = pi.vert * 2
    local start  = vert_list[vi - 1]
    local target = vert_list[vi]
    local ray = minetest.raycast(start, target, false, true)
    pi.touching_nodes[pi.vert] = {}
    local list = pi.touching_nodes[pi.vert]
    for pt in ray do
        local node = minetest.get_node_or_nil(pt.under)
        local ndef = node and aom_playerapi._on_player_touch_nodes[node.name]
        local is_in = false
        for vi = 1, 4 do for i, record in ipairs(pi.touching_nodes[vi] or {}) do
            if record.pos:equals(pt.under) then is_in = true; break end
        end end
        if ndef and not is_in then
            table.insert(list, {pos = pt.under, node = node, ndef = ndef})
        end
    end
end

local t = 0
local avgtime = 0
function aom_playerapi.physics.on_step_raycast(player, dtime)
    local pi = aom_playerapi.check_player(player)
    pi.touching_nodes = pi.touching_nodes or {}
    local props = player:get_properties()
    local pos = player:get_pos()
    local vert_list
    if (pi.vert_list == nil) or (pi.vert and pi.vert == 1) then
        vert_list = aom_playerapi.physics.collision_box_to_vertex_list(props.collisionbox, pos)
        pi.vert_list = vert_list
    else vert_list = pi.vert_list end
    aom_playerapi.physics.raycast_collisionbox(player, vert_list)
end

function aom_playerapi.physics.do_on_player_touch(player, dtime)
    local pi = aom_playerapi.check_player(player)
    local pos = player:get_pos()
    local iters = 0
    for i = 1, 4 do for k, record in ipairs(pi.touching_nodes[i] or {}) do
        local node = minetest.get_node_or_nil(record.pos)
        iters = iters + 1
        if node.name == record.node.name then
            if record.ndef._on_player_standing_on and (record.pos.y + 0.499 < pos.y) then
                record.ndef._on_player_standing_on(record.pos, player, dtime)
            end
            if record.ndef._on_player_touch then
                record.ndef._on_player_touch(record.pos, player, dtime)
            end
        end
    end end
end

function aom_playerapi.physics.do_stair_slow(player, dtime)
    if not aom_playerapi.get_setting(nil, "gameplay_stair_slowness", false) then return end
    local pi = aom_playerapi.check_player(player)
    local info = minetest.get_player_information(player:get_player_name())
    local checkpos = player:get_pos()
    local pos = player:get_pos()
    local vel = vector.copy(pi.avg_vel)
    local latency = info.avg_rtt or 0.2
    local predict = (latency + 0.5) * vel
    checkpos = checkpos + predict
    checkpos.y = checkpos.y + 0.5
    local ray = minetest.raycast(checkpos, vector.offset(checkpos, 0, -2.2 + predict.y, 0), false, false)
    local stair = nil
    for pt in ray do
        local node = minetest.get_node(pt.under)
        if minetest.get_item_group(node.name, "stair") > 0 then
            stair = pt
            break
        end
    end
    if stair ~= nil then
        if math.abs(pi.avg_vel.y) < 0.8 * dtime then return end
        aom_statusfx.apply_status(player, "stair_slowness", 0.2, {speed = 0.6, no_stack = true})
    else
        aom_statusfx.remove_status(player, "stair_slowness")
    end
    -- debug
    if 1 then return end
    local col = "#613"
    if aom_statusfx.player_has_status(player, "stair_slowness") then
        col = "#fea"
    end
    minetest.add_particle({
        size = 4,
        pos = checkpos,
        texture = "white.png^[colorize:"..col..":255",
        velocity = vector.new(0, 2, 0),
        expirationtime = dtime*1.5,
        glow = 14,
    })
end

function aom_playerapi.physics.on_step(player, dtime)
    local pi = aom_playerapi.check_player(player)
    if not pi.last_pos then pi.last_pos = player:get_pos() end
    if not pi.avg_vel then pi.avg_vel = player:get_velocity() end

    aom_playerapi.physics.do_on_player_touch(player, dtime)
    aom_playerapi.physics.do_stair_slow(player, dtime)

    pi.real_vel = player:get_pos() - pi.last_pos
    pi.last_pos = player:get_pos()
    pi.avg_vel = ((pi.real_vel*10) + (pi.avg_vel*5)) /6
end

local playerindex = 1
function aom_playerapi.physics.iterate_players_on_step(conplayers, num, dtime)
    local max = #conplayers
    num = math.min(num, max)
    for i = 1, num do
        aom_playerapi.physics.on_step_raycast(conplayers[playerindex], dtime)
        playerindex = playerindex + 1
        if playerindex > max then playerindex = 1 end
    end
end

minetest.register_globalstep(function(dtime)
    -- local clo = os.clock()
    local conplayers = minetest.get_connected_players()
    local num = aom_playerapi.get_setting(nil, "performance_player_raycast_max", 10)
    aom_playerapi.physics.iterate_players_on_step(conplayers, num, dtime)
    for i, player in ipairs(conplayers) do
        aom_playerapi.physics.on_step(player, dtime)
    end
    -- avgtime = (avgtime*29 + ((os.clock() - clo) * 1000)) / 30
    -- minetest.log(avgtime .. " ms avg")
end)


