using System.Runtime.Versioning;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using Kompas6Constants3D;
using KompasAPI7;

namespace Kompas.Mcp.Core.Editing;

/// <summary>
/// Операции над телами импортированной/построенной геометрии: рассечение плоскостью
/// (<c>SplitSolids</c>), перемещение тела (<c>BodyRepositions</c>), объединение (<c>Booleans</c>).
/// Контейнеры операций берутся приведением детали (COM-QI): <c>IModelContainer</c>/<c>IAuxiliaryGeomContainer</c>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class SolidEditService
{
    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

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

    /// <summary>
    /// Рассечь тело активной детали смещённой плоскостью (база <paramref name="basePlane"/> + <paramref name="offset"/> мм).
    /// Возвращает габариты получившихся тел (с индексами) — по ним выбирают тело для <see cref="MoveBodyAsync"/>.
    /// </summary>
    public Task<IReadOnlyList<BodyBox>> SplitByPlaneAsync(
        BasePlane basePlane, double offset, bool direction = true, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync<IReadOnlyList<BodyBox>>(() =>
        {
            var (doc3d, top) = ActivePart();
            var ag = top as IAuxiliaryGeomContainer
                ?? throw new InvalidOperationException("Деталь не приводится к IAuxiliaryGeomContainer.");
            var plane = (IPlane3DByOffset)ag.Planes3D.Add(ksObj3dTypeEnum.o3d_planeOffset);
            plane.BasePlane = top.DefaultObject[PlaneType(basePlane)];
            plane.Offset = offset;
            plane.Direction = direction;
            if (!plane.Update())
                throw new InvalidOperationException("Не удалось построить секущую плоскость.");

            var mc = ModelContainer(top);
            var ss = mc.SplitSolids.Add();
            ss.CutObjects = plane;
            // Имя ставим до Update(): иначе в дереве останется системное.
            if (Modeling.FeatureName.Normalize(name) is { } title && ss is IModelObject named)
                named.Name = title;
            if (!ss.Update())
                throw new InvalidOperationException("SplitSolid.Update вернул FALSE (рассечение не удалось).");
            doc3d.RebuildDocument();
            return BodyBoxes(top);
        }, ct);

    /// <summary>Переместить тело (по индексу из <see cref="SplitByPlaneAsync"/>) на вектор (dx, dy, dz) мм.</summary>
    public Task MoveBodyAsync(int bodyIndex, double dx = 0, double dy = 0, double dz = 0, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var (doc3d, top) = ActivePart();
            var bodies = ResultBodies(top);
            if (bodyIndex < 0 || bodyIndex >= bodies.Count)
                throw new ArgumentOutOfRangeException(nameof(bodyIndex),
                    $"Индекс тела вне диапазона [0; {bodies.Count - 1}].");

            var mc = ModelContainer(top);
            var br = mc.BodyRepositions.Add();
            br.RepositionBody = (IKompasAPIObject)bodies[bodyIndex];
            var pos = br.Position;
            pos.SetStartingOrientation();
            pos.X = dx; pos.Y = dy; pos.Z = dz; // смещение начала ЛСК тела = перенос тела
            // Имя ставим до Update(): иначе в дереве останется системное.
            if (Modeling.FeatureName.Normalize(name) is { } title && br is IModelObject named)
                named.Name = title;
            if (!br.Update())
                throw new InvalidOperationException("BodyReposition.Update вернул FALSE.");
            doc3d.RebuildDocument();
        }, ct);

    /// <summary>Объединить все тела активной детали в одно (булева операция «объединение»).</summary>
    public Task UnionAllAsync(string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var (doc3d, top) = ActivePart();
            var bodies = ResultBodies(top);
            if (bodies.Count < 2) return; // объединять нечего

            var mc = ModelContainer(top);
            var bln = mc.Booleans.Add();
            bln.BaseObject = (IKompasAPIObject)bodies[0];
            bln.ModifyObjects = bodies.Skip(1).Cast<object>().ToArray();
            bln.BooleanType = ksBooleanType.ksUnion;
            // Имя ставим до Update(): иначе в дереве останется системное.
            if (Modeling.FeatureName.Normalize(name) is { } title && bln is IModelObject named)
                named.Name = title;
            if (!bln.Update())
                throw new InvalidOperationException("Boolean union.Update вернул FALSE.");
            doc3d.RebuildDocument();
        }, ct);

    // ---- внутреннее --------------------------------------------------------

    private (IKompasDocument3D doc, IPart7 top) ActivePart()
    {
        if (_session.Application.ActiveDocument is not IKompasDocument3D doc3d)
            throw new InvalidOperationException("Активный документ не является 3D-документом.");
        if (doc3d.TopPart is not IPart7 top)
            throw new InvalidOperationException("Не удалось получить TopPart активного документа.");
        return (doc3d, top);
    }

    private static IModelContainer ModelContainer(IPart7 top)
        => top as IModelContainer
           ?? throw new InvalidOperationException("Деталь не приводится к IModelContainer.");

    private static ksObj3dTypeEnum PlaneType(BasePlane p) => p switch
    {
        BasePlane.XOY => ksObj3dTypeEnum.o3d_planeXOY,
        BasePlane.XOZ => ksObj3dTypeEnum.o3d_planeXOZ,
        BasePlane.YOZ => ksObj3dTypeEnum.o3d_planeYOZ,
        _ => throw new ArgumentOutOfRangeException(nameof(p), p, "Неизвестная базовая плоскость."),
    };

    private static List<IBody7> ResultBodies(IPart7 top)
    {
        var list = new List<IBody7>();
        var rb = ((IFeature7)top).ResultBodies;
        if (rb is object[] arr) foreach (var o in arr) { if (o is IBody7 b) list.Add(b); }
        else if (rb is System.Collections.IEnumerable en) foreach (var o in en) { if (o is IBody7 b) list.Add(b); }
        return list;
    }

    private static IReadOnlyList<BodyBox> BodyBoxes(IPart7 top)
    {
        var bodies = ResultBodies(top);
        var list = new List<BodyBox>(bodies.Count);
        for (int i = 0; i < bodies.Count; i++)
        {
            double x1 = 0, y1 = 0, z1 = 0, x2 = 0, y2 = 0, z2 = 0;
            try { bodies[i].GetGabarit(out x1, out y1, out z1, out x2, out y2, out z2); } catch { /* ignore */ }
            list.Add(new BodyBox { Index = i, MinX = x1, MinY = y1, MinZ = z1, MaxX = x2, MaxY = y2, MaxZ = z2 });
        }
        return list;
    }
}
