namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;

/// <summary>
///     Minimal view over an ADS symbol that <see cref="TwinCATSymbolExpander"/> needs to walk a
///     struct/UDT/FB instance down to its addressable atomic leaves. An adapter in
///     <c>AdsTwinCATClient.BrowseSymbolsAsync</c> wraps the real <c>TwinCAT.TypeSystem.ISymbol</c>
///     onto this interface so the recursion is unit-testable without a live ADS target — the real
///     <c>ISymbol</c> / <c>SymbolLoaderFactory</c> surface is non-injectable.
/// </summary>
public interface ITwinCATSymbolNode
{
    /// <summary>Full dotted instance path of this symbol (e.g. <c>MAIN.Motor1.Speed</c>).</summary>
    string InstancePath { get; }

    /// <summary>
    ///     <c>true</c> when this symbol is a struct/UDT/FB instance (<c>DataType.Category == Struct</c>)
    ///     and therefore has addressable members under <see cref="Children"/> rather than a value of
    ///     its own.
    /// </summary>
    bool IsStruct { get; }

    /// <summary>
    ///     The symbol mapped onto the driver's atomic type surface plus an optional 1-D array length.
    ///     A <c>null</c> <c>Type</c> means an unsupported atomic leaf (pointer / reference / out-of-scope
    ///     type) which the expander drops.
    /// </summary>
    (TwinCATDataType? Type, int? ArrayLength) Mapped { get; }

    /// <summary>Struct members (empty for atomic leaves).</summary>
    IReadOnlyList<ITwinCATSymbolNode> Children { get; }

    /// <summary><c>true</c> when the symbol's access rights forbid writes.</summary>
    bool ReadOnly { get; }
}

/// <summary>
///     Pure, dependency-free walker that flattens a forest of <see cref="ITwinCATSymbolNode"/> roots
///     into the addressable atomic leaves the driver can surface as OPC UA variable nodes. A struct
///     node recurses into its <see cref="ITwinCATSymbolNode.Children"/> (depth-guarded); an atomic
///     leaf with a non-null mapped <see cref="TwinCATDataType"/> is yielded as a
///     <see cref="TwinCATDiscoveredSymbol"/>; unsupported leaves (null mapped type) and
///     depth-exceeded nodes are dropped. This is the §2 fix that makes discovered struct/UDT/FB
///     members individually addressable.
/// </summary>
public static class TwinCATSymbolExpander
{
    /// <summary>
    ///     Maximum struct-nesting depth the walker descends before dropping a node. Guards against a
    ///     pathological / self-referential type graph turning discovery into an unbounded walk. Deep
    ///     nesting beyond this is exceedingly rare in real PLC code; the operator can still predeclare
    ///     a member-path tag directly for anything past the floor.
    /// </summary>
    public const int MaxDepth = 8;

    /// <summary>
    ///     Walk <paramref name="roots"/> to their atomic leaves. A struct node recurses its children
    ///     (depth-guarded by <see cref="MaxDepth"/>); an atomic leaf with a non-null mapped
    ///     <see cref="TwinCATDataType"/> is yielded; unsupported leaves (null type) and depth-exceeded
    ///     nodes are dropped. De-duplicates by <see cref="ITwinCATSymbolNode.InstancePath"/> so any
    ///     Flat-vs-tree double-listing from the underlying loader collapses to a single emission.
    /// </summary>
    /// <param name="roots">The top-level symbols (e.g. <c>loader.Symbols</c>) to expand.</param>
    /// <returns>One <see cref="TwinCATDiscoveredSymbol"/> per addressable atomic leaf.</returns>
    public static IEnumerable<TwinCATDiscoveredSymbol> ExpandLeaves(IEnumerable<ITwinCATSymbolNode> roots)
    {
        var seen = new HashSet<string>(StringComparer.Ordinal);
        foreach (var root in roots)
            foreach (var leaf in Walk(root, depth: 0, seen))
                yield return leaf;
    }

    private static IEnumerable<TwinCATDiscoveredSymbol> Walk(
        ITwinCATSymbolNode node, int depth, HashSet<string> seen)
    {
        // Depth guard: drop a node whose nesting exceeds the floor rather than recursing further.
        if (depth >= MaxDepth) yield break;

        if (node.IsStruct)
        {
            // A struct contributes no value of its own — recurse its members. (Empty children =>
            // nothing emitted, which is the correct "no addressable atomic" outcome.)
            foreach (var child in node.Children)
                foreach (var leaf in Walk(child, depth + 1, seen))
                    yield return leaf;
            yield break;
        }

        var (type, arrayLength) = node.Mapped;
        if (type is null) yield break; // unsupported atomic leaf (pointer / out-of-scope type) — drop.

        // Dedup by InstancePath — neutralizes any Flat-vs-tree double-listing of the same leaf.
        if (!seen.Add(node.InstancePath)) yield break;

        yield return new TwinCATDiscoveredSymbol(node.InstancePath, type, node.ReadOnly, arrayLength);
    }
}
