using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;

namespace SolidWorksHoverProbe;

internal static class Program
{
    private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = false };
    private static string _lastSignature = "";
    private static int? _lastFeatureManagerWidthBeforeNormalize;
    private static int? _lastFeatureManagerWidthAfterNormalize;
    private static SldWorks _swApp;
    private static ModelDoc2 _activeDoc;
    private static DrawingDoc _drawingDoc;
    private static DDrawingDocEvents_Event _drawingEvents;
    private static DDrawingDocEvents_DynamicHighlightNotifyEventHandler _dynamicHighlightHandler;
    private static DMouseEvents_Event _mouseEvents;
    private static DMouseEvents_MouseMoveNotifyEventHandler _mouseMoveHandler;
    private static bool _suppressEventProbeOutput;

    [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);

    [DllImport("user32.dll")]
    private static extern bool GetCursorPos(out Point point);

    [DllImport("user32.dll")]
    private static extern bool SetCursorPos(int x, int y);

    [DllImport("user32.dll")]
    private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll")]
    private static extern bool ClientToScreen(IntPtr hWnd, ref Point point);

    [DllImport("user32.dll")]
    private static extern bool GetClientRect(IntPtr hWnd, out Rect rect);

    [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    [DllImport("user32.dll")]
    private static extern bool BringWindowToTop(IntPtr hWnd);

    [DllImport("user32.dll")]
    private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll")]
    private static extern bool IsIconic(IntPtr hWnd);

    [DllImport("user32.dll")]
    private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

    [DllImport("user32.dll")]
    private static extern short GetAsyncKeyState(int vKey);

    private const int VkEscape = 0x1B;
    private const int VkControl = 0x11;
    private const uint WmMouseMove = 0x0200;
    private const int SwRestore = 9;
    private const uint SwpNoSize = 0x0001;
    private const uint SwpNoMove = 0x0002;
    private const uint SwpShowWindow = 0x0040;
    private static readonly IntPtr HwndTopMost = new(-1);
    private static readonly IntPtr HwndNoTopMost = new(-2);

    [STAThread]
    private static int Main(string[] args)
    {
        try
        {
            var sw = ConnectSolidWorks();
            _swApp = sw;
            _activeDoc = sw.ActiveDoc as ModelDoc2
                ?? throw new InvalidOperationException("SolidWorks does not have an active document.");

            if (Safe(() => _activeDoc.GetType(), 0) != (int)swDocumentTypes_e.swDocDRAWING)
                _activeDoc = FindFirstOpenDrawing(sw)
                    ?? throw new InvalidOperationException("No open SolidWorks drawing found. Please open or activate a SLDDRW window first.");

            var activateErrors = 0;
            Safe(() => sw.ActivateDoc3(_activeDoc.GetTitle(), false, (int)swRebuildOnActivation_e.swDontRebuildActiveDoc, ref activateErrors), null);

            _drawingDoc = _activeDoc as DrawingDoc
                ?? throw new InvalidOperationException("Active document cannot be cast to DrawingDoc.");

            if (args.Any(a => string.Equals(a, "--hover-grid", StringComparison.OrdinalIgnoreCase)))
            {
                RunGridProbe(ParseGridOptions(args));
                return 0;
            }

            if (args.Any(a => string.Equals(a, "--draw-ownership-contours", StringComparison.OrdinalIgnoreCase)))
            {
                DrawOwnershipContours(ParseContourOptions(args));
                return 0;
            }

            if (args.Any(a => string.Equals(a, "--print-sheet-origin", StringComparison.OrdinalIgnoreCase)))
            {
                NormalizeDrawingView(ParseZoomFitDelayMs(args));
                PrintSheetOriginCalibration();
                return 0;
            }

            if (args.Any(a => string.Equals(a, "--move-sheet-point", StringComparison.OrdinalIgnoreCase)))
            {
                RunMoveSheetPoint(args);
                return 0;
            }

            if (args.Any(a => string.Equals(a, "--probe-sheet-point", StringComparison.OrdinalIgnoreCase)))
            {
                RunProbeSheetPoint(args);
                return 0;
            }

            if (args.Any(a => string.Equals(a, "--manual-hover-timing", StringComparison.OrdinalIgnoreCase)))
            {
                RunManualHoverTiming(args);
                return 0;
            }

            if (args.Any(a => string.Equals(a, "--legacy-hover-probe", StringComparison.OrdinalIgnoreCase)))
            {
                RunLegacyHoverProbe(args);
                return 0;
            }

            var pollMs = args.Length > 0 && int.TryParse(args[0], out var parsed) ? Math.Max(parsed, 50) : 200;
            RunLegacyHoverProbe(args, pollMs);
            return 0;
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine(ex);
            return 1;
        }
    }

    private static void RunLegacyHoverProbe(string[] args, int? positionalPollMs = null)
    {
        var pollMs = positionalPollMs ?? ParseIntArg(args, "--poll-ms", 200, min: 50);
        var durationMs = ParseIntArg(args, "--duration-ms", 0, min: 0);
        var started = DateTime.UtcNow;

        HookEvents();

        Console.OutputEncoding = Encoding.UTF8;
        Console.WriteLine("SolidWorks legacy hover probe is running.");
        Console.WriteLine("Move the mouse manually over a drawing entity without clicking.");
        Console.WriteLine(durationMs > 0 ? $"Will stop after {durationMs} ms." : "Press Ctrl+C to stop.");
        Console.WriteLine();

        while (durationMs <= 0 || (DateTime.UtcNow - started).TotalMilliseconds < durationMs)
        {
            PrintProbe("poll");
            Thread.Sleep(pollMs);
        }

        PrintProbe("final");
        Console.WriteLine("SolidWorks legacy hover probe finished.");
    }

    private static void RunGridProbe(GridOptions options)
    {
        Console.OutputEncoding = Encoding.UTF8;

        BringSolidWorksToFront();
        NormalizeDrawingView(options.ZoomFitDelayMs);
        _suppressEventProbeOutput = true;
        HookEvents();

        var sheet = _drawingDoc.GetCurrentSheet() as Sheet
            ?? throw new InvalidOperationException("Cannot get current drawing sheet.");

        double width = 0;
        double height = 0;
        _ = sheet.GetSize(ref width, ref height);
        if (width <= 0 || height <= 0)
            throw new InvalidOperationException($"Invalid sheet size: width={width}, height={height}");

        Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(options.OutputPath))!);

        var step = options.StepMm / 1000.0;
        var columns = (int)Math.Floor(width / step) + 1;
        var rows = (int)Math.Floor(height / step) + 1;
        var scanBounds = ReadDrawingViewBounds().ToList();
        if (!string.IsNullOrWhiteSpace(options.ViewName))
            scanBounds = scanBounds
                .Where(b => string.Equals(b.Name, options.ViewName, StringComparison.OrdinalIgnoreCase))
                .ToList();
        var total = CountGridPointsToVisit(rows, columns, step, width, height, scanBounds, options);
        var written = 0L;
        var hits = 0L;
        var visited = 0L;
        var stoppedByEsc = false;
        var started = DateTime.Now;

        Console.WriteLine("SolidWorks hover-grid ownership probe is running.");
        Console.WriteLine($"zoom_fit_applied=true zoom_fit_delay_ms={options.ZoomFitDelayMs}");
        Console.WriteLine($"feature_manager_width_before={_lastFeatureManagerWidthBeforeNormalize?.ToString() ?? "unknown"} after={_lastFeatureManagerWidthAfterNormalize?.ToString() ?? "unknown"}");
        Console.WriteLine($"sheet={Safe(() => sheet.GetName(), "")}");
        Console.WriteLine($"size_mm=({width * 1000.0:F3},{height * 1000.0:F3}) step_mm={options.StepMm:G6}");
        if (!string.IsNullOrWhiteSpace(options.ViewName))
            Console.WriteLine($"view_name={options.ViewName}");
        if (!double.IsNaN(options.RowYMm))
            Console.WriteLine($"row_y_mm={options.RowYMm:G6}");
        Console.WriteLine($"reverse_x={options.ReverseX}");
        Console.WriteLine($"sheet_correction_mm=({options.SheetCorrectionXM * 1000.0:G6},{options.SheetCorrectionYM * 1000.0:G6})");
        foreach (var item in options.ViewSheetCorrections.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase))
            Console.WriteLine($"view_sheet_correction_mm[{item.Key}]=({item.Value.XM * 1000.0:G6},{item.Value.YM * 1000.0:G6})");
        Console.WriteLine($"clear_before_point={options.ClearBeforePoint} clear_after_point={options.ClearAfterPoint} clear_delay_ms={options.ClearDelayMs}");
        Console.WriteLine($"points={total} views_only={options.ViewsOnly} output={Path.GetFullPath(options.OutputPath)}");
        foreach (var bound in scanBounds)
            Console.WriteLine($"view={bound.Name} outline_mm=({bound.XMin * 1000.0:F3},{bound.YMin * 1000.0:F3},{bound.XMax * 1000.0:F3},{bound.YMax * 1000.0:F3})");
        Console.WriteLine("Press Esc to stop after the current point, even when SolidWorks has focus.");
        Console.WriteLine();

        using var writer = new StreamWriter(options.OutputPath, false, new UTF8Encoding(false));
        WriteJsonLine(writer, new GridHeaderRecord(
            "header",
            Safe(() => _activeDoc.GetTitle(), ""),
            Safe(() => sheet.GetName(), ""),
            width,
            height,
            options.StepMm,
            columns,
            rows));

        for (var row = 0; row < rows; row++)
        {
            var y = Math.Min(row * step, height);
            if (!ShouldVisitGridRow(y, options))
                continue;

            foreach (var column in EnumerateColumns(columns, options.ReverseX))
            {
                if (ShouldStopByEsc())
                {
                    stoppedByEsc = true;
                    break;
                }

                var x = Math.Min(column * step, width);
                var scanView = FindContainingView(scanBounds, x, y);
                if (options.ViewsOnly && scanView == null)
                    continue;

                if (options.ClearBeforePoint)
                    ClearHoverState(options.ClearDelayMs);

                var correction = ResolveSheetCorrection(options, scanView?.Name);
                var record = ProbeHoverGridPoint(
                    row,
                    column,
                    x,
                    y,
                    options.HoverDelayMs,
                    options.PollMs,
                    correction.XM,
                    correction.YM,
                    scanView?.Name ?? "");
                visited++;

                if (record.Hit)
                    hits++;

                if (options.IncludeEmpty || record.Hit)
                {
                    WriteJsonLine(writer, record);
                    written++;
                }

                if (options.ClearAfterPoint)
                    ClearHoverState(options.ClearDelayMs);

                if (options.MaxPoints > 0 && visited >= options.MaxPoints)
                    break;
            }

            if (row % Math.Max(1, options.ProgressEveryRows) == 0 || row == rows - 1)
            {
                var elapsed = DateTime.Now - started;
                Console.WriteLine($"row={row + 1}/{rows} visited={visited}/{total} hits={hits} written={written} elapsed={elapsed:hh\\:mm\\:ss}");
            }

            if (stoppedByEsc || options.MaxPoints > 0 && visited >= options.MaxPoints)
                break;
        }

        _activeDoc.ClearSelection2(true);
        Console.WriteLine();
        Console.WriteLine($"done visited={visited} hits={hits} written={written} stopped_by_esc={stoppedByEsc}");
    }

    private static bool ShouldStopByEsc()
    {
        if ((GetAsyncKeyState(VkEscape) & 0x8000) != 0)
            return true;

        try
        {
            if (!Console.KeyAvailable)
                return false;

            var stop = false;
            while (Console.KeyAvailable)
            {
                var key = Console.ReadKey(intercept: true);
                if (key.Key == ConsoleKey.Escape)
                    stop = true;
            }

            return stop;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }

    private static void ClearHoverState(int clearDelayMs)
    {
        Safe(() =>
        {
            _activeDoc.ClearSelection2(true);
            return true;
        }, false);

        var clearPoint = GetHoverClearPoint();
        if (clearPoint != null)
            MoveMouseAndNotify(clearPoint);

        WaitForHoverClear(clearDelayMs);
    }

    private static void WaitForHoverClear(int clearDelayMs)
    {
        var timeoutMs = Math.Max(1, clearDelayMs);
        var selectionMgr = _activeDoc.SelectionManager as SelectionMgr;
        var started = DateTime.UtcNow;

        while (true)
        {
            var preselected = Safe(() => selectionMgr.GetPreSelectedObject(), null);
            if (!ReadEntityOwnership(preselected, null).IsEntity)
                return;

            var waitedMs = (int)Math.Round((DateTime.UtcNow - started).TotalMilliseconds);
            if (waitedMs >= timeoutMs)
                return;

            Thread.Sleep(Math.Min(5, timeoutMs - waitedMs));
        }
    }

    private static void RunManualHoverTiming(string[] args)
    {
        Console.OutputEncoding = Encoding.UTF8;

        var pollMs = ParseIntArg(args, "--poll-ms", 10, min: 1);
        var timeoutMs = ParseIntArg(args, "--timeout-ms", 10000, min: 1);

        Console.WriteLine("Manual hover timing probe.");
        Console.WriteLine("Put the mouse on a SolidWorks drawing entity.");
        Console.WriteLine("Press Ctrl to start timing, or Esc to cancel.");

        while (IsKeyDown(VkControl))
            Thread.Sleep(10);

        while (!IsKeyDown(VkControl))
        {
            if (IsKeyDown(VkEscape))
            {
                Console.WriteLine("cancelled");
                return;
            }

            Thread.Sleep(5);
        }

        var started = DateTime.UtcNow;
        var attempts = 0;
        object firstObject = null;
        EntityOwnership firstOwnership = null;

        while ((DateTime.UtcNow - started).TotalMilliseconds <= timeoutMs)
        {
            attempts++;
            var selectionMgr = _activeDoc.SelectionManager as SelectionMgr;
            var preselected = Safe(() => selectionMgr.GetPreSelectedObject(), null);
            var ownership = ReadEntityOwnership(preselected, null);

            if (ownership.IsEntity)
            {
                firstObject = preselected;
                firstOwnership = ownership;
                break;
            }

            Thread.Sleep(pollMs);
        }

        var elapsedMs = (DateTime.UtcNow - started).TotalMilliseconds;
        GetCursorPos(out var cursor);

        Console.WriteLine($"elapsed_ms={elapsedMs:F1}");
        Console.WriteLine($"attempts={attempts}");
        Console.WriteLine($"cursor_screen=({cursor.X},{cursor.Y})");

        if (firstOwnership == null)
        {
            Console.WriteLine("hit=false");
            var probe = ReadProbe("manual-hover-timeout");
            foreach (var line in probe.Lines)
                Console.WriteLine(line);
            return;
        }

        Console.WriteLine("hit=true");
        Console.WriteLine("object=" + DescribeObject(firstObject));
        Console.WriteLine("entity_component=" + firstOwnership.EntityComponent);
        foreach (var drawingComponent in firstOwnership.DrawingComponents)
            Console.WriteLine($"drawing_component[{drawingComponent.View}]=" + drawingComponent.DrawingComponent);
    }

    private static void DrawOwnershipContours(ContourOptions options)
    {
        Console.OutputEncoding = Encoding.UTF8;
        if (!File.Exists(options.InputPath))
            throw new FileNotFoundException($"ownership grid file not found: {options.InputPath}");

        var groups = ReadOwnershipGroups(options).ToList();
        if (groups.Count == 0)
            throw new InvalidOperationException($"No ownership groups with at least {options.MinGroupPoints} points: {options.InputPath}");

        var sheet = _drawingDoc.GetCurrentSheet() as Sheet
            ?? throw new InvalidOperationException("Cannot get current drawing sheet.");
        var sheetName = Safe(() => sheet.GetName(), "");
        if (!string.IsNullOrWhiteSpace(sheetName))
            Safe(() => _drawingDoc.ActivateSheet(sheetName), false);

        _activeDoc.ClearSelection2(true);
        var sketch = _activeDoc.SketchManager
            ?? throw new InvalidOperationException("Cannot get SketchManager.");

        Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(options.OutputPath))!);
        var drawn = new List<ContourRecord>();
        foreach (var group in groups)
        {
            var minX = group.Points.Min(point => point.XM) - options.PadMm / 1000.0;
            var minY = group.Points.Min(point => point.YM) - options.PadMm / 1000.0;
            var maxX = group.Points.Max(point => point.XM) + options.PadMm / 1000.0;
            var maxY = group.Points.Max(point => point.YM) + options.PadMm / 1000.0;

            Safe(() => sketch.CreateLine(minX, minY, 0, maxX, minY, 0), null);
            Safe(() => sketch.CreateLine(maxX, minY, 0, maxX, maxY, 0), null);
            Safe(() => sketch.CreateLine(maxX, maxY, 0, minX, maxY, 0), null);
            Safe(() => sketch.CreateLine(minX, maxY, 0, minX, minY, 0), null);

            drawn.Add(new ContourRecord(
                group.Key,
                group.DisplayName,
                group.Points.Count,
                minX,
                minY,
                maxX,
                maxY,
                minX * 1000.0,
                minY * 1000.0,
                maxX * 1000.0,
                maxY * 1000.0));
        }

        Safe(() => _activeDoc.ForceRebuild3(false), false);
        File.WriteAllText(options.OutputPath, JsonSerializer.Serialize(new
        {
            input = Path.GetFullPath(options.InputPath),
            output = Path.GetFullPath(options.OutputPath),
            sheet = sheetName,
            group_count = drawn.Count,
            min_group_points = options.MinGroupPoints,
            pad_mm = options.PadMm,
            contours = drawn
        }, new JsonSerializerOptions { WriteIndented = true }));

        Console.WriteLine($"drawn_contours={drawn.Count}");
        Console.WriteLine($"output={Path.GetFullPath(options.OutputPath)}");
        foreach (var item in drawn.Take(20))
            Console.WriteLine($"{item.PointCount} pts bbox_mm=({item.MinXMm:F3},{item.MinYMm:F3},{item.MaxXMm:F3},{item.MaxYMm:F3}) {item.DisplayName}");
    }

    private static IEnumerable<OwnershipGroup> ReadOwnershipGroups(ContourOptions options)
    {
        var groups = new Dictionary<string, OwnershipGroup>(StringComparer.OrdinalIgnoreCase);
        foreach (var line in File.ReadLines(options.InputPath, Encoding.UTF8))
        {
            if (string.IsNullOrWhiteSpace(line))
                continue;

            using var doc = JsonDocument.Parse(line);
            var root = doc.RootElement;
            if (!string.Equals(ReadJsonString(root, "Kind"), "point", StringComparison.OrdinalIgnoreCase) &&
                !string.Equals(ReadJsonString(root, "kind"), "point", StringComparison.OrdinalIgnoreCase))
                continue;
            if (!ReadJsonBool(root, "Hit") && !ReadJsonBool(root, "hit"))
                continue;

            var ownership = root.TryGetProperty("Ownership", out var ownershipPascal)
                ? ownershipPascal
                : root.TryGetProperty("ownership", out var ownershipCamel) ? ownershipCamel : default;
            if (ownership.ValueKind != JsonValueKind.Object)
                continue;

            var entityComponent = FirstNonEmpty(
                ReadJsonString(ownership, "EntityComponent"),
                ReadJsonString(ownership, "entity_component"));
            if (string.IsNullOrWhiteSpace(entityComponent) || string.Equals(entityComponent, "null", StringComparison.OrdinalIgnoreCase))
                continue;

            var drawingComponent = FirstNonEmptyDrawingComponent(ownership);
            var key = options.GroupByDrawingComponent
                ? $"{entityComponent}|{drawingComponent}"
                : entityComponent;
            var displayName = options.GroupByDrawingComponent && !string.IsNullOrWhiteSpace(drawingComponent)
                ? $"{entityComponent} / {drawingComponent}"
                : entityComponent;

            var x = ReadJsonDouble(root, "SheetXM", "sheet_xm");
            var y = ReadJsonDouble(root, "SheetYM", "sheet_ym");
            if (x == null || y == null)
                continue;

            if (!groups.TryGetValue(key, out var group))
            {
                group = new OwnershipGroup(key, displayName);
                groups.Add(key, group);
            }

            group.Points.Add(new OwnershipPoint(x.Value, y.Value));
        }

        return groups.Values
            .Where(group => group.Points.Count >= options.MinGroupPoints)
            .OrderByDescending(group => group.Points.Count);
    }

    private static string FirstNonEmptyDrawingComponent(JsonElement ownership)
    {
        var components = ownership.TryGetProperty("DrawingComponents", out var pascal)
            ? pascal
            : ownership.TryGetProperty("drawing_components", out var snake) ? snake : default;
        if (components.ValueKind != JsonValueKind.Array)
            return "";

        foreach (var item in components.EnumerateArray())
        {
            var value = FirstNonEmpty(
                ReadJsonString(item, "DrawingComponent"),
                ReadJsonString(item, "drawing_component"));
            if (!string.IsNullOrWhiteSpace(value) && !string.Equals(value, "null", StringComparison.OrdinalIgnoreCase))
                return value;
        }

        return "";
    }

    private static ContourOptions ParseContourOptions(string[] args)
    {
        var inputPath = "";
        var outputPath = Path.Combine("runtime", "solidworks-hover-grid", "ownership-contours.json");
        var minGroupPoints = 2;
        var padMm = 2.0;
        var groupByDrawingComponent = false;

        for (var i = 0; i < args.Length; i++)
        {
            var arg = args[i];
            if (string.Equals(arg, "--input", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                inputPath = args[++i];
            else if (string.Equals(arg, "--output", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                outputPath = args[++i];
            else if (string.Equals(arg, "--min-group-points", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                minGroupPoints = Math.Max(1, int.Parse(args[++i], CultureInfo.InvariantCulture));
            else if (string.Equals(arg, "--pad-mm", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                padMm = Math.Max(0.0, double.Parse(args[++i], CultureInfo.InvariantCulture));
            else if (string.Equals(arg, "--group-by-drawing-component", StringComparison.OrdinalIgnoreCase))
                groupByDrawingComponent = true;
        }

        if (string.IsNullOrWhiteSpace(inputPath))
            throw new ArgumentException("--draw-ownership-contours requires --input.");
        return new ContourOptions(inputPath, outputPath, minGroupPoints, padMm, groupByDrawingComponent);
    }

    private static string FirstNonEmpty(params string[] values)
    {
        return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? "";
    }

    private static string ReadJsonString(JsonElement element, string propertyName)
    {
        return element.ValueKind == JsonValueKind.Object &&
               element.TryGetProperty(propertyName, out var property) &&
               property.ValueKind == JsonValueKind.String
            ? property.GetString() ?? ""
            : "";
    }

    private static bool ReadJsonBool(JsonElement element, string propertyName)
    {
        return element.ValueKind == JsonValueKind.Object &&
               element.TryGetProperty(propertyName, out var property) &&
               property.ValueKind == JsonValueKind.True;
    }

    private static double? ReadJsonDouble(JsonElement element, params string[] propertyNames)
    {
        foreach (var propertyName in propertyNames)
        {
            if (element.ValueKind == JsonValueKind.Object &&
                element.TryGetProperty(propertyName, out var property) &&
                property.ValueKind == JsonValueKind.Number &&
                property.TryGetDouble(out var value))
                return value;
        }

        return null;
    }

    private static void RunMoveSheetPoint(string[] args)
    {
        Console.OutputEncoding = Encoding.UTF8;

        var sheetXMm = ParseDoubleArg(args, "--sheet-x-mm", double.NaN);
        var sheetYMm = ParseDoubleArg(args, "--sheet-y-mm", double.NaN);
        var holdMs = ParseIntArg(args, "--hold-ms", 10000, min: 0);
        var zoomFitDelayMs = ParseZoomFitDelayMs(args);
        var sheetCorrectionXM = ParseDoubleArg(args, "--sheet-correction-x-mm", 0.0) / 1000.0;
        var sheetCorrectionYM = ParseDoubleArg(args, "--sheet-correction-y-mm", 0.0) / 1000.0;
        if (double.IsNaN(sheetXMm) || double.IsNaN(sheetYMm))
            throw new ArgumentException("--move-sheet-point requires --sheet-x-mm and --sheet-y-mm.");

        BringSolidWorksToFront();
        NormalizeDrawingView(zoomFitDelayMs);

        var target = ProjectSheetPointToScreen(sheetXMm / 1000.0, sheetYMm / 1000.0, sheetCorrectionXM, sheetCorrectionYM);
        if (target == null)
            throw new InvalidOperationException("Cannot project sheet point to screen.");

        MoveMouseForHover(target);

        Console.WriteLine("Moved mouse to SolidWorks sheet point.");
        Console.WriteLine($"sheet_mm=({sheetXMm:F3},{sheetYMm:F3})");
        Console.WriteLine($"sheet_correction_mm=({sheetCorrectionXM * 1000.0:F3},{sheetCorrectionYM * 1000.0:F3})");
        Console.WriteLine($"projected_sheet_mm=({sheetXMm + sheetCorrectionXM * 1000.0:F3},{sheetYMm + sheetCorrectionYM * 1000.0:F3})");
        Console.WriteLine($"screen=({target.ScreenX},{target.ScreenY}) client=({target.ClientX},{target.ClientY})");
        Console.WriteLine($"hold_ms={holdMs}");
        Thread.Sleep(holdMs);
    }

    private static void RunProbeSheetPoint(string[] args)
    {
        Console.OutputEncoding = Encoding.UTF8;

        var sheetXMm = ParseDoubleArg(args, "--sheet-x-mm", double.NaN);
        var sheetYMm = ParseDoubleArg(args, "--sheet-y-mm", double.NaN);
        var maxHoverWaitMs = ParseIntArg(args, "--max-hover-wait-ms", 10000, min: 0);
        var pollMs = ParseIntArg(args, "--poll-ms", 50, min: 1);
        var zoomFitDelayMs = ParseZoomFitDelayMs(args);
        var sheetCorrectionXM = ParseDoubleArg(args, "--sheet-correction-x-mm", 0.0) / 1000.0;
        var sheetCorrectionYM = ParseDoubleArg(args, "--sheet-correction-y-mm", 0.0) / 1000.0;
        if (double.IsNaN(sheetXMm) || double.IsNaN(sheetYMm))
            throw new ArgumentException("--probe-sheet-point requires --sheet-x-mm and --sheet-y-mm.");

        BringSolidWorksToFront();
        NormalizeDrawingView(zoomFitDelayMs);

        var sheetX = sheetXMm / 1000.0;
        var sheetY = sheetYMm / 1000.0;
        var target = ProjectSheetPointToScreen(sheetX, sheetY, sheetCorrectionXM, sheetCorrectionYM);
        if (target == null)
            throw new InvalidOperationException("Cannot project sheet point to screen.");

        HookEvents();
        PrintProbe("before-move");
        MoveMouseForHover(target);

        var selectionMgr = _activeDoc.SelectionManager as SelectionMgr;
        var started = DateTime.UtcNow;
        var waitedMs = 0;
        var polls = 0;
        object preselected = null;
        EntityOwnership ownership = null;
        object firstPreselected = null;
        EntityOwnership firstOwnership = null;
        var firstHitMs = 0;

        while (true)
        {
            polls++;
            preselected = Safe(() => selectionMgr.GetPreSelectedObject(), null);
            ownership = ReadEntityOwnership(preselected, null);
            waitedMs = (int)Math.Round((DateTime.UtcNow - started).TotalMilliseconds);
            if (ownership.IsEntity && firstOwnership == null)
            {
                firstPreselected = preselected;
                firstOwnership = ownership;
                firstHitMs = waitedMs;
            }

            PrintProbe("poll");

            if (waitedMs >= maxHoverWaitMs)
                break;

            Thread.Sleep(pollMs);
        }

        PrintProbe("final");
        Console.WriteLine("SolidWorks sheet point hover ownership probe finished.");
        Console.WriteLine($"sheet_mm=({sheetXMm:F3},{sheetYMm:F3})");
        Console.WriteLine($"sheet_correction_mm=({sheetCorrectionXM * 1000.0:F3},{sheetCorrectionYM * 1000.0:F3})");
        Console.WriteLine($"projected_sheet_mm=({sheetXMm + sheetCorrectionXM * 1000.0:F3},{sheetYMm + sheetCorrectionYM * 1000.0:F3})");
        Console.WriteLine($"screen=({target.ScreenX},{target.ScreenY}) client=({target.ClientX},{target.ClientY})");
        Console.WriteLine($"waited_ms={waitedMs}");
        Console.WriteLine($"polls={polls}");
        Console.WriteLine($"hit={firstOwnership?.IsEntity == true}");
        Console.WriteLine($"first_hit_ms={firstHitMs}");
        Console.WriteLine("preselected=" + DescribeObject(firstPreselected ?? preselected));

        if (firstOwnership?.IsEntity != true)
            return;

        Console.WriteLine("entity_component=" + firstOwnership.EntityComponent);
        foreach (var drawingComponent in firstOwnership.DrawingComponents)
            Console.WriteLine($"drawing_component[{drawingComponent.View}]=" + drawingComponent.DrawingComponent);
    }

    private static int ParseIntArg(string[] args, string name, int defaultValue, int min)
    {
        for (var i = 0; i < args.Length; i++)
        {
            if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                return Math.Max(min, int.Parse(args[++i]));
        }

        return defaultValue;
    }

    private static double ParseDoubleArg(string[] args, string name, double defaultValue)
    {
        for (var i = 0; i < args.Length; i++)
        {
            if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                return double.Parse(args[++i], CultureInfo.InvariantCulture);
        }

        return defaultValue;
    }

    private static bool IsKeyDown(int virtualKey)
    {
        return (GetAsyncKeyState(virtualKey) & 0x8000) != 0;
    }

    private static void PrintSheetOriginCalibration()
    {
        Console.OutputEncoding = Encoding.UTF8;

        var sheet = _drawingDoc.GetCurrentSheet() as Sheet
            ?? throw new InvalidOperationException("Cannot get current drawing sheet.");

        double sheetWidth = 0;
        double sheetHeight = 0;
        _ = sheet.GetSize(ref sheetWidth, ref sheetHeight);
        if (sheetWidth <= 0 || sheetHeight <= 0)
            throw new InvalidOperationException($"Invalid sheet size: width={sheetWidth}, height={sheetHeight}");

        var modelView = _activeDoc.ActiveView as ModelView
            ?? throw new InvalidOperationException("Cannot get active ModelView.");

        var hwndValue = Safe(() => modelView.GetViewHWndx64(), 0L);
        if (hwndValue == 0)
            hwndValue = Safe(() => modelView.GetViewHWnd(), 0);
        if (hwndValue == 0)
            throw new InvalidOperationException("Cannot get SolidWorks graphics view HWND.");

        var hwnd = new IntPtr(hwndValue);
        if (!GetClientRect(hwnd, out var clientRect))
            throw new InvalidOperationException("GetClientRect failed.");

        var clientTopLeft = new Point(0, 0);
        if (!ClientToScreen(hwnd, ref clientTopLeft))
            throw new InvalidOperationException("ClientToScreen failed.");

        var clientWidth = clientRect.Right - clientRect.Left;
        var clientHeight = clientRect.Bottom - clientRect.Top;
        var scale = Math.Min(clientWidth / sheetWidth, clientHeight / sheetHeight);
        var sheetScreenWidth = sheetWidth * scale;
        var sheetScreenHeight = sheetHeight * scale;
        var sheetLeft = clientTopLeft.X + (clientWidth - sheetScreenWidth) / 2.0;
        var sheetTop = clientTopLeft.Y + (clientHeight - sheetScreenHeight) / 2.0;
        var originX = sheetLeft;
        var originY = sheetTop + sheetScreenHeight;

        Console.WriteLine("SolidWorks sheet origin calibration");
        Console.WriteLine($"doc_title={Safe(() => _activeDoc.GetTitle(), "")}");
        Console.WriteLine($"sheet={Safe(() => sheet.GetName(), "")}");
        Console.WriteLine($"sheet_size_mm=({sheetWidth * 1000.0:F3},{sheetHeight * 1000.0:F3})");
        Console.WriteLine($"view_hwnd=0x{hwndValue:X}");
        Console.WriteLine($"client_screen_rect=({clientTopLeft.X},{clientTopLeft.Y},{clientTopLeft.X + clientWidth},{clientTopLeft.Y + clientHeight})");
        Console.WriteLine($"client_size_px=({clientWidth},{clientHeight})");
        Console.WriteLine($"feature_manager_width_before={_lastFeatureManagerWidthBeforeNormalize?.ToString() ?? "unknown"} after={_lastFeatureManagerWidthAfterNormalize?.ToString() ?? "unknown"}");
        Console.WriteLine($"fit_scale_px_per_mm={scale / 1000.0:F6}");
        Console.WriteLine($"sheet_screen_rect=({sheetLeft:F1},{sheetTop:F1},{sheetLeft + sheetScreenWidth:F1},{sheetTop + sheetScreenHeight:F1})");
        Console.WriteLine($"sheet_origin_screen_px=({originX:F1},{originY:F1})");

        foreach (var bound in ReadDrawingViewBounds())
        {
            var left = sheetLeft + bound.XMin * scale;
            var right = sheetLeft + bound.XMax * scale;
            var top = sheetTop + (sheetHeight - bound.YMax) * scale;
            var bottom = sheetTop + (sheetHeight - bound.YMin) * scale;
            Console.WriteLine($"view={bound.Name} sheet_outline_mm=({bound.XMin * 1000.0:F3},{bound.YMin * 1000.0:F3},{bound.XMax * 1000.0:F3},{bound.YMax * 1000.0:F3}) screen_rect=({left:F1},{top:F1},{right:F1},{bottom:F1})");
        }
    }

    private static void NormalizeDrawingView(int delayMs)
    {
        HideFeatureManagerPane();

        Safe(() =>
        {
            _activeDoc.ViewZoomtofit2();
            return 0;
        }, 0);

        Thread.Sleep(Math.Max(0, delayMs));
    }

    private static void HideFeatureManagerPane()
    {
        _lastFeatureManagerWidthBeforeNormalize = Safe(() => _activeDoc.GetFeatureManagerWidth(), -1);

        Safe(() =>
        {
            _activeDoc.SetFeatureManagerWidth(0);
            return 0;
        }, 0);

        _lastFeatureManagerWidthAfterNormalize = Safe(() => _activeDoc.GetFeatureManagerWidth(), -1);
    }

    private static int ParseZoomFitDelayMs(string[] args)
    {
        var delayMs = 500;
        for (var i = 0; i < args.Length; i++)
        {
            if (string.Equals(args[i], "--zoom-fit-delay-ms", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                delayMs = Math.Max(0, int.Parse(args[++i]));
        }

        return delayMs;
    }

    private static IEnumerable<ViewBounds> ReadDrawingViewBounds()
    {
        var viewObj = Safe(() => _drawingDoc.GetFirstView(), null);
        var guard = 0;
        while (viewObj is View view && guard++ < 200)
        {
            var name = Safe(() => view.GetName2(), Safe(() => view.Name, ""));
            var type = Safe(() => view.Type, 0);
            var outline = DoubleArray(Safe(() => view.GetOutline(), null));

            if (guard > 1 && outline.Count >= 4)
            {
                yield return new ViewBounds(
                    name,
                    Math.Min(outline[0], outline[2]),
                    Math.Min(outline[1], outline[3]),
                    Math.Max(outline[0], outline[2]),
                    Math.Max(outline[1], outline[3]));
            }

            viewObj = Safe(() => view.GetNextView(), null);
        }
    }

    private static bool ContainsPoint(IReadOnlyList<ViewBounds> bounds, double x, double y)
    {
        return bounds.Any(b => x >= b.XMin && x <= b.XMax && y >= b.YMin && y <= b.YMax);
    }

    private static ViewBounds FindContainingView(IReadOnlyList<ViewBounds> bounds, double x, double y)
    {
        return bounds
            .Where(b => x >= b.XMin && x <= b.XMax && y >= b.YMin && y <= b.YMax)
            .OrderBy(b => (b.XMax - b.XMin) * (b.YMax - b.YMin))
            .FirstOrDefault();
    }

    private static SheetCorrection ResolveSheetCorrection(GridOptions options, string viewName)
    {
        if (!string.IsNullOrWhiteSpace(viewName) &&
            options.ViewSheetCorrections.TryGetValue(viewName, out var correction))
        {
            return correction;
        }

        return new SheetCorrection(options.SheetCorrectionXM, options.SheetCorrectionYM);
    }

    private static long CountGridPointsToVisit(
        int rows,
        int columns,
        double step,
        double width,
        double height,
        IReadOnlyList<ViewBounds> bounds,
        GridOptions options)
    {
        var count = 0L;
        for (var row = 0; row < rows; row++)
        {
            var y = Math.Min(row * step, height);
            if (!ShouldVisitGridRow(y, options))
                continue;

            for (var column = 0; column < columns; column++)
            {
                var x = Math.Min(column * step, width);
                if (!options.ViewsOnly || ContainsPoint(bounds, x, y))
                    count++;
            }
        }

        return count;
    }

    private static bool ShouldVisitGridRow(double y, GridOptions options)
    {
        var yMm = y * 1000.0;

        if (!double.IsNaN(options.RowYMm))
            return Math.Abs(yMm - options.RowYMm) <= options.StepMm / 2.0;

        if (!double.IsNaN(options.RowYMinMm) && yMm < options.RowYMinMm - options.StepMm / 2.0)
            return false;

        if (!double.IsNaN(options.RowYMaxMm) && yMm > options.RowYMaxMm + options.StepMm / 2.0)
            return false;

        return true;
    }

    private static IEnumerable<int> EnumerateColumns(int columns, bool reverse)
    {
        if (!reverse)
        {
            for (var column = 0; column < columns; column++)
                yield return column;
        }
        else
        {
            for (var column = columns - 1; column >= 0; column--)
                yield return column;
        }
    }

    private static GridPointRecord ProbeHoverGridPoint(
        int row,
        int column,
        double sheetX,
        double sheetY,
        int maxHoverWaitMs,
        int pollMs,
        double sheetCorrectionXM,
        double sheetCorrectionYM,
        string scanViewName)
    {
        var target = ProjectSheetPointToScreen(sheetX, sheetY, sheetCorrectionXM, sheetCorrectionYM);
        if (target == null)
        {
            return EmptyGridPointRecord(row, column, sheetX, sheetY, scanViewName, sheetCorrectionXM, sheetCorrectionYM, "project_failed");
        }

        MoveMouseForHover(target);

        var selectionMgr = _activeDoc.SelectionManager as SelectionMgr;
        var started = DateTime.UtcNow;
        object preselected = null;
        EntityOwnership ownership = null;

        while (true)
        {
            preselected = Safe(() => selectionMgr.GetPreSelectedObject(), null);
            ownership = ReadEntityOwnership(preselected, null);
            if (ownership.IsEntity)
                break;

            var waitedMs = (int)Math.Round((DateTime.UtcNow - started).TotalMilliseconds);
            if (waitedMs >= maxHoverWaitMs)
                break;

            Thread.Sleep(Math.Max(1, pollMs));
        }

        if (!ownership.IsEntity)
        {
            return EmptyGridPointRecord(row, column, sheetX, sheetY, scanViewName, sheetCorrectionXM, sheetCorrectionYM, "preselected_not_entity");
        }

        return new GridPointRecord(
            "point",
            row,
            column,
            sheetX,
            sheetY,
            sheetX * 1000.0,
            sheetY * 1000.0,
            scanViewName,
            sheetCorrectionXM * 1000.0,
            sheetCorrectionYM * 1000.0,
            true,
            "HOVER",
            "preselected",
            0,
            DescribeObject(preselected),
            "null",
            new List<double>(),
            null,
            ownership);
    }

    private static GridPointRecord EmptyGridPointRecord(
        int row,
        int column,
        double sheetX,
        double sheetY,
        string scanViewName,
        double sheetCorrectionXM,
        double sheetCorrectionYM,
        string reason)
    {
        return new GridPointRecord(
            "point",
            row,
            column,
            sheetX,
            sheetY,
            sheetX * 1000.0,
            sheetY * 1000.0,
            scanViewName,
            sheetCorrectionXM * 1000.0,
            sheetCorrectionYM * 1000.0,
            false,
            reason,
            "",
            0,
            "null",
            "null",
            new List<double>(),
            null,
            new EntityOwnership(false, "null", new List<DrawingComponentOwnership>()));
    }

    private static void MoveMouseForHover(ScreenPoint target)
    {
        var warmup = target with
        {
            ScreenX = target.ScreenX - 1,
            ClientX = target.ClientX - 1
        };

        MoveMouseAndNotify(warmup);
        Thread.Sleep(10);
        MoveMouseAndNotify(target);
    }

    private static void MoveMouseAndNotify(ScreenPoint point)
    {
        SetCursorPos(point.ScreenX, point.ScreenY);
        var lParam = MakeLParam(point.ClientX, point.ClientY);
        PostMessage(point.HWnd, WmMouseMove, IntPtr.Zero, lParam);
    }

    private static ScreenPoint GetHoverClearPoint()
    {
        var modelView = _activeDoc.ActiveView as ModelView;
        if (modelView == null)
            return null;

        var hwndValue = Safe(() => modelView.GetViewHWndx64(), 0L);
        if (hwndValue == 0)
            hwndValue = Safe(() => modelView.GetViewHWnd(), 0);
        if (hwndValue == 0)
            return null;

        var hwnd = new IntPtr(hwndValue);
        if (!GetClientRect(hwnd, out var clientRect))
            return null;

        var clientTopLeft = new Point(0, 0);
        if (!ClientToScreen(hwnd, ref clientTopLeft))
            return null;

        var clientX = Math.Max(2, clientRect.Right - clientRect.Left - 4);
        var clientY = Math.Max(2, clientRect.Bottom - clientRect.Top - 4);
        return new ScreenPoint(
            hwnd,
            clientTopLeft.X + clientX,
            clientTopLeft.Y + clientY,
            clientX,
            clientY);
    }

    private static IntPtr MakeLParam(int lowWord, int highWord)
    {
        return new IntPtr((highWord << 16) | (lowWord & 0xFFFF));
    }

    private static ScreenPoint ProjectSheetPointToScreen(double sheetX, double sheetY, double sheetCorrectionX = 0.0, double sheetCorrectionY = 0.0)
    {
        var modelView = _activeDoc.ActiveView as ModelView;
        if (modelView == null)
            return null;

        BringSolidWorksToFront();

        var hwndValue = Safe(() => modelView.GetViewHWndx64(), 0L);
        if (hwndValue == 0)
            hwndValue = Safe(() => modelView.GetViewHWnd(), 0);
        if (hwndValue == 0)
            return null;

        var hwnd = new IntPtr(hwndValue);
        SetForegroundWindow(hwnd);

        if (!GetClientRect(hwnd, out var clientRect))
            return null;

        var clientTopLeft = new Point(0, 0);
        if (!ClientToScreen(hwnd, ref clientTopLeft))
            return null;

        var sheet = _drawingDoc.GetCurrentSheet() as Sheet;
        if (sheet == null)
            return null;

        double sheetWidth = 0;
        double sheetHeight = 0;
        _ = sheet.GetSize(ref sheetWidth, ref sheetHeight);
        if (sheetWidth <= 0 || sheetHeight <= 0)
            return null;

        var clientWidth = clientRect.Right - clientRect.Left;
        var clientHeight = clientRect.Bottom - clientRect.Top;
        var scale = Math.Min(clientWidth / sheetWidth, clientHeight / sheetHeight);
        var sheetScreenWidth = sheetWidth * scale;
        var sheetScreenHeight = sheetHeight * scale;
        var sheetLeft = clientTopLeft.X + (clientWidth - sheetScreenWidth) / 2.0;
        var sheetTop = clientTopLeft.Y + (clientHeight - sheetScreenHeight) / 2.0;

        var projectedSheetX = sheetX + sheetCorrectionX;
        var projectedSheetY = sheetY + sheetCorrectionY;
        var screenX = sheetLeft + projectedSheetX * scale;
        var screenY = sheetTop + (sheetHeight - projectedSheetY) * scale;
        var screenXi = (int)Math.Round(screenX);
        var screenYi = (int)Math.Round(screenY);
        return new ScreenPoint(
            hwnd,
            screenXi,
            screenYi,
            screenXi - clientTopLeft.X,
            screenYi - clientTopLeft.Y);
    }

    private static void BringSolidWorksToFront()
    {
        if (_swApp == null)
            return;

        Safe(() =>
        {
            _swApp.Visible = true;
            return true;
        }, false);

        Safe(() =>
        {
            var errors = 0;
            _swApp.ActivateDoc3(
                _activeDoc.GetTitle(),
                false,
                (int)swRebuildOnActivation_e.swDontRebuildActiveDoc,
                ref errors);
            return true;
        }, false);

        var frame = Safe(() => _swApp.Frame() as Frame, null);
        var frameHwndValue = frame == null ? 0L : Safe(() => frame.GetHWndx64(), 0L);
        if (frameHwndValue == 0 && frame != null)
            frameHwndValue = Safe(() => frame.GetHWnd(), 0);
        if (frameHwndValue == 0)
            return;

        var frameHwnd = new IntPtr(frameHwndValue);
        if (IsIconic(frameHwnd))
            ShowWindow(frameHwnd, SwRestore);
        SetWindowPos(frameHwnd, HwndTopMost, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpShowWindow);
        BringWindowToTop(frameHwnd);
        SetForegroundWindow(frameHwnd);
        Thread.Sleep(80);
        SetWindowPos(frameHwnd, HwndNoTopMost, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpShowWindow);
    }

    private static EntityOwnership ReadEntityOwnership(object obj, View selectedView)
    {
        if (obj is not IEntity entity)
            return new EntityOwnership(false, "null", new List<DrawingComponentOwnership>());

        var component = Safe(() => entity.GetComponent() as Component2, null);
        var drawingComponents = new List<DrawingComponentOwnership>();

        foreach (var view in EnumerateCandidateViews(selectedView))
        {
            var drawingComponent = Safe(() => entity.GetDrawingComponent(view), null);
            drawingComponents.Add(new DrawingComponentOwnership(
                DescribeView(view),
                drawingComponent == null ? "null" : DescribeDrawingComponent(drawingComponent)));
        }

        return new EntityOwnership(true, DescribeComponent(component), drawingComponents);
    }

    private static GridOptions ParseGridOptions(string[] args)
    {
        var outputPath = Path.Combine(
            "runtime",
            "solidworks-hover-grid",
            "grid-ownership-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".jsonl");
        var stepMm = 1.0;
        var includeEmpty = false;
        var hoverDelayMs = 60;
        var pollMs = 10;
        var zoomFitDelayMs = 500;
        var viewsOnly = false;
        var viewName = "";
        var rowYMm = double.NaN;
        var rowYMinMm = double.NaN;
        var rowYMaxMm = double.NaN;
        var reverseX = false;
        var maxPoints = 0;
        var progressEveryRows = 25;
        var clearBeforePoint = true;
        var clearAfterPoint = false;
        var clearDelayMs = 20;
        var sheetCorrectionXM = 0.0;
        var sheetCorrectionYM = 0.0;
        var viewSheetCorrections = new Dictionary<string, SheetCorrection>(StringComparer.OrdinalIgnoreCase);

        for (var i = 0; i < args.Length; i++)
        {
            var arg = args[i];
            if (string.Equals(arg, "--output", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                outputPath = args[++i];
            else if (string.Equals(arg, "--step-mm", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                stepMm = Math.Max(0.001, double.Parse(args[++i]));
            else if (string.Equals(arg, "--include-empty", StringComparison.OrdinalIgnoreCase))
                includeEmpty = true;
            else if (string.Equals(arg, "--hover-delay-ms", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                hoverDelayMs = Math.Max(0, int.Parse(args[++i]));
            else if (string.Equals(arg, "--max-hover-wait-ms", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                hoverDelayMs = Math.Max(0, int.Parse(args[++i]));
            else if (string.Equals(arg, "--poll-ms", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                pollMs = Math.Max(1, int.Parse(args[++i]));
            else if (string.Equals(arg, "--zoom-fit-delay-ms", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                zoomFitDelayMs = Math.Max(0, int.Parse(args[++i]));
            else if (string.Equals(arg, "--views-only", StringComparison.OrdinalIgnoreCase))
                viewsOnly = true;
            else if (string.Equals(arg, "--view-name", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                viewName = args[++i];
            else if (string.Equals(arg, "--row-y-mm", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                rowYMm = double.Parse(args[++i]);
            else if (string.Equals(arg, "--row-y-min-mm", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                rowYMinMm = double.Parse(args[++i], CultureInfo.InvariantCulture);
            else if (string.Equals(arg, "--row-y-max-mm", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                rowYMaxMm = double.Parse(args[++i], CultureInfo.InvariantCulture);
            else if (string.Equals(arg, "--reverse-x", StringComparison.OrdinalIgnoreCase))
                reverseX = true;
            else if (string.Equals(arg, "--max-points", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                maxPoints = Math.Max(0, int.Parse(args[++i]));
            else if (string.Equals(arg, "--progress-every-rows", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                progressEveryRows = Math.Max(1, int.Parse(args[++i]));
            else if (string.Equals(arg, "--clear-before-point", StringComparison.OrdinalIgnoreCase))
                clearBeforePoint = true;
            else if (string.Equals(arg, "--no-clear-before-point", StringComparison.OrdinalIgnoreCase))
                clearBeforePoint = false;
            else if (string.Equals(arg, "--clear-after-point", StringComparison.OrdinalIgnoreCase))
                clearAfterPoint = true;
            else if (string.Equals(arg, "--no-clear-after-point", StringComparison.OrdinalIgnoreCase))
                clearAfterPoint = false;
            else if (string.Equals(arg, "--clear-delay-ms", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                clearDelayMs = Math.Max(0, int.Parse(args[++i]));
            else if (string.Equals(arg, "--sheet-correction-x-mm", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                sheetCorrectionXM = double.Parse(args[++i], CultureInfo.InvariantCulture) / 1000.0;
            else if (string.Equals(arg, "--sheet-correction-y-mm", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
                sheetCorrectionYM = double.Parse(args[++i], CultureInfo.InvariantCulture) / 1000.0;
            else if ((string.Equals(arg, "--view-sheet-correction-mm", StringComparison.OrdinalIgnoreCase) ||
                      string.Equals(arg, "--view-correction-mm", StringComparison.OrdinalIgnoreCase)) &&
                     i + 1 < args.Length)
            {
                var value = args[++i];
                var separator = value.LastIndexOf('=');
                if (separator <= 0 || separator >= value.Length - 1)
                    throw new ArgumentException("--view-sheet-correction-mm requires viewName=dxMm,dyMm.");

                var name = value[..separator].Trim();
                var parts = value[(separator + 1)..].Split(',', StringSplitOptions.TrimEntries);
                if (parts.Length != 2)
                    throw new ArgumentException("--view-sheet-correction-mm requires viewName=dxMm,dyMm.");

                viewSheetCorrections[name] = new SheetCorrection(
                    double.Parse(parts[0], CultureInfo.InvariantCulture) / 1000.0,
                    double.Parse(parts[1], CultureInfo.InvariantCulture) / 1000.0);
            }
        }

        return new GridOptions(
            outputPath,
            stepMm,
            includeEmpty,
            hoverDelayMs,
            pollMs,
            zoomFitDelayMs,
            viewsOnly,
            viewName,
            rowYMm,
            rowYMinMm,
            rowYMaxMm,
            reverseX,
            maxPoints,
            progressEveryRows,
            clearBeforePoint,
            clearAfterPoint,
            clearDelayMs,
            sheetCorrectionXM,
            sheetCorrectionYM,
            viewSheetCorrections);
    }

    private static void WriteJsonLine<T>(TextWriter writer, T value)
    {
        writer.WriteLine(JsonSerializer.Serialize(value, JsonOptions));
    }

    private static SldWorks ConnectSolidWorks()
    {
        int hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
        if (hr < 0)
            Marshal.ThrowExceptionForHR(hr);
        return (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero);
    }

    private static ModelDoc2 FindFirstOpenDrawing(SldWorks sw)
    {
        var docObj = Safe(() => sw.IGetFirstDocument2(), null);
        var guard = 0;
        while (docObj is ModelDoc2 doc && guard++ < 200)
        {
            if (Safe(() => doc.GetType(), 0) == (int)swDocumentTypes_e.swDocDRAWING)
                return doc;

            docObj = Safe(() => doc.IGetNext(), null);
        }

        return null;
    }

    private static void HookEvents()
    {
        try
        {
            _drawingEvents = _drawingDoc as DDrawingDocEvents_Event;
            if (_drawingEvents != null)
            {
                _dynamicHighlightHandler = OnDynamicHighlight;
                _drawingEvents.DynamicHighlightNotify += _dynamicHighlightHandler;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("DynamicHighlightNotify hook failed: " + ex.Message);
        }

        try
        {
            var modelView = _activeDoc.ActiveView as ModelView;
            var mouse = modelView?.GetMouse();
            _mouseEvents = mouse as DMouseEvents_Event;
            if (_mouseEvents != null)
            {
                _mouseMoveHandler = OnMouseMove;
                _mouseEvents.MouseMoveNotify += _mouseMoveHandler;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("MouseMoveNotify hook failed: " + ex.Message);
        }
    }

    private static int OnDynamicHighlight(bool highlightState)
    {
        if (_suppressEventProbeOutput)
            return 0;

        PrintProbe("DynamicHighlightNotify highlight=" + highlightState);
        return 0;
    }

    private static int OnMouseMove(int x, int y, int wParam)
    {
        if (_suppressEventProbeOutput)
            return 0;

        PrintProbe($"MouseMoveNotify x={x} y={y}");
        return 0;
    }

    private static void PrintProbe(string trigger)
    {
        try
        {
            var probe = ReadProbe(trigger);
            if (probe.Signature == _lastSignature)
                return;

            _lastSignature = probe.Signature;
            Console.WriteLine("---- " + DateTime.Now.ToString("HH:mm:ss.fff") + " " + trigger + " ----");
            foreach (var line in probe.Lines)
                Console.WriteLine(line);
            Console.WriteLine();
        }
        catch (Exception ex)
        {
            Console.WriteLine("probe failed: " + ex.Message);
        }
    }

    private static ProbeResult ReadProbe(string trigger)
    {
        var lines = new List<string>();
        GetCursorPos(out var cursor);
        lines.Add($"cursor_screen=({cursor.X},{cursor.Y})");
        lines.Add("doc_title=" + Safe(() => _activeDoc.GetTitle(), ""));

        var selectionMgr = _activeDoc.SelectionManager as SelectionMgr;
        if (selectionMgr == null)
        {
            lines.Add("selection_manager=null");
            return new ProbeResult(trigger + "|selection_manager=null", lines);
        }

        var preselected = Safe(() => selectionMgr.GetPreSelectedObject(), null);
        lines.Add("preselected=" + DescribeObject(preselected));
        AppendEntityOwnership(lines, "preselected", preselected, null);

        var selectedCount = Safe(() => selectionMgr.GetSelectedObjectCount2(-1), 0);
        lines.Add("selected_count=" + selectedCount);
        for (var i = 1; i <= Math.Min(selectedCount, 5); i++)
        {
            var selected = Safe(() => selectionMgr.GetSelectedObject6(i, -1), null);
            var selectedType = Safe(() => selectionMgr.GetSelectedObjectType3(i, -1), 0);
            var drawingView = Safe(() => selectionMgr.GetSelectedObjectsDrawingView2(i, -1), null);
            var point = DoubleArray(Safe(() => selectionMgr.GetSelectionPoint2(i, -1), null));
            lines.Add($"selected[{i}].type={SelectionTypeName(selectedType)}({selectedType})");
            lines.Add($"selected[{i}].object={DescribeObject(selected)}");
            lines.Add($"selected[{i}].drawing_view={DescribeView(drawingView)}");
            if (point.Count > 0)
                lines.Add($"selected[{i}].point_m=({string.Join(",", point.Select(v => v.ToString("G6")))})");
            AppendEntityOwnership(lines, $"selected[{i}]", selected, drawingView);
        }

        var signature = string.Join("|", lines);
        return new ProbeResult(signature, lines);
    }

    private static void AppendEntityOwnership(List<string> lines, string prefix, object obj, View selectedView)
    {
        if (obj is not IEntity entity)
        {
            lines.Add(prefix + ".entity=no");
            return;
        }

        lines.Add(prefix + ".entity=yes");

        var component = Safe(() => entity.GetComponent() as Component2, null);
        lines.Add(prefix + ".entity_component=" + DescribeComponent(component));

        var candidateViews = EnumerateCandidateViews(selectedView).ToList();
        foreach (var view in candidateViews)
        {
            var drawingComponent = Safe(() => entity.GetDrawingComponent(view), null);
            if (drawingComponent == null)
            {
                lines.Add(prefix + $".drawing_component[{DescribeView(view)}]=null");
                continue;
            }

            lines.Add(prefix + $".drawing_component[{DescribeView(view)}]=" + DescribeDrawingComponent(drawingComponent));
        }
    }

    private static IEnumerable<View> EnumerateCandidateViews(View selectedView)
    {
        if (selectedView != null)
            yield return selectedView;

        var activeView = Safe(() => _drawingDoc.ActiveDrawingView as View, null);
        if (activeView != null && !SameComObject(activeView, selectedView))
            yield return activeView;

        var viewObj = Safe(() => _drawingDoc.GetFirstView(), null);
        var guard = 0;
        while (viewObj is View view && guard++ < 200)
        {
            if (!SameComObject(view, selectedView) && !SameComObject(view, activeView))
                yield return view;
            viewObj = Safe(() => view.GetNextView(), null);
        }
    }

    private static string DescribeObject(object obj)
    {
        if (obj == null)
            return "null";

        var parts = new List<string>
        {
            "runtime=" + (obj.GetType().FullName ?? obj.GetType().Name),
            "com=" + ComIdentity(obj)
        };

        AddIf(parts, "name", Safe(() => Convert.ToString(GetMember(obj, "Name")), ""));
        AddIf(parts, "name2", Safe(() => Convert.ToString(GetMember(obj, "Name2")), ""));
        AddIf(parts, "typeName", Safe(() => Convert.ToString(Invoke(obj, "GetTypeName2")), ""));
        AddIf(parts, "selectName", Safe(() => Convert.ToString(Invoke(obj, "GetSelectByIDString")), ""));

        return string.Join("; ", parts);
    }

    private static string DescribeDrawingComponent(DrawingComponent drawingComponent)
    {
        if (drawingComponent == null)
            return "null";

        var component = Safe(() => drawingComponent.Component as Component2, null);
        return string.Join("; ", new[]
        {
            "drawing_name=" + Safe(() => drawingComponent.Name, ""),
            "visible=" + Safe(() => drawingComponent.Visible, false),
            "component=" + DescribeComponent(component)
        });
    }

    private static string DescribeComponent(Component2 component)
    {
        if (component == null)
            return "null";

        return string.Join("; ", new[]
        {
            "name=" + Safe(() => component.Name2, ""),
            "path=" + Safe(() => component.GetPathName(), ""),
            "config=" + Safe(() => component.ReferencedConfiguration, "")
        });
    }

    private static string DescribeView(View view)
    {
        if (view == null)
            return "null";

        var name = Safe(() => view.GetName2(), Safe(() => view.Name, ""));
        var type = Safe(() => view.Type, 0);
        return $"{name}/type={DrawingViewTypeName(type)}({type})";
    }

    private static string DrawingViewTypeName(int typeCode)
    {
        return typeCode switch
        {
            1 => "Model",
            2 => "Projected",
            3 => "Auxiliary",
            4 => "Section",
            5 => "Detail",
            6 => "Relative",
            7 => "Detached",
            8 => "Standard",
            9 => "Named",
            10 => "Empty",
            _ => "Unknown"
        };
    }

    private static string SelectionTypeName(int type)
    {
        return Enum.GetName(typeof(swSelectType_e), type) ?? "unknown";
    }

    private static object Invoke(object target, string name, params object[] args)
    {
        return target?.GetType().InvokeMember(
            name,
            System.Reflection.BindingFlags.InvokeMethod,
            null,
            target,
            args);
    }

    private static object GetMember(object target, string name)
    {
        return target?.GetType().InvokeMember(
            name,
            System.Reflection.BindingFlags.GetProperty | System.Reflection.BindingFlags.GetField,
            null,
            target,
            Array.Empty<object>());
    }

    private static void AddIf(List<string> parts, string key, string value)
    {
        if (!string.IsNullOrWhiteSpace(value))
            parts.Add(key + "=" + value);
    }

    private static string ComIdentity(object obj)
    {
        if (obj == null)
            return "";

        try
        {
            var ptr = Marshal.GetIUnknownForObject(obj);
            try { return "0x" + ptr.ToInt64().ToString("X"); }
            finally { Marshal.Release(ptr); }
        }
        catch
        {
            return "";
        }
    }

    private static bool SameComObject(object left, object right)
    {
        if (left == null || right == null)
            return false;
        return ComIdentity(left) == ComIdentity(right);
    }

    private static List<double> DoubleArray(object value)
    {
        if (value == null)
            return new List<double>();
        if (value is double[] doubles)
            return doubles.ToList();
        if (value is object[] objects)
            return objects.Where(x => x != null).Select(Convert.ToDouble).ToList();
        if (value is Array array)
        {
            var values = new List<double>();
            foreach (var item in array)
            {
                if (item != null)
                    values.Add(Convert.ToDouble(item));
            }
            return values;
        }
        return new List<double>();
    }

    private static T Safe<T>(Func<T> action, T fallback)
    {
        try { return action(); }
        catch { return fallback; }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct Point
    {
        public int X;
        public int Y;

        public Point(int x, int y)
        {
            X = x;
            Y = y;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct Rect
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    private sealed record ProbeResult(string Signature, List<string> Lines);

    private sealed record GridOptions(
        string OutputPath,
        double StepMm,
        bool IncludeEmpty,
        int HoverDelayMs,
        int PollMs,
        int ZoomFitDelayMs,
        bool ViewsOnly,
        string ViewName,
        double RowYMm,
        double RowYMinMm,
        double RowYMaxMm,
        bool ReverseX,
        int MaxPoints,
        int ProgressEveryRows,
        bool ClearBeforePoint,
        bool ClearAfterPoint,
        int ClearDelayMs,
        double SheetCorrectionXM,
        double SheetCorrectionYM,
        IReadOnlyDictionary<string, SheetCorrection> ViewSheetCorrections);

    private sealed record SheetCorrection(double XM, double YM);

    private sealed record ContourOptions(
        string InputPath,
        string OutputPath,
        int MinGroupPoints,
        double PadMm,
        bool GroupByDrawingComponent);

    private sealed record ViewBounds(string Name, double XMin, double YMin, double XMax, double YMax);

    private sealed record ScreenPoint(IntPtr HWnd, int ScreenX, int ScreenY, int ClientX, int ClientY);

    private sealed record OwnershipPoint(double XM, double YM);

    private sealed class OwnershipGroup(string key, string displayName)
    {
        public string Key { get; } = key;
        public string DisplayName { get; } = displayName;
        public List<OwnershipPoint> Points { get; } = new();
    }

    private sealed record ContourRecord(
        string Key,
        string DisplayName,
        int PointCount,
        double MinXM,
        double MinYM,
        double MaxXM,
        double MaxYM,
        double MinXMm,
        double MinYMm,
        double MaxXMm,
        double MaxYMm);

    private sealed record GridHeaderRecord(
        string Kind,
        string DocumentTitle,
        string SheetName,
        double SheetWidthM,
        double SheetHeightM,
        double StepMm,
        int Columns,
        int Rows);

    private sealed record GridPointRecord(
        string Kind,
        int Row,
        int Column,
        double SheetXM,
        double SheetYM,
        double SheetXMm,
        double SheetYMm,
        string ScanViewName,
        double AppliedSheetCorrectionXMm,
        double AppliedSheetCorrectionYMm,
        bool Hit,
        string RequestedSelectType,
        string SelectedTypeName,
        int SelectedTypeCode,
        string SelectedObject,
        string DrawingView,
        IReadOnlyList<double> SelectionPointM,
        double? SelectionDistanceM,
        EntityOwnership Ownership);

    private sealed record EntityOwnership(
        bool IsEntity,
        string EntityComponent,
        IReadOnlyList<DrawingComponentOwnership> DrawingComponents);

    private sealed record DrawingComponentOwnership(string View, string DrawingComponent);
}

