Editor/WeaponImporter/Core/Import/FbxLayer.cs
#nullable enable annotations

using WeaponImporter.Core.Formats.Fbx;

namespace WeaponImporter.Core.Import;

/// <summary>
/// One FBX geometry layer element (LayerElementNormal / LayerElementUV / ...): mapping
/// (ByPolygonVertex, ByVertice/ByControlPoint, ByPolygon, AllSame) and reference
/// (Direct, IndexToDirect) resolved to a value per corner.
/// </summary>
internal sealed class FbxLayer
{
    private readonly string _mapping;
    private readonly double[] _values;
    private readonly int[]? _index;
    private readonly int _stride;

    private FbxLayer(string mapping, double[] values, int[]? index, int stride)
    {
        _mapping = mapping;
        _values = values;
        _index = index;
        _stride = stride;
    }

    /// <summary>The first (lowest layer number) element named <paramref name="element"/>, or null.</summary>
    public static FbxLayer? Read(FbxNode geometry, string element, string valuesName, string[] indexNames, int stride)
    {
        var node = First(geometry, element);
        if (node is null)
            return null;
        var values = node.Child(valuesName)?.AsDoubleArray(0);
        if (values is null || values.Length < stride)
            return null;
        var mapping = StringOf(node, "MappingInformationType") ?? "ByPolygonVertex";
        var reference = StringOf(node, "ReferenceInformationType") ?? "Direct";
        int[]? index = null;
        if (reference is "IndexToDirect" or "Index")
        {
            foreach (var name in indexNames)
                if (node.Child(name) is { } ix)
                {
                    index = ix.AsIntArray(0);
                    break;
                }
            if (index is null)
                return null;
        }
        return new FbxLayer(mapping, values, index, stride);
    }

    /// <summary>Value slice for a corner, or null when the layer does not cover it.</summary>
    public double[]? Get(int polygonVertex, int controlPoint, int polygon)
    {
        var i = _mapping switch
        {
            "ByPolygonVertex" => polygonVertex,
            "ByVertice" or "ByVertex" or "ByControlPoint" => controlPoint,
            "ByPolygon" => polygon,
            "AllSame" => 0,
            _ => polygonVertex,
        };
        if (_index is not null)
        {
            if ((uint)i >= (uint)_index.Length)
                return null;
            i = _index[i];
        }
        if (i < 0 || (long)(i + 1) * _stride > _values.Length)
            return null;
        var result = new double[_stride];
        Array.Copy(_values, i * _stride, result, 0, _stride);
        foreach (var d in result)
            if (!double.IsFinite(d))
                return null;
        return result;
    }

    /// <summary>LayerElementMaterial: per-polygon index into the model's connected materials.</summary>
    public static MaterialLayer? ReadMaterials(FbxNode geometry)
    {
        var node = First(geometry, "LayerElementMaterial");
        var values = node?.Child("Materials")?.AsIntArray(0);
        if (node is null || values is null || values.Length == 0)
            return null;
        return new MaterialLayer(StringOf(node, "MappingInformationType") ?? "AllSame", values);
    }

    internal sealed class MaterialLayer
    {
        private readonly bool _allSame;
        private readonly int[] _values;

        public MaterialLayer(string mapping, int[] values)
        {
            _allSame = mapping == "AllSame";
            _values = values;
        }

        public int Get(int polygon) => _allSame ? _values[0] : (uint)polygon < (uint)_values.Length ? _values[polygon] : _values[0];
    }

    private static FbxNode? First(FbxNode geometry, string element)
    {
        FbxNode? best = null;
        var bestLayer = long.MaxValue;
        foreach (var n in geometry.ChildrenNamed(element))
        {
            long layer = 0;
            if (n.Properties.Count > 0 && n.Properties[0] is int or long or short)
                layer = n.Prop<long>(0);
            if (layer < bestLayer)
            {
                best = n;
                bestLayer = layer;
            }
        }
        return best;
    }

    private static string? StringOf(FbxNode node, string name)
        => node.Child(name) is { Properties.Count: > 0 } c && c.Properties[0] is string s ? s.Trim() : null;
}