-- GateController — auto-opens gates the herd walks through.
--
-- Every gate we've found (RDM British Field Gates, the map's hedgerow/parlour
-- gates, ...) is an FS25 AnimatedObject living in a fence/placeable's
-- spec_animatedObjects: a .triggerNode (the player-interaction node, which sits
-- AT the gate), an .animation (time 0 = closed -> 1 = open), and standard
-- AnimatedObject methods reached via its metatable. setDirection(±1) drives it
-- (confirmed in-game via phTestGate).
--
-- WalkSession detects which gates sit on a walk path, opens each as the herd
-- approaches, and closes it once the whole herd is through.

GateController = {}

-- Tunables (metres). Kept tight so only the gate the herd actually walks
-- THROUGH is driven — a wider net catches neighbouring gates near the path.
GateController.PATH_RADIUS = 2.25   -- a gate counts as "on the path" within this of any segment
GateController.OPEN_RADIUS = 5.5    -- open once a walker gets this close to the gate
GateController.CLOSE_RADIUS = 7.0   -- close once the herd is all beyond this (and all spawned)

-- World (x,z) of a gate AnimatedObject. Prefer the triggerNode / animation part
-- nodes (they sit at the gate); .nodeId can be a shared placeable/parent node
-- parked far away (fence roots live at 0,-151,0), so it's a last resort.
function GateController.pos(ao)
    if type(ao) ~= "table" then return nil end
    local candidates = { ao.triggerNode }
    local anim = ao.animation
    if type(anim) == "table" and type(anim.parts) == "table" then
        for _, part in ipairs(anim.parts) do
            if type(part) == "table" and part.node ~= nil then
                candidates[#candidates + 1] = part.node
            end
        end
    end
    candidates[#candidates + 1] = ao.nodeId
    candidates[#candidates + 1] = ao.node
    for _, node in ipairs(candidates) do
        if node ~= nil and node ~= 0 and (entityExists == nil or entityExists(node)) then
            local x, _, z = getWorldTranslation(node)
            if x ~= nil then return x, z end
        end
    end
    return nil
end

-- Drive a gate AnimatedObject open/closed. Try the FS25 entry points in order;
-- setDirection(+1 open / -1 close) is confirmed working, the rest are fallbacks
-- for other gate mods. Returns the method name used, or nil.
function GateController.drive(ao, open)
    if type(ao) ~= "table" then return nil end
    if type(ao.setDirection) == "function" then
        if pcall(function() ao:setDirection(open and 1 or -1, true) end) then return "setDirection" end
    end
    if type(ao.setState) == "function" then
        local target = open and 1 or 0
        if type(ao.numStates) == "number" and ao.numStates >= 2 then
            target = open and ao.numStates or 1
        end
        if pcall(function() ao:setState(target, false) end) then return "setState" end
    end
    if type(ao.setAnimTime) == "function" then
        if pcall(function() ao:setAnimTime(open and 1 or 0) end) then return "setAnimTime" end
    end
    return nil
end

-- Every gate-ish AnimatedObject in the world, as { ao, x, z }. Gates live in two
-- places depending on the placeable:
--   * husbandry / map objects -> spec_animatedObjects.animatedObjects
--   * newFence placeables (RDM field gates, map hedgerow gates) ->
--     spec_newFence.fence.segments[i].animatedObjects[j]
function GateController.enumerateAll()
    local out = {}
    local sys = g_currentMission and g_currentMission.placeableSystem
    if sys == nil or sys.placeables == nil then return out end
    local function add(ao)
        if type(ao) == "table" then
            local x, z = GateController.pos(ao)
            if x ~= nil then out[#out + 1] = { ao = ao, x = x, z = z } end
        end
    end
    for _, p in pairs(sys.placeables) do
        if type(p) == "table" then
            local spec = p.spec_animatedObjects
            if spec ~= nil and type(spec.animatedObjects) == "table" then
                for _, ao in ipairs(spec.animatedObjects) do add(ao) end
            end
            local fence = p.spec_newFence and p.spec_newFence.fence
            local segments = fence and fence.segments
            if type(segments) == "table" then
                for _, seg in pairs(segments) do
                    if type(seg) == "table" and type(seg.animatedObjects) == "table" then
                        for _, ao in ipairs(seg.animatedObjects) do add(ao) end
                    end
                end
            end
        end
    end
    return out
end

-- Same, filtered to within `radius` of (px,pz), nearest first. Used by phTestGate.
function GateController.enumerateNear(px, pz, radius)
    local near = {}
    for _, g in ipairs(GateController.enumerateAll()) do
        local dx, dz = g.x - px, g.z - pz
        local d = math.sqrt(dx * dx + dz * dz)
        if d <= radius then
            near[#near + 1] = { ao = g.ao, x = g.x, z = g.z, d = d }
        end
    end
    table.sort(near, function(a, b) return a.d < b.d end)
    return near
end

-- Shortest distance from point (px,pz) to segment (ax,az)-(bx,bz).
local function distPointToSeg(px, pz, ax, az, bx, bz)
    local dx, dz = bx - ax, bz - az
    local len2 = dx * dx + dz * dz
    local t = 0
    if len2 > 1e-6 then
        t = ((px - ax) * dx + (pz - az) * dz) / len2
        if t < 0 then t = 0 elseif t > 1 then t = 1 end
    end
    local cx, cz = ax + t * dx, az + t * dz
    local ex, ez = px - cx, pz - cz
    return math.sqrt(ex * ex + ez * ez)
end

-- Gates that sit on `pathPoints` (array of {x,z}). Returns tracking records the
-- WalkSession updates: { ao, x, z, opened=false, done=false }.
function GateController.detectOnPath(pathPoints, radius)
    radius = radius or GateController.PATH_RADIUS
    local gates = {}
    if type(pathPoints) ~= "table" or #pathPoints < 2 then return gates end
    for _, g in ipairs(GateController.enumerateAll()) do
        local best = math.huge
        for i = 1, #pathPoints - 1 do
            local a, b = pathPoints[i], pathPoints[i + 1]
            local d = distPointToSeg(g.x, g.z, a.x, a.z, b.x, b.z)
            if d < best then best = d end
        end
        if best <= radius then
            gates[#gates + 1] = { ao = g.ao, x = g.x, z = g.z, opened = false, done = false }
        end
    end
    return gates
end
