using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using Microsoft.Win32;

var options = CliOptions.Parse(args);
if (!options.HasReferenceCenter && (string.IsNullOrWhiteSpace(options.ReferenceDwgPath) || !File.Exists(options.ReferenceDwgPath)))
    throw new FileNotFoundException($"参考 DWG 不存在: {options.ReferenceDwgPath}");
if (string.IsNullOrWhiteSpace(options.StudentDwgPath) || !File.Exists(options.StudentDwgPath))
    throw new FileNotFoundException($"学生 DWG 不存在: {options.StudentDwgPath}");
if (string.IsNullOrWhiteSpace(options.OutputDwgPath))
    throw new ArgumentException("缺少 --output-dwg。为避免破坏原始学生图，必须输出到副本。");

Directory.CreateDirectory(Path.GetDirectoryName(options.OutputDwgPath) ?? Environment.CurrentDirectory);
var result = StaRunner.Run(() => DwgSheetFrameAligner.Align(options));
var reportPath = Path.Combine(
    Path.GetDirectoryName(options.OutputDwgPath) ?? Environment.CurrentDirectory,
    Path.GetFileNameWithoutExtension(options.OutputDwgPath) + ".align-report.json");
File.WriteAllText(reportPath, JsonSerializer.Serialize(result, JsonDefaults.Options));
Console.WriteLine(JsonSerializer.Serialize(result, JsonDefaults.Options));

sealed class DwgSheetFrameAligner
{
    const double AxisTolerance = 0.05;
    const double FrameTolerance = 1.0;

    [DllImport("ole32.dll", CharSet = CharSet.Unicode)]
    private static extern int CLSIDFromProgID(string progId, out Guid clsid);

    [DllImport("oleaut32.dll", PreserveSig = false)]
    [return: MarshalAs(UnmanagedType.IUnknown)]
    private static extern object GetActiveObject(ref Guid clsid, IntPtr reserved);

    public static AlignResult Align(CliOptions options)
    {
        var started = DateTimeOffset.Now;
        var app = ConnectOrCreate(options.ProgId, options.Visible, out _);
        object? referenceDoc = null;
        object? studentDoc = null;
        try
        {
            FrameRect referenceFrame;
            if (options.HasReferenceCenter)
            {
                referenceFrame = FrameRect.FromCenter(options.ReferenceCenterX!.Value, options.ReferenceCenterY!.Value);
            }
            else
            {
                WaitForQuiescent(app, TimeSpan.FromSeconds(30));
                referenceDoc = OpenDocument(app, options.ReferenceDwgPath, readOnly: true);
                referenceFrame = DetectInnerFrame(referenceDoc);
                TryInvoke(referenceDoc, "Close", [false]);
                ReleaseCom(referenceDoc);
                referenceDoc = null;
            }

            File.Copy(options.StudentDwgPath, options.OutputDwgPath, overwrite: true);
            WaitForQuiescent(app, TimeSpan.FromSeconds(30));
            studentDoc = OpenDocument(app, options.OutputDwgPath, readOnly: false);
            var studentFrameBefore = DetectInnerFrame(studentDoc);

            var dx = referenceFrame.CenterX - studentFrameBefore.CenterX;
            var dy = referenceFrame.CenterY - studentFrameBefore.CenterY;
            var moved = MoveAllEntities(studentDoc, dx, dy);
            TryInvoke(studentDoc, "Save", []);

            var studentFrameAfter = DetectInnerFrame(studentDoc);
            if (options.CloseAfterAlign)
                TryInvoke(studentDoc, "Close", [false]);

            return new AlignResult
            {
                Ok = true,
                StartedAt = started,
                CompletedAt = DateTimeOffset.Now,
                ReferenceDwgPath = options.ReferenceDwgPath,
                StudentDwgPath = options.StudentDwgPath,
                OutputDwgPath = options.OutputDwgPath,
                ReferenceInnerFrame = referenceFrame,
                StudentInnerFrameBefore = studentFrameBefore,
                StudentInnerFrameAfter = studentFrameAfter,
                DeltaX = dx,
                DeltaY = dy,
                MovedEntityCount = moved,
                Message = "已按图框内圈中心整体平移学生 DWG 副本。"
            };
        }
        finally
        {
            if (referenceDoc != null)
            {
                TryInvoke(referenceDoc, "Close", [false]);
                ReleaseCom(referenceDoc);
            }
            if (studentDoc != null)
                ReleaseCom(studentDoc);
            ReleaseCom(app);
        }
    }

    static FrameRect DetectInnerFrame(object doc)
    {
        var lines = new List<Line2d>();
        lines.AddRange(ReadLines(doc, "ModelSpace"));
        lines.AddRange(ReadLines(doc, "PaperSpace"));
        if (lines.Count == 0)
            throw new InvalidOperationException("DWG 中没有可用于识别图框的直线。");

        var horizontal = lines
            .Where(line => Math.Abs(line.Y1 - line.Y2) <= AxisTolerance)
            .Select(line => line.Normalized())
            .ToList();
        var vertical = lines
            .Where(line => Math.Abs(line.X1 - line.X2) <= AxisTolerance)
            .Select(line => line.Normalized())
            .ToList();

        var maxHorizontal = horizontal.Select(line => line.Length).DefaultIfEmpty(0).Max();
        var maxVertical = vertical.Select(line => line.Length).DefaultIfEmpty(0).Max();
        var longHorizontal = horizontal
            .Where(line => line.Length >= maxHorizontal * 0.65)
            .OrderByDescending(line => line.Length)
            .Take(20)
            .ToList();
        var longVertical = vertical
            .Where(line => line.Length >= maxVertical * 0.65)
            .OrderByDescending(line => line.Length)
            .Take(20)
            .ToList();

        var rectangles = new List<FrameRect>();
        for (var hi = 0; hi < longHorizontal.Count; hi++)
        {
            for (var hj = hi + 1; hj < longHorizontal.Count; hj++)
            {
                var bottom = longHorizontal[hi].Y1 <= longHorizontal[hj].Y1 ? longHorizontal[hi] : longHorizontal[hj];
                var top = ReferenceEquals(bottom, longHorizontal[hi]) ? longHorizontal[hj] : longHorizontal[hi];
                for (var vi = 0; vi < longVertical.Count; vi++)
                {
                    for (var vj = vi + 1; vj < longVertical.Count; vj++)
                    {
                        var left = longVertical[vi].X1 <= longVertical[vj].X1 ? longVertical[vi] : longVertical[vj];
                        var right = ReferenceEquals(left, longVertical[vi]) ? longVertical[vj] : longVertical[vi];
                        if (!SpansX(bottom, left.X1, right.X1) || !SpansX(top, left.X1, right.X1))
                            continue;
                        if (!SpansY(left, bottom.Y1, top.Y1) || !SpansY(right, bottom.Y1, top.Y1))
                            continue;

                        rectangles.Add(new FrameRect
                        {
                            MinX = left.X1,
                            MinY = bottom.Y1,
                            MaxX = right.X1,
                            MaxY = top.Y1,
                            SourceHandles =
                            [
                                bottom.Handle,
                                top.Handle,
                                left.Handle,
                                right.Handle
                            ]
                        });
                    }
                }
            }
        }

        var unique = rectangles
            .GroupBy(rect => $"{Math.Round(rect.MinX, 3)}|{Math.Round(rect.MinY, 3)}|{Math.Round(rect.MaxX, 3)}|{Math.Round(rect.MaxY, 3)}")
            .Select(group => group.First())
            .Where(rect => rect.Width > 0 && rect.Height > 0)
            .OrderByDescending(rect => rect.Area)
            .ToList();

        if (unique.Count == 0)
            throw new InvalidOperationException("未能识别图框矩形。");

        // 工程图一般有外框和内框；面积最大的是外框，第二大的是内框。
        return unique.Count >= 2 ? unique[1] : unique[0];
    }

    static bool SpansX(Line2d line, double left, double right) =>
        line.MinX <= left + FrameTolerance && line.MaxX >= right - FrameTolerance;

    static bool SpansY(Line2d line, double bottom, double top) =>
        line.MinY <= bottom + FrameTolerance && line.MaxY >= top - FrameTolerance;

    static List<Line2d> ReadLines(object doc, string spaceName)
    {
        var result = new List<Line2d>();
        var space = GetProperty(doc, spaceName);
        if (space == null)
            return result;

        try
        {
            var count = ToInt(GetProperty(space, "Count")) ?? 0;
            for (var i = 0; i < count; i++)
            {
                var entity = GetCollectionItem(space, i);
                if (entity == null)
                    continue;
                try
                {
                    var objectName = ReadString(entity, "ObjectName");
                    if (!string.Equals(objectName, "AcDbLine", StringComparison.OrdinalIgnoreCase))
                        continue;

                    var start = DoubleList(GetProperty(entity, "StartPoint"));
                    var end = DoubleList(GetProperty(entity, "EndPoint"));
                    if (start.Count < 2 || end.Count < 2)
                        continue;

                    result.Add(new Line2d(
                        start[0],
                        start[1],
                        end[0],
                        end[1],
                        ReadString(entity, "Handle"),
                        spaceName));
                }
                finally
                {
                    ReleaseCom(entity);
                }
            }
        }
        finally
        {
            ReleaseCom(space);
        }

        return result;
    }

    static int MoveAllEntities(object doc, double dx, double dy)
    {
        var moved = 0;
        moved += MoveSpaceEntities(doc, "ModelSpace", dx, dy);
        moved += MoveSpaceEntities(doc, "PaperSpace", dx, dy);
        return moved;
    }

    static int MoveSpaceEntities(object doc, string spaceName, double dx, double dy)
    {
        var moved = 0;
        var space = GetProperty(doc, spaceName);
        if (space == null)
            return moved;

        try
        {
            var count = ToInt(GetProperty(space, "Count")) ?? 0;
            var from = new double[] { 0, 0, 0 };
            var to = new double[] { dx, dy, 0 };
            for (var i = 0; i < count; i++)
            {
                var entity = GetCollectionItem(space, i);
                if (entity == null)
                    continue;
                try
                {
                    TryInvoke(entity, "Move", [from, to]);
                    moved++;
                }
                finally
                {
                    ReleaseCom(entity);
                }
            }
        }
        finally
        {
            ReleaseCom(space);
        }

        return moved;
    }

    static object ConnectOrCreate(string progId, bool visible, out DateTimeOffset started)
    {
        started = DateTimeOffset.Now;
        object? app = null;
        try
        {
            var type = Type.GetTypeFromProgID(progId);
            if (type != null)
                app = GetActiveObjectFromProgId(progId);
        }
        catch
        {
            app = null;
        }

        if (app == null)
        {
            StartAutoCadFromRegistry(progId);
            var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(60);
            while (DateTime.UtcNow < deadline && app == null)
            {
                try { app = GetActiveObjectFromProgId(progId); }
                catch { Thread.Sleep(500); }
            }
        }

        if (app == null)
            throw new InvalidOperationException($"AutoCAD 启动或 COM 连接失败: {progId}");

        TrySetProperty(app, "Visible", visible);
        return app;
    }

    static object GetActiveObjectFromProgId(string progId)
    {
        var hr = CLSIDFromProgID(progId, out var clsid);
        if (hr < 0)
            Marshal.ThrowExceptionForHR(hr);
        return GetActiveObject(ref clsid, IntPtr.Zero);
    }

    static void StartAutoCadFromRegistry(string progId)
    {
        var clsid = ReadDefaultValue($@"{progId}\CLSID");
        var command = string.IsNullOrWhiteSpace(clsid) ? "" : ReadDefaultValue($@"CLSID\{clsid}\LocalServer32");
        var exe = ExtractExePath(command);
        if (string.IsNullOrWhiteSpace(exe) || !File.Exists(exe))
            exe = Environment.GetEnvironmentVariable("AUTOCAD_EXE") ?? "";
        if (!File.Exists(exe))
            throw new FileNotFoundException($"找不到 acad.exe。ProgID={progId}, LocalServer32={command}");

        Process.Start(new ProcessStartInfo
        {
            FileName = exe,
            Arguments = "/Automation",
            UseShellExecute = true,
            WorkingDirectory = Path.GetDirectoryName(exe) ?? Environment.CurrentDirectory
        });
    }

    static string ReadDefaultValue(string subKey)
    {
        using var key = Registry.ClassesRoot.OpenSubKey(subKey);
        return key?.GetValue("")?.ToString() ?? "";
    }

    static string ExtractExePath(string command)
    {
        if (string.IsNullOrWhiteSpace(command))
            return "";
        var trimmed = command.Trim();
        if (trimmed.StartsWith('"'))
        {
            var end = trimmed.IndexOf('"', 1);
            return end > 1 ? trimmed[1..end] : "";
        }
        var exeIndex = trimmed.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
        return exeIndex >= 0 ? trimmed[..(exeIndex + 4)] : trimmed;
    }

    static object OpenDocument(object app, string drawingPath, bool readOnly)
    {
        var documents = GetProperty(app, "Documents") ?? throw new InvalidOperationException("AutoCAD.Application.Documents 不可用。");
        try
        {
            return TryInvoke(documents, "Open", [drawingPath, readOnly])
                ?? TryInvoke(documents, "Open", [drawingPath])
                ?? throw new InvalidOperationException($"AutoCAD 打开 DWG 失败: {drawingPath}");
        }
        finally
        {
            ReleaseCom(documents);
        }
    }

    static void WaitForQuiescent(object app, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            var state = TryInvoke(app, "GetAcadState", []);
            if (state == null)
                return;
            try
            {
                if (ToBool(GetProperty(state, "IsQuiescent")) ?? true)
                    return;
            }
            finally
            {
                ReleaseCom(state);
            }
            Thread.Sleep(250);
        }
    }

    static object? GetProperty(object obj, string name) => RetryCom(() =>
    {
        try
        {
            return obj.GetType().InvokeMember(name, BindingFlags.GetProperty, null, obj, null, CultureInfo.InvariantCulture);
        }
        catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { throw; }
        catch (COMException ex) when (IsRetryableCom(ex)) { throw; }
        catch { return null; }
    });

    static object? GetCollectionItem(object obj, int index) => RetryCom(() =>
    {
        foreach (var flags in new[] { BindingFlags.GetProperty, BindingFlags.InvokeMethod })
        {
            try { return obj.GetType().InvokeMember("Item", flags, null, obj, [index], CultureInfo.InvariantCulture); }
            catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { throw; }
            catch (COMException ex) when (IsRetryableCom(ex)) { throw; }
            catch { }
        }
        return null;
    });

    static object? TryInvoke(object obj, string name, object?[] args) => RetryCom(() =>
    {
        try { return obj.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, obj, args, CultureInfo.InvariantCulture); }
        catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { throw; }
        catch (COMException ex) when (IsRetryableCom(ex)) { throw; }
        catch { return null; }
    });

    static void TrySetProperty(object obj, string name, object? value)
    {
        RetryCom(() =>
        {
            try { obj.GetType().InvokeMember(name, BindingFlags.SetProperty, null, obj, [value], CultureInfo.InvariantCulture); }
            catch { }
            return null;
        });
    }

    static object? RetryCom(Func<object?> action)
    {
        Exception? last = null;
        for (var i = 0; i < 180; i++)
        {
            try { return action(); }
            catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
            {
                last = ex.InnerException;
                Thread.Sleep(100);
            }
            catch (COMException ex) when (IsRetryableCom(ex))
            {
                last = ex;
                Thread.Sleep(100);
            }
        }
        if (last != null)
            throw last;
        return action();
    }

    static bool IsRetryableCom(Exception? ex) =>
        ex is COMException com && unchecked((uint)com.HResult) is 0x80010001 or 0x8001010A;

    static string ReadString(object obj, string name) => Convert.ToString(GetProperty(obj, name), CultureInfo.InvariantCulture) ?? "";

    static int? ToInt(object? value)
    {
        if (value == null)
            return null;
        return int.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
            ? parsed
            : null;
    }

    static bool? ToBool(object? value)
    {
        if (value == null)
            return null;
        if (value is bool b)
            return b;
        return bool.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), out var parsed) ? parsed : null;
    }

    static List<double> DoubleList(object? value)
    {
        if (value == null)
            return [];
        if (value is double[] doubles)
            return doubles.ToList();
        if (value is object[] objects)
            return objects.SelectMany(DoubleList).ToList();
        if (value is Array array)
        {
            var result = new List<double>();
            foreach (var item in array)
            {
                if (double.TryParse(Convert.ToString(item, CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
                    result.Add(parsed);
            }
            return result;
        }
        return [];
    }

    static void ReleaseCom(object? obj)
    {
        if (obj != null && Marshal.IsComObject(obj))
            Marshal.ReleaseComObject(obj);
    }
}

sealed class CliOptions
{
    public string ReferenceDwgPath { get; set; } = "";
    public string StudentDwgPath { get; set; } = "";
    public string OutputDwgPath { get; set; } = "";
    public string ProgId { get; set; } = "AutoCAD.Application.23.1";
    public double? ReferenceCenterX { get; set; }
    public double? ReferenceCenterY { get; set; }
    public bool HasReferenceCenter => ReferenceCenterX != null && ReferenceCenterY != null;
    public bool Visible { get; set; } = true;
    public bool CloseAfterAlign { get; set; } = true;

    public static CliOptions Parse(string[] args)
    {
        var options = new CliOptions();
        for (var i = 0; i < args.Length; i++)
        {
            var key = args[i];
            var value = i + 1 < args.Length ? args[i + 1] : "";
            switch (key)
            {
                case "--reference-dwg":
                    options.ReferenceDwgPath = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--student-dwg":
                    options.StudentDwgPath = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--output-dwg":
                    options.OutputDwgPath = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--reference-center-x":
                    options.ReferenceCenterX = double.Parse(value, CultureInfo.InvariantCulture);
                    i++;
                    break;
                case "--reference-center-y":
                    options.ReferenceCenterY = double.Parse(value, CultureInfo.InvariantCulture);
                    i++;
                    break;
                case "--prog-id":
                    options.ProgId = value;
                    i++;
                    break;
                case "--visible":
                    options.Visible = bool.TryParse(value, out var visible) ? visible : options.Visible;
                    i++;
                    break;
                case "--close-after-align":
                    options.CloseAfterAlign = bool.TryParse(value, out var close) ? close : options.CloseAfterAlign;
                    i++;
                    break;
            }
        }
        return options;
    }
}

sealed record Line2d(double X1, double Y1, double X2, double Y2, string Handle, string SpaceName)
{
    public double Length => Math.Sqrt(Math.Pow(X2 - X1, 2) + Math.Pow(Y2 - Y1, 2));
    public double MinX => Math.Min(X1, X2);
    public double MaxX => Math.Max(X1, X2);
    public double MinY => Math.Min(Y1, Y2);
    public double MaxY => Math.Max(Y1, Y2);

    public Line2d Normalized()
    {
        if (Math.Abs(Y1 - Y2) <= Math.Abs(X1 - X2))
            return X1 <= X2 ? this : this with { X1 = X2, Y1 = Y2, X2 = X1, Y2 = Y1 };
        return Y1 <= Y2 ? this : this with { X1 = X2, Y1 = Y2, X2 = X1, Y2 = Y1 };
    }
}

sealed class FrameRect
{
    public double MinX { get; set; }
    public double MinY { get; set; }
    public double MaxX { get; set; }
    public double MaxY { get; set; }
    public double Width => MaxX - MinX;
    public double Height => MaxY - MinY;
    public double Area => Width * Height;
    public double CenterX => (MinX + MaxX) / 2.0;
    public double CenterY => (MinY + MaxY) / 2.0;
    public List<string> SourceHandles { get; set; } = [];

    public static FrameRect FromCenter(double x, double y)
    {
        return new FrameRect
        {
            MinX = x,
            MaxX = x,
            MinY = y,
            MaxY = y,
            SourceHandles = ["explicit_reference_center"]
        };
    }
}

sealed class AlignResult
{
    public bool Ok { get; set; }
    public DateTimeOffset StartedAt { get; set; }
    public DateTimeOffset CompletedAt { get; set; }
    public string ReferenceDwgPath { get; set; } = "";
    public string StudentDwgPath { get; set; } = "";
    public string OutputDwgPath { get; set; } = "";
    public FrameRect ReferenceInnerFrame { get; set; } = new();
    public FrameRect StudentInnerFrameBefore { get; set; } = new();
    public FrameRect StudentInnerFrameAfter { get; set; } = new();
    public double DeltaX { get; set; }
    public double DeltaY { get; set; }
    public int MovedEntityCount { get; set; }
    public string Message { get; set; } = "";
}

static class StaRunner
{
    public static T Run<T>(Func<T> action)
    {
        T? result = default;
        Exception? error = null;
        var thread = new Thread(() =>
        {
            try { result = action(); }
            catch (Exception ex) { error = ex; }
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
        if (error != null)
            throw error;
        return result!;
    }
}

static class JsonDefaults
{
    public static readonly JsonSerializerOptions Options = new()
    {
        WriteIndented = true,
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
    };
}
