-- A single walking cow visual that traverses a list of waypoints from
-- source to destination, then signals completion.
--
-- 3b-iter1: we don't load the cow i3d from disk. Instead we clone the first
-- VisualAnimal root node we can find inside the source placeable's husbandry
-- (the cows you'd see in the pen). This sidesteps the engine-internal
-- AnimalSystem i3d-loading flow entirely. Trade-off: source must have at
-- least one visible animal at spawn time. For the barn->pasture transition
-- this is true (we haven't moved the data yet). For pasture->barn it's true
-- because the pasture currently holds the herd.

Walker = {}
local Walker_mt = Class(Walker)

Walker.DEFAULT_SPEED = 2.0          -- m/s
Walker.ARRIVE_RADIUS = 0.5          -- m: how close to a waypoint counts as 'reached'
Walker.BABY_AGE_MONTHS = 6          -- below this age (months) an animal is treated
                                    -- as a young CALF: it plays the RUN clip
                                    -- instead of WALK. So ages 0-5 months RUN
                                    -- (a small calf's short walk stride can't cover
                                    -- the herd's travel speed and visibly slides),
                                    -- while 6 months and up WALK (long enough stride
                                    -- not to slide).

-- Is this RLM animal record a calf/baby (for animation purposes)? Prefers an
-- explicit flag if RLM exposes one, else compares age (months) to BABY_AGE_MONTHS.
function Walker.isBabyAnimal(animal)
    if type(animal) ~= "table" then return false end
    if animal.isChild == true or animal.isBaby == true then return true end
    local age = animal.age
    if type(age) ~= "number" and type(animal.getAge) == "function" then
        local ok, a = pcall(function() return animal:getAge() end)
        if ok and type(a) == "number" then age = a end
    end
    if type(age) == "number" then
        return age < Walker.BABY_AGE_MONTHS
    end
    return false
end
Walker.MAX_TURN_RATE = math.pi      -- radians/sec (180 deg/s) cap on rotation change
Walker.DEFAULT_LANE_SPREAD = 1.5    -- m: half-width of lateral spread for a herd
Walker.TURN_EASE_RATE = 3.5         -- 1/s: how fast heading eases toward travel dir
                                    -- (smooths facing + lane offset so the body
                                    --  swings gently instead of snapping at each
                                    --  recorded waypoint)

-- Resolve the engine scene-node for a specific RLM animal record. The animal
-- knows itself as "<husbandryIdx>-<animalId>" via its `id` field (RLM format).
-- We parse, look up the engine husbandry id, then ask the engine for the root.
-- Returns nil if the animal isn't currently being rendered (no visual binding).
local function getRootNodeForAnimal(animal, sourcePlaceable)
    if animal == nil or sourcePlaceable == nil then return nil end
    if getAnimalRootNode == nil then return nil end
    local spec = sourcePlaceable.spec_husbandryAnimals
    if spec == nil then return nil end
    local clusterHusbandry = spec.clusterHusbandry
    if clusterHusbandry == nil or clusterHusbandry.husbandryIds == nil then return nil end

    if type(animal.id) ~= "string" then return nil end
    local dashAt = string.find(animal.id, "-", 1, true)
    if dashAt == nil then return nil end
    local husbandryIdx = tonumber(string.sub(animal.id, 1, dashAt - 1))
    local animalSlot = tonumber(string.sub(animal.id, dashAt + 1))
    if husbandryIdx == nil or animalSlot == nil then return nil end

    local engineId = clusterHusbandry.husbandryIds[husbandryIdx]
    if engineId == nil or engineId == 0 then return nil end

    local ok, root = pcall(getAnimalRootNode, engineId, animalSlot)
    if ok and root ~= nil and root ~= 0 then return root end
    return nil
end

-- Public helper: collect the animal records that currently have a visual
-- binding in the placeable (i.e. were actually drawn in the world). Caller
-- can pass these into WalkSession to drive per-record walkers.
function Walker.collectAnimalsWithVisuals(placeable)
    local out = {}
    if placeable == nil then return out end
    local spec = placeable.spec_husbandryAnimals
    if spec == nil or spec.clusterSystem == nil then return out end
    local list = (spec.clusterSystem.getAnimals and spec.clusterSystem:getAnimals())
                  or spec.clusterSystem.animals or {}
    for _, animal in pairs(list) do
        if type(animal) == "table" and animal.id ~= nil and animal.id ~= "0-0" then
            out[#out + 1] = animal
        end
    end
    return out
end

local function sampleY(x, z)
    if g_currentMission == nil then return 0 end
    local id = g_currentMission.terrainRootNode or 0
    if getTerrainHeightAtWorldPos == nil then return 0 end
    return getTerrainHeightAtWorldPos(id, x, 0, z) or 0
end

local SHADER_PARAM_PROBES = {
    "atlasInvSizeAndOffsetUV",
    "colorScale",
    "blendOutSharpness",
    "colorMat0",
    "colorMat1",
    "colorMat2",
    "mappingOffsetAndScale",
    "textureAtlasOffsetAndScale",
    "skinDataAtlas",
}

local function resolveEngineIdAndSlot(animal, sourcePlaceable)
    if animal == nil or animal.id == nil or sourcePlaceable == nil then return nil end
    local spec = sourcePlaceable.spec_husbandryAnimals
    if spec == nil or spec.clusterHusbandry == nil then return nil end
    local engineIds = spec.clusterHusbandry.husbandryIds
    if engineIds == nil then return nil end
    local dashAt = string.find(animal.id, "-", 1, true)
    if dashAt == nil then return nil end
    local hIdx = tonumber(string.sub(animal.id, 1, dashAt - 1))
    local slot = tonumber(string.sub(animal.id, dashAt + 1))
    if hIdx == nil or slot == nil then return nil end
    return engineIds[hIdx], slot
end

local function getAnimalTilesFromHusbandry(animal, sourcePlaceable)
    local engineId, slot = resolveEngineIdAndSlot(animal, sourcePlaceable)
    if engineId == nil or engineId == 0 or getAnimalShaderParameter == nil then return nil end
    local ok, x, y, z, w = pcall(getAnimalShaderParameter, engineId, slot, "atlasInvSizeAndOffsetUV")
    if not ok or x == nil then return nil end
    return { x, y, z, w }
end

-- Public: capture the animal's current render tiles + visualAnimalIndex BEFORE
-- a move event so the walker can be built with the correct visuals after
-- the animal's id has rekeyed. Returns a small snapshot table.
function Walker.captureAnimalTiles(animal, sourcePlaceable)
    return getAnimalTilesFromHusbandry(animal, sourcePlaceable)
end

-- Public: forcibly evict the source-side visual of an animal so the cow
-- visibly LEAVES the husbandry at walker-spawn time. RLM's updateVisuals
-- cleanup loop only removes sold/dead animals on its own \xE2\x80\x94 in-transit ones
-- need this explicit step. Combined with our setClusters filter, the cow
-- will not be re-added to the source husbandry while in transit.
function Walker.hideSourceVisual(animal, sourcePlaceable)
    if animal == nil or animal.id == nil then return false end
    if removeHusbandryAnimal == nil then return false end
    local spec = sourcePlaceable and sourcePlaceable.spec_husbandryAnimals
    local clusterHusbandry = spec and spec.clusterHusbandry
    local engineIds = clusterHusbandry and clusterHusbandry.husbandryIds
    if engineIds == nil then return false end
    local dashAt = string.find(animal.id, "-", 1, true)
    if dashAt == nil then return false end
    local hIdx = tonumber(string.sub(animal.id, 1, dashAt - 1))
    local slot = tonumber(string.sub(animal.id, dashAt + 1))
    if hIdx == nil or slot == nil then return false end
    local engineId = engineIds[hIdx]
    if engineId == nil or engineId == 0 then return false end

    pcall(removeHusbandryAnimal, engineId, slot)

    -- Mirror RLM's own removal bookkeeping so it doesn't double-process
    -- this slot on its next cleanup pass.
    if clusterHusbandry.animalIdToCluster ~= nil and clusterHusbandry.animalIdToCluster[hIdx] ~= nil then
        clusterHusbandry.animalIdToCluster[hIdx][slot] = nil
    end
    if clusterHusbandry.visualAnimalCount ~= nil then
        clusterHusbandry.visualAnimalCount = math.max(0, clusterHusbandry.visualAnimalCount - 1)
    end
    if clusterHusbandry.husbandryIdsToVisualAnimalCount ~= nil
       and clusterHusbandry.husbandryIdsToVisualAnimalCount[engineId] ~= nil then
        clusterHusbandry.husbandryIdsToVisualAnimalCount[engineId] =
            math.max(0, clusterHusbandry.husbandryIdsToVisualAnimalCount[engineId] - 1)
    end
    if type(animal.deleteVisual) == "function" then
        pcall(function() animal:deleteVisual() end)
    end
    -- Clear id binding so updateVisuals's add-loop will re-resolve when
    -- the animal arrives at its destination (RL re-assigns a fresh slot).
    animal.id = nil
    animal.idFull = nil
    return true
end

-- Diagnostic: dump every shader param we know about on the source engine animal.
local function probeSourceShaderParams(animal, sourcePlaceable)
    local engineId, slot = resolveEngineIdAndSlot(animal, sourcePlaceable)
    if engineId == nil or engineId == 0 or getAnimalShaderParameter == nil then return end
    for _, name in ipairs(SHADER_PARAM_PROBES) do
        local ok, x, y, z, w = pcall(getAnimalShaderParameter, engineId, slot, name)
        if ok and x ~= nil then
            PastureHerdingLog.dev("Walker[probeSrc] engineId=%s slot=%s %s = (%s, %s, %s, %s)",
                tostring(engineId), tostring(slot), name,
                tostring(x), tostring(y), tostring(z), tostring(w))
        else
            PastureHerdingLog.dev("Walker[probeSrc] %s = <not set>", name)
        end
    end
end

local function spawnViaHerdingLite(animal, sourcePlaceable, animalManager, preCapturedTiles)
    -- Defensive: a nil animal must never reach animal.visualAnimalIndex below.
    if animal == nil or sourcePlaceable == nil then return nil end
    -- animalManager must be resolved by the CALLER (PastureHerding.lua's
    -- scope can see g_animalManager; Walker.lua's scope can't.
    local am = animalManager
    if am == nil then
        PastureHerdingLog.warning("Walker[HL]: animalManager not provided (caller failed to resolve g_animalManager)")
        return nil
    end
    if type(am.createHerdableAnimalFromData) ~= "function" then
        PastureHerdingLog.warning("Walker[HL]: createHerdableAnimalFromData missing on g_animalManager")
        return nil
    end
    local spec = sourcePlaceable.spec_husbandryAnimals
    if spec == nil then
        PastureHerdingLog.warning("Walker[HL]: source has no spec_husbandryAnimals")
        return nil
    end
    local typeIdx = spec.animalTypeIndex
    local visualIdx = animal.visualAnimalIndex or 1
    if typeIdx == nil then
        PastureHerdingLog.warning("Walker[HL]: spec.animalTypeIndex is nil")
        return nil
    end
    PastureHerdingLog.dev("Walker[HL]: calling createHerdableAnimalFromData(type=%d, visIdx1=%d)",
        typeIdx, visualIdx)

    -- Use pre-captured tiles if the caller snapshotted them BEFORE a move
    -- event re-keyed the animal. Otherwise probe the source now.
    local tiles = preCapturedTiles or getAnimalTilesFromHusbandry(animal, sourcePlaceable) or { 1, 1, 0, 0 }
    -- visualIdx is 1-based; HL's getVisualAnimalFromCache is keyed 1-based.
    -- Passing visualIdx - 1 here was off-by-one and landed on the wrong cache
    -- entry — same-subType near-misses looked OK; cross-age (calf/kid/cow)
    -- showed wrong bum ID / nose ring / monitor because RL paths assume the
    -- skeleton structure of the variant we asked for.
    local ok, herdable = pcall(function()
        return am:createHerdableAnimalFromData(typeIdx, visualIdx, tiles)
    end)
    if not ok then
        PastureHerdingLog.warning("Walker: createHerdableAnimalFromData raised: %s", tostring(herdable))
        return nil
    end
    if herdable == nil then
        PastureHerdingLog.warning("Walker: createHerdableAnimalFromData returned nil (cache miss? typeIdx=%s visualIdx=%s)",
            tostring(typeIdx), tostring(visualIdx))
        return nil
    end

    -- Bind RLM cluster + apply ear tags, bum ID, marker etc. mirroring HL's
    -- own makeAnimalHerdable migration path.
    if type(herdable.setCluster) == "function" then
        pcall(function() herdable:setCluster(animal) end)
    end
    if type(herdable.setData) == "function" then
        -- Also 1-based — applyRLVisuals reads animal.visualAnimalIndex and
        -- feeds it to getVisualAnimalFromCache.
        pcall(function() herdable:setData(visualIdx, tiles[1], tiles[2], tiles[3], tiles[4]) end)
    end
    if type(am.applyRLVisuals) == "function" then
        pcall(function() am:applyRLVisuals(herdable) end)
    end
    return herdable
end

-- pathSegments: array of { x, z } [start, waypoint1, ..., end]
-- animal: the specific RLM animal record this walker represents
function Walker.new(sourcePlaceable, pathSegments, speed, animal, animalManager, preCapturedTiles, laneOffset)
    if pathSegments == nil or #pathSegments < 2 then
        PastureHerdingLog.warning("Walker.new: need at least 2 path points")
        return nil
    end

    -- Preferred path: Herding Lite gives us a properly-set-up HerdableAnimal
    -- with bound animations + correct skeleton orientation. Falls back to
    -- bare-clone-from-husbandry below if HL isn't available.
    local herdable = spawnViaHerdingLite(animal, sourcePlaceable, animalManager, preCapturedTiles)
    if herdable ~= nil then
        local nodes = herdable.nodes
        local hlRoot = nodes and nodes.root
        PastureHerdingLog.dev("Walker[HL]: HerdableAnimal returned. nodes.root=%s nodes.mesh=%s animation=%s",
            tostring(hlRoot), tostring(nodes and nodes.mesh), tostring(herdable.animation))
        if hlRoot == nil or hlRoot == 0 then
            PastureHerdingLog.warning("Walker[HL]: nodes.root is nil/0; falling back to bare clone")
        else
            local self = setmetatable({}, Walker_mt)
            self.node = hlRoot
            self.herdable = herdable
            self.animal = animal
            self.path = pathSegments
            self.speed = speed or Walker.DEFAULT_SPEED
            self.segmentIdx = 1
            self.laneOffset = laneOffset or 0
            self.currentHeading = nil
            self.done = false
            self._lastX, self._lastZ = nil, nil

            local startX, startZ = pathSegments[1].x, pathSegments[1].z
            local startY = sampleY(startX, startZ)
            if setWorldTranslation ~= nil then setWorldTranslation(self.node, startX, startY, startZ) end
            local nextPt = pathSegments[2]
            if nextPt ~= nil and nextPt.x ~= nil and nextPt.z ~= nil then
                self:_faceTowards(nextPt.x, nextPt.z)
            else
                PastureHerdingLog.warning("Walker[HL]: pathSegments[2] missing or has no x/z; skipping initial face")
            end
            PastureHerdingLog.dev("Walker: spawned via Herding Lite (animal=%s, root=%s, startedAt=(%.1f,%.1f))",
                tostring(animal and animal.id), tostring(self.node), startX, startZ)
            return self
        end
    end

    -- Fallback: bare clone from husbandry visual (no animation).
    local templateRoot = getRootNodeForAnimal(animal, sourcePlaceable)
    if templateRoot == nil then
        PastureHerdingLog.warning("Walker.new: no visual for animal %s in source; can't clone",
            tostring(animal and animal.id or "<nil>"))
        return nil
    end

    if clone == nil or link == nil or setWorldTranslation == nil then
        PastureHerdingLog.error("Walker.new: engine clone/link/setWorldTranslation APIs missing")
        return nil
    end

    -- clone(template, copyShaderParams, copyShapes, copyAttributes)
    -- We need shader params + shapes copied so RLM's per-animal customisations
    -- (ear tag colours, bum-ID glyph, breed shader tweaks) carry over to the
    -- walker. Without these flags the clone has only the base mesh.
    local node = clone(templateRoot, true, true, false)
    if node == nil or node == 0 then
        PastureHerdingLog.error("Walker.new: clone returned nil/0")
        return nil
    end
    link(getRootNode(), node)
    if setVisibility ~= nil then setVisibility(node, true) end

    -- Animation deliberately left unbound here. Stage 3b-iter2.5c will use
    -- Herding Lite's createHerdableAnimalFromData (via g_animalManager) for
    -- proper animation playback. For now the walker just glides.

    local self = setmetatable({}, Walker_mt)
    self.node = node
    self.animal = animal
    self.path = pathSegments
    self.speed = speed or Walker.DEFAULT_SPEED
    self.segmentIdx = 1  -- moving from path[1] -> path[2]
    self.laneOffset = laneOffset or 0
    self.currentHeading = nil
    self.done = false

    -- Place at start
    local x, z = pathSegments[1].x, pathSegments[1].z
    local y = sampleY(x, z)
    setWorldTranslation(node, x, y, z)
    self:_faceTowards(pathSegments[2].x, pathSegments[2].z)

    return self
end

function Walker:_faceTowards(targetX, targetZ)
    -- Snap version: used only for the very first frame so the spawn pose
    -- is already pointed roughly the right way.
    if setRotation == nil or getWorldTranslation == nil then return end
    local x, _, z = getWorldTranslation(self.node)
    local dx, dz = targetX - x, targetZ - z
    if dx == 0 and dz == 0 then return end
    local angle = math.atan2(dx, dz)
    self.currentHeading = angle
    setRotation(self.node, 0, angle, 0)
end

local function shortestAngularDelta(current, target)
    local d = target - current
    while d > math.pi do d = d - 2 * math.pi end
    while d < -math.pi do d = d + 2 * math.pi end
    return d
end

-- Returns target x,z for the current path segment with lateral lane offset
-- applied (perpendicular to segment direction).
local function laneTarget(self)
    local nextIdx = self.segmentIdx + 1
    local nextPt = self.path[nextIdx]
    if nextPt == nil then return nil, nil end
    if not self.laneOffset or self.laneOffset == 0 then
        return nextPt.x, nextPt.z
    end
    local prev = self.path[self.segmentIdx] or nextPt
    local sx, sz = nextPt.x - prev.x, nextPt.z - prev.z
    local len = math.sqrt(sx * sx + sz * sz)
    if len < 0.001 then return nextPt.x, nextPt.z end
    -- Perpendicular (right-hand) of segment direction in XZ.
    local px, pz = -sz / len, sx / len
    return nextPt.x + px * self.laneOffset, nextPt.z + pz * self.laneOffset
end

function Walker:update(dt)
    if self.done or self.node == nil then return end
    local dtSec = (dt or 0) / 1000
    if dtSec <= 0 then return end

    -- Drive Herding Lite's animation system if we spawned via HL. AnimalAnimation
    -- picks idle/walk/run based on the HerdableAnimal.state flags + speed.
    if self.herdable ~= nil then
        local state = self.herdable.state
        if state ~= nil then
            -- Young calves play the RUN clip (walk slides at herd speed); older
            -- calves and adults walk. Computed once per walker.
            if self._isBaby == nil then
                self._isBaby = Walker.isBabyAnimal(self.animal)
            end
            state.isIdle = false
            state.isWalking = not self._isBaby
            state.isRunning = self._isBaby == true
        end
        if self.herdable.animation ~= nil and type(self.herdable.animation.update) == "function" then
            local okA, errA = pcall(function() self.herdable.animation:update(dt, self.speed) end)
            if not okA then
                PastureHerdingLog.warning("Walker: herdable.animation:update raised: %s", tostring(errA))
                self.herdable = nil -- stop trying to animate
            end
        end
    end

    local nextPt = self.path[self.segmentIdx + 1]
    if nextPt == nil then
        self.done = true
        return
    end

    -- Follow the RAW path with a "spine" position (px,pz): smooth, and it never
    -- weaves. (Offsetting each waypoint by its own segment's perpendicular
    -- zig-zags on a hand-recorded, slightly-wiggly path — that was the jitter.)
    -- The per-walker lane offset is applied ONLY to the rendered position,
    -- perpendicular to the heading — so the animal walks a parallel line
    -- off-centre while still facing straight ahead (no crab-walk).
    local px, pz = self.px, self.pz
    if px == nil then px, pz = self.path[1].x, self.path[1].z end
    local dx, dz = nextPt.x - px, nextPt.z - pz
    local dist = math.sqrt(dx * dx + dz * dz)

    if dist < Walker.ARRIVE_RADIUS then
        -- Reached this waypoint: advance to the next segment WITHOUT snapping the
        -- spine onto the waypoint. Snapping would hop the body up to ARRIVE_RADIUS
        -- forward at every point (the jumpiness). Keep the current spine position;
        -- next frame steers toward the new waypoint, cutting the corner smoothly.
        self.segmentIdx = self.segmentIdx + 1
        if self.path[self.segmentIdx + 1] == nil then
            -- Final point: converge on the actual gate (drop the lane offset).
            self.done = true
            local y = sampleY(nextPt.x, nextPt.z)
            setWorldTranslation(self.node, nextPt.x, y, nextPt.z)
        end
        return
    end

    local step = math.min(self.speed * dtSec, dist)
    local dirx, dirz = dx / dist, dz / dist
    px = px + dirx * step
    pz = pz + dirz * step
    self.px, self.pz = px, pz

    -- Ease the heading toward the travel direction. The raw segment direction
    -- snaps at every recorded waypoint; offsetting by that snapping perpendicular
    -- teleports the body sideways at each point (the "jumping to points"). Easing
    -- the heading makes both the facing AND the lateral offset swing gently.
    local target = math.atan2(dirx, dirz)
    if self.currentHeading == nil then
        self.currentHeading = target
    else
        local d = (target - self.currentHeading) % (2 * math.pi)
        if d > math.pi then d = d - 2 * math.pi end
        self.currentHeading = self.currentHeading + d * math.min(1, dtSec * Walker.TURN_EASE_RATE)
    end
    local h = self.currentHeading

    -- Render at spine + a constant lateral offset perpendicular to the (eased)
    -- heading, so each walker holds a parallel lane off-centre.
    local lane = self.laneOffset or 0
    local hx, hz = math.sin(h), math.cos(h)
    local rx = px + (-hz) * lane
    local rz = pz + (hx) * lane
    local ry = sampleY(rx, rz)
    setWorldTranslation(self.node, rx, ry, rz)

    if setRotation ~= nil then
        setRotation(self.node, 0, h, 0)
    end
end

function Walker:isDone()
    return self.done == true
end

-- The walker's position along the path (the smooth "spine", not the laterally
-- offset render position) — used for gate proximity so the distance is measured
-- to where the animal actually travels. Falls back to the node's world pos.
function Walker:getSpinePos()
    if self.px ~= nil and self.pz ~= nil then return self.px, self.pz end
    if self.node ~= nil and getWorldTranslation ~= nil then
        local x, _, z = getWorldTranslation(self.node)
        return x, z
    end
    return nil
end

function Walker:delete()
    -- If this walker is backed by a HerdableAnimal from HL, delete the entire
    -- root which carries the rig + animation state. Otherwise delete our
    -- bare clone.
    if self.node ~= nil and self.node ~= 0 and delete ~= nil then
        pcall(delete, self.node)
    end
    self.node = nil
    self.herdable = nil
end
