-- Tracks which barn placeables pair with which pasture placeables.
-- A barn is any placeable with spec_husbandryAnimals (cow shed / vanilla pen).
-- A pasture is any placeable with spec_husbandryAnimals AND outdoorContourPolygon
-- (i.e. a fenceless-pasture-style placeable).
--
-- Each barn has an ordered list of paired pastures and an "active" index
-- indicating where cows go at sunrise.
--
-- Lookups by placeable are O(1) via a reverse index. Save/load round-trips
-- through GrazingScheduler which serialises this structure.

GrazingRegistry = {}
local GrazingRegistry_mt = Class(GrazingRegistry)

function GrazingRegistry.new()
    local self = setmetatable({}, GrazingRegistry_mt)
    self.barns = {} -- array of { placeable, pastures = {...}, activeIndex, autoRotate }
    self.byPlaceable = {} -- placeable -> { kind = "barn"|"pasture", barnIdx, pastureIdx }
    -- Per-PLACEABLE land record. A pasture is one physical husbandry, so its
    -- land state (quality, lastGrazed, fertilizer, capacity, auto-water, the
    -- painted-fertilizer flag) belongs to the placeable, NOT to a (barn,pIdx)
    -- pair — otherwise two barns sharing one paddock would keep divergent
    -- copies of the same physical thing. Paths stay per-barn (different routes).
    -- Keyed by the placeable table; entries are created lazily by _land().
    self.land = {}
    return self
end

-- Resolve the shared land record for the pasture at (barnIdx, pIdx), creating
-- it on first touch. Returns nil for an invalid (barn, pasture). All the
-- per-pasture land getters/setters go through this, so the (barnIdx, pIdx) API
-- is unchanged for callers while the storage is shared per placeable.
function GrazingRegistry:_land(barnIdx, pIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return nil end
    local placeable = barn.pastures[pIdx]
    if placeable == nil then return nil end
    return self:_landForPlaceable(placeable)
end

function GrazingRegistry:_landForPlaceable(placeable)
    if placeable == nil then return nil end
    self.land = self.land or {}
    local rec = self.land[placeable]
    if rec == nil then
        rec = {} -- quality/lastGrazedDay/fertilizerExpireDay/fertPainted/capacity/autoWater
        self.land[placeable] = rec
    end
    return rec
end

function GrazingRegistry:cycleActivePasture(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return nil, "no such barn" end
    if #barn.pastures == 0 then return nil, "barn has no pastures" end
    local next = barn.activeIndex + 1
    if next > #barn.pastures then next = 1 end
    barn.activeIndex = next
    return next
end

function GrazingRegistry:setAutoRotate(barnIdx, enabled)
    local barn = self.barns[barnIdx]
    if barn == nil then return false, "no such barn" end
    barn.autoRotate = enabled == true
    return true
end

function GrazingRegistry:getAutoRotate(barnIdx)
    local barn = self.barns[barnIdx]
    return barn ~= nil and barn.autoRotate == true
end

-- Per-barn daily-cycle toggle. When false the scheduler won't run the
-- sunrise/sunset transitions for this barn. Default OFF — the player opts
-- into grazing per-barn from the menu rather than every newly-registered
-- barn auto-starting a daily cycle.
function GrazingRegistry:getAutoSchedule(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return false end
    return barn.autoSchedule == true
end
function GrazingRegistry:setAutoSchedule(barnIdx, enabled)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    barn.autoSchedule = enabled == true
end

-- Per-barn Feed Trough toggle. Controls whether the pasture's food trough is
-- shown/used: the food-pile mesh is visible and the player can hand-feed at
-- pasture. Default follows herd type — BEEF on (finishing / supplement),
-- DAIRY off (pure graze; the grass-productivity floor already gives grazing
-- dairy full milk). A player can flip it either way (e.g. hand-feed a dairy
-- pasture, or turn a dairy pasture into a beef-style one). Replaces the old
-- concentrate-pump toggle (pump removed).
function GrazingRegistry:getTroughEnabled(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return false end
    if barn.troughEnabled == nil then
        -- Default by PRODUCTION type (auto), not schedule: meat animals want the
        -- concentrate trough, milk animals graze pure (floor gives full milk).
        return self:getProductionType(barnIdx) == "meat"
    end
    return barn.troughEnabled == true
end
function GrazingRegistry:setTroughEnabled(barnIdx, enabled)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    barn.troughEnabled = enabled == true
end

-- Per-barn herd type. "dairy" runs the daily sunrise/sunset cycle; "beef"
-- runs a phase-driven model where the player flips between grazing (cows
-- live on pasture continuously) and finishing (cows live in the shed on
-- TMR). Default auto-detected from spec_husbandryMilk.hasMilkProduction at
-- registerBarn time.
function GrazingRegistry:getHerdType(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return "dairy" end
    return barn.herdType or "dairy"
end
function GrazingRegistry:setHerdType(barnIdx, t)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    if t == "dairy" or t == "beef" then barn.herdType = t end
end

-- Beef-phase only meaningful when herdType == "beef". "grazing" -> cows
-- live on the active pasture and the scheduler tries to keep them out as
-- long as the season + weather gates allow. "finishing" -> cows are pulled
-- home and stay home regardless of season; player tops up TMR in the
-- barn trough manually.
function GrazingRegistry:getBeefPhase(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return "grazing" end
    return barn.beefPhase or "grazing"
end
function GrazingRegistry:setBeefPhase(barnIdx, phase)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    if phase == "grazing" or phase == "finishing" then barn.beefPhase = phase end
end

-- PRODUCTION type is AUTOMATIC from the animal, never a player choice: a
-- milk-capable husbandry ("milk"), else ("meat"). Drives the productivity floor,
-- the feed-trough default, and which grazing boost applies (milk vs weight) — all
-- independent of the schedule below, so e.g. a milk herd grazed full-time still
-- gets its full-milk floor.
function GrazingRegistry:getProductionType(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return "meat" end
    local milkSpec = barn.placeable and barn.placeable.spec_husbandryMilk
    if milkSpec ~= nil and milkSpec.hasMilkProduction ~= false then return "milk" end
    return "meat"
end

-- SCHEDULE mode: the player-facing, animal-neutral view over the internal schedule
-- engine (herdType/beefPhase). Existing saves map straight through (no migration):
--   "day"        -> out by day, home at night   (herdType dairy)
--   "continuous" -> lives out on pasture         (herdType beef + phase grazing)
--   "housed"     -> kept in the barn/shed        (herdType beef + phase finishing)
function GrazingRegistry:getScheduleMode(barnIdx)
    if self:getHerdType(barnIdx) == "dairy" then return "day" end
    if self:getBeefPhase(barnIdx) == "finishing" then return "housed" end
    return "continuous"
end
function GrazingRegistry:setScheduleMode(barnIdx, mode)
    if mode == "day" then
        self:setHerdType(barnIdx, "dairy")
    elseif mode == "continuous" then
        self:setHerdType(barnIdx, "beef")
        self:setBeefPhase(barnIdx, "grazing")
    elseif mode == "housed" then
        self:setHerdType(barnIdx, "beef")
        self:setBeefPhase(barnIdx, "finishing")
    end
end

-- Per-herd "grazing condition" (0..1). Rises while the herd grazes good grass,
-- decays while housed; the beef weight boost and dairy milk boost both scale by
-- it. Persisted per barn. See GrazingScheduler:_updateGrazingCondition and the
-- server-only Animal hooks in HusbandryHook.
function GrazingRegistry:getGrazingCondition(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return 0 end
    return barn.grazingCondition or 0
end
function GrazingRegistry:setGrazingCondition(barnIdx, v)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    if type(v) ~= "number" then return end
    if v < 0 then v = 0 elseif v > 1 then v = 1 end
    barn.grazingCondition = v
end

-- Resolve the barn index for a barn placeable (a husbandry's owner). Returns nil
-- if the placeable isn't a registered barn. Used by the Animal weight/milk hooks
-- to map an animal -> its husbandry -> our barn -> grazing condition.
function GrazingRegistry:getBarnIndexByPlaceable(placeable)
    if placeable == nil then return nil end
    local ref = self.byPlaceable[placeable]
    if ref ~= nil and ref.kind == "barn" then return ref.barnIdx end
    return nil
end

-- Per-barn sunrise / sunset overrides. nil = use the global scheduler hours.
-- Values 0..23 representing the hour at which this barn's cows transition.
function GrazingRegistry:getBarnSunrise(barnIdx)
    local barn = self.barns[barnIdx]
    return barn and barn.sunriseHour or nil
end
function GrazingRegistry:setBarnSunrise(barnIdx, hour)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    if hour == nil then
        barn.sunriseHour = nil
    elseif type(hour) == "number" and hour >= 0 and hour <= 23 then
        barn.sunriseHour = math.floor(hour)
    end
end
function GrazingRegistry:getBarnSunset(barnIdx)
    local barn = self.barns[barnIdx]
    return barn and barn.sunsetHour or nil
end
function GrazingRegistry:setBarnSunset(barnIdx, hour)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    if hour == nil then
        barn.sunsetHour = nil
    elseif type(hour) == "number" and hour >= 0 and hour <= 23 then
        barn.sunsetHour = math.floor(hour)
    end
end

-- Pasture quality (soil health) 0..1. Stored values are POST-LAST-GRAZING.
-- Returns the directly-stored quality, no on-read accumulation. Both decay
-- (cows grazing) and recovery (no cows) are applied per-hour by the
-- scheduler's tick helpers, so this stored value is always current.
-- Clamped to the 0.1 floor and 1.0 ceiling.
function GrazingRegistry:getPastureQuality(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return 1.0 end
    local stored = L.quality or 1.0
    return math.max(0.1, math.min(1.0, stored))
end

-- Raw stored quality (nil if never set) — lets the PF soil sampler tell a FRESH
-- pasture (sample it) from one that already has a quality (leave it alone).
function GrazingRegistry:getStoredPastureQuality(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    return L and L.quality or nil
end

-- Has this pasture's Precision Farming soil been sampled yet? Set true once
-- sampled so we sample each pasture exactly once — and so pre-PF saves get a
-- one-time BACKFILL on load (they have no flag) without re-sampling shared or
-- already-done pastures. Persisted on the land record.
function GrazingRegistry:getPFSampled(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    return L ~= nil and L.pfSampled == true
end
function GrazingRegistry:setPFSampled(barnIdx, pIdx, v)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    L.pfSampled = (v == true) or nil
end

-- BASE PF pH factor (0..1, 1.0 = at/near optimal for the soil type). Cached on the
-- land record, refreshed hourly by the scheduler and at registration; NOT persisted
-- (derived live from PF). Default 1.0 (no PF / neutral).
function GrazingRegistry:getBasePHFactor(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    return (L and type(L.pHFactor) == "number") and L.pHFactor or 1.0
end
function GrazingRegistry:setPHFactor(barnIdx, pIdx, f)
    local L = self:_land(barnIdx, pIdx)
    if L == nil or type(f) ~= "number" then return end
    if f < 0 then f = 0 elseif f > 1 then f = 1 end
    L.pHFactor = f
end

-- Player LIME correction (0..1). Precision Farming won't lime husbandry (non-field)
-- pasture ground, so the player drives a lime spreader over the pasture and
-- SprayerHook raises this. It lifts the effective pH factor toward optimal (1.0).
-- Persisted on the land record.
function GrazingRegistry:getLimeCorrection(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    return (L and type(L.pHLimeCorrection) == "number") and L.pHLimeCorrection or 0
end
function GrazingRegistry:setLimeCorrection(barnIdx, pIdx, c)
    local L = self:_land(barnIdx, pIdx)
    if L == nil or type(c) ~= "number" then return end
    if c < 0 then c = 0 elseif c > 1 then c = 1 end
    L.pHLimeCorrection = c
end

-- Effective pH factor = the PF base, lifted toward 1.0 by the lime correction. This
-- is what getEffectiveQuality and the menu use, so liming a pasture visibly removes
-- its pH penalty. 1.0 when at optimal, no PF, or fully limed.
function GrazingRegistry:getPHFactor(barnIdx, pIdx)
    local base = self:getBasePHFactor(barnIdx, pIdx)
    local corr = self:getLimeCorrection(barnIdx, pIdx)
    return base + (1.0 - base) * corr
end

-- Effective pasture quality = stored soil quality x live pH factor, clamped. This
-- is "how good the pasture is RIGHT NOW" — use it for grass regrowth, the grazing
-- boost target, the herdsman picker and the menu. Decay/recovery keep operating on
-- the raw stored quality (getPastureQuality), so pH never corrupts that.
function GrazingRegistry:getEffectiveQuality(barnIdx, pIdx)
    local q = self:getPastureQuality(barnIdx, pIdx) * self:getPHFactor(barnIdx, pIdx)
    return math.max(0.1, math.min(1.0, q))
end

-- Pasture RENEWAL (reseeding). A renewed pasture is renovated back to full soil
-- quality but locked OUT of grazing until renewalUnlockDay (an absolute in-game
-- day = currentDay + 2 months' worth of days). Stored on the shared land record
-- so two barns sharing one paddock share the lockout. See
-- GrazingScheduler:renewPasture. renewalUnlockDay nil / <= 0 = not renewing.
function GrazingRegistry:getRenewalUnlockDay(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return 0 end
    return L.renewalUnlockDay or 0
end
function GrazingRegistry:setRenewalUnlockDay(barnIdx, pIdx, day)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    if type(day) ~= "number" or day <= 0 then
        L.renewalUnlockDay = nil
    else
        L.renewalUnlockDay = day
    end
end
-- True while the pasture is still inside its reseeding lockout window.
function GrazingRegistry:isRenewing(barnIdx, pIdx)
    local unlockDay = self:getRenewalUnlockDay(barnIdx, pIdx)
    if unlockDay <= 0 then return false end
    local env = g_currentMission and g_currentMission.environment
    local today = env and env.currentDay
    if type(today) ~= "number" then return true end  -- no clock yet: treat as locked
    return today < unlockDay
end

-- Apply a hourly recovery increment. Called per hour-rollover by the
-- scheduler when no cows are at the pasture. Fertilizer doubles the rate
-- during the boost window.
function GrazingRegistry:applyHourlyRecovery(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    local current = L.quality or 1.0
    if current >= 1.0 then return end  -- already topped out
    local f = rawget(_G, "g_pastureHerding")
    local dailyRecovery = (f and f.grazingQualityRecoveryPerRestDay) or 0.10
    local hourlyRecovery = dailyRecovery / 24
    if self:isPastureFertilized(barnIdx, pIdx) then
        local fertMul = (f and f.fertilizerQualityMultiplier) or 2.0
        hourlyRecovery = hourlyRecovery * fertMul
    end
    L.quality = math.min(1.0, current + hourlyRecovery)
end

-- Apply a single grazing-day decay to the given pasture. Called by the
-- scheduler when cows go OUT to this pasture. Uses getPastureQuality to
-- fold in accumulated recovery first, then subtracts decay.
-- hoursFraction (optional, default 1.0) lets the caller apply a partial
-- day's decay — e.g. cows that grazed for 6h apply 6/24 = 0.25 of the
-- daily decay. Used by the IN transition (sunset) so quality drops in
-- proportion to actual time spent grazing.
function GrazingRegistry:applyGrazingDecay(barnIdx, pIdx, hoursFraction)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    local current = self:getPastureQuality(barnIdx, pIdx)
    local f = rawget(_G, "g_pastureHerding")
    local decay = (f and f.grazingQualityDecayPerGrazingDay) or 0.20
    local frac = hoursFraction or 1.0
    if type(frac) ~= "number" or frac < 0 then frac = 1.0 end
    if frac > 1.0 then frac = 1.0 end
    L.quality = math.max(0.1, current - decay * frac)
end

-- Track when cows arrived at a pasture (for proportional decay on return).
function GrazingRegistry:setGrazingStartHour(barnIdx, hour)
    local barn = self.barns[barnIdx]
    if barn == nil then return end
    barn._grazingStartHour = hour
end
function GrazingRegistry:getGrazingStartHour(barnIdx)
    local barn = self.barns[barnIdx]
    return barn and barn._grazingStartHour or nil
end

-- Per-pasture fertilizer / slurry boost. Stored as the in-game day the
-- boost EXPIRES on. While currentDay <= expireDay the regrowth throttle
-- multiplies growth, the quality recovery doubles, and the menu shows a
-- "fertilized" indicator. Default applies ~7 days of boost.
function GrazingRegistry:applyFertilizer(barnIdx, pIdx, days)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return false end
    local today = g_currentMission and g_currentMission.environment
                  and g_currentMission.environment.currentDay
    if today == nil then return false end
    local currentExpire = L.fertilizerExpireDay or 0
    -- Re-applying STACKS: extend duration rather than overwrite.
    local base = math.max(today, currentExpire)
    L.fertilizerExpireDay = base + (days or 7)
    return true, L.fertilizerExpireDay
end

function GrazingRegistry:getFertilizerDaysRemaining(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return 0 end
    local expire = L.fertilizerExpireDay
    if expire == nil then return 0 end
    local today = g_currentMission and g_currentMission.environment
                  and g_currentMission.environment.currentDay
    if today == nil then return 0 end
    return math.max(0, expire - today)
end

function GrazingRegistry:isPastureFertilized(barnIdx, pIdx)
    return self:getFertilizerDaysRemaining(barnIdx, pIdx) > 0
end

-- Per-pasture auto-water override. nil = leave the spec field at whatever
-- the i3d/XML set. true/false = scheduler force-overrides
-- spec_husbandryWater.automaticWaterSupply every frame.
function GrazingRegistry:getPastureAutoWater(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return nil end
    return L.autoWater
end
function GrazingRegistry:setPastureAutoWater(barnIdx, pIdx, enabled)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    if enabled == nil then
        L.autoWater = nil
    else
        L.autoWater = (enabled == true)
    end
end

-- Per-pasture animal capacity. nil = no override (vanilla maxNumAnimals
-- applies). Otherwise the scheduler clamps the pasture's spec_husbandryAnimals
-- maxNumAnimals down to this value every frame, scaled with whatever the
-- player picks in the Grazing tab.
function GrazingRegistry:getPastureCapacity(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return nil end
    return L.capacity
end
function GrazingRegistry:setPastureCapacity(barnIdx, pIdx, cap)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    if cap == nil or cap <= 0 then
        L.capacity = nil
    else
        L.capacity = math.floor(cap)
    end
end

-- Per-pasture "fertilizer visual is painted on the terrain" flag. Set true
-- when the player sprays over the pasture (SprayerHook paints the ground);
-- cleared when the scheduler wipes the visual on the next graze. Persisted so
-- the clear survives a save/reload (the painted density map is saved by the
-- engine, but our runtime cell-set is not).
function GrazingRegistry:getFertPainted(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return false end
    return L.fertPainted == true
end
function GrazingRegistry:setFertPainted(barnIdx, pIdx, v)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    L.fertPainted = (v == true) or nil
    -- Stamp the day it was (re)painted so the visual can also auto-clear after a
    -- few days (as well as on next graze). Cleared when the paint is cleared.
    if v == true then
        local env = g_currentMission and g_currentMission.environment
        L.fertPaintDay = env and env.currentDay or nil
    else
        L.fertPaintDay = nil
    end
end
-- Absolute in-game day the pasture's spray/lime look was last painted (nil if not).
function GrazingRegistry:getFertPaintDay(barnIdx, pIdx)
    local L = self:_land(barnIdx, pIdx)
    return L and L.fertPaintDay or nil
end
-- Restore the paint day (used on load so the timed fade keeps the ORIGINAL day
-- rather than resetting to the load day).
function GrazingRegistry:setFertPaintDay(barnIdx, pIdx, day)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    L.fertPaintDay = type(day) == "number" and day or nil
end

-- Bulk set on load (used by GrazingPersistence).
function GrazingRegistry:setPastureQuality(barnIdx, pIdx, quality)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    L.quality = math.max(0.1, math.min(1.0, quality or 1.0))
end

-- Record / read the in-game day each pasture was last grazed. Stored as the
-- environment day number (so "days ago" is easy to compute). Returns the
-- stored day or nil if never grazed.
function GrazingRegistry:setLastGrazed(barnIdx, pastureIdx)
    local L = self:_land(barnIdx, pastureIdx)
    if L == nil then return end
    local day = g_currentMission and g_currentMission.environment and g_currentMission.environment.currentDay
    if day == nil then return end
    L.lastGrazedDay = day
end

function GrazingRegistry:getLastGrazed(barnIdx, pastureIdx)
    local L = self:_land(barnIdx, pastureIdx)
    if L == nil then return nil end
    return L.lastGrazedDay
end

-- Remove a pasture from its barn. Shifts subsequent pasture indices down by
-- one, fixes the byPlaceable map, repairs activeIndex if it pointed at the
-- removed slot or beyond, and shifts lastGrazed entries to follow.
-- Player UN-LINKS a pasture from ONE barn (it isn't sold — another barn may
-- still own it). Removes only this barn's link; a shared pasture and its land
-- record survive for the remaining owners.
function GrazingRegistry:removePastureFromBarn(barnIdx, pastureIdx)
    local barn = self.barns[barnIdx]
    if barn == nil or barn.pastures[pastureIdx] == nil then return false end
    local placeable = barn.pastures[pastureIdx]
    self:_removePastureAt(barn, barnIdx, pastureIdx)
    if #self:_ownersOf(placeable) == 0 then
        -- Last owner gone: wipe any painted fertilizer tint before the land
        -- record is dropped, else the field-flagged terrain patch lingers with
        -- nothing left to clear it.
        local L = self.land and self.land[placeable]
        if L ~= nil and L.fertPainted == true
           and _G.SprayerHook ~= nil and type(SprayerHook.clearFertVisual) == "function" then
            pcall(function() SprayerHook.clearFertVisual(placeable) end)
        end
    end
    -- Repoint the primary to a surviving owner, or clear the ref + drop the
    -- shared land record when no owner remains.
    self:_repointOrDrop(placeable)
    -- Same grid-invalidation rationale as :unregister — see comment there.
    if _G.SprayerHook ~= nil and type(SprayerHook.clearGridsForBarn) == "function" then
        SprayerHook.clearGridsForBarn(barnIdx)
    end
    return true
end

-- Shared reindex helper for the four per-pasture data containers a barn
-- carries. Call AFTER `table.remove(barn.pastures, removedIdx)` so the
-- pastures array is already short by one. Handles the remaining PER-BARN
-- index-keyed data (paths). Land state (quality/lastGrazed/fertilizer/capacity/
-- auto-water/fertPainted) now lives in self.land keyed by the PLACEABLE, so it
-- needs no reindex here — the placeable identity is stable across removals.
-- byPlaceable is reindexed by callers because they need access to the
-- placeable references; that's separate from this prop-only fixup.
function GrazingRegistry:_reindexPasturePropMaps(barn, removedIdx)
    if type(barn.pastureData) == "table" then
        table.remove(barn.pastureData, removedIdx)
    end
    -- Pasture-pasture paths key on pasture indices at both ends. Drop any pair
    -- touching the removed pasture; shift higher indices down by one.
    if type(barn.pasturePaths) == "table" then
        local kept = {}
        for _, e in ipairs(barn.pasturePaths) do
            if e.from ~= removedIdx and e.to ~= removedIdx then
                if e.from > removedIdx then e.from = e.from - 1 end
                if e.to > removedIdx then e.to = e.to - 1 end
                kept[#kept + 1] = e
            end
        end
        barn.pasturePaths = kept
    end
end

-- Used by GrazingPersistence to bulk-set on load.
function GrazingRegistry:setLastGrazedDay(barnIdx, pastureIdx, day)
    local L = self:_land(barnIdx, pastureIdx)
    if L == nil then return end
    L.lastGrazedDay = day
end

-- Raw fertilizer expire-day setter (used by persistence load; applyFertilizer
-- stacks, this overwrites).
function GrazingRegistry:setFertilizerExpireDay(barnIdx, pIdx, day)
    local L = self:_land(barnIdx, pIdx)
    if L == nil then return end
    L.fertilizerExpireDay = day
end

-- Read-only access to the raw shared land record for (barnIdx, pIdx), or nil if
-- none exists yet. Used by persistence to serialise land state without creating
-- empty records. Does NOT create (unlike _land).
function GrazingRegistry:getLandRecord(barnIdx, pIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return nil end
    local placeable = barn.pastures[pIdx]
    if placeable == nil then return nil end
    return self.land and self.land[placeable] or nil
end

function GrazingRegistry:registerBarn(placeable)
    if placeable == nil then return nil, "no placeable" end
    if self.byPlaceable[placeable] ~= nil then
        return nil, "placeable is already registered as a " .. self.byPlaceable[placeable].kind
    end
    -- Auto-detect dairy vs beef from the husbandry's milk-production spec.
    -- A husbandry without spec_husbandryMilk, or with hasMilkProduction
    -- explicitly false, is treated as a beef shed by default. Player can
    -- override via the Status pane herd-type picker.
    local milkSpec = placeable.spec_husbandryMilk
    local detectedType = "dairy"
    if milkSpec == nil or milkSpec.hasMilkProduction == false then
        detectedType = "beef"
    end

    local entry = {
        placeable = placeable,
        pastures = {},
        activeIndex = 0,
        autoRotate = false,   -- default OFF: Auto Pasture Select counts as
                              -- "herdsman" automation (player pays wage when
                              -- it's ON), so don't enable it by surprise.
        herdType = detectedType,
        beefPhase = "grazing",
        -- Feed Trough default is derived from herd type in getTroughEnabled
        -- (beef on, dairy off), so leave troughEnabled nil here — nil means
        -- "follow herd type" until the player explicitly toggles it.
        troughEnabled = nil,
    }
    self.barns[#self.barns + 1] = entry
    local idx = #self.barns
    self.byPlaceable[placeable] = { kind = "barn", barnIdx = idx }
    PastureHerdingLog.dev("GrazingRegistry: registered barn#%d herdType auto-detected=%s",
        idx, detectedType)
    return idx, nil
end

-- All (barnIdx, pastureIdx) that reference this placeable as a pasture. A
-- pasture may be SHARED by more than one barn (Part B), so any code that mutates
-- global state on removal must consult this — not just byPlaceable, which holds
-- the PRIMARY owner only.
function GrazingRegistry:_ownersOf(placeable)
    local owners = {}
    for bIdx, barn in pairs(self.barns) do
        if type(barn) == "table" and type(barn.pastures) == "table" then
            for pIdx, p in ipairs(barn.pastures) do
                if p == placeable then
                    owners[#owners + 1] = { barnIdx = bIdx, pastureIdx = pIdx }
                end
            end
        end
    end
    return owners
end

function GrazingRegistry:addPastureToBarn(barnIdx, placeable)
    local barn = self.barns[barnIdx]
    if barn == nil then return nil, "no such barn idx" end
    if placeable == nil then return nil, "no placeable" end
    local existing = self.byPlaceable[placeable]
    if existing ~= nil then
        if existing.kind == "barn" then
            return nil, "placeable is already registered as a barn"
        end
        -- It's a pasture already registered. Allow SHARING it with a second
        -- barn (Part B), but block re-adding it to the SAME barn.
        for _, p in ipairs(barn.pastures) do
            if p == placeable then
                return nil, "pasture is already linked to this barn"
            end
        end
    end
    barn.pastures[#barn.pastures + 1] = placeable
    local pIdx = #barn.pastures
    -- Each pasture entry also carries a waypoint path (barn -> pasture) — this
    -- IS per-barn (each barn has its own route to a shared pasture).
    barn.pastureData = barn.pastureData or {}
    barn.pastureData[pIdx] = { path = {} }
    -- byPlaceable holds the PRIMARY owner (first barn to register the pasture);
    -- additional owners share it. Only claim it if unclaimed, so the primary is
    -- stable. Land state (quality/grass/fertilizer/...) is shared automatically
    -- via self.land[placeable], so nothing to seed here.
    if existing == nil then
        self.byPlaceable[placeable] = { kind = "pasture", barnIdx = barnIdx, pastureIdx = pIdx }
    end
    if barn.activeIndex == 0 then barn.activeIndex = 1 end
    return pIdx, nil
end

function GrazingRegistry:getPath(barnIdx, pastureIdx)
    local barn = self.barns[barnIdx]
    if barn == nil or barn.pastureData == nil then return nil end
    local data = barn.pastureData[pastureIdx]
    return data and data.path or nil
end

function GrazingRegistry:addWaypoint(barnIdx, pastureIdx, x, z)
    local barn = self.barns[barnIdx]
    if barn == nil then return nil, "no such barn" end
    if barn.pastures[pastureIdx] == nil then return nil, "no such pasture" end
    barn.pastureData = barn.pastureData or {}
    barn.pastureData[pastureIdx] = barn.pastureData[pastureIdx] or { path = {} }
    local path = barn.pastureData[pastureIdx].path
    path[#path + 1] = x
    path[#path + 1] = z
    return #path / 2
end

function GrazingRegistry:clearPath(barnIdx, pastureIdx)
    local barn = self.barns[barnIdx]
    if barn == nil or barn.pastureData == nil then return false end
    if barn.pastureData[pastureIdx] == nil then return false end
    barn.pastureData[pastureIdx].path = {}
    return true
end

function GrazingRegistry:reversePath(barnIdx, pastureIdx)
    local path = self:getPath(barnIdx, pastureIdx)
    if path == nil or #path < 4 then return false end
    local rev = {}
    for i = #path - 1, 1, -2 do
        rev[#rev + 1] = path[i]
        rev[#rev + 1] = path[i + 1]
    end
    self.barns[barnIdx].pastureData[pastureIdx].path = rev
    return true
end

-- ── Pasture - pasture paths ──────────────────────────────────────────────
-- A herd already out at pasture A can walk straight to pasture B without
-- routing home through the barn. Paths are stored per unordered pair in
-- barn.pasturePaths = { { from=idx, to=idx, path={x,z,...} }, ... }, recorded
-- in the from->to direction. Requesting the opposite direction auto-reverses
-- (mirrors barn-pasture), so the player only records each pair once.

-- Locate the stored entry for the unordered pair {a,b}, in either orientation.
function GrazingRegistry:_findPairEntry(barn, a, b)
    if barn == nil or type(barn.pasturePaths) ~= "table" then return nil end
    for _, e in ipairs(barn.pasturePaths) do
        if (e.from == a and e.to == b) or (e.from == b and e.to == a) then
            return e
        end
    end
    return nil
end

-- Resolve a pair path by pasture PLACEABLE identity, searching the preferred
-- barn first and then every other barn. Pair paths are stored per barn keyed by
-- that barn's local pasture indices, but a pasture shared under two barns is the
-- SAME placeable — so a path recorded under barn A between two shared pastures
-- can be reused by barn B (which owns the same two placeables at different
-- indices). This lets the player record each pasture-pasture route only once.
-- Returns (entry, ownerBarn) or nil.
function GrazingRegistry:_findPairEntryByPlaceable(fromP, toP, preferBarnIdx)
    if fromP == nil or toP == nil then return nil end
    local function scan(barn)
        if barn == nil or type(barn.pasturePaths) ~= "table" then return nil end
        for _, e in ipairs(barn.pasturePaths) do
            local a = barn.pastures[e.from]
            local b = barn.pastures[e.to]
            if (a == fromP and b == toP) or (a == toP and b == fromP) then
                return e, barn
            end
        end
        return nil
    end
    -- Preferred barn wins so a barn's own recording takes priority over a shared one.
    if preferBarnIdx ~= nil then
        local e, ob = scan(self.barns[preferBarnIdx])
        if e ~= nil then return e, ob end
    end
    for bIdx, barn in pairs(self.barns) do
        if bIdx ~= preferBarnIdx then
            local e, ob = scan(barn)
            if e ~= nil then return e, ob end
        end
    end
    return nil
end

-- Returns the pair path as a flat {x,z,...} list oriented fromIdx->toIdx
-- (auto-reversed if it was recorded the other way), or nil if none/too short.
-- Resolves by placeable identity so a route recorded under any barn that owns
-- both pastures is shared to every other barn that owns them too.
function GrazingRegistry:getPairPath(barnIdx, fromIdx, toIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return nil end
    local fromP = barn.pastures[fromIdx]
    local toP = barn.pastures[toIdx]
    if fromP == nil or toP == nil then return nil end
    local e, ownerBarn = self:_findPairEntryByPlaceable(fromP, toP, barnIdx)
    if e == nil or type(e.path) ~= "table" or #e.path < 4 then return nil end
    -- Orient the stored path fromP->toP by comparing placeables (the owner barn's
    -- indices may differ from ours, so we can't compare indices directly).
    if ownerBarn.pastures[e.from] == fromP then
        return e.path
    end
    local rev = {}
    for i = #e.path - 1, 1, -2 do
        rev[#rev + 1] = e.path[i]
        rev[#rev + 1] = e.path[i + 1]
    end
    return rev
end

function GrazingRegistry:hasPairPath(barnIdx, fromIdx, toIdx)
    return self:getPairPath(barnIdx, fromIdx, toIdx) ~= nil
end

-- Append one waypoint to the pair path being recorded fromIdx->toIdx. Creates
-- the entry (fixing its stored orientation) on the first waypoint. If a path
-- already exists for this pair but in the OPPOSITE orientation, we prepend so
-- the stored order always runs entry.from->entry.to.
function GrazingRegistry:addPairWaypoint(barnIdx, fromIdx, toIdx, x, z)
    local barn = self.barns[barnIdx]
    if barn == nil then return nil, "no such barn" end
    if fromIdx == toIdx then return nil, "from and to are the same pasture" end
    if barn.pastures[fromIdx] == nil then return nil, "no such from-pasture" end
    if barn.pastures[toIdx] == nil then return nil, "no such to-pasture" end
    barn.pasturePaths = barn.pasturePaths or {}
    local e = self:_findPairEntry(barn, fromIdx, toIdx)
    if e == nil then
        e = { from = fromIdx, to = toIdx, path = {} }
        barn.pasturePaths[#barn.pasturePaths + 1] = e
    end
    if e.from == fromIdx then
        e.path[#e.path + 1] = x
        e.path[#e.path + 1] = z
    else
        table.insert(e.path, 1, z)
        table.insert(e.path, 1, x)
    end
    return #e.path / 2
end

function GrazingRegistry:clearPairPath(barnIdx, fromIdx, toIdx)
    local barn = self.barns[barnIdx]
    if barn == nil or type(barn.pasturePaths) ~= "table" then return false end
    for i = #barn.pasturePaths, 1, -1 do
        local e = barn.pasturePaths[i]
        if (e.from == fromIdx and e.to == toIdx) or (e.from == toIdx and e.to == fromIdx) then
            table.remove(barn.pasturePaths, i)
            return true
        end
    end
    return false
end

-- Every pasture this one has a pasture-pasture path to (resolved across barns,
-- so shared routes count), as an array of { idx = otherPastureIdx, placeable }.
function GrazingRegistry:listPairPathTargets(barnIdx, pastureIdx)
    local out = {}
    local barn = self.barns[barnIdx]
    if barn == nil then return out end
    for j, p in ipairs(barn.pastures) do
        if j ~= pastureIdx and self:hasPairPath(barnIdx, pastureIdx, j) then
            out[#out + 1] = { idx = j, placeable = p }
        end
    end
    return out
end

-- Remove the pasture-pasture path between two pastures wherever it's stored —
-- it may live under a DIFFERENT barn that shares both pastures (see
-- _findPairEntryByPlaceable). Returns true if an entry was removed. Because the
-- route is shared, removing it removes it for every barn that was using it.
function GrazingRegistry:clearPairPathAnywhere(barnIdx, fromIdx, toIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return false end
    local fromP = barn.pastures[fromIdx]
    local toP = barn.pastures[toIdx]
    if fromP == nil or toP == nil then return false end
    local e, ownerBarn = self:_findPairEntryByPlaceable(fromP, toP, barnIdx)
    if e == nil or ownerBarn == nil or type(ownerBarn.pasturePaths) ~= "table" then return false end
    for i = #ownerBarn.pasturePaths, 1, -1 do
        if ownerBarn.pasturePaths[i] == e then
            table.remove(ownerBarn.pasturePaths, i)
            return true
        end
    end
    return false
end

function GrazingRegistry:findNearestPasture(px, pz)
    local bestB, bestP, bestD2 = nil, nil, math.huge
    for bIdx, barn in self:iterBarns() do
        for pIdx, p in ipairs(barn.pastures) do
            if p.rootNode and p.rootNode ~= 0 then
                local x, _, z = getWorldTranslation(p.rootNode)
                local dx, dz = x - px, z - pz
                local d2 = dx*dx + dz*dz
                if d2 < bestD2 then bestD2 = d2; bestB = bIdx; bestP = pIdx end
            end
        end
    end
    return bestB, bestP, bestD2
end

function GrazingRegistry:setActivePasture(barnIdx, pastureIdx)
    local barn = self.barns[barnIdx]
    if barn == nil then return false, "no such barn" end
    if pastureIdx < 1 or pastureIdx > #barn.pastures then
        return false, string.format("pasture index %d out of range (have %d)", pastureIdx, #barn.pastures)
    end
    barn.activeIndex = pastureIdx
    return true, nil
end

function GrazingRegistry:getBarn(idx)
    return self.barns[idx]
end

function GrazingRegistry:getActivePasture(barnIdx)
    local barn = self.barns[barnIdx]
    if barn == nil or barn.activeIndex == 0 then return nil end
    return barn.pastures[barn.activeIndex]
end

function GrazingRegistry:findBarnFor(placeable)
    local ref = self.byPlaceable[placeable]
    if ref == nil or ref.kind ~= "barn" then return nil end
    return ref.barnIdx
end

-- Common field names a placeable / mod stashes its display name under. We
-- overwrite these directly, then fall back to a value-based find-and-replace.
local NAME_KEYS = { "name", "customName", "savedName", "title", "displayName" }

-- Keys we must NEVER overwrite even if their string value currently equals the
-- placeable's reported name — they're internal identifiers, filenames, store /
-- type labels, or localisation keys, not display names. Without this guard the
-- value-based find-and-replace below could rewrite, say, a spec's `typeName`
-- that happens to match the store label, corrupting unrelated gameplay/UI state.
local BLOCKED_NAME_KEYS = {
    typeName = true, className = true, configFileName = true, xmlFilename = true,
    filename = true, saveId = true, storeName = true, imageFilename = true,
    l10nName = true, functionName = true, uniqueId = true, name3D = true,
    typeDescName = true, category = true, categoryName = true,
}

-- Propagate a rename to the placeable's OWN name so RLRM and the vanilla
-- husbandry / map menus show it too ("rename here -> everywhere"). FS25 has no
-- universal placeable-rename API and RLRM keeps its custom name in its own spec
-- field, so we do it generically: capture whatever name the placeable reports
-- right now, then overwrite every well-known name key (on the placeable and each
-- spec_* table) plus any spec string field whose value currently equals that
-- reported name. Whichever field getName() reads from gets rewritten, so
-- getName() returns the new name afterwards. Best-effort and fully guarded — if
-- nothing matches, our own customName still drives our menus.
local function applyGlobalName(placeable, newName)
    if type(placeable) ~= "table" or type(newName) ~= "string" or newName == "" then return end
    -- A real setter wins if the placeable/mod provides one.
    if type(placeable.setName) == "function" then
        pcall(function() placeable:setName(newName) end)
    end
    -- What does it report now? That's the string we hunt for in the spec tables.
    local curName
    if type(placeable.getName) == "function" then
        local ok, n = pcall(function() return placeable:getName() end)
        if ok and type(n) == "string" then curName = n end
    end
    local function overwriteKnownKeys(tbl)
        for _, k in ipairs(NAME_KEYS) do
            if type(tbl[k]) == "string" then tbl[k] = newName end
        end
    end
    overwriteKnownKeys(placeable)
    for k, v in pairs(placeable) do
        if type(k) == "string" and k:sub(1, 5) == "spec_" and type(v) == "table" then
            overwriteKnownKeys(v)
            -- Catch an oddly-named RLRM field by matching the reported name, but
            -- skip internal-identifier keys so we don't corrupt type/store/id
            -- fields that merely happen to equal the current display name.
            if curName ~= nil and curName ~= "" and curName ~= newName then
                for kk, vv in pairs(v) do
                    if type(kk) == "string" and not BLOCKED_NAME_KEYS[kk]
                       and type(vv) == "string" and vv == curName then
                        v[kk] = newName
                    end
                end
            end
        end
    end
end

-- Re-push every registered barn/pasture's custom name onto its placeable. Called
-- after a savegame load so the global (RLRM / vanilla) name is restored from OUR
-- persisted customName even if the host mod didn't persist it itself.
function GrazingRegistry:reapplyGlobalNames()
    for _, barn in pairs(self.barns) do
        if barn ~= nil then
            if type(barn.customName) == "string" and barn.customName ~= "" and barn.placeable ~= nil then
                pcall(applyGlobalName, barn.placeable, barn.customName)
            end
            if type(barn.pastures) == "table" then
                for _, p in ipairs(barn.pastures) do
                    local L = self.land and self.land[p]
                    local cn = L and L.customName
                    if type(cn) == "string" and cn ~= "" and p ~= nil then
                        pcall(applyGlobalName, p, cn)
                    end
                end
            end
        end
    end
end

-- Player-assigned display name for a registered barn/pasture (nil = none).
-- Barns store it on the barn record; pastures store it on the shared per-
-- placeable land record so a shared pasture reads the same name under every barn.
function GrazingRegistry:getCustomName(placeable)
    local ref = placeable ~= nil and self.byPlaceable[placeable]
    if ref == nil then return nil end
    if ref.kind == "barn" then
        local b = self.barns[ref.barnIdx]
        return b and b.customName or nil
    end
    local L = self.land and self.land[placeable]
    return L and L.customName or nil
end

function GrazingRegistry:setCustomName(placeable, name)
    local ref = placeable ~= nil and self.byPlaceable[placeable]
    if ref == nil then return false, "not registered" end
    if type(name) == "string" then
        name = name:gsub("^%s+", ""):gsub("%s+$", "")
        if name == "" then name = nil end
    else
        name = nil
    end
    if ref.kind == "barn" then
        local b = self.barns[ref.barnIdx]
        if b == nil then return false, "no such barn" end
        b.customName = name
    else
        self:_landForPlaceable(placeable).customName = name
    end
    -- Push the name onto the placeable itself so RLRM and the vanilla menus show
    -- it too. Only when a non-empty name was set (clearing the name leaves the
    -- placeable's current global name alone rather than blanking it).
    if type(name) == "string" and name ~= "" then
        pcall(applyGlobalName, placeable, name)
    end
    return true
end

function GrazingRegistry:findPastureRef(placeable)
    local ref = self.byPlaceable[placeable]
    if ref == nil or ref.kind ~= "pasture" then return nil end
    return ref.barnIdx, ref.pastureIdx
end

-- After an owner is removed, re-point byPlaceable[placeable] to a SURVIVING
-- owner (keeps the primary stable-ish) or clear it + drop the shared land record
-- when the last owner is gone. Only meaningful for shared pastures.
function GrazingRegistry:_repointOrDrop(placeable)
    local owners = self:_ownersOf(placeable)
    if #owners == 0 then
        self.byPlaceable[placeable] = nil
        if self.land ~= nil then self.land[placeable] = nil end
    else
        local o = owners[1]
        self.byPlaceable[placeable] = { kind = "pasture", barnIdx = o.barnIdx, pastureIdx = o.pastureIdx }
    end
end

-- Remove one pasture (already located at pIdx) from a single barn, reindexing
-- that barn's pasture props, byPlaceable primary refs, and activeIndex.
function GrazingRegistry:_removePastureAt(barn, barnIdx, pIdx)
    table.remove(barn.pastures, pIdx)
    self:_reindexPasturePropMaps(barn, pIdx)
    -- Fix byPlaceable pastureIdx for THIS barn's later pastures that it's the
    -- primary of (byPlaceable holds the primary owner only).
    for i = pIdx, #barn.pastures do
        local p = barn.pastures[i]
        local r = self.byPlaceable[p]
        if r ~= nil and r.barnIdx == barnIdx then r.pastureIdx = i end
    end
    if barn.activeIndex == pIdx then
        barn.activeIndex = (#barn.pastures > 0) and 1 or 0
    elseif barn.activeIndex > pIdx then
        barn.activeIndex = barn.activeIndex - 1
    end
    if barn.activeIndex > #barn.pastures then
        barn.activeIndex = math.max(0, #barn.pastures)
    end
end

function GrazingRegistry:unregister(placeable)
    local ref = self.byPlaceable[placeable]
    if ref == nil then return false end
    if ref.kind == "barn" then
        local removedIdx = ref.barnIdx
        local barn = self.barns[removedIdx]
        -- Snapshot the barn's pastures before removal so we can repoint any that
        -- are SHARED with another barn (rather than orphaning them).
        local ownedPastures = {}
        if barn ~= nil then
            for _, p in ipairs(barn.pastures) do ownedPastures[#ownedPastures + 1] = p end
        end
        self.byPlaceable[placeable] = nil -- the barn's own ref
        -- Remove DENSELY (not `= nil`) so #self.barns and ipairs stay correct —
        -- a hole makes later barns silently vanish. Then shift every surviving
        -- byPlaceable ref whose barnIdx sat above the removed one.
        table.remove(self.barns, removedIdx)
        for _, r in pairs(self.byPlaceable) do
            if type(r) == "table" and type(r.barnIdx) == "number" and r.barnIdx > removedIdx then
                r.barnIdx = r.barnIdx - 1
            end
        end
        -- Now repoint/drop each pasture the deleted barn owned. Shared pastures
        -- still owned by another barn survive (with a repointed primary); solo
        -- pastures drop and release their land record.
        for _, p in ipairs(ownedPastures) do
            self:_repointOrDrop(p)
        end
        if _G.SprayerHook ~= nil and type(SprayerHook.clearGridsForBarn) == "function" then
            SprayerHook.clearGridsForBarn(removedIdx)
        end
        return true
    end
    -- Pasture PHYSICALLY deleted (sold): remove it from EVERY owning barn.
    local owners = self:_ownersOf(placeable)
    local ownerBarnIdxs = {}
    for _, o in ipairs(owners) do ownerBarnIdxs[o.barnIdx] = true end
    for _, o in ipairs(owners) do
        local barn = self.barns[o.barnIdx]
        if barn ~= nil then
            local pIdx = nil
            for i, p in ipairs(barn.pastures) do
                if p == placeable then pIdx = i; break end
            end
            if pIdx ~= nil then self:_removePastureAt(barn, o.barnIdx, pIdx) end
        end
    end
    self.byPlaceable[placeable] = nil
    if self.land ~= nil then self.land[placeable] = nil end
    for bIdx, _ in pairs(ownerBarnIdxs) do
        if _G.SprayerHook ~= nil and type(SprayerHook.clearGridsForBarn) == "function" then
            SprayerHook.clearGridsForBarn(bIdx)
        end
    end
    return true
end

function GrazingRegistry:iterBarns()
    local i = 0
    local n = #self.barns
    return function()
        while i < n do
            i = i + 1
            if self.barns[i] ~= nil then return i, self.barns[i] end
        end
    end
end

function GrazingRegistry:describe()
    local lines = {}
    for idx, barn in self:iterBarns() do
        local active = barn.activeIndex > 0 and tostring(barn.activeIndex) or "<none>"
        local autoFlag = barn.autoRotate and "autoRotate=ON" or "autoRotate=off"
        lines[#lines + 1] = string.format("barn#%d  (active pasture: %s, %d pasture(s), %s)",
            idx, active, #barn.pastures, autoFlag)
        for pIdx, p in ipairs(barn.pastures) do
            local marker = (pIdx == barn.activeIndex) and "ACTIVE" or "      "
            local path = self:getPath(idx, pIdx)
            local wp = (path and #path > 0) and (#path / 2) or 0
            lines[#lines + 1] = string.format("   %s  pasture#%d  waypoints=%d  %s",
                marker, pIdx, wp, tostring(p))
        end
    end
    if #lines == 0 then lines[1] = "(no barns registered)" end
    return table.concat(lines, "\n")
end
