ContourReader = {}

local function getWorldXYZ(node)
    local x, y, z = getWorldTranslation(node)
    return x, y, z
end

function ContourReader.readNavigationNode(navigationNode)
    if navigationNode == nil or navigationNode == 0 then
        return nil
    end

    local childCount = getNumOfChildren(navigationNode)
    if childCount < 3 then
        PastureHerdingLog.warning("ContourReader: navigationNode %s has only %d children; need >= 3 for a contour. Falling back to AABB of the node itself.",
            tostring(navigationNode), childCount)
        return ContourReader.readAABB(navigationNode)
    end

    local contour = {}
    for i = 0, childCount - 1 do
        local child = getChildAt(navigationNode, i)
        if child ~= nil and child ~= 0 then
            local x, y, z = getWorldXYZ(child)
            contour[#contour + 1] = { x = x, y = y, z = z }
        end
    end

    if #contour < 3 then
        return nil
    end
    return contour
end

function ContourReader.readAABB(node)
    local cx, cy, cz = getWorldXYZ(node)
    local r = 5.0
    return {
        { x = cx - r, y = cy, z = cz - r },
        { x = cx + r, y = cy, z = cz - r },
        { x = cx + r, y = cy, z = cz + r },
        { x = cx - r, y = cy, z = cz + r },
    }
end

function ContourReader.flatten(contour)
    if contour == nil then return nil end
    local flat = {}
    for _, p in ipairs(contour) do
        flat[#flat + 1] = p.x
        flat[#flat + 1] = p.y
        flat[#flat + 1] = p.z
    end
    return flat
end

-- Read the engine-parsed outdoorContourPolygon from a husbandry placeable's spec.
-- Returns a flat {x,y,z, x,y,z, ...} array suitable for ContourBuilder, or nil.
-- We support several shapes the engine might use:
--   * array of {x,y,z}   tables
--   * array of {x,z}     tables
--   * array of vectors with .x .y .z fields
--   * flat array of numbers {x,y,z, x,y,z, ...}
--   * flat array of numbers {x,z, x,z, ...}
-- A samplePlaceable can be provided to look up terrain height when only x/z are present.
-- Walks spec_husbandryFence to extract the LIVE corner-node world positions.
-- These follow the player's customized fence shape, unlike outdoorContourPolygon
-- which is cached at placement time. Returns a flat {x,y,z,...} 3D contour.
function ContourReader.readLiveFenceContour(placeable)
    if placeable == nil then return nil end
    local fenceSpec = placeable.spec_husbandryFence
    if fenceSpec == nil then return nil end
    if getWorldTranslation == nil then return nil end

    -- Try several plausible paths. We've seen the placeable XML define
    -- <fence><sections><section><node node="fence1Node1"/>...></section>...
    -- The engine usually parses these into spec.fence.sections[i].nodes
    -- as a list of resolved scene node IDs.
    local candidates = {
        fenceSpec.fence and fenceSpec.fence.sections,
        fenceSpec.fenceSegmentsData and fenceSpec.fenceSegmentsData.sections,
        fenceSpec.sections,
    }
    for _, sections in ipairs(candidates) do
        if type(sections) == "table" then
            local nodes = {}
            for _, section in pairs(sections) do
                local sectionNodes = section and (section.nodes or section.cornerNodes or section.nodeList)
                if type(sectionNodes) == "table" then
                    for _, n in ipairs(sectionNodes) do
                        local node = (type(n) == "number") and n or (n and n.node)
                        if type(node) == "number" and node ~= 0 then
                            nodes[#nodes + 1] = node
                        end
                    end
                end
            end
            if #nodes >= 3 then
                local terrainId = g_currentMission and g_currentMission.terrainRootNode or 0
                local flat = {}
                for _, nd in ipairs(nodes) do
                    local x, _, z = getWorldTranslation(nd)
                    local y = 0
                    if getTerrainHeightAtWorldPos ~= nil then
                        y = getTerrainHeightAtWorldPos(terrainId, x, 0, z) or 0
                    end
                    flat[#flat + 1] = x
                    flat[#flat + 1] = y
                    flat[#flat + 1] = z
                end
                PastureHerdingLog.dev("ContourReader: read %d-point LIVE fence contour from spec_husbandryFence",
                    #nodes)
                return flat
            end
        end
    end
    return nil
end

function ContourReader.readOutdoorContourPolygon(placeable)
    if placeable == nil then return nil, "no placeable" end
    -- Preferred path: live fence node positions (reflect any player
    -- customisation of the fence shape after placement).
    local live = ContourReader.readLiveFenceContour(placeable)
    if live ~= nil and #live >= 9 then
        return live
    end
    -- Fallback: the cached outdoor contour polygon that the placeable
    -- computes at placement time. Doesn't update if the player reshapes.
    local spec = placeable.spec_husbandryAnimals
    if spec == nil then return nil, "no spec_husbandryAnimals" end
    local poly = spec.outdoorContourPolygon
    if poly == nil then return nil, "no outdoorContourPolygon on spec" end
    if type(poly) ~= "table" then return nil, "outdoorContourPolygon is not a table" end

    local n = #poly
    if n == 0 then
        -- Maybe keyed — log the actual keys so we can discover the shape
        local keys = {}
        local total = 0
        for k, v in pairs(poly) do
            total = total + 1
            keys[#keys + 1] = string.format("%s(%s)", tostring(k), type(v))
            if total >= 12 then break end
        end
        PastureHerdingLog.dev("ContourReader: outdoorContourPolygon is keyed (not array). %d keys, first: %s",
            total, table.concat(keys, ", "))
        -- Try known-likely nested fields (don't use ipairs — first nil would stop iteration)
        local altNames = { "vertices", "points", "contour", "polygon", "nodes" }
        for _, altName in ipairs(altNames) do
            local alt = poly[altName]
            if type(alt) == "table" and #alt > 0 then
                PastureHerdingLog.dev("ContourReader: recursing into outdoorContourPolygon.%s (length %d)",
                    altName, #alt)
                return ContourReader.readContourArray(alt)
            end
        end
        return nil, string.format("outdoorContourPolygon is empty/keyed (keys: %s)", table.concat(keys, ", "))
    end

    return ContourReader.readContourArray(poly)
end

function ContourReader.readContourArray(poly)
    if type(poly) ~= "table" then return nil, "not a table" end
    local n = #poly
    if n == 0 then return nil, "array length 0" end

    local function pushXZ(out, x, z, y)
        if y == nil and getTerrainHeightAtWorldPos ~= nil and g_currentMission ~= nil then
            y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode or 0, x, 0, z)
        end
        out[#out + 1] = x
        out[#out + 1] = y or 0
        out[#out + 1] = z
    end

    local flat = {}
    local first = poly[1]
    if type(first) == "number" then
        -- Flat array of numbers — could be 2D (x,z pairs) or 3D (x,y,z triples).
        -- Prefer 2D when both fit, because FS25 pasture polygons (e.g.
        -- outdoorContourPolygon.vertices) are flat (x,z) pairs and we want
        -- the engine to sample terrain Y itself rather than us guessing.
        if (n % 2) == 0 then
            for i = 1, n, 2 do pushXZ(flat, poly[i], poly[i + 1]) end
        elseif (n % 3) == 0 then
            for i = 1, n, 3 do
                flat[#flat + 1] = poly[i]
                flat[#flat + 1] = poly[i + 1]
                flat[#flat + 1] = poly[i + 2]
            end
        else
            return nil, string.format("flat polygon has odd length %d (neither 2D nor 3D-aligned)", n)
        end
    elseif type(first) == "table" then
        for _, p in ipairs(poly) do
            local x = p.x or p[1]
            local y = p.y or p[2]
            local z = p.z or p[3]
            -- if y appears to be z (2D table), shift
            if z == nil and y ~= nil then
                pushXZ(flat, x, y)  -- treat the 2nd field as z
            else
                pushXZ(flat, x, z, y)
            end
        end
    else
        return nil, string.format("unrecognized polygon element type: %s", type(first))
    end

    if #flat < 9 then return nil, string.format("polygon too small after parse: %d numbers", #flat) end
    return flat, nil
end

function ContourReader.describe(contour)
    if contour == nil then return "<nil>" end
    local parts = {}
    for _, p in ipairs(contour) do
        parts[#parts + 1] = string.format("(%.1f,%.1f,%.1f)", p.x, p.y, p.z)
    end
    return string.format("%d pts: %s", #contour, table.concat(parts, " "))
end
