using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;

namespace Kompas.Mcp.Core.Query;

/// <summary>Запросы к активной 3D-модели: массово-центровочные характеристики.</summary>
[SupportedOSPlatform("windows")]
public sealed class QueryService
{
    // bitVector для CalcMassInertiaProperties: ST_MIX_MM (0x1, длина — мм) | ST_MIX_KG (0x10, масса — кг).
    // Иначе КОМПАС вернёт значения в единицах по умолчанию (см/г).
    private const int MixMmKg = 0x1 | 0x10;

    // ST_MIX_MM (0x1) — площади граней / длины рёбер в мм.
    private const uint MixMm = 0x1;

    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

    public QueryService(KompasSession session, KompasDispatcher dispatcher)
    {
        _session = session ?? throw new ArgumentNullException(nameof(session));
        _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
    }

    /// <summary>Рассчитать и вернуть МЦХ активной детали (объём, масса, площадь, центр масс).</summary>
    /// <remarks>
    /// Используется API5 <c>ksPart.CalcMassInertiaProperties</c> — он считает МЦХ «по запросу»
    /// и согласован с построением детали через API5. Путь API7 <c>IMassInertiaParam7.Calculate()</c>
    /// здесь непригоден: его флаг <c>Actual</c> «залипает» после первого расчёта и не сбрасывается
    /// при изменении геометрии через API5 (повторный запрос отдавал бы устаревшее значение).
    /// </remarks>
    public Task<PartInfo> GetPartInfoAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            ksMassInertiaParam? mass = null;
            try
            {
                mass = part.CalcMassInertiaProperties(MixMmKg) as ksMassInertiaParam
                    ?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null (модель пуста или не перестроена?).");

                return new PartInfo
                {
                    Volume = mass.v,   // объём, мм³
                    Mass = mass.m,     // масса, кг
                    Area = mass.F,     // площадь, мм²
                    CenterX = mass.xc, // центр масс в глобальной СК, мм
                    CenterY = mass.yc,
                    CenterZ = mass.zc,
                };
            }
            finally
            {
                ComHelper.Release(mass);
                ComHelper.Release(part);
            }
        }, ct);

    /// <summary>
    /// Перечислить компоненты верхнего уровня активной сборки (детали/подсборки): имя,
    /// обозначение, файл, признак детали и габарит. Обход через API7 <c>TopPart → Parts</c>.
    /// </summary>
    public Task<IReadOnlyList<ComponentInfo>> ListComponentsAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync<IReadOnlyList<ComponentInfo>>(() =>
        {
            if (_session.Application.ActiveDocument is not KompasAPI7.IKompasDocument3D doc3d)
                throw new InvalidOperationException("Активный документ не является 3D-документом.");
            if (doc3d.TopPart is not KompasAPI7.IPart7 top)
                throw new InvalidOperationException("Не удалось получить TopPart активного документа.");

            var parts = top.Parts;
            int count = parts.Count;
            var list = new List<ComponentInfo>(count);
            for (int i = 0; i < count; i++)
            {
                if (parts.Part[i] is not KompasAPI7.IPart7 p) continue;
                double x1 = 0, y1 = 0, z1 = 0, x2 = 0, y2 = 0, z2 = 0;
                bool gab = false;
                try { gab = p.GetGabarit(false, false, out x1, out y1, out z1, out x2, out y2, out z2); }
                catch { /* габарит может быть недоступен у отдельных компонентов */ }

                // Inline safe-геттеры: избегаем лямбда-аллокаций в горячем цикле по компонентам.
                string name = "", marking = "", fileName = "";
                bool isDetail = false;
                try { name = p.Name ?? ""; } catch { }
                try { marking = p.Marking ?? ""; } catch { }
                try { fileName = p.FileName ?? ""; } catch { }
                try { isDetail = p.Detail; } catch { }

                list.Add(new ComponentInfo
                {
                    Index = i,
                    Name = name,
                    Marking = marking,
                    FileName = fileName,
                    IsDetail = isDetail,
                    SizeX = gab ? Math.Abs(x2 - x1) : 0,
                    SizeY = gab ? Math.Abs(y2 - y1) : 0,
                    SizeZ = gab ? Math.Abs(z2 - z1) : 0,
                });
            }
            return list;
        }, ct);

    /// <summary>Перечислить грани активной детали: индекс, тип поверхности и площадь (мм²).</summary>
    public Task<IReadOnlyList<FaceInfo>> ListFacesAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync<IReadOnlyList<FaceInfo>>(() =>
        {
            var part = GetTopPart();
            try
            {
                var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
                    ?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");

                var count = faces.GetCount();
                var list = new List<FaceInfo>(count);
                for (int i = 0; i < count; i++)
                {
                    if (faces.GetByIndex(i) is not ksEntity entity) continue;
                    if (entity.GetDefinition() is not ksFaceDefinition def) continue;
                    list.Add(new FaceInfo
                    {
                        Index = i,
                        Type = GeomClassifiers.FaceType(def),
                        Area = def.GetArea(MixMm),
                    });
                }
                return list;
            }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>Перечислить рёбра активной детали: индекс, тип кривой и длину (мм).</summary>
    public Task<IReadOnlyList<EdgeInfo>> ListEdgesAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync<IReadOnlyList<EdgeInfo>>(() =>
        {
            var part = GetTopPart();
            try
            {
                var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection
                    ?? throw new InvalidOperationException("Не удалось получить коллекцию рёбер.");

                var count = edges.GetCount();
                var list = new List<EdgeInfo>(count);
                for (int i = 0; i < count; i++)
                {
                    if (edges.GetByIndex(i) is not ksEntity entity) continue;
                    if (entity.GetDefinition() is not ksEdgeDefinition def) continue;
                    list.Add(new EdgeInfo
                    {
                        Index = i,
                        Type = GeomClassifiers.EdgeType(def),
                        Length = def.GetLength(MixMm),
                    });
                }
                return list;
            }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>Габаритный параллелепипед активной детали (по телам), мм.</summary>
    public Task<BoundingBox> GetBoundingBoxAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try
            {
                // full=false → только тела (без вспомогательной геометрии); customizable игнорируется.
                if (!part.GetGabarit(false, false,
                        out double x1, out double y1, out double z1,
                        out double x2, out double y2, out double z2))
                    throw new InvalidOperationException("GetGabarit вернул FALSE (модель пуста?).");

                return new BoundingBox
                {
                    MinX = Math.Min(x1, x2), MinY = Math.Min(y1, y2), MinZ = Math.Min(z1, z2),
                    MaxX = Math.Max(x1, x2), MaxY = Math.Max(y1, y2), MaxZ = Math.Max(z1, z2),
                };
            }
            finally { ComHelper.Release(part); }
        }, ct);

    // ── низкоуровневые помощники ──────────────────────────────────────────

    /// <summary>
    /// Получить вершинный компонент активной детали. Освобождает RCW doc3d сразу после
    /// GetPart — ksPart имеет собственный COM-счётчик ссылок.
    /// </summary>
    private ksPart GetTopPart()
    {
        var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
            ?? throw new InvalidOperationException("Нет активного 3D-документа.");
        try
        {
            return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
                ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
        }
        finally { ComHelper.Release(doc3d); }
    }
}
